A software bill of materials (SBOM) is a machine-readable inventory of the components in software: names, versions, producers, identifiers and relationships. CISA calls it "a nested inventory, a list of ingredients that make up software applications and systems." It lets you check whether a newly reported vulnerability involves a component you ship.

This guide is for the security lead, DevOps engineer or software vendor who has been asked for an SBOM. Tools, standards and dates are as published on 9 October 2026. For the build stages around the SBOM, see our CI/CD pipeline guide. If you run a CMS website instead, start with our website security checklist.

What must an SBOM contain?

The baseline is CISA's 2026 Minimum Elements for a Software Bill of Materials, version 2.1, dated 29 July 2026. It "updates and replaces" the 2021 NTIA minimum elements, whose data fields were supplier name, component name, component version, other unique identifiers, dependency relationship, author of SBOM data and timestamp. CISA says its elements "do not create new requirements; they refine how organizations should generate and request SBOMs".

What the recipient needsNTIA 2021CISA 2026
Who made the componentSupplier NameComponent Producer, because Supplier Name "has proven ambiguous in practice"
Which versionVersion of the ComponentComponent Version, or "unknown" if the producer gives none
How to look it upOther Unique IdentifiersComponent Identifiers: at least one, such as CPE or purl
How much is listedDepth: all top-level dependencies at a minimum, with enough detail to find the transitive onesCoverage: all components, "including transitive dependencies. There is no minimum depth."
How it can be checkedHash of the Component, recommended onlyComponent Hash Value and Hash Algorithm, plus SBOM Author Signature
Which licence appliesLicense Information, recommended onlyComponent License
How the SBOM was madeAuthor of SBOM Data and Timestamp; Lifecycle Phase, recommended onlySBOM Author, Timestamp (RFC 9557), Tool Name and Version, SBOM Version, Data Format Name and Version, Generation Context

For vulnerability checks, CISA gives coverage an explicit test: the recipient "should be able to conclude that a newly reported vulnerability does not affect them if the SBOM does not list the component associated with the vulnerability." By that test, an SBOM that stops at the top-level dependencies cannot clear a product, because the affected component may sit deeper.

A tree of grey boxes crossed by a dashed line under its second row. Arrows copy the boxes above the line into a document of grey bars; one orange box just below the line has no arrow.
Fig. 1 Stopping at the top-level dependencies hides the component a vulnerability report names.

Generation context is a new element. Component data "may differ based on the phase of the software lifecycle during which the SBOM was generated", so the SBOM now says which phase it came from. CISA's examples: "before build" for an SBOM generated from source code, "after build" for binary analysis tools.

Which laws and customers require an SBOM?

Each row follows the wording of its primary text.

RuleWhat the text saysDates
US Executive Order 14028NIST's software supply chain guidance must include standards, procedures or criteria on "providing a purchaser a Software Bill of Materials (SBOM) for each product directly or by publishing it on a public website"Signed 12 May 2021
US OMB memo M-26-05Rescinds M-22-18 and M-23-16; agencies "may also choose to adopt contractual terms that require a software producer to provide a current software bill of materials (SBOM) upon request"23 January 2026
EU Cyber Resilience ActManufacturers must identify and document the components of a product "including by drawing up a software bill of materials in a commonly used and machine-readable format covering at the very least the top-level dependencies of the products"Applies from 11 December 2027; Article 14 from 11 September 2026
US FDA guidance on medical device cybersecuritySection 524B(b)(3) of the FD&C Act "requires manufacturers of cyber devices to provide an SBOM, including commercial, open-source, and off-the-shelf software components"Guidance issued 3 February 2026; section 524B effective for premarket submissions since 29 March 2023

Four details matter. The CRA names no format beyond "a commonly used and machine-readable format", and Article 13(24) says the Commission "may" specify the format and elements by implementing act. Annex VII has the SBOM, "where applicable", go to a market surveillance authority "further to a reasoned request", and Annex II lets the manufacturer decide whether users get it. The CRA's floor, the top-level dependencies, is lower than CISA's coverage. And FDA's guidance, issued before CISA's update, recommends machine-readable SBOMs "consistent with the minimum elements" that NTIA identified in October 2021.

