Skip to content

Commit e8ef844

Browse files
committed
fixup
1 parent 9fcf5b6 commit e8ef844

3 files changed

Lines changed: 203 additions & 466 deletions

File tree

dep_checker/main.py

Lines changed: 2 additions & 212 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,14 @@
2121
from gql import gql, Client
2222
from gql.transport.aiohttp import AIOHTTPTransport
2323
from nvdlib import searchCVE # type: ignore
24-
from packaging.specifiers import InvalidSpecifier, SpecifierSet
25-
from packaging.version import InvalidVersion
24+
from packaging.specifiers import SpecifierSet
2625
from typing import Optional, List
2726
from pathlib import Path
2827

2928
import json
3029
import logging
3130
import sys
3231
import traceback
33-
import urllib.parse
34-
import urllib.request
3532

3633

3734
class Vulnerability:
@@ -112,9 +109,6 @@ def default(self, obj):
112109
"""
113110
)
114111

115-
CIRCL_PRODUCT_URL = "https://vulnerability.circl.lu/api/vulnerability/"
116-
117-
118112
def preferred_advisory_id(advisory: dict) -> str:
119113
for identifier in advisory.get("identifiers") or []:
120114
if identifier.get("type") == "CVE" and identifier.get("value"):
@@ -148,194 +142,6 @@ def merge_vulnerabilities(vulnerabilities: list[Vulnerability]) -> list[Vulnerab
148142
return list(merged.values())
149143

150144

151-
def circl_lookup_name(name: str, dep: Dependency) -> str:
152-
if dep.cpe is not None:
153-
return dep.cpe.product
154-
if dep.npm_name is not None:
155-
return dep.npm_name
156-
if dep.keyword is not None:
157-
return dep.keyword
158-
return name.lower().replace(" ", "-")
159-
160-
161-
def extract_circl_candidates(payload) -> list[dict]:
162-
if isinstance(payload, list):
163-
return [item for item in payload if isinstance(item, dict)]
164-
if not isinstance(payload, dict):
165-
return []
166-
for key in ("data", "results", "items", "vulnerabilities"):
167-
value = payload.get(key)
168-
if isinstance(value, list):
169-
return [item for item in value if isinstance(item, dict)]
170-
return []
171-
172-
173-
def extract_circl_candidate_id(item: dict) -> Optional[str]:
174-
for key in ("id", "vuln_id", "vulnerability_id", "cve"):
175-
value = item.get(key)
176-
if isinstance(value, str) and value.startswith(("CVE-", "GHSA-")):
177-
return value
178-
metadata = item.get("cveMetadata")
179-
if isinstance(metadata, dict):
180-
value = metadata.get("cveId")
181-
if isinstance(value, str) and value.startswith("CVE-"):
182-
return value
183-
source = (((item.get("containers") or {}).get("cna") or {}).get("source") or {}).get("advisory")
184-
if isinstance(source, str) and source.startswith("GHSA-"):
185-
return source
186-
return None
187-
188-
189-
def extract_circl_candidate_url(item: dict, vuln_id: str) -> str:
190-
for key in ("url", "href", "permalink"):
191-
value = item.get(key)
192-
if isinstance(value, str) and value:
193-
return value
194-
references = (((item.get("containers") or {}).get("cna") or {}).get("references") or [])
195-
for reference in references:
196-
if isinstance(reference, dict):
197-
value = reference.get("url")
198-
if isinstance(value, str) and value:
199-
return value
200-
if vuln_id.startswith("CVE-"):
201-
return f"https://www.cve.org/CVERecord?id={vuln_id}"
202-
return f"https://github.com/advisories/{vuln_id}"
203-
204-
205-
def extract_circl_candidate_summary(item: dict) -> str:
206-
cna = ((item.get("containers") or {}).get("cna") or {})
207-
for key in ("title",):
208-
value = cna.get(key)
209-
if isinstance(value, str) and value:
210-
return value
211-
descriptions = cna.get("descriptions") or []
212-
for description in descriptions:
213-
if isinstance(description, dict):
214-
value = description.get("value")
215-
if isinstance(value, str) and value:
216-
return value
217-
for key in ("summary", "description", "title"):
218-
value = item.get(key)
219-
if isinstance(value, str) and value:
220-
return value
221-
return ""
222-
223-
224-
def extract_circl_candidate_severity(item: dict) -> Optional[str]:
225-
severity = item.get("severity")
226-
if isinstance(severity, str) and severity:
227-
return severity.upper()
228-
metrics = ((item.get("containers") or {}).get("cna") or {}).get("metrics") or []
229-
for metric in metrics:
230-
if not isinstance(metric, dict):
231-
continue
232-
for key in ("cvssV4_0", "cvssV3_1", "cvssV3_0"):
233-
cvss = metric.get(key)
234-
if isinstance(cvss, dict):
235-
value = cvss.get("baseSeverity")
236-
if isinstance(value, str) and value:
237-
return value.upper()
238-
return None
239-
240-
241-
def extract_circl_candidate_aliases(item: dict, preferred_id: str) -> list[str]:
242-
aliases: list[str] = []
243-
for value in item.get("aliases") or []:
244-
if isinstance(value, str) and value and value != preferred_id and value not in aliases:
245-
aliases.append(value)
246-
source = (((item.get("containers") or {}).get("cna") or {}).get("source") or {}).get("advisory")
247-
if isinstance(source, str) and source and source != preferred_id and source not in aliases:
248-
aliases.append(source)
249-
return aliases
250-
251-
252-
def circl_version_matches(package_version: str, version_entry: dict) -> bool:
253-
status = version_entry.get("status")
254-
if status != "affected":
255-
return False
256-
version = version_entry.get("version")
257-
less_than = version_entry.get("lessThan")
258-
less_than_or_equal = version_entry.get("lessThanOrEqual")
259-
if not isinstance(version, str):
260-
return False
261-
normalized = version.strip()
262-
if normalized.startswith((">", "<", "=")):
263-
try:
264-
return SpecifierSet(normalized.replace(",", ", ")).contains(package_version, prereleases=True)
265-
except InvalidSpecifier:
266-
return False
267-
clauses = []
268-
if normalized not in ("", "*"):
269-
clauses.append(f">={normalized}")
270-
if isinstance(less_than, str) and less_than not in ("", "*"):
271-
clauses.append(f"<{less_than}")
272-
if isinstance(less_than_or_equal, str) and less_than_or_equal not in ("", "*"):
273-
clauses.append(f"<={less_than_or_equal}")
274-
if clauses:
275-
try:
276-
return SpecifierSet(",".join(clauses)).contains(package_version, prereleases=True)
277-
except InvalidSpecifier:
278-
return False
279-
try:
280-
return package_version == normalized
281-
except InvalidVersion:
282-
return False
283-
284-
285-
def circl_candidate_affects_version(item: dict, lookup_name: str, package_version: str) -> bool:
286-
affected = (((item.get("containers") or {}).get("cna") or {}).get("affected") or [])
287-
lookup_name = lookup_name.lower()
288-
for entry in affected:
289-
if not isinstance(entry, dict):
290-
continue
291-
product = str(entry.get("product") or "").lower()
292-
package_entry = str(entry.get("packageName") or "").lower()
293-
if product != lookup_name and package_entry != lookup_name:
294-
continue
295-
for version in entry.get("versions") or []:
296-
if isinstance(version, dict) and circl_version_matches(package_version, version):
297-
return True
298-
return False
299-
300-
301-
def query_circl(
302-
dependencies: dict[str, Dependency], repo_path: Path
303-
) -> tuple[list[Vulnerability], list[str]]:
304-
found_vulnerabilities: list[Vulnerability] = []
305-
failures: list[str] = []
306-
for name, dep in dependencies.items():
307-
version = dep.version_parser(repo_path)
308-
lookup_name = circl_lookup_name(name, dep)
309-
query = urllib.parse.urlencode({"product": lookup_name, "per_page": 100})
310-
request = urllib.request.Request(
311-
f"{CIRCL_PRODUCT_URL}?{query}",
312-
headers={"Accept": "application/json", "User-Agent": "nsolid-dependency-vuln-assessments"},
313-
)
314-
try:
315-
with urllib.request.urlopen(request, timeout=30) as response:
316-
payload = json.load(response)
317-
except Exception as exc:
318-
failures.append(f"{name}: {exc}")
319-
continue
320-
for item in extract_circl_candidates(payload):
321-
vuln_id = extract_circl_candidate_id(item)
322-
if not vuln_id or vuln_id in ignore_list:
323-
continue
324-
if not circl_candidate_affects_version(item, lookup_name, version):
325-
continue
326-
found_vulnerabilities.append(
327-
Vulnerability(
328-
id=vuln_id,
329-
url=extract_circl_candidate_url(item, vuln_id),
330-
dependency=name,
331-
version=version,
332-
severity=extract_circl_candidate_severity(item),
333-
advisory_aliases=extract_circl_candidate_aliases(item, vuln_id),
334-
)
335-
)
336-
return found_vulnerabilities, failures
337-
338-
339145
def resolve_dependencies(
340146
repo_path: Path, repo_branch: str
341147
) -> tuple[dict[str, Dependency], list[str]]:
@@ -618,22 +424,6 @@ def main() -> int:
618424
print(f"Warning: NVD query failed: {e}", file=sys.stderr)
619425
print(traceback.format_exc(), file=sys.stderr)
620426

621-
circl_vulnerabilities: list[Vulnerability] = []
622-
try:
623-
circl_vulnerabilities, circl_failures = query_circl(dependencies, repo_path)
624-
if circl_failures:
625-
scan_complete = False
626-
print(
627-
f"Warning: CIRCL query was incomplete; {len(circl_failures)} dependenc(ies) failed:",
628-
file=sys.stderr,
629-
)
630-
for failure in circl_failures:
631-
print(f" - {failure}", file=sys.stderr)
632-
except Exception as e:
633-
scan_complete = False
634-
print(f"Warning: CIRCL query failed: {e}", file=sys.stderr)
635-
print(traceback.format_exc(), file=sys.stderr)
636-
637427
# NPM package vulnerability checking
638428
npm_vulnerabilities: list[Vulnerability] = []
639429
if include_npm:
@@ -667,7 +457,7 @@ def main() -> int:
667457
print(f"Traceback: {traceback.format_exc()}", file=sys.stderr)
668458

669459
merged_vulnerabilities = merge_vulnerabilities(
670-
ghad_vulnerabilities + nvd_vulnerabilities + circl_vulnerabilities + npm_vulnerabilities
460+
ghad_vulnerabilities + nvd_vulnerabilities + npm_vulnerabilities
671461
)
672462
all_vulnerabilities = {
673463
"vulnerabilities": merged_vulnerabilities,

0 commit comments

Comments
 (0)