Skip to content

Commit 1b2992d

Browse files
authored
fix: group alerts by normalized name and link to GHSA pages (#15)
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
1 parent efa135e commit 1b2992d

1 file changed

Lines changed: 20 additions & 8 deletions

File tree

security-updates/update.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,16 @@ class Advisory:
9595
severity: str
9696

9797
def markdown_link(self) -> str:
98+
# Prefer the GHSA advisory page: it always exists for a Dependabot
99+
# alert, whereas a freshly-assigned CVE is often still RESERVED and
100+
# returns "CVE ID Not Found" on NVD. Show the CVE as the link text
101+
# when we have one, since it's the more familiar identifier.
102+
if self.ghsa:
103+
text = self.cve or self.ghsa
104+
return f"[{text}](https://github.com/advisories/{self.ghsa})"
98105
if self.cve:
99106
return f"[{self.cve}](https://nvd.nist.gov/vuln/detail/{self.cve})"
100-
return f"[{self.ghsa}](https://github.com/advisories/{self.ghsa})"
107+
return "unknown advisory"
101108

102109
def __str__(self) -> str:
103110
return f"{self.markdown_link()} ({self.severity}): {self.summary}"
@@ -142,27 +149,32 @@ def fetch_alerts(repo: str) -> list[VulnerablePackage]:
142149
)
143150
alerts = json.loads(raw)
144151

145-
# Group by package name
152+
# Group by normalized package name. GitHub's advisory database reports
153+
# the same package under inconsistent casing across advisories (e.g.
154+
# "starlette" and "Starlette"); grouping by the raw name would split it
155+
# into two packages, each with its own (lower) fix version, and the
156+
# second upgrade would downgrade what the first one fixed.
146157
grouped: dict[str, VulnerablePackage] = {}
147158
for a in alerts:
148159
name = a["name"]
160+
key = normalize_name(name)
149161
fixed = a.get("fixed")
150162
advisory = Advisory(
151163
ghsa=a.get("ghsa"),
152164
cve=a.get("cve"),
153165
summary=a["summary"],
154166
severity=a["severity"],
155167
)
156-
if name not in grouped:
157-
grouped[name] = VulnerablePackage(name=name, fixed=fixed)
168+
if key not in grouped:
169+
grouped[key] = VulnerablePackage(name=normalize_name(name), fixed=fixed)
158170
else:
159171
# Keep the highest fix version (None means no known fix)
160172
if fixed and (
161-
grouped[name].fixed is None
162-
or parse_version(fixed) > parse_version(grouped[name].fixed)
173+
grouped[key].fixed is None
174+
or parse_version(fixed) > parse_version(grouped[key].fixed)
163175
):
164-
grouped[name].fixed = fixed
165-
grouped[name].advisories.append(advisory)
176+
grouped[key].fixed = fixed
177+
grouped[key].advisories.append(advisory)
166178

167179
return list(grouped.values())
168180

0 commit comments

Comments
 (0)