For a contract, CISA advises that an organization "should explicitly address these elements in any policy, contract, or arrangement to ask for or provide SBOMs". Specify the format and version (CISA says to avoid accepting SBOMs for new software in deprecated versions of any format), how often SBOMs arrive, how they are delivered and how unknown fields are marked.

Which format should you use: SPDX or CycloneDX?

CISA names two data formats "widely used by software ecosystem stakeholders to generate and consume SBOMs": SPDX and CycloneDX. It advises organizations to "accept any widely used, interoperable, and machine-processable SBOM format", so let your customer and your toolchain decide. SPDX describes its specification as an international open standard, ISO/IEC 5962:2021, and lists 3.0 as the current document version. CycloneDX lists 1.7 (released 21 October 2025) as current, and the Ecma International standard ECMA-424 as published on 10 December 2025.

CISA's Component Identifiers element asks for "at least one software identifier" that serves as a look-up key for databases, and points to two: CPE, "a structured naming scheme for information technology systems, software, and packages", and the Package URL (purl), a URL of seven components, scheme:type/namespace/name@version?qualifiers#subpath. Ecma International standardized purl as ECMA-427 in December 2025.

Here is a minimal CycloneDX 1.7 document for a container image with two components. All values are illustrative and sbom-tool is a made-up tool name. The file has been checked against the CycloneDX 1.7 JSON schema.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:uuid:4f4e7a60-b501-413b-a9a6-48f463ac575c",
  "version": 1,
  "metadata": {
    "timestamp": "2026-10-09T15:04:05Z",
    "lifecycles": [{ "phase": "post-build" }],
    "tools": {
      "components": [
        { "type": "application", "name": "sbom-tool", "version": "1.0.0" }
      ]
    },
    "manufacturer": { "name": "Example Corp" },
    "component": {
      "type": "container",
      "bom-ref": "shop-api",
      "name": "shop-api",
      "version": "2.4.0",
      "purl": "pkg:oci/shop-api@sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
    }
  },
  "components": [
    {
      "type": "library",
      "bom-ref": "pkg:npm/acme-parser@2.1.0",
      "name": "acme-parser",
      "version": "2.1.0",
      "purl": "pkg:npm/acme-parser@2.1.0",
      "licenses": [{ "license": { "id": "MIT" } }],
      "hashes": [
        {
          "alg": "SHA-256",
          "content": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
        }
      ]
    },
    {
      "type": "library",
      "bom-ref": "pkg:deb/debian/libacme1@1.4.2-1?arch=amd64&distro=debian-12",
      "name": "libacme1",
      "version": "1.4.2-1",
      "purl": "pkg:deb/debian/libacme1@1.4.2-1?arch=amd64&distro=debian-12"
    }
  ],
  "dependencies": [
    {
      "ref": "shop-api",
      "dependsOn": [
        "pkg:npm/acme-parser@2.1.0",
        "pkg:deb/debian/libacme1@1.4.2-1?arch=amd64&distro=debian-12"
      ]
    },
    { "ref": "pkg:npm/acme-parser@2.1.0", "dependsOn": [] }
  ]
}

In CycloneDX, purl carries the identifier, dependencies carries the relationships and lifecycles carries the generation context. The value post-build is the phase for a BOM made after a build process has completed.

How do you generate an SBOM?

Where you generate the SBOM decides what it can see, which is why CISA added generation context. These commands come from each tool's documentation.

Generate fromCommandWhat the documentation says
Source, before buildcdxgen -o bom.json .cdxgen generates a CycloneDX BOM "from a local repo, git URL, purl, or container image"
The built imagesyft IMAGE -o cyclonedx-json=sbom.cdx.jsonSyft generates SBOMs "from container images and filesystems" and takes one or more output formats
The built imagetrivy image --format cyclonedx --output sbom.cdx.json IMAGETrivy logs that --format cyclonedx "disables security scanning"; add --scanners vuln to include vulnerabilities
The build itselfdocker buildx build --sbom=true --push -t IMAGE .BuildKit attaches an SPDX document to the image as an in-toto attestation and scans only the final stage by default

If customers ask for different formats, Syft can write several in one run: syft IMAGE -o spdx-json=./spdx.json -o cyclonedx-json=./cdx.json.

