Skip to content

Extract CPE/PURL from all CycloneDX objects that carry them - #318

Merged
alex-vulncheck merged 2 commits into
mainfrom
2632-purl-file-component-gap
Jul 24, 2026
Merged

Extract CPE/PURL from all CycloneDX objects that carry them#318
alex-vulncheck merged 2 commits into
mainfrom
2632-purl-file-component-gap

Conversation

@alex-vulncheck

@alex-vulncheck alex-vulncheck commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Rewrite bill.GetPURLDetail to mirror bill.GetCPEDetail: after iterating the packages Syft surfaces, walk the raw-JSON inputRefs and append any PURL that Syft did not already account for. Dedupe by PURL string; apply the same pkg:github filter. Previously inputRefs was used only to backfill SbomRef metadata onto packages Syft had found, so a CycloneDX component carrying only a purl (e.g. a type: "file" entry) was silently dropped.
  • Extend the raw-JSON walk in bill.LoadSBOM to also read metadata.component. That object describes what the SBOM is about — often the top-level scanned artifact (an OS image, a firmware blob, an application) — and is the natural place to attach a purl or cpe identifying the product as a whole. Syft does not surface it as a package, and the raw walk only looked at the top-level components[] array, so any identifier attached there was ignored.
  • Tests: extend TestGetPURLDetail with the same shape TestGetCPEDetail already uses (dedupe, empty, filter). Add TestLoadSBOM_MetadataComponent and a minimal fixture. Each new assertion was verified to fail against the pre-change code.

Why

Offline mode is meant to emulate the API for air-gapped users, but a CycloneDX SBOM has several standard places where a purl or cpe can legitimately appear beyond the top-level components[]. When we miss any of them we silently under-scan — the customer sees fewer vulnerabilities than online mode would find. Two of those gaps came up in prior customer reports (both fixed for CPE, one still open for PURL, and neither covered metadata.component). This PR closes both concrete gaps and adds regression coverage so the same class of bug can't come back.

Test plan

  • go test ./pkg/bill/... passes
  • go build ./... clean
  • Scanning a CycloneDX SBOM whose metadata.component declares a cpe or purl now reports vulnerabilities against it instead of ignoring it
  • Scanning a CycloneDX SBOM whose only PURL lives on a type: "file" (or otherwise non-package) component now surfaces that PURL's vulnerabilities

GetCPEDetail already reaches past Syft's decoded packages into the raw
SBOM component list to pick up CPEs on components Syft doesn't surface
(components of type "file" being the common case). GetPURLDetail didn't
do the equivalent for PURLs: it iterated only sbm.Artifacts.Packages and
used inputRefs solely to backfill SbomRef metadata onto packages Syft
had already found. A component carrying only a purl — no matching Syft
package — was silently dropped, so its vulnerabilities never got looked
up.

Rewrites GetPURLDetail to mirror GetCPEDetail: dedupe by PURL string,
apply the same pkg:github filter, then append any PURL from inputRefs
that wasn't already accounted for.

Extends TestGetPURLDetail to assert PURLs from inputRefs come through,
duplicates collapse, empty PURLs and pkg:github refs get filtered.
Verified the extended assertion fails against the previous code.
The CycloneDX metadata.component object describes what the SBOM is
about — often the top-level scanned artifact (an OS image, a firmware
blob, an application). It's the natural place to attach a purl or cpe
identifying the product as a whole, but Syft does not surface it as a
package, and the raw-JSON pass only walked the top-level components[]
array. So a purl/cpe declared there was silently ignored.

Extracts a small appendRef helper from the existing components walk and
uses it for metadata.component as well.

Adds a minimal fixture (testdata/cyclonedx-metadata-component.json) and
TestLoadSBOM_MetadataComponent covering the case. Verified the assertion
fails against the previous code.
@alex-vulncheck alex-vulncheck self-assigned this Jul 23, 2026
@alex-vulncheck
alex-vulncheck merged commit e172509 into main Jul 24, 2026
12 checks passed
@alex-vulncheck
alex-vulncheck deleted the 2632-purl-file-component-gap branch July 24, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants