#!/usr/bin/env python3 """R-VULN-08: acceptance parsing, expiry and fail-safe behaviour.""" import sys from datetime import date, datetime, timedelta, timezone from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE.parents[3] / "platform/vuln-scanner/src")) import acceptances # noqa: E402 import report # noqa: E402 FAILED = [] TODAY = date(2026, 11, 9) NOW = datetime(2026, 11, 9, 12, 0, tzinfo=timezone.utc) A1 = "sha256:" + "a1" * 32 def check(name, condition): print(("ok " if condition else "FAIL ") + name) if not condition: FAILED.append(name) def parse(text): return acceptances.parse_text(text, TODAY) entries, errors = parse("""# header comment - id: CVE-2026-1 image: docker.io/library/nginx # exact match reason: not reachable - feature disabled expires: 2027-02-01 - id: CVE-2026-2 reason: "platform wide" expires: 2026-12-01 """) check("valid flat entries parse with no errors", len(entries) == 2 and errors == []) check("inline comment is stripped", entries[0]["image"] == "docker.io/library/nginx") check("omitted image means platform-wide", entries[1]["image"] is None) check("empty list form is accepted", parse("# none\n[]\n") == ([], [])) check("empty file is accepted", parse("") == ([], [])) for name, text in ( ("missing reason", "- id: CVE-1\n expires: 2026-12-01\n"), ("missing expires", "- id: CVE-1\n reason: x\n"), ("expires beyond 180 days", "- id: CVE-1\n reason: x\n expires: 2027-06-01\n"), ("unknown key", "- id: CVE-1\n reason: x\n expires: 2026-12-01\n scope: all\n"), ("not a date", "- id: CVE-1\n reason: x\n expires: soon\n"), ("impossible date", "- id: CVE-1\n reason: x\n expires: 2026-13-45\n"), ("duplicate key", "- id: CVE-1\n reason: x\n reason: y\n expires: 2026-12-01\n"), ("flow value", "- id: CVE-1\n reason: [x]\n expires: 2026-12-01\n"), ): found, problems = parse(text) check("rejected: " + name, found == [] and len(problems) == 1) found, problems = parse("- id: CVE-1\n reason: x\n expires: 2026-12-01\n- id: bad entry\n expires: 2026-12-01\n") check("one bad entry is ignored, the valid one kept", len(found) == 1 and len(problems) == 1) found, problems = parse("- id: CVE-1\n reason: x\n") check("bad indentation invalidates the whole file", found == [] and problems) found, problems = parse("garbage\n- id: CVE-1\n reason: x\n expires: 2026-12-01\n") check("a stray line invalidates the whole file", found == [] and problems) check("missing file is an error, not an empty allowlist", acceptances.load("/nonexistent/acceptances.yaml")[1]) entry = {"id": "CVE-1", "image": None, "reason": "r", "expires": date(2026, 11, 9)} check("acceptance holds on its expiry day", acceptances.match([entry], "CVE-1", "any/image", TODAY) is entry) check("acceptance gone the day after", acceptances.match([entry], "CVE-1", "any/image", TODAY + timedelta(days=1)) is None) scoped = {"id": "CVE-1", "image": "docker.io/library/nginx", "reason": "r", "expires": date(2026, 12, 1)} check("image match is exact", acceptances.match([scoped], "CVE-1", "docker.io/library/nginx-extra", TODAY) is None) check("image match accepts the exact image", acceptances.match([scoped], "CVE-1", "docker.io/library/nginx", TODAY) is scoped) def state_with(findings): target = {"id": "docker.io/library/nginx@" + A1, "type": "image", "image": "docker.io/library/nginx", "digest": A1, "ref": "", "reason": None, "workloads": []} return { "inventory": [target], "database": {"updatedAt": "2026-11-09T06:00:00Z", "version": "2"}, "run": {"startedAt": "2026-11-09T09:00:00Z", "finishedAt": "2026-11-09T09:30:00Z", "status": "complete"}, "remediation": [], "inventoryError": None, "loadError": None, "scans": {A1: {"firstDiscoveredAt": "2026-11-01T00:00:00Z", "lastInventoriedAt": "2026-11-09T09:00:00Z", "lastAttemptAt": "2026-11-09T09:00:00Z", "lastSuccessAt": "2026-11-09T09:00:00Z", "outcome": "ok", "reason": None, "failedSince": None, "findings": findings, "firstSeen": {f["id"]: "2026-11-01T00:00:00Z" for f in findings}}}, } FINDING = {"id": "CVE-1", "severity": "CRITICAL", "cvss": 9.0, "package": "p", "installedVersion": "1", "fixedVersion": "2", "title": "t", "url": ""} ST = state_with([FINDING]) def status(when, accepted, errs=()): built = report.build(ST, accepted, list(errs), when) return built, built["findings"][0] def reason_entry(length): return acceptances.parse_text("- id: CVE-1\n reason: " + "r" * length + "\n expires: 2026-12-01\n", NOW.date()) found, problems = reason_entry(3000) check("a reason over 2048 characters ignores the entry and reports it", found == [] and any("2048" in p for p in problems)) found, problems = reason_entry(2048) check("mutation: a reason of exactly 2048 characters is valid", len(found) == 1 and problems == []) entry = {"id": "CVE-1", "image": None, "reason": "not reachable", "expires": date(2026, 11, 9)} built, finding = status(NOW, [entry]) check("unexpired acceptance marks the finding accepted", finding["status"] == "accepted" and finding["acceptance"] == {"reason": "not reachable", "expires": "2026-11-09"}) check("accepted report is schema-valid", report.validate(built) == []) _, finding = status(NOW + timedelta(days=1), [entry]) check("expired acceptance reverts to the finding's own status", finding["status"] == "fix-available" and "acceptance" not in finding) _, finding = status(NOW, []) check("mutation: no acceptance leaves it open/fix-available", finding["status"] == "fix-available") built, finding = status(NOW, [], ["bad entry"]) check("an invalid entry degrades the run", built["run"]["status"] == "degraded") check("an invalid entry never hides the finding", finding["status"] == "fix-available") print("FAILED: " + str(len(FAILED)) if FAILED else "all checks passed") sys.exit(1 if FAILED else 0)