Scanning an image and scanning a source tree give different answers. Syft's cataloger documentation says an image scan finds installed packages, while a directory scan finds both installed packages and declared dependencies. Declared dependencies are weaker evidence: a requirements.txt "might specify a package without a version, or with a version range", which most of the time cannot give the exact version that would be installed. After the build tooling has run, an image scan "can provide more accurate version information". What it can miss is the build itself: BuildKit's SBOM leaves out "build-time dependencies installed in earlier stages, or that exist in the build context" unless you set the build arguments BUILDKIT_SBOM_SCAN_STAGE and BUILDKIT_SBOM_SCAN_CONTEXT.

Two panels. Left: a tray of file sheets and a lens arrow to a short document of grey bars. Right: a container drawn as four stacked slabs with an orange base slab, and a lens arrow to a much longer document.
Fig. 2 A source folder and the finished image give different component lists.

Copied code needs deliberate handling: CISA's footnote says it "is effectively a dependency that is better tracked as a fork and a dependency relationship". A workable default: generate from the built image in every CI run and record it as "after build", then add a source-level SBOM ("before build") when a customer wants what the project declares as well as what shipped.

How do you sign an SBOM and check the signature?

CISA says a recipient "may be concerned about verifying the source of the SBOM data and confirming its integrity". It added an SBOM Author Signature element for that, and says to use "existing software signing infrastructure, tools, and key management".

For container images, one way to do this is an attestation. An in-toto attestation is "authenticated metadata about one or more software artifacts". Its predicate holds the metadata, here the SBOM, and its statement binds the attestation to a subject, here the image digest, which identifies the exact image being attested. An envelope handles authentication and serialization: cosign signs with the "DSSE signing spec", as the Sigstore documentation puts it. The commands follow the attest and verify-attestation references for cosign v3.1.3:

cosign attest --yes --type cyclonedx --predicate sbom.cdx.json \
  ghcr.io/example-corp/shop-api@sha256:<digest>

cosign verify-attestation --type cyclonedx \
  --certificate-identity <signer identity> \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/example-corp/shop-api@sha256:<digest>

For keyless flows, verify-attestation must be told which identity and which OIDC issuer to expect; the issuer above is the one GitHub Actions OIDC tokens carry. The Sigstore documentation warns that "Signatures can guarantee a file has not been tampered with, but they can't guarantee the file arrives at all", and that systems should "fail closed rather than open". Design the deploy gate so that a missing attestation blocks a release as surely as a wrong one. How a cluster enforces image signatures differs by platform; our OpenShift vs Kubernetes comparison has a row on it.

On GitHub, actions/attest does the same job inside a workflow. Passing sbom-path makes it create an SBOM attestation from an SPDX or CycloneDX JSON file of up to 16 MB, signed with a short-lived Sigstore certificate. It recognizes a CycloneDX file only if bomFormat, serialNumber and specVersion are all present, and gives it the predicate type https://cyclonedx.org/bom. Artifact attestations are available in public repositories on all current GitHub plans; private or internal repositories need GitHub Enterprise Cloud, and GitHub Enterprise Server is not supported.

Isometric conveyor through three machines: a gear machine pushes a box onto the belt, a printer-like machine puts a document of grey bars into an envelope, and a press sets a round orange seal on the flap.
Fig. 3 Signing the SBOM in the pipeline lets a reader check where it came from.

Check it with gh attestation verify. By default the command enforces the https://slsa.dev/provenance/v1 predicate type, so an SBOM check names its own type with --predicate-type. The manual says that ideally the path of the signer workflow is validated too, with --signer-workflow:

gh attestation verify oci://ghcr.io/example-corp/shop-api@sha256:<digest> \
  --owner example-corp \
  --signer-workflow example-corp/shop-api/.github/workflows/release.yml \
  --predicate-type https://cyclonedx.org/bom

Warning

A valid signature shows who signed, not that the SBOM is complete. The GitHub CLI manual says only the signing certificate and the timestamps hold values "that cannot be manipulated by the workflow that originated the attestation", so an attacker with access to your workflow's execution context could falsify the predicate. Its mitigation is a "trusted builder": a reusable workflow whose execution cannot be influenced by input from the caller workflow.

