fix(scan): Maven/Java SBOMs reported zero CVEs — PURL match mismatch - #523
Merged
Conversation
Trivy names Maven packages groupId:artifactId (a colon). _build_purl only split a namespace on '/', so the colon was percent-encoded to 'group%3Aartifact' instead of the canonical 'group/artifact', and the finding->component lookup compared purl_with_version by EXACT string. A stored component 'pkg:maven/g/a@v?type=jar' therefore never matched the Trivy finding 'pkg:maven/g%3Aa@v', so every Maven/Java component was skipped as an unknown component and the project showed 0 vulnerabilities even when Trivy found many (observed: a Spring Boot SBOM where trivy found 65, TRUSCA persisted 0). - _build_purl: for maven, split groupId:artifactId on the colon onto the PURL namespace path. - lookup: match ignoring PURL qualifiers (?type=jar etc.) via split_part(purl_with_version,'?',1) == purl; .first() (not scalar_one_or_none) since two rows can share a core PURL. - tests: _build_purl fixtures now use the REAL groupId:artifactId colon form. The prior slash fixture masked the bug (hardening rule H-1). Affects both source scans and SBOM uploads for JVM ecosystems (maven/gradle/ jar/pom).
Address security review of the qualifier-insensitive PURL match: - The qualifier-strip lookup was UNSCOPED and used .first() with no ORDER BY. purl_with_version is globally unique, so stripping the qualifier could match a ComponentVersion from a DIFFERENT project/scan and bind the finding to it, and a nondeterministic pick flipped cv.id between rematch runs, resetting the X1 SLA-aging key. Now JOIN ScanComponent and constrain to the current scan (both source and ingest persist ScanComponent before Trivy matching; rematch keeps the original scan's rows), ORDER BY purl_with_version + LIMIT 1 for a deterministic single row, and keep scalar_one_or_none (LIMIT 1 => <=1 row). Scoping also shrinks the candidate set to the scan's components, so stripping the qualifier per row no longer defeats the global unique index. - test_vulnerability_rematch_db: seed a ScanComponent so the scoped lookup resolves (a real scan always writes it before matching). Known follow-up (CHANGELOG): a scan carrying two qualifier variants of one GAV attaches the CVE to only the first; a Maven persist-boundary integration test with a ?type=jar component + real Trivy colon fixture.
persist_trivy_findings now scopes its component lookup to the scan's ScanComponent set. This test called it directly while seeding only Component/ComponentVersion (no ScanComponent), so every lookup would miss and inserted==0, failing the X1 SLA assertions. Seed a ScanComponent per (scan, component_version) — the re-scan SLA cases reuse the same versions across scan1/scan2, so link every scan expected to match. Mirrors what a real scan writes (persist_sbom_components) before Trivy matching.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
A real Spring Boot SBOM uploaded to the Demo Sandbox showed 0 vulnerabilities, while
trivy sbomon the exact same file finds 65. Every Maven/Java component was logged astrivy_finding_skipped_unknown_component,inserted=0 unique_pairs=65.Root cause
Two PURL-format mismatches in the finding→component matcher (
services/vulnerability_matching.py), so an exact-string PURL lookup never hit:_build_purl)pkg:maven/org.springframework/spring-context@6.1.1pkg:maven/org.springframework%3Aspring-context@6.1.1…@6.1.1?type=jar…@6.1.1(none)PkgNameasgroupId:artifactId(a colon)._build_purlonly split a namespace on/, so it percent-encoded the colon to%3Ainstead of the canonicalgroup/artifactpath.purl_with_versionby exact string, so the?type=jarqualifier (cdxgen/BomLens emit it, Trivy omits it) also broke the match.This affects both source scans and SBOM uploads for JVM ecosystems (
maven/gradle/jar/pom) — any Java project reported 0 CVEs.Fix
_build_purl: split MavengroupId:artifactIdon the colon onto the PURL namespace path (pkg:maven/group/artifact@ver). Other ecosystems unchanged.split_part(purl_with_version, '?', 1) == purl;.first()(notscalar_one_or_none) since two rows can legitimately share a core PURL.Tests
_build_purlunit fixtures now use the realgroupId:artifactIdcolon form (Spring, Tomcat, Logback). The prior fixture used a slash — the exact synthetic-fixture blind spot hardening rule H-1 warns about. Verified locally: maven colon →pkg:maven/group/artifact@ver; npm/golang/pypi unchanged.?type=jarcomponent (the existing rematch integration test only covers npm, which has no colon/qualifier issue — which is why this slipped through).Verification
Will be verified end-to-end on the live demo after release: re-upload the Spring Boot SBOM → expect ~65 CVEs surfaced instead of 0.