advisory-scan: attribute vendor-changelog CVEs to the release that fixed them
nginx publishes NO GitHub security advisories. Every nginx CVE we can see comes
from nginx.org/en/CHANGES, and the scan scraped ids out of it without attributing
them to a release - so they had no patched version, could never be classified, and
every nginx bump in the fleet reported 0 CVEs. nginx is a sidecar in most recipes,
so this was a fleet-wide blind spot.
Measured on the two PRs that prompted the question:
lasuite-docs#7 nginx 1.31.1 -> 1.31.3 0 -> 6 CVEs
lasuite-drive#6 nginx 1.31.2 -> 1.31.3 0 -> 3 CVEs
matching a hand count of the changelog exactly (three fixed in 1.31.2, three in
1.31.3; the narrower window correctly counts only the latter).
How: when a vendor page is organised by release, each CVE is attributed to the
nearest preceding release heading ('Changes with nginx 1.31.3', '## v1.31.3'),
and that becomes its fixed-in version. The CVE is tied to a window by the image
name appearing in the page URL (window 'nginx' <-> nginx.org/...). A changelog
lists the project's whole history, so only releases the window actually crosses
count - asserted by a test that the 2013 entries stay out.
76 tests. discourse 140 / gitea 2 / mailu 2 / keycloak 12 unchanged.
This commit is contained in:
@@ -537,6 +537,59 @@ class TestReleaseLineSemantics(unittest.TestCase):
|
||||
self.assertEqual(rep["fixed_by_this_upgrade"], ["CVE-2025-49844"])
|
||||
|
||||
|
||||
class TestChangelogAttribution(unittest.TestCase):
|
||||
"""Projects that publish no advisory feed still say which release fixed what — in their changelog."""
|
||||
|
||||
CHANGES = """
|
||||
Changes with nginx 1.31.3 11 Aug 2026
|
||||
*) Security: a flaw ... (CVE-2026-60005)
|
||||
*) Security: another ... (CVE-2026-56434)
|
||||
|
||||
Changes with nginx 1.31.2 04 Aug 2026
|
||||
*) Security: something ... (CVE-2026-48142)
|
||||
|
||||
Changes with nginx 1.31.1 21 Jul 2026
|
||||
*) Security: older ... (CVE-2026-9256)
|
||||
|
||||
Changes with nginx 1.20.0 01 Jan 2021
|
||||
*) Security: ancient ... (CVE-2013-2028)
|
||||
"""
|
||||
|
||||
def test_each_cve_is_attributed_to_the_release_that_fixed_it(self):
|
||||
got = A._changelog_versions(self.CHANGES)
|
||||
self.assertEqual(got["CVE-2026-60005"], "1.31.3")
|
||||
self.assertEqual(got["CVE-2026-48142"], "1.31.2")
|
||||
self.assertEqual(got["CVE-2026-9256"], "1.31.1")
|
||||
self.assertEqual(got["CVE-2013-2028"], "1.20.0")
|
||||
|
||||
def _scan(self, wfrom, wto):
|
||||
# nginx publishes NO GitHub advisories — the feed is empty and the changelog is everything.
|
||||
return run_scan(
|
||||
[gh("nginx/nginx", [])],
|
||||
[{"source": "https://nginx.org/en/CHANGES", "status": "ok",
|
||||
"cves": sorted(A._changelog_versions(self.CHANGES)),
|
||||
"context": {}, "fixed_in": A._changelog_versions(self.CHANGES)}],
|
||||
images=[("nginx", wfrom, wto)], urls=["https://github.com/nginx/nginx"])
|
||||
|
||||
def test_window_counts_only_the_releases_it_crosses(self):
|
||||
rep = self._scan("1.31.1", "1.31.3") # 1.31.1 is the FROM, so its CVE is already fixed
|
||||
self.assertEqual(set(rep["fixed_by_this_upgrade"]),
|
||||
{"CVE-2026-48142", "CVE-2026-56434", "CVE-2026-60005"})
|
||||
|
||||
def test_a_narrower_window_counts_fewer(self):
|
||||
rep = self._scan("1.31.2", "1.31.3")
|
||||
self.assertEqual(set(rep["fixed_by_this_upgrade"]), {"CVE-2026-56434", "CVE-2026-60005"})
|
||||
|
||||
def test_ancient_entries_are_not_swept_in(self):
|
||||
# The changelog lists the project's whole history; only the crossed releases may count.
|
||||
rep = self._scan("1.31.1", "1.31.3")
|
||||
self.assertNotIn("CVE-2013-2028", rep["fixed_by_this_upgrade"])
|
||||
|
||||
def test_evidence_is_recorded(self):
|
||||
rep = self._scan("1.31.1", "1.31.3")
|
||||
self.assertEqual(rep["resolved_by_changelog"]["CVE-2026-60005"], "1.31.3")
|
||||
|
||||
|
||||
class TestComposeDerivedWindows(unittest.TestCase):
|
||||
"""Windows read off a compose diff, so nobody has to remember which --image args an upgrade needs."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user