Read an evidence bundle
An audit writes a directory of provenance-bearing artifacts. With inputs and run context pinned, the release artifacts are reproducible. This page explains the ones you will read most.
Key artifacts#
| Artifact | What it is |
|---|---|
evidence-pack.json | The canonical record: components, dependency edges, evidence items, gaps, requirement assessments, vulnerabilities, provenance. |
cyclonedx-sbom.json / spdx-sbom.json | Standard SBOMs derived from the observed composition. |
cra-coverage.json | Per-requirement CRA-oriented coverage and gaps for the declared product class. |
issues.json / issues.md | Evidence-gap issues suitable for a tracker. |
release-gate.json | The pass/fail gate result and the reason. |
evidence-bundle.html / .json / .zip | A portable reviewer/auditor handoff. |
audit-log.jsonl | An append-only, hash-chained record designed to make later modification detectable. |
Note
Release artifacts are reproducible when inputs and run context are pinned;
opt-in operational phase durations are excluded from determinism checks. The
bundle composes existing artifacts — it adds no authority or conclusion of
its own.
Reading the coverage picture#
cra-coverage.json reports, for each in-scope requirement, whether evidence was observed and where gaps remain. A gap means evidence was not observed — it does not mean the requirement is unmet or that the product is non-conformant. A human reviewer decides.
Observed vs supplier-imported composition#
When a run includes supplier SBOM documents (--import-sbom), every component carries a claim_origin label so you always know which composition facts SBOMFlow verified against the build and which it recorded from a supplier document:
- observed — extracted from the scanned build tree by SBOMFlow.
- imported-sbom — recorded from a supplier-provided SBOM document; a supplier claim SBOMFlow did not independently observe in the build.
- corroborated — both: claimed by a supplier document and observed in the build.
The label appears on component rows in evidence-pack.json, as a column in the assessment report and reviewer console, on findings whose component is a supplier claim, in the bundle's component counts, and in cra-coverage.json (including which manufacturer-referenced components rest solely on an imported claim). It is factual origin, not judgment: imported components remain first-class for advisory matching and the release gate — nothing is downweighted, excluded, or re-decided by the label.
Why a finding is not blocking#
A finding that meets a gate threshold can still be non-blocking because a human decided so — a VEX review (fixed / valid not_affected) or a time-boxed waiver. So that "why is this not blocking?" is answerable everywhere you look, the gate records a suppression provenance entry per finding in release-gate.json (finding_suppressions), and the same record is rendered on the reviewer console, the issues list, and the evidence bundle:
- VEX — names the reviewer, the effective status, and its CISA justification. The finding's identity join is exact (
finding_key). - waiver — names the approver, the reason, and the expiry date, so an exception that is about to lapse is visible.
The two suppressor types are never collapsed into one label. This is provenance of a decision already recorded by a human — it asserts nothing new about the finding, and a known-exploited (KEV) finding that a reviewer marked not_affected still appears in the issues list, now carrying its suppressor so it is never mistaken for active-and-unaddressed.
Explaining a result#
To understand why a specific gap, finding, or gate decision appears:
sbomflow explain ./evidence --gap <gap-id>
sbomflow explain ./evidence --finding <finding-id>
sbomflow explain ./evidence --gateReviewing#
Machine observation is not human review. To record review decisions and approvals, use the review workflow:
sbomflow review ./evidenceSee Observed vs reviewed for why this boundary matters.
Printing a report (A4 or Letter)#
Every generated HTML surface is print-ready as-is. Use your browser's Print to PDF; no extra tooling is needed, and nothing is fetched from the network.
Both paper sizes produce the same document. The stylesheet sets page margins but deliberately does not pin a page size — pinning A4 would letterbox Letter, and pinning Letter would clip A4 — so the sheet you load decides the page box.
When printing, the report drops the navigation and the skip link, expands every collapsed <details> section so nothing is hidden on paper, and prints links as plain text (a URL you cannot click is noise in the margin).
Use this drill on any surface you intend to hand to an auditor:
- Long tables repeat their header row at the top of every page.
- No table row is split across a page boundary.
- Section headings are never stranded at the foot of a page, away from the content they introduce.
- No paragraph leaves a single line alone at the top or bottom of a page.
- Wide tables print all of their columns — nothing is cut off at the right edge.
- The result is identical on A4 and on Letter, apart from the page count.
Those last two are the ones that used to bite: the printed content box is narrower than the responsive breakpoint on both paper sizes, so a rule intended for phones would otherwise turn tables into horizontally scrolling blocks — which on paper means clipped columns and headers that never repeat.
Verifying a handed-over bundle#
sbomflow verify-bundle <dir> runs the fast manifest-only check: it confirms the evidence-bundle.json bytes match the recorded digest and, with --key, that the signature verifies over those bytes. That proves the manifest is authentic — but a valid signature over the manifest says nothing about whether the listed member artifacts were altered.
For full trust, run the layered verification (a directory or a .zip):
sbomflow verify-bundle ./evidence --layeredPoint it at a .zip instead of a directory to verify a zipped handover. To also check the signature and signer authority, add --key reviewer.pub and one or more --authorized-key-sha256 <sha256-of-the-key-PEM> (the operator allow-list).
It reports four independent dimensions, never collapsed into one verdict:
- content — every listed member is present inside the bundle and matches its recorded sha256 and size (a tampered member fails here even if the signature is intact);
- envelope — the signature verifies over the exact manifest bytes;
- semantics — a DSSE/in-toto attestation has the expected payload type, statement type, predicate and subject binding (a wrong, attacker-chosen payload type is rejected before it is ever interpreted);
- authority — the verified signer is on your
--authorized-key-sha256allow-list; a cryptographically valid signature from a signer you did not authorise is reportedvalid_but_unauthorizedand is not trusted.
Exit 0 only when no dimension failed; exit 3 on any failure — a tampered member, a wrong payload type, or an unauthorised signer. From a .zip, hostile archives (path traversal, symlinks, duplicate members, decompression bombs) are refused before any byte is trusted; nothing is ever extracted outside a temporary working directory. Verification is read-only and offline, and writes nothing.
Verifying a bundle written by an older SBOMFlow#
An evidence bundle is meant to be verifiable years after the release, on a machine whose SBOMFlow has moved on. When the bundle names a different tool or bundle-schema version than the one reading it, --layered adds one more line:
versions written by SBOMFlow 0.1.0, verified by 0.2.0 - reported as context; it changes no layer resultThat line is context, not a verdict. Version skew never changes a dimension, never changes the exit code, and an older bundle is the expected case rather than a defect. Where a check genuinely cannot be made, the dimension says so: an old bundle whose signature was not verified reports envelope not_checked, never a bare "verified".
sbomflow gate verify is stricter about age, deliberately. It re-runs a release's recorded gate, which needs the current evidence-pack shape, so against a release whose pack predates that shape it refuses with what it found and what to do — evidence-pack schema v2 predates v4; run 'sbomflow upgrade migrate-copy' on this release first — instead of re-deciding a gate on a shape it would have to guess at.
Both behaviours are pinned by a cross-version corpus of old output directories (tests/fixtures/xver_verify/) that today's verifier runs against on every build, including deliberately tampered copies of each.
Encrypting a bundle at rest#
SBOMFlow does not implement encryption or key handling — evidence bundles are plain files you control. When you email or archive a bundle and want it encrypted at rest, wrap it with a standard external tool. Encryption is byte-preserving: after the recipient decrypts, the bundle is identical and still passes sbomflow validate / sbomflow verify-bundle.
# Sender — zip, then encrypt with age (or gpg):
sbomflow bundle ./evidence --zip evidence-bundle.zip
age -r age1exampleRecipientKey -o evidence-bundle.zip.age evidence-bundle.zip
# gpg --encrypt --recipient you@example.com evidence-bundle.zip
# Recipient — decrypt, extract, then verify integrity as usual:
age -d -i key.txt -o evidence-bundle.zip evidence-bundle.zip.age
unzip evidence-bundle.zip -d ./evidence
sbomflow validate ./evidence # exit 4 if anything was alteredThe decrypt-then-verify round-trip is regression-tested: a byte-preserving encrypt/decrypt leaves the bundle byte-identical and validation still passes. Choose recipients, keys, and key rotation with your own tooling — that is deliberately outside SBOMFlow.