Modern software supply chains pull in hundreds of open-source dependencies for every line of code your team writes, and most organizations have no reliable, automated record of what's actually shipping in production. When the next widely-exploited library vulnerability makes headlines, the first question from security leadership is always the same: are we affected, and where? Without an SBOM generation CI pipeline in place, answering that takes days of manual dependency archaeology instead of minutes of automated lookup.
This guide walks through building an SBOM generation CI pipeline from scratch: installing Syft to scan source trees and container images, wiring that scan into GitHub Actions so every build produces a fresh bill of materials, signing the result so it can be trusted, and storing it somewhere you can actually query during an incident. By the end, you'll have a repeatable process for producing a software bill of materials CI/CD teams can rely on, without meaningfully slowing down your builds.
Step 1: Plan Your SBOM Generation CI Pipeline Strategy
Before writing any YAML, decide three things: which SBOM format you'll standardize on, what you'll scan, and where SBOMs will live afterward.
- Format: CycloneDX and SPDX are the two formats worth considering. CycloneDX has stronger native support for vulnerability and license metadata and tends to be the default choice for security-focused pipelines; SPDX is preferred in some regulated and government contexts. Pick one as your canonical format — you can always convert later — rather than generating both everywhere.
- Scan targets: source repositories, container images, and compiled build artifacts each produce different SBOMs. A source-only scan misses dependencies pulled in at build or base-image layers, so most mature pipelines scan at multiple points.
- Storage and retention: decide up front whether SBOMs live as CI artifacts, get pushed to an internal registry, or feed into a dedicated SBOM/vulnerability management platform. Retrofitting this after you have thousands of untracked SBOM files is painful.
Writing this down as a one-page policy before implementation saves you from having five slightly different SBOM formats scattered across repos six months from now.
Step 2: Install and Configure Syft for SBOM Generation
Syft is the most widely adopted open-source tool for generating SBOMs, and it's the backbone of this syft SBOM tutorial. It can scan filesystems, container images, and archives, and it outputs CycloneDX, SPDX, or its own native format.
Install it locally to test before wiring it into CI:
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
syft version
Generate a baseline SBOM for a repository:
syft dir:. -o cyclonedx-json=sbom.cyclonedx.json
And for a container image already built and available locally or in a registry:
syft myregistry/myimage:latest -o cyclonedx-json=sbom.cyclonedx.json
Open the resulting JSON and spot-check it against your lockfile (package-lock.json, go.sum, requirements.txt, etc.) to confirm Syft is picking up the package managers you expect. This five-minute sanity check catches most configuration issues before they reach CI.
Step 3: Add SBOM Generation to Your GitHub Actions Workflow
With the tool validated locally, the next step is to generate SBOM GitHub Actions style — on every push and pull request, so no build ships without one. Anchore maintains an official action that wraps Syft:
name: sbom
on:
push:
branches: [main]
pull_request:
jobs:
generate-sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate SBOM with Syft
uses: anchore/sbom-action@v0
with:
path: .
format: cyclonedx-json
output-file: sbom.cyclonedx.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.cyclonedx.json
Tagging the artifact name with github.sha makes it trivial to pull the exact SBOM that corresponds to a specific commit later, which matters a lot when you're trying to answer "what did we ship on Tuesday" during an incident review.
Step 4: Generate SBOMs for Multiple Artifact Types
A single source-tree scan is a good start but leaves gaps. Extend the workflow to cover container images and build output separately, since base images and compiled binaries frequently introduce dependencies that never appear in your application manifest:
# Application source
syft dir:. -o cyclonedx-json=source-sbom.json
# Built container image, tagged with the commit SHA
syft myregistry/myimage:${{ github.sha }} -o cyclonedx-json=image-sbom.json
# Compiled build output (e.g., a packaged binary or bundle)
syft dir:./dist -o cyclonedx-json=build-sbom.json
Run the image scan as a separate CI job that depends on your existing "build and push image" job, so Syft is scanning the actual artifact that will be deployed, not just its Dockerfile.
Step 5: Store and Version SBOMs as Build Artifacts
Uploading SBOMs as ephemeral GitHub Actions artifacts is fine for short-term debugging, but most teams also want durable storage. Two common patterns:
- Attach to releases: on tagged releases, attach the SBOM as a release asset alongside binaries, so consumers of your software can retrieve it directly.
- Push to a central store: forward each SBOM to an internal object store, artifact repository, or a dedicated SBOM management tool (such as Dependency-Track) keyed by image digest or commit SHA.
- name: Publish SBOM to release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: sbom.cyclonedx.json
Whichever pattern you choose, make sure the SBOM is addressable by the same identifier (commit SHA, image digest, or release tag) you'd use to look up the corresponding build in every other system — CI logs, deployment records, vulnerability scans.
Step 6: Sign and Attest Your SBOMs
An SBOM you can't trust is only marginally better than no SBOM at all. Use cosign to attach a signed attestation to your container image, binding the SBOM cryptographically to the artifact it describes:
cosign attest --predicate sbom.cyclonedx.json --type cyclonedx myregistry/myimage@sha256:<digest>
Consumers — including your own deployment pipeline — can then verify the attestation before trusting the SBOM:
cosign verify-attestation --type cyclonedx myregistry/myimage@sha256:<digest>
This closes a common gap: without signing, anyone with registry write access could substitute a doctored SBOM that hides a vulnerable dependency, defeating the entire point of generating one.
Step 7: Automate Downstream Delivery and Policy Checks
The final piece is making the SBOM do something useful automatically, rather than sitting in a bucket. Forward each generated SBOM to a vulnerability-matching service or policy engine as part of the same workflow, and fail the build (or open a ticket) when it detects a component with a known critical CVE or a disallowed license:
- name: Submit SBOM for policy evaluation
run: |
curl -X POST https://your-sbom-platform.example.com/api/v1/sboms \
-H "Authorization: Bearer ${{ secrets.SBOM_PLATFORM_TOKEN }}" \
-H "Content-Type: application/json" \
--data-binary @sbom.cyclonedx.json
This is the step that turns a pile of JSON files into an actual security control.
Troubleshooting and Verification
SBOM comes back empty or suspiciously small. Confirm the scan path is correct and that lockfiles are present at scan time — Syft reads manifests like package-lock.json or go.sum directly, and if a npm install or go mod download hasn't run yet in the pipeline, dependency resolution may be incomplete. Run the scan after your install/build step, not before it.
Package counts don't match your lockfile. Diff the number of packages in the generated SBOM against npm ls --all, pip list, or your ecosystem's equivalent. A large discrepancy usually means Syft is scanning the wrong directory, or a monorepo has multiple manifests it isn't traversing.
Format mismatches breaking downstream tools. If a vulnerability scanner or SBOM management tool rejects your file, double check you're consistently producing CycloneDX or SPDX — not switching between them across jobs — and that you're on a schema version the consumer actually supports.
Private registry authentication failures during image scans. Syft needs the same registry credentials your CI already uses to pull images. Make sure the docker login or equivalent credential helper step runs before the Syft image scan step, not after.
Slow scans on large monorepos. Scope the scan to changed subdirectories where possible, or cache Syft's own dependency database between runs to avoid re-downloading it on every job.
Verifying end-to-end. As a final check, intentionally introduce a dependency with a known CVE in a test branch and confirm it shows up in the generated SBOM and triggers your downstream policy check. If it doesn't surface, the gap is usually in the scan scope or the format conversion step, not in Syft itself.
How Safeguard Helps
Generating SBOMs is the easy part — the hard part is keeping them accurate, trustworthy, and actually useful at the moment you need them, which is usually during an active incident. Safeguard ingests SBOMs directly from your CI pipeline, whether produced by Syft, another generator, or a mix across repos, and continuously matches every component against live vulnerability and license intelligence instead of a point-in-time snapshot.
Safeguard also verifies SBOM signatures and attestations automatically, so you get an early warning if an SBOM doesn't match the artifact it claims to describe, and it gives you a single searchable inventory across every service, image, and release — so when the next critical CVE drops, you can answer "are we affected" in seconds instead of re-running scans across dozens of repositories. Teams that pair a solid SBOM generation CI pipeline with Safeguard's continuous monitoring get both halves of the problem solved: reliable generation at build time, and reliable answers when it matters most.