Supply ChainGENERALTOOL-SPECIFIC

Software Bill of Materials

A machine-readable inventory of what is actually inside an artifact, generated at build time — the thing that turns "are we affected" from an investigation into a query.

The question, the obvious approach, and why it breaks

Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.

The production question

A critical vulnerability is announced in a library. Which of our artifacts contain it, at which version, and which of those are running right now?

The problem

The contents of a deployed artifact are the transitive closure of dependencies, base image packages, and anything vendored or statically linked. Nobody knows that list, and it is only ever needed urgently.

What teams do first

Search the repositories for the package name when something is announced. The dependency files are in git, so the answer is there.

How it breaks

Manifests record what you asked for, not what you got. The resolved graph is in the lockfile, and the built artifact can differ from both.

How it breaks in production
  • Manifests record what you asked for, not what you got. The resolved graph is in the lockfile, and the built artifact can differ from both.
  • Transitive dependencies are invisible to a search for a package name. Most incidents of this kind involve a library nobody chose directly.
  • Base image packages do not appear in any repository file. A substantial share of a container's components come from the distribution layer.
  • Statically linked and vendored code carries no version metadata at all — a search finds nothing and the component is present.
  • The search tells you what is in the repository today, not what is inside the artifact that has been running in production since March.
  • The answer arrives in days. The question is asked in a context where hours matter.
CodeBuildTestArtifactReleaseDeployRunObserveOperateIncidentRecoverLearnImprove

What is actually happening

Underneath the tooling, which is the part that survives a change of tool.

  • An SBOM is generated during the build from the resolved state: the lockfile after resolution, the package database inside the image, and whatever the build tooling can identify in binaries.
  • Each component is recorded with a name, a version, and ideally a package URL — an ecosystem-qualified identifier that makes matching mechanical rather than fuzzy string comparison.
  • It is attached to the artifact by digest, so the inventory and the bytes it describes cannot drift apart. An SBOM attached to a tag describes whatever the tag pointed at when it was generated.
  • Where in the pipeline you generate it changes what it can see. A source-side SBOM sees your dependency graph and no operating system packages; an image-side SBOM sees the filesystem and may miss which of those the application actually uses. Most teams need both.
  • The formats — the two widely used ones being SPDX and CycloneDX — differ in structure and emphasis and both express the same core: components, versions, identifiers and relationships. Tooling generally reads both.
  • The SBOM is an input to scanning and to incident response, not a control by itself. It makes the question answerable; something else has to ask it (Scanning, and Why a Finding Is Not a Risk).

What a component entry contains

The structure is unremarkable and that is the point: it is a list, in a format a machine can join against an advisory feed. The identifier is the field that matters, because matching by name alone is ambiguous across ecosystems.

A fragment of a CycloneDX inventory
1{
2 "bomFormat": "CycloneDX",
3 "specVersion": "1.5",
4 "metadata": {
5 "component": {
6 "type": "container",
7 "name": "orders-api",
8 "version": "sha256:9f2c1d4a..."
9 }
10 },
11 "components": [
12 {
13 "type": "library",
14 "name": "express",
15 "version": "4.19.2",
16 "purl": "pkg:npm/express@4.19.2"
17 },
18 {
19 "type": "library",
20 "name": "openssl",
21 "version": "3.0.13",
22 "purl": "pkg:deb/debian/openssl@3.0.13"
23 }
24 ]
25}

Note the two different ecosystems in one artifact: an application dependency and a distribution package. A repository search finds the first and never the second, which is the gap this closes.

Where you generate it decides what it can see

TOOL-SPECIFICThe stage boundaries are drawn for a container build from a multi-stage Dockerfile. A build that produces a single static binary collapses several of these rows into one and makes the resulting inventory much less complete, because there is no package database left to read.

The single most common mistake is generating one SBOM at one point and assuming it covers the artifact. Each stage sees a different set, and the differences are exactly where the incident-time surprises come from.

Generated atSeesMissesAnswers
Source treeDeclared direct dependenciesResolved transitive versions, OS packagesWhat we intended to depend on
After resolution (lockfile)The full resolved application graph with exact versionsOS packages, vendored code, anything added at buildWhat the application code will load
Built imageFilesystem contents: app dependencies plus distribution packagesWhich of those are actually used; statically linked internalsWhat is present in the thing that runs
Running containerWhat is present now, including anything fetched at startNothing much — this is the most faithful and the least availableWhat is actually there, if you can capture it

From disclosure to answer

The value is realised only through the query path. Building the inventory and not building the query is the standard half-finished version of this practice.

