fix(vuln): survive OOM kills, isolate bad upstream strings, label findings per line

Review findings on the integrated scanner:
- An OOM kill no longer wedges the loop: inFlight is persisted before each scan, and on restart
  that image is marked failed and sits out full runs for 20h.
- One bad upstream string can no longer freeze the report: safe_url requires an exact lowercase
  scheme, acceptance reasons are bounded, /triage urls use the same rule, and report.build
  repairs or drops any entry the schema would refuse. lastCompleteRunAt advances only after a
  report is actually published.
- The private registry reports credential-required, not failed: VULN_PRIVATE_REGISTRIES names
  it, and Trivy is not invoked.
- loadError clears after a full run against a good inventory.
- Every image under platform/vuln-scanner stays out of Renovate auto-merge.
- The page labels status and target state per line, and mixed acceptance reads as mixed.

The Keycloak client is still an operator action: its key must go in an existing encrypted file
that an agent must not modify. platform/vuln-scanner/README.md has the exact steps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
jakman85andClaude Opus 5.5 committed 2026-11-09 12:28:29 -06:00
1 parent 4c82c64b2f
commit d8b25a67fe
15 files changed
+557 -43

No files matched your search

@@ -170,6 +170,39 @@ _, _, body = render()
check("default view excludes nothing", all(f["id"] in body for f in REPORT["findings"]))
check("remediation column shows pr-open with a link", 'href="https://gitea.test/pulls/1"' in body and "pr-open" in body)
# Status and state belong to the line they describe (R-VULN-04).
images = [t for t in REPORT["targets"] if t["type"] == "image" and t["workloads"]]
check("fixture has two image targets with workloads", len(images) >= 2)
mixed = copy.deepcopy(REPORT)
first, second = next(t for t in mixed["targets"] if t["id"] == images[0]["id"]), next(t for t in mixed["targets"] if t["id"] == images[1]["id"])
first["state"], first["reason"] = "scanned", None
second["state"], second["reason"] = "stale", "last success long ago"
base = dict(REPORT["findings"][0], id="CVE-MIXED")
base.pop("acceptance", None)
mixed["findings"] = [
dict(base, targetId=first["id"], status="accepted", acceptance={"reason": "only-on-the-first", "expires": "2026-12-01"}),
dict(base, targetId=second["id"], status="fix-available"),
]
_, _, body = render(mixed, "view=cves&q=CVE-MIXED")
check("a mixed CVE reads as mixed, with counts", "mixed: 1 fix-available, 1 accepted" in body)
check("a mixed CVE's row does not carry one image's acceptance", "<td>mixed: 1 fix-available, 1 accepted</td>" in body)
first_line = page.e(first["id"]) + " — target scanned — status accepted (only-on-the-first, until 2026-12-01)"
second_line = page.e(second["id"]) + " — target stale (last success long ago) — status fix-available"
check("the accepted image's line carries its own acceptance", first_line in body)
check("the other image's line carries its own status and state", second_line in body)
check("no line attaches the acceptance to the unaccepted status", "status fix-available (only-on-the-first" not in body)
check("workload lines inside the expansion carry state and status",
"target stale (last success long ago) — status fix-available</li>" in body
and "containers: " + page.e(", ".join(second["workloads"][0]["containers"]) or "none") + " — target stale" in body)
_, _, plain = render(query="view=cves&status=accepted")
check("mutation: a uniformly accepted CVE does not read as mixed", "mixed:" not in plain and "CVE-2026-1003" in plain)
_, _, body = render(mixed, "view=workloads")
check("the workloads images column labels each image's state", page.e(second["id"]) + " — target stale (last success long ago)" in body)
check("the workloads expansion labels each finding's status, image and state",
"status fix-available — " + page.e(second["id"]) + " — target stale (last success long ago)</li>" in body)
check("the workloads expansion keeps an acceptance on its own line",
"status accepted (only-on-the-first, until 2026-12-01) — " + page.e(first["id"]) + " — target scanned</li>" in body)
big = copy.deepcopy(REPORT)
big["findings"] = [dict(REPORT["findings"][0], id="CVE-2026-" + format(i, "05d")) for i in range(250)]
_, _, body = render(big, "view=cves&size=50&page=2")