From 0981532fa5a63d72301be2281421e5e4b877f895 Mon Sep 17 00:00:00 2001 From: Alberto Torres Date: Fri, 19 Jun 2026 14:33:08 +0200 Subject: [PATCH] fix: group alerts by normalized name and link to GHSA pages Dependabot reports the same package under inconsistent casing across advisories (e.g. "starlette" and "Starlette"). Grouping by the raw name split it into two packages with different fix versions, so the second upgrade downgraded what the first one fixed, leaving the lockfile on a still-vulnerable version. Group by the PEP 503 normalized name so all advisories merge and the highest fix version wins. Also prefer the GHSA advisory URL over NVD for vulnerability links: freshly-assigned CVEs are often still RESERVED and 404 on NVD, while the GHSA page always exists for a Dependabot alert. Claude-Session: https://claude.ai/code/session_01VFS7dXstJqCtX4zYF9Gp3x --- security-updates/update.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/security-updates/update.py b/security-updates/update.py index e85b328..aedbb55 100644 --- a/security-updates/update.py +++ b/security-updates/update.py @@ -95,9 +95,16 @@ class Advisory: severity: str def markdown_link(self) -> str: + # Prefer the GHSA advisory page: it always exists for a Dependabot + # alert, whereas a freshly-assigned CVE is often still RESERVED and + # returns "CVE ID Not Found" on NVD. Show the CVE as the link text + # when we have one, since it's the more familiar identifier. + if self.ghsa: + text = self.cve or self.ghsa + return f"[{text}](https://github.com/advisories/{self.ghsa})" if self.cve: return f"[{self.cve}](https://nvd.nist.gov/vuln/detail/{self.cve})" - return f"[{self.ghsa}](https://github.com/advisories/{self.ghsa})" + return "unknown advisory" def __str__(self) -> str: return f"{self.markdown_link()} ({self.severity}): {self.summary}" @@ -142,10 +149,15 @@ def fetch_alerts(repo: str) -> list[VulnerablePackage]: ) alerts = json.loads(raw) - # Group by package name + # Group by normalized package name. GitHub's advisory database reports + # the same package under inconsistent casing across advisories (e.g. + # "starlette" and "Starlette"); grouping by the raw name would split it + # into two packages, each with its own (lower) fix version, and the + # second upgrade would downgrade what the first one fixed. grouped: dict[str, VulnerablePackage] = {} for a in alerts: name = a["name"] + key = normalize_name(name) fixed = a.get("fixed") advisory = Advisory( ghsa=a.get("ghsa"), @@ -153,16 +165,16 @@ def fetch_alerts(repo: str) -> list[VulnerablePackage]: summary=a["summary"], severity=a["severity"], ) - if name not in grouped: - grouped[name] = VulnerablePackage(name=name, fixed=fixed) + if key not in grouped: + grouped[key] = VulnerablePackage(name=normalize_name(name), fixed=fixed) else: # Keep the highest fix version (None means no known fix) if fixed and ( - grouped[name].fixed is None - or parse_version(fixed) > parse_version(grouped[name].fixed) + grouped[key].fixed is None + or parse_version(fixed) > parse_version(grouped[key].fixed) ): - grouped[name].fixed = fixed - grouped[name].advisories.append(advisory) + grouped[key].fixed = fixed + grouped[key].advisories.append(advisory) return list(grouped.values())