The path the question takes
  1. 1
    Disclosure arrives

    A package name and an affected version range become public.

    fails by Nobody is watching the feeds for the ecosystems you use.

    evidence Advisory ingestion is automated and routed to an owner.

  2. 2
    Query the inventory

    Match name and version range across every stored SBOM.

    fails by SBOMs stored per build artifact but not queryable across artifacts.

    evidence A single query returns artifact digests, not a list of repositories to check.

  3. 3
    Join to what is deployed

    Intersect affected digests with the digests currently running (The Release Manifest).

    fails by No authoritative record of what is deployed where, so everything is treated as potentially affected.

    evidence The deployed-digest inventory is generated from the running platform, not maintained by hand.

  4. 4
    Assess exploitability

    Decide whether presence means exposure, per service.

    fails by Skipping this and treating all matches as an emergency, which exhausts the team before the real one (Scanning, and Why a Finding Is Not a Risk).

    evidence The assessment is written down and reusable next time the same component appears.

  5. 5
    Rebuild and ship

    Patch, rebuild, re-sign, redeploy through the normal path.

    fails by An emergency path that skips signing or scanning, creating a second problem.

    evidence Time from disclosure to patched artifact in production, measured (Continuous Delivery).

  6. 6
    Confirm

    Re-query the inventory and show no deployed artifact matches the affected range.

    fails by Declaring completion from a deployment log rather than from the inventory.

    evidence The query returns empty, and that is the closing evidence in the incident record (Reconstructing What Actually Happened).

How to do it properly

Most important first.

  • Generate on every build, automatically, for every artifact. An inventory produced on request is produced during the incident, which is the one time it is too late.
  • Store it addressed by artifact digest, in a place queryable across all artifacts, and keep it for as long as the artifact could still be running (Artifact Retention).
  • Generate both source-side and image-side where both apply, and know which questions each one can answer.
  • Record the build inputs alongside it — commit, builder identity, base image digest — so an affected component leads back to a rebuild path (Build Provenance).
  • Query it on a schedule, not only on disclosure. A periodic query against current advisories finds the things nobody announced loudly.
  • Test the query path with a drill: pick a package you know is present and time how long it takes to list every deployed artifact containing it.

How much can this affect

Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.

Blast radius if this is wrongEveryone
One testEveryone
What contains it

The blast radius here is response time rather than availability. A missing inventory causes no outage; it extends every supply chain incident from hours into days, across everything you run.

What can go wrong

Failure modes, including of the mitigation
  • Generated and never stored anywhere queryable — the most common outcome, and it looks like compliance.
  • Generated at the wrong stage, so it describes the build image rather than the runtime image, or the source tree rather than the artifact (Multi-Stage Builds).
  • Incomplete by construction: statically linked libraries, vendored code and binaries fetched at runtime do not appear, and the SBOM does not say that they are missing.
  • Attached to a mutable tag, so it describes a previous artifact.
  • Retained for less time than artifacts run, so the oldest deployed artifact has no inventory.
  • Treated as a document for auditors rather than as an operational index, so nobody notices it has been empty for two months.
  • An SBOM for the deployed artifact with no corresponding record of which artifacts are actually deployed — half the question unanswered (The Release Manifest).
Misreads this invites
  • "An SBOM is a compliance artifact." It is an operational index. If you never query it, you have the cost and none of the value.
  • "The SBOM tells us we are secure." It tells you what is present. Whether any of it is exploitable is a separate analysis with different inputs.
  • "One SBOM per repository is enough." The unit is the artifact. Two artifacts from the same repository, built a month apart, have different contents.
  • "It is complete." It contains what the generator could identify. Vendored and statically linked code is routinely missing, and the omission is silent.

Operating it

Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.

How you know it worked
  • A timed drill: name a package, get the list of affected deployed artifacts in minutes.
  • Every artifact in the registry has an SBOM attached to its digest; the count of artifacts without one is zero and is monitored.
  • The inventory for a randomly chosen artifact matches an independent inspection of the running container.
How you get back
  • Not a deployable change in itself, so there is nothing to roll back. The relevant risk is the opposite: an SBOM that stopped being generated is invisible until the day it is needed.
  • Monitor generation as a pipeline property with an alert, the same way you would monitor a missing test stage.
What to automate, and what stays human
  • Automate: generation, attachment by digest, storage, retention, and periodic re-querying against new advisories.
  • Keep human: interpreting a match. "This component is present" becomes "we are affected" only after someone checks whether it is loaded, reachable and exposed (Scanning, and Why a Finding Is Not a Risk).
What this costs
  • Storage grows with every build. Retention has to be tied to how long an artifact can plausibly still be running, which is usually longer than people assume.
  • Generation adds time to every build, modestly, and adds a tool whose failures now block or silently skip a stage.
  • Completeness has a ceiling. Chasing statically linked components produces diminishing returns and a false sense that the inventory is exhaustive.

Where this applies

This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.

  • GENERALThe idea — a machine-readable inventory tied to a specific artifact — applies to any packaging format. What varies is completeness: interpreted languages with lockfiles produce near-complete inventories, while compiled and statically linked artifacts produce partial ones regardless of tooling.
  • TOOL-SPECIFICGenerators differ in what they detect and will disagree on the same artifact. That is a property of their detection heuristics, not a defect: one reads the package database, another inspects binaries, and they see overlapping but different sets. Knowing which one you run tells you what your blind spots are.

Where the depth lives

This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.

Domains that do not exist yet
  • Testing & Reliability Engineering — the drill as the only evidence that an inventory is usable, since an unqueried index and a broken one look identical.