Files
home-network-k3s-impl/conformance/tests/l1-rendering/vuln-scanner-page/check.py
T
jakman85andClaude Opus 5.5 50ec2297ee feat(vuln): scanner service and fixture-driven L1 tests
Stdlib-only scanner per docs/17: Kubernetes API inventory, Trivy runner,
persisted state, schema-validated report.json, strict acceptances parser,
server-rendered page, aggregate metrics, internal API, and a scan loop
that stays live while scanning.

L1 tests under conformance/tests/l1-rendering/vuln-scanner-* run the
actual modules against recorded fixtures with a fake Trivy runner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 09:41:23 -06:00

182 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""R-VULN-03/04: the rendered page. Assertions run against the actual HTML output."""
import base64
import copy
import hashlib
import json
import re
import sys
from datetime import datetime, timedelta, timezone
from html.parser import HTMLParser
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parents[3] / "platform/vuln-scanner/src"))
import page # noqa: E402
FAILED = []
NOW = datetime(2026, 11, 9, 12, 0, tzinfo=timezone.utc)
REPORT = json.loads((HERE / "fixtures/report.json").read_text())
CLEAN_RE = re.compile(r"(?i)\bclean\b|no vulnerabilit")
def check(name, condition):
print(("ok " if condition else "FAIL ") + name)
if not condition:
FAILED.append(name)
def render(report=REPORT, query="", now=NOW):
status, headers, body = page.render(report, query, now)
return status, headers, body.decode("utf-8")
class Tags(HTMLParser):
def __init__(self):
super().__init__()
self.tags = []
self.attrs = []
def handle_starttag(self, tag, attrs):
self.tags.append(tag)
self.attrs.append((tag, dict(attrs)))
def tags(body):
parser = Tags()
parser.feed(body)
return parser
def covers_everything(body, report):
"""Every fixture CVE id, workload name and container name appears in the document."""
missing = [f["id"] for f in report["findings"] if f["id"] not in body]
for target in report["targets"]:
for workload in target["workloads"]:
if workload["name"] not in body:
missing.append(workload["name"])
missing.extend(c for c in workload["containers"] if c not in body)
return missing
def workload_view_covers(body, report):
"""Every workload row with its containers, and every CVE in the expansions."""
missing = covers_everything(body, report)
return missing
for view in ("cves", "workloads", "inventory"):
status, headers, body = render(query="view=" + view)
check(view + " view returns 200", status == 200)
check(view + " view has the Content-Security-Policy with no script source", "script-src" not in headers["Content-Security-Policy"]
and headers["Content-Security-Policy"].startswith("default-src 'none'"))
check(view + " view sets nosniff", headers["X-Content-Type-Options"] == "nosniff")
check(view + " view has no script element", "script" not in tags(body).tags)
style_body = re.search(r"<style>(.*?)</style>", body, re.S).group(1)
expected = base64.b64encode(hashlib.sha256(style_body.encode()).digest()).decode()
check(view + " view's CSP hash matches its inline style", "'sha256-" + expected + "'" in headers["Content-Security-Policy"])
check(view + " view header shows every state count", all(state + ": <strong>" in body for state in page.report_mod.STATES))
check(view + " view shows a coverage warning", "INCOMPLETE COVERAGE" in body)
check(view + " view makes no clean claim with unscanned targets", not CLEAN_RE.search(body))
status, headers, cves = render(query="view=cves")
check("CVE view contains every CVE", all(f["id"] in cves for f in REPORT["findings"]))
affected = {f["targetId"] for f in REPORT["findings"]}
expected_in_cves = {(t["id"], w["name"]) for t in REPORT["targets"] if t["id"] in affected for w in t["workloads"]}
check("CVE view expansions name every workload of an affected target", all(name in cves for _, name in expected_in_cves))
check("CVE view expansions name every container of an affected target",
all(c in cves for t in REPORT["targets"] if t["id"] in affected for w in t["workloads"] for c in w["containers"]))
for name in ("web", "api", "app", "server"):
check("CVE view expansion names " + name, name in cves)
status, headers, workloads = render(query="view=workloads")
check("workloads view contains every workload and container, including zero-finding ones", covers_everything(workloads, REPORT) == [])
status, headers, inventory = render(query="view=inventory")
check("inventory view contains every target", all(t["id"] in inventory for t in REPORT["targets"]))
check("inventory view contains every workload and container", covers_everything(inventory, REPORT) == [])
mutated = copy.deepcopy(REPORT)
mutated["findings"] = [f for f in mutated["findings"] if f["id"] != "CVE-2026-1003"]
check("mutation: the coverage evaluator catches a dropped CVE", "CVE-2026-1003" in covers_everything(render(mutated, "view=workloads")[2], REPORT))
_, _, body = render(query="view=cves")
check("hostile title is escaped", "&lt;script&gt;alert(1)&lt;/script&gt;" in body)
check("hostile title is never live", "<script>" not in body)
hostile = copy.deepcopy(REPORT)
hostile["findings"][0]["url"] = "javascript:alert(1)"
hostile["findings"][0]["title"] = '"><img src=x onerror=alert(1)>'
hostile["targets"][0]["reason"] = "<b onmouseover=alert(1)>x</b>"
hostile["remediation"][0]["url"] = "javascript:evil()"
_, _, body = render(hostile, "view=cves")
parsed = tags(body)
check("a javascript: advisory URL never becomes a link", not any("javascript:" in (a.get("href") or "") for t, a in parsed.attrs if t == "a"))
check("no element carries a hostile inline handler", not any(k.startswith("on") for _, a in parsed.attrs for k in a))
check("no img element was injected", "img" not in parsed.tags)
_, _, body = render(hostile, "view=inventory")
check("hostile reason is escaped in inventory", "<b onmouseover" not in body and "&lt;b onmouseover" in body)
_, _, body = render(query="view=cves&q=" + "%3Cscript%3E")
check("a hostile search term is escaped", "<script>" not in body)
check("a linked CVE points at its advisory", 'href="https://example.test/CVE-2026-1001"' in render(query="view=cves")[2])
stale = copy.deepcopy(REPORT)
stale["generatedAt"] = (NOW - timedelta(hours=27)).strftime("%Y-%m-%dT%H:%M:%SZ")
_, _, body = render(stale)
check("stale report shows a warning", "STALE REPORT" in body)
check("stale report shows no normal state", "Status: ATTENTION NEEDED" in body and "Status: CURRENT" not in body)
check("mutation: a fresh report has no stale warning", "STALE REPORT" not in render(REPORT)[2])
degraded = copy.deepcopy(REPORT)
degraded["run"]["status"] = "degraded"
_, _, body = render(degraded)
check("degraded run shows a warning and no normal state", "DEGRADED RUN" in body and "Status: ATTENTION NEEDED" in body and "Status: CURRENT" not in body)
allgood = copy.deepcopy(REPORT)
allgood["targets"] = [t for t in allgood["targets"] if t["state"] == "scanned"]
allgood["findings"] = []
_, _, body = render(allgood)
check("the clean claim appears when every target is scanned with zero findings", "Clean: all 1 targets" in body)
check("mutation: an all-scanned report shows the current state", "Status: CURRENT" in body)
one_pending = copy.deepcopy(allgood)
one_pending["targets"].append({"id": "x@sha256:" + "0" * 64, "type": "image", "image": "x", "digest": "sha256:" + "0" * 64,
"state": "pending", "reason": None, "lastScannedAt": None, "workloads": []})
_, _, body = render(one_pending)
check("one pending target removes the clean claim", "Clean:" not in body and not CLEAN_RE.search(body))
for state in ("failed", "stale", "credential-required", "by-tag", "not-covered"):
variant = copy.deepcopy(allgood)
variant["targets"].append({**one_pending["targets"][-1], "state": state})
check("a " + state + " target removes the clean claim", not CLEAN_RE.search(render(variant)[2]))
empty = copy.deepcopy(allgood)
empty["targets"] = []
check("zero targets is never a clean claim", not CLEAN_RE.search(render(empty)[2]))
degraded_clean = copy.deepcopy(allgood)
degraded_clean["run"]["status"] = "degraded"
check("a degraded run never makes a clean claim", not CLEAN_RE.search(render(degraded_clean)[2]))
status, headers, body = render(None)
check("no report yet is a 503 with the CSP", status == 503 and "Content-Security-Policy" in headers)
check("no report yet makes no clean claim", not CLEAN_RE.search(body))
_, _, body = render(query="view=cves&severity=CRITICAL")
check("the severity filter narrows the list", "CVE-2026-1001" in body and "CVE-2026-1004" not in body)
check("a filter is carried in pagination and sort links", "severity=CRITICAL" in body)
_, _, body = render(query="view=cves&status=accepted")
check("accepted findings stay listed with their reason", "CVE-2026-1003" in body and "not reachable" in body)
_, _, body = render(query="view=cves&fix=no")
check("the fix filter excludes fixed CVEs", "CVE-2026-1001" not in body and "CVE-2026-1002" in body)
_, _, body = render(query="view=inventory&status=credential-required")
check("inventory filters on coverage state", "ghcr.io/org/private" in body and "postgres" not in body.split("<main>")[1])
_, _, body = render(query="view=workloads&namespace=ops")
check("the namespace filter narrows workloads", "nightly" in body and "<td>web</td>" not in body)
_, _, body = render(query="view=cves&q=openssl")
check("free text search matches packages", "CVE-2026-1001" in body and "CVE-2026-1004" not in body)
_, _, 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)
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")
check("pagination states the total", "of 250 CVEs (page 2 of 5)" in body)
check("pagination shows only its page", "CVE-2026-00049" not in body and "CVE-2026-00050" in body and "CVE-2026-00100" not in body)
check("every page's links include next and previous", ">next<" in body and ">previous<" in body)
print("FAILED: " + str(len(FAILED)) if FAILED else "all checks passed")
sys.exit(1 if FAILED else 0)