SLSA, Supply-chain Levels for Software Artifacts, is "a security framework, a checklist of standards and controls to prevent tampering", and it answers a different question from an SBOM. Version 1.2, announced on 24 November 2025, introduced the Source Track alongside the Build Track. Build levels run from L1, where provenance exists but "is trivial to bypass or forge", to L3, where builds run on "a hardened build platform that offers strong tamper protection". The SLSA FAQ says SBOMs and provenance "operate at different levels of abstraction": an SBOM describes the components in an artifact, provenance describes the build, and provenance "can increase the trustworthiness of an SBOM by describing how the SBOM was created".

How do you generate and sign an SBOM in GitHub Actions?

This workflow builds an image, generates a CycloneDX SBOM from the pushed image by digest, and attaches it as a signed attestation. It is adapted from each action's documentation and has not been run end to end for this article, so try it on a test repository first. Each action is pinned to the commit SHA of its latest release as resolved on 9 October 2026.

name: release
on:
  push:
    tags: ["v*"]
jobs:
  image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write
      artifact-metadata: write
    env:
      IMAGE: ghcr.io/example-corp/shop-api
    steps:
      - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
      - id: push
        uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
        with:
          push: true
          tags: ${{ env.IMAGE }}:${{ github.ref_name }}
      - uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
        with:
          image: ${{ env.IMAGE }}@${{ steps.push.outputs.digest }}
          format: cyclonedx-json
          output-file: sbom.cdx.json
      - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
        with:
          subject-name: ${{ env.IMAGE }}
          subject-digest: ${{ steps.push.outputs.digest }}
          sbom-path: sbom.cdx.json
          push-to-registry: true

GitHub's secure use reference says pinning to a full-length commit SHA "is currently the only way to use an action as an immutable release". build-push-action uses the Git context by default, so no checkout step is needed, and its digest output feeds the later steps. The sbom-action fetches the image with the Docker daemon if it is available, "which will use any authentication available to the daemon", so the earlier login applies. With push-to-registry, actions/attest needs subject-name to be the fully qualified image name: "Do NOT include a tag as part of the image name", because the digest identifies the image.

Two crates with different square patterns sit on a shelf. A blank tag moves on a dashed arrow from the left crate's peg to the right one, while an envelope stays tied to the left crate by an orange cord.
Fig. 4 A tag can move to another image; the SBOM attestation stays attached to the digest.

Docker's attestation example sets up Buildx before the build, so this workflow does too. The same page says build-push-action adds a provenance attestation by default, with mode=max in public repositories, and that it then contains the values of build arguments. Pass secrets with secret mounts instead of build arguments.

How do you use an SBOM to find vulnerable components?

A stored SBOM turns a new advisory into a lookup. CISA says that after learning new vulnerability information, "an organization can assess whether its SBOMs contain or reference a vulnerable component, significantly improving response time compared to organizations without SBOMs." These tools do the matching from a file:

ToolCommandNotes from the documentation
Grypegrype sbom:./sbom.cdx.jsonScans container images, filesystems and SBOMs; filters results with OpenVEX documents
Trivytrivy sbom sbom.cdx.jsonDetects the format automatically; CycloneDX XML is not supported, and SBOMs from other tools "may result in inaccurate detection" because Trivy relies on custom properties
OSV-Scannerosv-scanner scan source -L sbom.cdx.jsonReads SPDX and CycloneDX SBOMs that use Package URLs; recognizes CycloneDX files named bom.json or *.cdx.json

Dependency-Track runs this matching as a service: its version 5 documentation says it is "built around the Bill of Materials (BOM) concept" and surfaces known vulnerabilities "as they emerge". A finding there is "the association between a component in one of your projects and a vulnerability that affects it".

Matching has to be repeated, because the SBOM stays the same while vulnerability data changes. Re-run it on a schedule against every stored SBOM, and in CI on each pull request. Our secure coding checklist covers keeping dependencies under control.

A container drawn as stacked slabs beside a document of eight grey rows. One row is solid orange and joined by a line to a warning triangle on a database cylinder at the right.
Fig. 5 A stored SBOM matches one advisory to the exact image and component it affects.

