SBOM
How to generate an SBOM in GitHub Actions
There are four ways to get an SBOM out of a GitHub Actions pipeline, and they describe different things: the repository, the files on disk, a signed copy of either, or what the build actually fetched. Pick by what you need the SBOM to prove. Each approach below comes with the YAML or commands to run it.
Updated
Four approaches
| Capability | Approach | What the SBOM describes | Effort |
|---|---|---|---|
| Dependency graph export | GitHub's graph | The repository's manifests and lockfiles, on the default branch | None |
| Generator step | Syft, Trivy, cdxgen | The files or image in front of it when the step runs | One step per job |
| Attestation | actions/attest | Whatever SBOM you pass it, signed and bound to an artifact | One step after generation |
| Build-time recording | CRACI | What the job fetched while it ran, with a completeness state | Change runs-on |
1. Export the dependency graph
GitHub builds a dependency graph from the manifests and lockfiles in a repository and can export it as an SPDX SBOM. It needs no workflow change: open the repository's Insights tab, choose Dependency graph and click Export SBOM. It describes the repository on the default branch, not a particular build, so tools and packages that a workflow installs are not in it.
To script it, use the REST API. GET /repos/{owner}/{repo}/dependency-graph/sbom
returns the SPDX JSON in one call, but GitHub is closing it down after November 13, 2026. The replacement is
asynchronous: request a report, then fetch the sbom_url it returns. That URL answers 202 while the SBOM
is being generated, then redirects to the SPDX JSON file, which GitHub keeps for up to a week.
# Export the dependency graph as SPDX JSON (closing down after November 13, 2026)
curl -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/repos/OWNER/REPO/dependency-graph/sbom
# The replacement: request a report; the response carries an sbom_url
curl -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "X-GitHub-Api-Version: 2026-03-10" \
https://api.github.com/repos/OWNER/REPO/dependency-graph/sbom/generate-report 2. Add a generator step
A step in the workflow runs a generator against the workspace or an image. anchore/sbom-action runs Syft: by default
it scans the workspace directory and outputs SPDX JSON, it can scan a container image instead, and it uploads the
SBOM as a workflow artifact and attaches it to releases. Its README asks for contents: write, plus
actions: read when it attaches the SBOM to a release:
name: sbom
on:
push:
branches: [main]
release:
types: [published]
jobs:
sbom:
runs-on: ubuntu-latest
permissions:
contents: write # upload the SBOM
actions: read # attach it to a release
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
# Scan the workspace after the install step
- uses: anchore/sbom-action@v0
with:
path: .
format: cyclonedx-json format takes spdx, spdx-json, cyclonedx or
cyclonedx-json. To scan an image, replace path with image, for example
image: ghcr.io/OWNER/app:1.0. See Docker SBOM for BuildKit's own
image SBOMs.
Trivy and cdxgen can be run the same way, as plain run steps. cdxgen writes CycloneDX; since version 13
its npm package is @cdxgen/cdxgen:
steps:
- uses: actions/checkout@v7
- run: npm ci
- run: npm install -g @cdxgen/cdxgen --ignore-scripts
- run: cdxgen -o bom.json .
- uses: actions/upload-artifact@v7
with:
name: sbom
path: bom.json Timing matters. Run the step after dependencies are installed, or it will not see them. Even then, a generator sees the files in front of it: a package a script downloaded, used and deleted, or a tool installed outside a package manager, can be missing.
3. Sign it as an attestation
GitHub's artifact attestations bind an SBOM to the artifact it describes and sign it with Sigstore.
actions/attest@v4 accepts SPDX or CycloneDX JSON through sbom-path, up to 16 MB. Without
sbom-path it produces SLSA build provenance instead. actions/attest-sbom is being deprecated in favor of
actions/attest, which it now wraps. The job needs id-token: write to get a signing certificate and
attestations: write to store the result:
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write # sign with a Sigstore certificate
contents: read
attestations: write # store the attestation
steps:
- uses: actions/checkout@v7
- run: make build # writes dist/my-app
- uses: anchore/sbom-action@v0
with:
path: .
format: spdx-json
output-file: sbom.spdx.json
upload-artifact: false
- uses: actions/attest@v4
with:
subject-path: dist/my-app
sbom-path: sbom.spdx.json
For a container image, pass subject-name and subject-digest instead of
subject-path, set push-to-registry: true and add packages: write. Artifact
attestations work in public repositories on every current GitHub plan; private and internal repositories need
GitHub Enterprise Cloud.
The GitHub CLI then checks who produced the SBOM and for which artifact. SBOM attestations need
--predicate-type; add --format json to see the SBOM itself. For an image, use
oci:// followed by the image name in place of the file path.
gh attestation verify dist/my-app \
-R OWNER/REPO \
--predicate-type https://spdx.dev/Document/v2.3 An attestation proves who produced the SBOM and for which artifact. It does not make the SBOM more complete.
4. Record it during the build
CRACI records the SBOM while the job runs. You change runs-on to craci:
jobs:
build:
runs-on: craci # was: ubuntu-latest A package-aware proxy on the runner then records every package the job fetched, including packages restored from CI caches and hidden dependencies that no lockfile lists, such as code downloaded by install scripts. There is no generator step to place, so the timing question goes away. Each SBOM, in CycloneDX or SPDX, states its completeness per job and per cache, and the API returns SBOMs, network traces and provenance. Setup, including the CRACI GitHub App and network settings, is in the CRACI documentation, and what each completeness state means is in job and cache SBOM completeness.
Which one to use
- A quick inventory of a repository: the dependency graph export.
- An SBOM of a container image: a generator step against the image.
- Proof of who produced an SBOM for which artifact: an attestation, on top of either.
- A record of what the build actually used, for vulnerability response or the Cyber Resilience Act: build-time recording. See SBOM tools compared.
SBOMs in GitHub Actions: frequently asked questions
How do I generate an SBOM in GitHub Actions?
Add a generator step after your install step. The shortest is anchore/sbom-action@v0, which runs Syft on the workspace and uploads an SPDX JSON SBOM as a workflow artifact. For a signed SBOM, pass the file to actions/attest@v4 with sbom-path.
Does GitHub generate an SBOM automatically?
GitHub can export a repository's dependency graph as an SPDX SBOM, from the Insights tab or the REST API. It describes the manifests and lockfiles in the repository, not a particular build, so it will not show tools or packages a workflow downloads.
Which action generates an SBOM in GitHub Actions?
anchore/sbom-action runs Syft. By default it scans the workspace directory and outputs SPDX JSON; it can scan a container image instead, uploads the SBOM as a workflow artifact, and attaches it to releases.
Which permissions does an SBOM workflow need?
anchore/sbom-action asks for contents: write, plus actions: read to attach the SBOM to a release. Signing with actions/attest needs id-token: write and attestations: write, and packages: write when the attestation is pushed to a container registry.
How do I sign an SBOM in GitHub Actions?
Use GitHub's artifact attestations. actions/attest signs an SPDX or CycloneDX JSON SBOM with Sigstore and binds it to an artifact; verify it with gh attestation verify. actions/attest-sbom is being deprecated in favor of actions/attest, which it now wraps.
How do I generate an SBOM for a container image in GitHub Actions?
Pass the image to anchore/sbom-action with the image input instead of path. To sign it, give actions/attest the image name and digest through subject-name and subject-digest, set push-to-registry: true, and verify with gh attestation verify oci://IMAGE.
Should the workflow produce SPDX or CycloneDX?
Whichever the tool that reads the SBOM expects. The dependency graph export is SPDX only and cdxgen writes CycloneDX. anchore/sbom-action writes either, and actions/attest accepts either. See CycloneDX vs SPDX.
Should the SBOM step run before or after the build?
After dependencies are installed, or it will miss them. Even then it sees what is on disk, not what the job downloaded and removed, or what a script fetched. Recording during the build avoids the timing question.
Record an SBOM from one of your builds
Change runs-on on one job, run it on CRACI, and compare the recorded SBOM with the one your current step produces.
Book a demo