Skip to content

Commit 92b9a82

Browse files
committed
Prefer CVE ids and merge duplicate vuln findings
Use CVE identifiers when GitHub advisories provide them, while keeping alternate advisory ids as aliases for reconciliation. Also merge duplicate findings across the final vulnerability output so the same issue is not reported separately under GHSA and CVE ids. Preserve ignores across both preferred and alias ids when filtering GitHub advisory matches.
1 parent 8ac2707 commit 92b9a82

1 file changed

Lines changed: 68 additions & 12 deletions

File tree

dep_checker/main.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ def default(self, obj):
9696
vulnerableVersionRange
9797
advisory {
9898
ghsaId
99+
identifiers {
100+
type
101+
value
102+
}
99103
permalink
100104
withdrawnAt
101105
}
@@ -105,6 +109,50 @@ def default(self, obj):
105109
"""
106110
)
107111

112+
def preferred_advisory_id(advisory: dict) -> str:
113+
for identifier in advisory.get("identifiers") or []:
114+
if identifier.get("type") == "CVE" and identifier.get("value"):
115+
return identifier["value"]
116+
return advisory["ghsaId"]
117+
118+
119+
def advisory_aliases(advisory: dict, preferred_id: str) -> list[str]:
120+
aliases: list[str] = []
121+
for identifier in advisory.get("identifiers") or []:
122+
value = identifier.get("value")
123+
if value and value != preferred_id and value not in aliases:
124+
aliases.append(value)
125+
return aliases
126+
127+
128+
def merge_vulnerabilities(vulnerabilities: list[Vulnerability]) -> list[Vulnerability]:
129+
merged: dict[str, Vulnerability] = {}
130+
ordered: list[Vulnerability] = []
131+
132+
def index_vulnerability(vuln: Vulnerability) -> None:
133+
aliases = list(dict.fromkeys(alias for alias in (vuln.advisory_aliases or []) if alias != vuln.id))
134+
vuln.advisory_aliases = aliases
135+
for candidate_id in [vuln.id, *aliases]:
136+
merged[candidate_id] = vuln
137+
138+
for vuln in vulnerabilities:
139+
candidate_ids = [vuln.id, *(vuln.advisory_aliases or [])]
140+
for candidate_id in candidate_ids:
141+
existing = merged.get(candidate_id)
142+
if existing is None:
143+
continue
144+
aliases = list(existing.advisory_aliases or [])
145+
for alias in [vuln.id, *(vuln.advisory_aliases or [])]:
146+
if alias != existing.id and alias not in aliases:
147+
aliases.append(alias)
148+
existing.advisory_aliases = aliases
149+
index_vulnerability(existing)
150+
break
151+
else:
152+
index_vulnerability(vuln)
153+
ordered.append(vuln)
154+
return ordered
155+
108156

109157
def resolve_dependencies(
110158
repo_path: Path, repo_branch: str
@@ -197,23 +245,28 @@ def query_ghad(
197245
github_vulnerabilities_query, variable_values=variables_package
198246
)
199247
dep_version = dep.version_parser(repo_path)
200-
matching_vulns = [
201-
v
202-
for v in result["securityVulnerabilities"]["nodes"]
203-
if v["advisory"]["withdrawnAt"] is None
204-
and dep_version in SpecifierSet(v["vulnerableVersionRange"])
205-
and v["advisory"]["ghsaId"] not in ignore_list
206-
]
248+
matching_vulns = []
249+
for vuln in result["securityVulnerabilities"]["nodes"]:
250+
if vuln["advisory"]["withdrawnAt"] is not None:
251+
continue
252+
if dep_version not in SpecifierSet(vuln["vulnerableVersionRange"]):
253+
continue
254+
preferred_id = preferred_advisory_id(vuln["advisory"])
255+
aliases = advisory_aliases(vuln["advisory"], preferred_id)
256+
if any(candidate_id in ignore_list for candidate_id in [preferred_id, *aliases]):
257+
continue
258+
matching_vulns.append((vuln, preferred_id, aliases))
207259
if matching_vulns:
208260
found_vulnerabilities.extend(
209261
[
210262
Vulnerability(
211-
id=vuln["advisory"]["ghsaId"],
263+
id=preferred_id,
212264
url=vuln["advisory"]["permalink"],
213265
dependency=name,
214266
version=dep_version,
267+
advisory_aliases=aliases,
215268
)
216-
for vuln in matching_vulns
269+
for vuln, preferred_id, aliases in matching_vulns
217270
]
218271
)
219272

@@ -398,7 +451,7 @@ def main() -> int:
398451

399452
from npm_audit import NPMAuditChecker
400453
print("Running npm package vulnerability audit...", file=sys.stderr)
401-
npm_checker = NPMAuditChecker(repo_path, npm_timeout, gh_token=gh_token)
454+
npm_checker = NPMAuditChecker(repo_path, npm_timeout, gh_token=gh_token, nvd_key=nvd_key)
402455
npm_vulnerabilities = npm_checker.check_npm_vulnerabilities(Vulnerability)
403456
if npm_checker.failed_packages:
404457
scan_complete = False
@@ -417,11 +470,14 @@ def main() -> int:
417470
print(f"Warning: npm vulnerability checking failed: {e}", file=sys.stderr)
418471
print(f"Traceback: {traceback.format_exc()}", file=sys.stderr)
419472

473+
merged_vulnerabilities = merge_vulnerabilities(
474+
ghad_vulnerabilities + nvd_vulnerabilities + npm_vulnerabilities
475+
)
420476
all_vulnerabilities = {
421-
"vulnerabilities": ghad_vulnerabilities + nvd_vulnerabilities + npm_vulnerabilities,
477+
"vulnerabilities": merged_vulnerabilities,
422478
"scan_complete": scan_complete,
423479
}
424-
no_vulnerabilities_found = not ghad_vulnerabilities and not nvd_vulnerabilities and not npm_vulnerabilities
480+
no_vulnerabilities_found = not merged_vulnerabilities
425481

426482
if scan_file is not None:
427483
with open(scan_file, "w") as f:

0 commit comments

Comments
 (0)