Skip to content

A transformed --sbom export republishes its source's document identity, so two different documents share one namespace #433

Description

@bomly-guy

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:

  1. 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.
  2. 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.goapplySourceAssertions, inheritDocumentIdentity.
  • internal/cli/scan_cmd.go — where selectedGraph and sbomEntries are passed together.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions