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.
sbomflow sbom-set compare supplier.cdx.json integrator.spdx.json --subject firmware.binIt 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:
| Binding | When | Verified by SBOMFlow? |
|---|---|---|
release_id_and_artifact_digest | every document states the same version and the same SHA-256 for the thing it describes | yes: the statements agree (and, with --subject, the digest is the released bytes') |
verified_envelope_subject_digest | the subject digest arrived through a verified signature envelope | not reachable from this command in this release |
declared_by_operator | you pass --declared-same-subject | no: 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.
| Dimension | Compared on | States |
|---|---|---|
| Components | the 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) |
| Versions | the package without its version, each source's set of stated versions | exact, representation_only (a leading v, case), semantic_conflict, unknown_scheme, unstated |
| Identifiers | how each source spells the package URL | exact, alias_candidate, conflict, malformed |
| Hashes | algorithm and value | equal, conflicting, absent, unsupported_algorithm |
| Licences | the stated expression, verbatim | exact, syntactic_normalization (whitespace), conflict, unknown |
| Dependency graph | edges between exact identities | shared, source-only, dangling, not projected (an endpoint has no identity, or its coordinate is used twice); zero edges keeps completeness unknown |
| Scope | the component's stated scope | same, different, unstated |
| Generator | the declared tools | same, changed, absent, conflicting |
| Coverage | per field, numerator and denominator per source | ratios 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.
| Exit | Meaning |
|---|---|
| 0 | a comparison was produced, whatever it found (including incomparable_subject) |
| 2 | a 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.