From 6346e827df805977e96b933cbbfede367745c380 Mon Sep 17 00:00:00 2001 From: Haksung Jang Date: Sun, 23 Aug 2026 14:43:07 +0900 Subject: [PATCH] fix(scanner): fall back to a CVE alias when grype's primary id isn't one grype sometimes assigns a non-CVE primary vulnerability id sourced from a non-NVD advisory (e.g. an Apache mailing-list thread), with the real CVE listed only under relatedVulnerabilities. The deep-cve sidecar builder silently dropped these findings entirely instead of checking for a CVE alias, losing real matches (confirmed against Apache Kafka CVE-2024-27309 in a supplier SBOM). --- docker/lib/scan-nvd-cpe.py | 14 +++++++++++++- tests/test-postprocess.sh | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docker/lib/scan-nvd-cpe.py b/docker/lib/scan-nvd-cpe.py index bd771e91..47006982 100644 --- a/docker/lib/scan-nvd-cpe.py +++ b/docker/lib/scan-nvd-cpe.py @@ -261,7 +261,19 @@ def build_sidecar(sbom_path, out_prefix): vuln = m.get("vulnerability", {}) cve = vuln.get("id", "") if not cve.startswith("CVE-"): - continue + # grype's primary vulnerability id is sometimes a non-CVE alias + # from a non-NVD advisory source (e.g. "BIT-kafka-2024-27309", + # built from an Apache mailing-list thread) with the actual CVE + # listed only in relatedVulnerabilities. Fall back to the first + # CVE-prefixed alias there rather than silently dropping a real + # match. + cve = next( + (r.get("id", "") for r in (m.get("relatedVulnerabilities") or []) + if r.get("id", "").startswith("CVE-")), + "", + ) + if not cve: + continue is_nvd = vuln.get("namespace") == "nvd:cpe" art = m.get("artifact", {}) ver = art.get("version", "") diff --git a/tests/test-postprocess.sh b/tests/test-postprocess.sh index 447034bd..e7250841 100755 --- a/tests/test-postprocess.sh +++ b/tests/test-postprocess.sh @@ -1509,6 +1509,39 @@ else pass "no deep-cve-progress line when SECURITY_NVD_VERIFY is off (the default)" fi +echo "== F-1f: scan-nvd-cpe.py falls back to a CVE alias in relatedVulnerabilities ==" +# grype's primary vulnerability id is sometimes a non-CVE alias from a +# non-NVD advisory source (e.g. "BIT-kafka-2024-27309", built from an Apache +# mailing-list thread), with the actual CVE listed only under +# relatedVulnerabilities. Confirmed against a real corpus finding (Apache +# Kafka CVE-2024-27309): grype's primary match previously got silently +# dropped because .id didn't start with "CVE-". +cat > "$WORK/grype-alias-stub" <<'SH' +#!/usr/bin/env bash +if [ "$1" = "db" ]; then echo '{}'; exit 1; fi +cat <<'JSON' +{"matches":[ + {"vulnerability":{"id":"BIT-kafka-2024-27309","namespace":"github:language:java","severity":"high"}, + "artifact":{"name":"kafka-clients","version":"3.6.1","purl":"pkg:maven/org.apache.kafka/kafka-clients@3.6.1"}, + "relatedVulnerabilities":[{"id":"CVE-2024-27309","namespace":"nvd:cpe"}]}, + {"vulnerability":{"id":"BIT-no-cve-alias","namespace":"github:language:java","severity":"low"}, + "artifact":{"name":"baz","version":"1.0","purl":"pkg:maven/baz/baz@1.0"}, + "relatedVulnerabilities":[{"id":"GHSA-xxxx-yyyy-zzzz","namespace":"github:language:java"}]} +]} +JSON +SH +chmod +x "$WORK/grype-alias-stub" +echo '{}' > "$WORK/nvdcpe-alias-sbom.json" +GRYPE_BIN="$WORK/grype-alias-stub" python3 "$LIB/scan-nvd-cpe.py" "$WORK/nvdcpe-alias-sbom.json" "$WORK/nvdcpe-alias" >/dev/null 2>&1 +alias_cves=$(jq -r '[.Results[0].Vulnerabilities[].VulnerabilityID] | join(",")' "$WORK/nvdcpe-alias_security_grype.json") +[ "$alias_cves" = "CVE-2024-27309" ] \ + && pass "non-CVE primary id resolves via a CVE alias in relatedVulnerabilities" \ + || fail "alias resolution failed, got VulnerabilityIDs='$alias_cves'" +alias_n=$(jq '[.Results[0].Vulnerabilities[]] | length' "$WORK/nvdcpe-alias_security_grype.json") +[ "$alias_n" = "1" ] \ + && pass "a match with no CVE alias anywhere is still dropped (no guessing)" \ + || fail "expected exactly 1 kept finding, got $alias_n" + echo "== F-2: firmware cve-bin-tool CVEs merge into the Trivy security contract (Plan 2) ==" # Sidecar (Trivy-shaped) + a Trivy report must merge into one .Results[].Vulnerabilities[] # file without breaking the contract server.py security_summary reads.