An SBOM is a structured list of every component in a piece of
software. For an image that means every package, library and — where
the format is present — every binary the cataloguer could identify.
The promise is that “what versions of openssl are running in
production?” becomes a query rather than an archaeology project. The
promise is real. It is also conditional on something almost nobody does
at the same time as turning the flag on.
Generating
BuildKit, at build time
Configuration changebuild with an SBOM attestation— --sbom=true is shorthand for --attest type=sbom. The result is attached to the image, not written to a file.
BuildKit’s SBOM is generated from what the builder observed while
building, and is attached “to the final image as a JSON-encoded SPDX
document, using the format defined by the in-toto SPDX predicate”.
Syft, for images you did not build
Read-only / Safegenerate SBOMs in both formats— Syft can emit several formats in one pass. Scan by digest so the SBOM names a specific artefact.
Syft catalogues a filesystem after the fact. That is the right tool for
a third-party image, a base image you are evaluating, or an artefact
whose build you do not control.
The formats
Format
Steward
Heritage
Where it fits
SPDX
Linux Foundation
License and compliance
Regulatory reporting, licence obligations, the format BuildKit emits
CycloneDX
OWASP
Security
Vulnerability management, and it carries VEX natively
Those are the two SBOM formats. Both express the same core idea —
components, versions, identifiers, relationships — and good tooling
converts between them with acceptable fidelity.
Choose SPDX when licence and compliance reporting is the driver, or
when you want the format BuildKit produces natively. Choose CycloneDX
when vulnerability management is the driver and you want VEX statements
in the same document. If you have no strong reason, produce both — the
marginal cost is one extra output flag.
Verifying an image actually has one
This is the check that would have caught the “a third of our SBOMs are
empty” problem in the story above, and it takes one command.
Read-only / Safedoes this image carry an SBOM?— Reads the SBOM attestation from the registry. An error here means the attestation is absent, whatever the build log said.
Presence is not enough. An SBOM listing four packages for a
Debian-based image is a cataloguer failure, not a minimal image, and it
will pass every “does an SBOM exist” check ever written.
Read-only / Safeis the SBOM plausible?— Lists every package with its version, then counts them. Compare the count against what you expect for this base image.
IMG=registry.example.com/myorg/myapp:1.0.0
docker buildx imagetools inspect "$IMG" \
--format '{{ range .SBOM.SPDX.packages }}{{ .name }}@{{ .versionInfo }}{{ println }}{{ end }}' \
| tee /tmp/sbom-packages.txt \
| wc -l
412
Illustrative output
Assert a floor in CI. A debian:bookworm-slim base yields a few
hundred packages; if the count drops to single digits between one build
and the next, something in the build changed in a way nobody intended.
That assertion is cheap and it is the difference between an SBOM
programme that works and one that is discovered to be hollow during an
incident.
Acting on it
The workflow that turns SBOMs from paperwork into an answer:
Generate at build time, attached to the image, so the SBOM and the artefact cannot become separated.
Store keyed by image digest. Not by build number, not by tag. The digest is the only identifier that is stable and that a running container can be resolved to.
Index the component list into something queryable. A database table of (image digest, package name, version) is enough; this does not need a product.
Re-scan on a schedule against a current vulnerability database — the SBOM is fixed, the database is not, and the whole value is in re-matching an unchanged component list against new knowledge.
Join against what is deployed. An SBOM index that cannot be joined to running containers by digest tells you what you have built, not what you are running.
Rehearse the query on a normal day, so the first time you run it is not during an incident.
Step four is the one that makes the whole thing worthwhile and the one
people skip. The vulnerability that matters is almost never known when
the image is built — it is disclosed months later, against a component
list that has not changed. Scanning at build time catches what was
known then; re-scanning the stored SBOM catches what is known now,
without pulling or rebuilding anything.
Read-only / Safere-scan a stored SBOM against today's database— No image pull, no registry round trip for layers. This is what makes fleet-wide daily re-scanning practical.
Read-only / Safethe query the whole programme exists for— Which deployed images contain a given package. Against a flat directory of digest-named SBOMs; a real index makes this a SQL query.
PKG=libssl3
SBOM_DIR=/srv/sbom-index
grep -l "\"name\": \"${PKG}\"" "$SBOM_DIR"/*.spdx.json \
| while IFS= read -r f; do
basename "$f" .spdx.json
done
Those digests join directly to docker inspect --format '{{.Image}}'
across the fleet, which is the entire chain from “a CVE was announced”
to “these seventeen containers on these five hosts”. If that chain has
a broken link anywhere, the SBOM programme is not finished.
Knowledge check
Knowledge check · 5 questions
Q1. Where does a BuildKit SBOM attestation physically live?
Q2. What is in-toto, in relation to SPDX and CycloneDX?
Q3. Which are required before an SBOM can answer "which running images contain package X" quickly? Select all that apply.
Q4. Building with `--sbom=true` and `--load` on a daemon using the classic image store produces an image with an SBOM attestation.
Q5. Re-scanning a stored SBOM on a schedule finds vulnerabilities that a build-time scan could not have found.
Passing score: 75%. Answers are checked in this browser.