Comparing SBOMs for the same build

You were handed more than one SBOM for the same release: the supplier's, your build pipeline's, an integrator's. They rarely agree. Different generators read different metadata, see different parts of the build, and spell the same thing differently. sbomflow sbom-set compare says where they agree and where they differ, and what you would need to know before a person decides which to rely on.

It never merges them, never picks one, and never scores them. The files you give it are never changed.

bash
sbomflow sbom-set compare supplier.cdx.json integrator.spdx.json --subject firmware.bin

It reads CycloneDX JSON (1.x) and SPDX 2.x, in JSON or tag-value. It is offline: nothing is fetched and nothing is sent.

First: are these SBOMs about the same bytes?#

Two SBOMs that name the same product and version are not necessarily about the same build, and a comparison across two different builds would produce numbers that mean nothing. So the command first decides whether a comparison is meaningful at all. The documents are comparable when one of these holds:

BindingWhenVerified by SBOMFlow?
release_id_and_artifact_digestevery document states the same version and the same SHA-256 for the thing it describesyes: the statements agree (and, with --subject, the digest is the released bytes')
verified_envelope_subject_digestthe subject digest arrived through a verified signature envelopenot reachable from this command in this release
declared_by_operatoryou pass --declared-same-subjectno: recorded as your statement

"The thing it describes" is CycloneDX metadata.component, or the one package an SPDX document DESCRIBES. A document that describes several packages, or states no SHA-256 for its subject, or states two different ones, has no subject digest: which of two it means is not SBOMFlow's to choose, and the input's note says so.

When no binding holds, the result is incomparable_subject: the reason is stated, and no count or figure is computed. Different subject digests are never "low agreement", and your declaration cannot override them. A document that states no digest is reported as unknown, not as different; --declared-same-subject can bind it, and the output says the binding was declared.

--subject FILE hashes the released bytes. A document stating a different digest makes the result incomparable. It only adds a check: it never makes a binding stronger.

What is compared#

Each document becomes a set of facts, each kept with its coordinate in that document (a CycloneDX bom-ref, an SPDX SPDXID, or a JSON pointer such as #/components/3 when the document gave none), so every row leads back to a line in the file.

DimensionCompared onStates
Componentsthe exact identifier: a package URL, else a CPE, SWID, SWHID or gitoid, else the component's own SHA-256 when it states exactly one. Never the name.present in all, in some (named), ambiguous_identity (no identifier: matched to nothing)
Versionsthe package without its version, each source's set of stated versionsexact, representation_only (a leading v, case), semantic_conflict, unknown_scheme, unstated
Identifiershow each source spells the package URLexact, alias_candidate, conflict, malformed
Hashesalgorithm and valueequal, conflicting, absent, unsupported_algorithm
Licencesthe stated expression, verbatimexact, syntactic_normalization (whitespace), conflict, unknown
Dependency graphedges between exact identitiesshared, source-only, dangling, not projected (an endpoint has no identity, or its coordinate is used twice); zero edges keeps completeness unknown
Scopethe component's stated scopesame, different, unstated
Generatorthe declared toolssame, changed, absent, conflicting
Coverageper field, numerator and denominator per sourceratios only; no completeness verdict

Two versions of one package are a version disagreement, not two unrelated components, and the per-source versions are listed. A fact present in two sources is corroborated presence: two generators sharing a parser are wrong the same way. Three sources stay three: nothing is decided by majority.

Documents it cannot read#

A file that is not a supported CycloneDX or SPDX 2.x document (an SPDX 3 document, a CycloneDX version newer than this build models, a malformed or empty file) is counted as unsupported with the reason and the import warning codes, and it never takes part in a count. It is never treated as an SBOM with nothing in it. SPDX 3 documents are read elsewhere in SBOMFlow, but this comparison does not project them yet, and says so.

A file that does not exist or cannot be read stops the command with exit 2 before anything is written.

Output#

The summary lists each dimension, caps each listing at 50 rows and says how many more the JSON holds. --json prints the sbomflow-sbom-concordance-v1 document on stdout; --output FILE writes the same document to a file (never over one of the inputs). The document is deterministic: the order you list the files in does not change a byte.

ExitMeaning
0a comparison was produced, whatever it found (including incomparable_subject)
2a refusal: fewer than two SBOMs, a missing or unreadable file, or an --output that names an input; nothing is written

Warnings on stderr: sbom_concordance_incomparable_subject, sbom_concordance_binding_declared_not_verified and sbom_concordance_unsupported_input (see warnings).

What it does not do#

  • It does not merge SBOMs, produce a "golden" SBOM or choose a winner.
  • It does not change any release's inventory, findings or gate.
  • It does not decide which document is correct. A person reviewing the disagreements does; the comparison tells them where to look.