Generate and Scan an SBOM in CI
Produce a software bill of materials for every build, scan it for known vulnerabilities, retain it with the release, and fail the pipeline on high-severity findings.
Task
Know exactly which direct and transitive packages are in a build artifact, and make that inventory available for vulnerability response.
When to use this
A transitive dependency receives a critical CVE. The team cannot answer which deployed versions contain it because there is no machine-readable inventory of the build.
Prerequisites
- A CI pipeline that builds the application
- A filesystem or container image artifact to scan
- Permission to upload build artifacts and fail the pipeline
Steps
1. Generate an SPDX or CycloneDX SBOM from the built artifact, not only from the lockfile.
syft dir:. -o spdx-json=sbom.spdx.jsonExpected output: sbom.spdx.json lists packages with names, versions, and identifiers.
2. Scan the SBOM for known vulnerabilities and fail on high or critical findings according to policy.
grype sbom:sbom.spdx.json --fail-on highExpected output: The command exits non-zero when a high-severity vulnerability is present.
3. Upload the SBOM with the build artifact and retain it according to the release policy.
- uses: actions/upload-artifact@<pinned-sha> # v4
with:
name: sbom-${{ github.sha }}
path: sbom.spdx.json
retention-days: 90
Expected output: Each release has a downloadable SBOM tied to its commit or image digest.
4. Verify the SBOM contains the application's transitive dependencies and the runtime base image packages.
jq '.packages | length' sbom.spdx.jsonExpected output: The package count is plausible for the stack and includes known transitive packages.
Troubleshooting
Problem
The SBOM is nearly empty
Solution
Syft scanned the repository without installed dependencies or the wrong directory. Scan the built container image or install dependencies before scanning.
Problem
The vulnerability scan is noisy
Solution
Triage by reachability and severity, document exceptions with an expiry date, and avoid disabling the gate globally.
Limitations
- An SBOM is an inventory, not a proof of exploitability. A finding still requires triage.
- Scanners have different databases and matching accuracy; record the tool and database version with the report.
Inventory the artifact you ship
A lockfile describes a dependency resolution, not necessarily the artifact that runs in production. A container image includes the runtime base, OS packages, and copied binaries. Generate the SBOM from the final artifact so the inventory matches what is deployed. SPDX and CycloneDX are both widely consumed; choose one and keep the format stable for downstream tooling.
syft myapp:1.4.0 -o cyclonedx-json=sbom.cdx.json
grype sbom:sbom.cdx.json --fail-on high
Make it part of the release
An SBOM generated ad hoc is rarely available during an incident. Generate it in CI for every build, name it with the commit or image digest, and attach it to the release. Store the scanner and database versions alongside the report so a later comparison is meaningful. If the build is rebuilt for the same commit, regenerate the SBOM; base images and transitive resolution can change.
Scan with a triage policy
A hard fail on every finding creates alert fatigue and encourages bypass. Fail on high and critical findings by default, allow documented exceptions with an owner and expiry, and review exceptions regularly. Prioritize vulnerabilities that are reachable from the application, but do not ignore a critical package merely because reachability is unknown. The gate should be strict enough to matter and specific enough to act on.
Verify the inventory
Check that known transitive packages appear in the SBOM and that the runtime base image packages are included. A count alone can be misleading, but a sudden drop usually means the scanner missed dependencies. Keep the SBOM generation step pinned and reproducible; a scanner update can change the output and the gate result.