advisory-scan: fix version classification (multi-line patches + range expressions)
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.
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user