SBOM (Software Bill of Materials)

An SBOM is a machine-readable inventory of the components and versions inside a build output. What the formats record, and how a workflow generates one.

A software bill of materials, usually written SBOM, is a machine-readable inventory of the components and versions that went into a build output, together with the dependency relationships between them. It answers one question about an artifact that has already shipped: what is inside it, at which version, and under which identifier.

The value of that inventory shows up on the day an advisory lands for a widely used library. With an SBOM stored per release, finding affected artifacts is a query over documents you already have. Without one, the same question means rebuilding every release from source and hoping the dependency resolution reproduces.

Definition

An SBOM is a document produced by tooling during or after a build. Each entry names a component, pins a version, and carries identifiers that other systems can match against. The common identifier is a package URL, or purl, such as pkg:npm/[email protected], which encodes the ecosystem, the name, and the version in one string (package URL specification, checked on 2026-08-13). Entries reference each other through dependency relationships, so the document describes a graph rather than a flat list.

A baseline for what the document has to carry comes from the US National Telecommunications and Information Administration, which published a set of minimum elements in 2021 (minimum elements for an SBOM, checked on 2026-08-13).

Minimum elementWhat it records
Supplier nameThe party that produced each component
Component nameThe name each component is published under
Version of the componentThe exact version string, rather than a range
Other unique identifiersKeys such as purl or CPE that support lookup
Dependency relationshipWhich component includes which other component
Author of SBOM dataThe tool or party that assembled the document
TimestampWhen the document was assembled

The two formats in common use

Two specifications carry most of the traffic, and both are standardized.

FormatStewardStandardizationUsual suffixSerializations
SPDXLinux FoundationVersion 2.2.1 published as ISO/IEC 5962:2021.spdx.jsonJSON, YAML, RDF, tag-value
CycloneDXOWASPVersion 1.6 published as ECMA-424.cdx.jsonJSON, XML, protobuf

The SPDX specification and the CycloneDX specification (both checked on 2026-08-13) describe overlapping shapes: a document header, a component list, and relationships. Most generators can write either format from a single scan, so the choice is usually driven by what the consuming system parses. Converting between them is possible and lossy, because each format carries fields the other has no slot for.

When the inventory is taken

An SBOM describes the components a tool could see at the moment it ran, so the point in the pipeline where it is generated changes what ends up in the document. The US Cybersecurity and Infrastructure Security Agency names six types by that generation point (types of SBOM documents, checked on 2026-08-13).

TypeGenerated fromTypically misses
DesignPlanned components, before code existsAnything the implementation actually pulled in
SourceThe repository and its dependency manifestsComponents injected by the build itself
BuildThe build process, as the artifact is assembledFiles added later by a deployment step
AnalyzedInspection of the finished artifactProvenance for components with stripped metadata
DeployedThe system as installed, including configurationCode loaded only while the process runs
RuntimeObservation of the running systemComponents on paths the observation never hit

The build SBOM is the one a workflow produces, and it is the strongest of the six for release records, because the tool runs with both the source tree and the finished artifact in reach.

Two limits are worth stating plainly. An SBOM records inventory and carries no judgement about severity, so risk ranking happens later when a scanner joins the component list against advisory data, and the quality of that join depends on how well the identifiers resolve. An SBOM also says nothing about how the artifact was built or by whom; that record is a separate document, covered in provenance attestation.

Example

This release workflow builds a container image, generates an SBOM in SPDX JSON form from the pushed image, publishes the document as an attestation bound to the image digest, and attaches the same file to the GitHub release as a downloadable asset.

name: release
on:
  push:
    tags: ["v*"]

permissions:
  contents: write
  packages: write
  id-token: write
  attestations: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - id: push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}

      - uses: anchore/sbom-action@v0
        with:
          image: ghcr.io/${{ github.repository }}@${{ steps.push.outputs.digest }}
          format: spdx-json
          output-file: sbom.spdx.json

      - uses: actions/attest-sbom@v2
        with:
          subject-name: ghcr.io/${{ github.repository }}
          subject-digest: ${{ steps.push.outputs.digest }}
          sbom-path: sbom.spdx.json
          push-to-registry: true

      - run: gh release upload "${{ github.ref_name }}" sbom.spdx.json
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Three details in that file do the work. The scan targets the digest in steps.push.outputs.digest rather than the tag, so the inventory describes the exact manifest this run pushed even if the tag moves afterwards. The format: spdx-json input selects the serialization, and the generator writes CycloneDX from the same scan when the value is cyclonedx-json (anchore/sbom-action, checked on 2026-08-13). The attestation step binds the document to that digest and to the workflow identity that produced it (actions/attest-sbom, checked on 2026-08-13), while gh release upload leaves a copy a human can download without registry credentials.

A consumer verifies the attested copy with the GitHub CLI, which resolves the image and checks the signature against the owner you name:

gh attestation verify oci://ghcr.io/acme/api@sha256:<digest> --owner acme

BuildKit can also produce the document during the build instead of scanning the result afterwards, which turns the whole thing into one flag on the build command (Docker SBOM attestations, checked on 2026-08-13):

docker buildx build --sbom=true --provenance=mode=max --push \
  -t ghcr.io/acme/api:v1.4.0 .

That form stores the SBOM in the registry beside the image rather than as a file in the job workspace, so nothing needs uploading. Pointing the same command at a remote builder is a builder configuration question, covered in the WarpBuild Docker builders documentation.

The three destinations behave differently once the run is over.

DestinationRetrieved withSurvives a tag moveAvailable without registry access
Release assetA browser or gh release downloadYes, the file is immutable once uploadedYes
Registry attestationgh attestation verify, cosign, or docker buildx imagetools inspectYes, the subject is a digestNo
Workflow artifactThe run page, until retention expiresYesYes, for users with repository access

FAQ

What is the difference between an SBOM and a vulnerability scan?

An SBOM is the inventory: the components, their versions, and the identifiers that name them. A vulnerability scan is a join between that inventory and an advisory database, performed at scan time. The same SBOM produces different findings a month later, because the advisory data moved while the artifact stayed the same.

Should a build emit SPDX or CycloneDX?

Emit whichever format the consumers of the document accept, and emit both when they disagree. SPDX 2.2.1 was published as ISO/IEC 5962:2021 and CycloneDX 1.6 was published as ECMA-424, so both are standardized and most generators can write either one from a single scan. Converters exist, and they lose fields that have no counterpart in the target format.

Does an SBOM need to be signed?

An unsigned SBOM is a JSON file that anyone with write access to the release can replace. Signing it, or publishing it as an attestation bound to the artifact digest, gives a consumer something to verify: that this document describes this exact artifact and was produced by the identity it claims.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.