From 3e59924450b93424af1ef74a0c59b473fe8c5da6 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Mon, 10 Aug 2026 18:50:07 +0000 Subject: [PATCH] advisory-scan: fix version classification (multi-line patches + range expressions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposed by asking whether the scan catches the n8n CVEs (CVE-2026-42231/42232). It did not — the advisories were fetched correctly but both misclassified as out-of-window. Two bugs: 1. Only vulnerabilities[0] was read. An advisory carries ONE ENTRY PER PATCHED RELEASE LINE: n8n patches three (1.123.32, 2.17.4, 2.18.1), so whichever line the deployment is actually on was silently dropped. gitea passed only because it patches a single line. Now all entries are kept. 2. patched_versions is a RANGE EXPRESSION ('>= 2.18.1'), not a bare version. Naive parsing produced (18,1) instead of (2,18,1), so no comparison could ever match. Version tokens are now extracted with a regex and the advisory counts as fixed-by-this-upgrade if ANY patched line falls in (from, to]. Verified: n8n 2.17.0 -> 2.18.1 now reports 12 CVEs including both criticals (CVE-2026-42231 GHSA-q5f4-99jv-pgg5, CVE-2026-42232); gitea 1.27.0 -> 1.27.1 still reports exactly 2. Note our deployed n8n (2.27.2+) is already past all three patched lines, so these were never outstanding for us — the bug was in detection, not exposure. --- cc-ci-plan/advisory-scan.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cc-ci-plan/advisory-scan.py b/cc-ci-plan/advisory-scan.py index 9e5d334..61994b0 100755 --- a/cc-ci-plan/advisory-scan.py +++ b/cc-ci-plan/advisory-scan.py @@ -138,7 +138,11 @@ def github_advisories(urls: list[str]) -> list[dict]: hdrs["Authorization"] = f"Bearer {tok}" entry = {"source": f"github-advisories:{owner}/{repo}", "status": "ok", "advisories": []} try: - for a in json.loads(_fetch(api, hdrs)): # noqa: PERF401 — explicit for readability + for a in json.loads(_fetch(api, hdrs)): + # An advisory carries ONE ENTRY PER PATCHED RELEASE LINE. n8n patches three + # (1.123.32, 2.17.4, 2.18.1); reading only vulnerabilities[0] silently dropped the + # line our deployment is actually on, so CVE-2026-42231/42232 classified as + # out-of-window. Keep them ALL and let the classifier match any of them. vulns = a.get("vulnerabilities") or [] entry["advisories"].append( { @@ -146,8 +150,12 @@ def github_advisories(urls: list[str]) -> list[dict]: "ghsa": a.get("ghsa_id"), "severity": a.get("severity"), "summary": (a.get("summary") or "")[:200], - "vulnerable_range": vulns[0].get("vulnerable_version_range") if vulns else None, - "patched": vulns[0].get("patched_versions") if vulns else None, + "vulnerable_range": "; ".join( + filter(None, (v.get("vulnerable_version_range") for v in vulns)) + ) or None, + "patched": "; ".join( + filter(None, (v.get("patched_versions") for v in vulns)) + ) or None, "url": a.get("html_url"), } ) @@ -266,7 +274,11 @@ def scan(recipe: str, v_from: str | None, v_to: str | None, registry_dir: str) - kf, kt = _vkey(v_from), _vkey(v_to) fixed, unknown = [], [] for cve, e in report["cves"].items(): - kp = _vkey((e.get("patched") or "").split(",")[0].strip() or None) + # `patched_versions` is a RANGE EXPRESSION (">= 2.18.1"), not a bare version, and there may + # be several (one per patched release line, joined with ";"). Pull every version-looking + # token and treat the advisory as fixed-by-this-upgrade if ANY of them lands in (from, to]. + cands = [_vkey(t) for t in re.findall(r"\d+(?:\.\d+)*", e.get("patched") or "")] + kp = next((c for c in cands if kf and kt and kf < c <= kt), None) or (cands[0] if cands else ()) if kf and kt and kp and kf < kp <= kt: e["classification"] = "fixed-by-this-upgrade" fixed.append(cve)