A match is not yet a priority. Our guide to predicting which vulnerabilities to patch first covers the signals for ranking them, and our guide to penetration testing vs vulnerability scanning explains what a scanner can and cannot find. Outdated components also count as debt, and our technical debt guide shows how to measure dependency age and end-of-life dates.

What is VEX, and how does it cut false alerts?

A match is not proof of exposure: the vulnerable code may not be present in your product, or may never execute. VEX, the Vulnerability Exploitability eXchange, lets the producer say so in a machine-readable way. Several formats carry it. OpenVEX is "designed to be lightweight, and embeddable". CSAF is an OASIS standard whose VEX profile exists "to state that and why a certain product is, or is not, affected by a vulnerability". CycloneDX VEX conveys "the exploitability of vulnerable components in the context of the product in which they're used".

An OpenVEX statement has one of four statuses: not_affected, affected, fixed or under_investigation. A not_affected statement needs one of five fixed justification labels or a free-text impact statement, which the specification calls "highly discouraged for automated systems". This document says the libacme1 package in the example image does not contain the vulnerable code. CVE-2023-12345 is the identifier the OpenVEX specification uses in its own samples:

{
  "@context": "https://openvex.dev/ns/v0.2.0",
  "@id": "https://vex.example.com/shop-api/2.4.0/1",
  "author": "Example Corp Product Security",
  "timestamp": "2026-10-09T16:00:00Z",
  "version": 1,
  "statements": [
    {
      "vulnerability": { "name": "CVE-2023-12345" },
      "products": [
        {
          "@id": "pkg:oci/shop-api@sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
          "subcomponents": [
            {
              "@id": "pkg:deb/debian/libacme1@1.4.2-1?arch=amd64&distro=debian-12"
            }
          ]
        }
      ],
      "status": "not_affected",
      "justification": "vulnerable_code_not_present"
    }
  ]
}

Feed it to Grype with grype IMAGE@sha256:<digest> --vex vex.json. Grype accepts OpenVEX and CSAF VEX documents and, by default, moves not_affected and fixed results to its ignored list; --show-suppressed brings them back, marked "(suppressed by VEX)". Its documentation matches a VEX product by image digest (the most reliable form), by tag (less reliable, because tags can move) or by package purl, as the subcomponents entry above does. Dependency-Track "both produces and consumes VEX documents in CycloneDX format", and cosign can attest a VEX document with --type openvex.

A scanner drops grey warning triangles onto a sieve plate linked to a document with a check mark. Most slide off into a bin on the left; one orange triangle falls through a narrow slot into a tray holding a wrench.
Fig. 6 A VEX statement removes alerts that do not apply and leaves the ones that do.

What goes wrong with SBOMs in practice?

PitfallWhat happensFix
Made once, never refreshedThe file describes an old build, but CISA says each software version or update "should have an associated SBOM"Generate in CI for every release and store the SBOM with the release
One SBOM per repositoryA repository can build several images, and one file describes none of them exactlyOne SBOM per artifact and version, tied to the image digest
Name and version onlyMatching is ambiguous: CISA replaced Supplier Name with Component Producer because it "has proven ambiguous in practice"Require a purl or CPE and a component producer; state whether a missing value is unknown or withheld
Alert flood without VEXMatches include components whose vulnerable code the product never runsPublish VEX statements with justifications
Stored where nobody can find itA customer or incident responder cannot retrieve the SBOM for the release in questionAttach it to the image, or publish it at a version-specific URL, through an API or in a public repository
Unsigned, unchecked or tag-basedA swapped file goes unnoticed, a missing attestation passes the gate, or a moved tag leaves the SBOM describing the wrong imageVerify with a named identity, fail closed, and reference digests rather than tags

What should you do first?

  1. Ask each customer or regulator for the format, version and elements they need, and write CISA's 2026 elements into your policy.
  2. Generate an SBOM from each image in CI on every release, and record the generation context.
  3. Sign it as an attestation on the image digest, and make one pipeline step verify it with the signer identity you expect.
  4. Store the SBOMs where security can query them, and re-match them against vulnerability data on a schedule.
  5. Publish VEX statements for matches that do not apply.

For help setting this up, Computese's security testing service lists Syft and Grype among its code and supply-chain tools, and its cloud transformation work covers signed images and SBOMs.