Filed from review on #430 after that PR's AI-review round cap was reached. Reported by Codex on internal/sbom/document_assertions.go:111.
The finding
Single-source identity inheritance is unconditional. applySourceAssertions adopts the source's SPDX namespace (and CycloneDX serial + revision) whenever exactly one document was ingested — regardless of whether the exported graph still represents that document.
scan --sbom --scope runtime and scan --sbom --enrich both hand the exporter a transformed graph alongside the original entry: internal/cli/scan_cmd.go passes selectedGraph with sbomEntries.
Reproduced:
full components=1 namespace="https://acme.example/spdx/acme-platform-7f3c"
filtered components=0 namespace="https://acme.example/spdx/acme-platform-7f3c"
same identity for different content: true
sources linked in the filtered export: 0
Two documents with different content, one identity, and no link back to the source from either.
Why it matters
SPDX requires documentNamespace to be unique per document; a CycloneDX serial plus revision names one specific issue of one BOM. A consumer keying on identity — a cache, a provenance store, a diff — is entitled to treat these as the same document and will pick whichever it saw first. That is a correctness problem in exactly the field that exists to disambiguate documents.
Failure is silent: both documents are individually well-formed.
Why it is not a one-line fix
Identity adoption is load-bearing, not incidental. It is what makes the export → ingest → export fixed point hold without pinning a serial: TestSingleSourceExportIsAFixedPoint deliberately pins nothing, and a conversion that minted a fresh identity each run would never be byte-stable. ADR-0037 also implies adoption for the single-source case ("a merged export states its own aggregate identity rather than inheriting one source's").
So the fix has to distinguish a faithful restatement from a transformation, and that distinction does not currently exist anywhere in the model:
internal/sbom cannot see it. It receives a graph and some entries; nothing says whether a scope filter ran or enrichment added data.
- The caller knows.
scan_cmd.go knows whether --scope, --enrich, or --analyze were in play.
Candidate shapes, both needing validation:
- The caller declares it. A
BuildOptions field (something like SourceFaithful bool, default false) that only the conversion path sets. Safe by default — an unset caller mints its own identity — but every export path has to be audited for which side it is on.
- Derive it. Compare the exported component set against the source's. Cheap-looking and probably wrong: enrichment adds fields without changing the component set, so equality of identities is not equality of content.
Whichever is taken, TestSingleSourceExportIsAFixedPoint must keep passing unpinned, and a new test should cover the transformed case: a filtered or enriched export mints its own identity and links the source, so provenance is not lost in exchange.
Related
Context
internal/sbom/document_assertions.go — applySourceAssertions, inheritDocumentIdentity.
internal/cli/scan_cmd.go — where selectedGraph and sbomEntries are passed together.
Filed from review on #430 after that PR's AI-review round cap was reached. Reported by Codex on
internal/sbom/document_assertions.go:111.The finding
Single-source identity inheritance is unconditional.
applySourceAssertionsadopts the source's SPDX namespace (and CycloneDX serial + revision) whenever exactly one document was ingested — regardless of whether the exported graph still represents that document.scan --sbom --scope runtimeandscan --sbom --enrichboth hand the exporter a transformed graph alongside the original entry:internal/cli/scan_cmd.gopassesselectedGraphwithsbomEntries.Reproduced:
Two documents with different content, one identity, and no link back to the source from either.
Why it matters
SPDX requires
documentNamespaceto be unique per document; a CycloneDX serial plus revision names one specific issue of one BOM. A consumer keying on identity — a cache, a provenance store, a diff — is entitled to treat these as the same document and will pick whichever it saw first. That is a correctness problem in exactly the field that exists to disambiguate documents.Failure is silent: both documents are individually well-formed.
Why it is not a one-line fix
Identity adoption is load-bearing, not incidental. It is what makes the export → ingest → export fixed point hold without pinning a serial:
TestSingleSourceExportIsAFixedPointdeliberately pins nothing, and a conversion that minted a fresh identity each run would never be byte-stable. ADR-0037 also implies adoption for the single-source case ("a merged export states its own aggregate identity rather than inheriting one source's").So the fix has to distinguish a faithful restatement from a transformation, and that distinction does not currently exist anywhere in the model:
internal/sbomcannot see it. It receives a graph and some entries; nothing says whether a scope filter ran or enrichment added data.scan_cmd.goknows whether--scope,--enrich, or--analyzewere in play.Candidate shapes, both needing validation:
BuildOptionsfield (something likeSourceFaithful bool, default false) that only the conversion path sets. Safe by default — an unset caller mints its own identity — but every export path has to be audited for which side it is on.Whichever is taken,
TestSingleSourceExportIsAFixedPointmust keep passing unpinned, and a new test should cover the transformed case: a filtered or enriched export mints its own identity and links the source, so provenance is not lost in exchange.Related
Context
internal/sbom/document_assertions.go—applySourceAssertions,inheritDocumentIdentity.internal/cli/scan_cmd.go— whereselectedGraphandsbomEntriesare passed together.