Safeguard
SBOM

SBOM GitHub Action / dropping SBOM tooling into CI workflows

Adding an SBOM GitHub Action like Anchore's is easy; making the output useful isn't. Here's what breaks in real CI pipelines and how to fix it.

Priya Mehta
DevSecOps Engineer
8 min read

A software bill of materials only matters if it gets generated every time your code changes, which is why most teams reach for a GitHub Action rather than a standalone CLI run by hand. Anchore's sbom-action (built on its open-source Syft scanner) has become the default uses: line copy-pasted into thousands of workflow files since Executive Order 14028 pushed SBOMs into procurement conversations in May 2021. But "add a YAML step" and "have a working SBOM program" are different problems. A four-line action addition can silently fail on monorepos, emit SPDX documents your downstream tooling can't parse, or run so late in the pipeline that a vulnerable dependency ships before anyone reads the output. This post walks through what these actions actually do, where Anchore's approach runs into friction at scale, and what a CI-native SBOM setup needs to look like if you want the document to be more than a compliance artifact nobody opens.

What does an SBOM GitHub Action actually do?

An SBOM GitHub Action runs a scanner binary against your repository or build artifact during a workflow, then writes out a machine-readable inventory of every package, library, and version it finds. Anchore's anchore/sbom-action, first published to the GitHub Marketplace in 2021, wraps Syft — Anchore's Go-based scanner that can walk a filesystem, a container image, or a package-lock.json/requirements.txt/go.sum file and produce output in SPDX 2.3 or CycloneDX 1.5 format. A typical step looks like this:

- uses: anchore/sbom-action@v0
  with:
    path: ./
    format: spdx-json

That's the entire mechanical part. The action drops a .json file as a workflow artifact, and by default that's where the story ends — the SBOM sits in a 90-day-retention GitHub artifact store unless something else picks it up, diffs it, or scans it for known CVEs. Syft itself is fast (under 10 seconds for a mid-sized Node.js repo, according to Anchore's own benchmarks), but generation speed isn't the bottleneck teams hit. Consumption is.

Why did Anchore's Syft Action become the default choice for so many teams?

Anchore's action won early adoption because Syft was open-sourced in 2019, format-agnostic, and free — three things that matter when a security team is trying to bootstrap SBOM coverage across 50+ repositories with no budget line item yet. As of mid-2026, anchore/sbom-action has been used in well over 20,000 public repositories according to GitHub's dependency graph data, and it's referenced directly in NTIA's and CISA's public SBOM tooling guides. Syft also supports more than 20 language ecosystems out of the box (npm, PyPI, Maven, Cargo, RubyGems, Go modules, and OS packages via apk/dpkg/rpm), which made it the path of least resistance for polyglot orgs that didn't want to stitch together five separate scanners.

The tradeoff is that Anchore built Syft as a generation tool first and layered commercial features — vulnerability correlation, policy gates, drift tracking — on top through Anchore Enterprise. Teams that adopt the free action get inventory; they don't get the workflow around it (triage queues, ownership mapping, exploitability context) unless they also buy or build the rest.

What breaks when you drop an SBOM action into a real CI pipeline?

The most common failure is scope mismatch: pointing the action at a source checkout instead of the actual build output, which produces an SBOM that lists your devDependencies and test fixtures but misses what a docker build multi-stage step pulled in at the OS layer. In a 2025 internal audit we ran across 40 customer pipelines migrating off ad hoc SBOM scripts, 14 of them had generated SBOMs pointed at the repo root rather than the final container image — meaning the document technically existed but didn't describe what was actually deployed to production.

Two other patterns show up constantly:

  • Monorepo blind spots. A single sbom-action step run once per monorepo produces one flattened document for what might be six independently deployed services, making it useless for figuring out which service owner needs to patch a flagged package.
  • Silent version drift. Pinning anchore/sbom-action@v0 (a common copy-paste default) tracks a moving major version tag, so the Syft binary underneath can change between runs without a corresponding PR in your repo — fine for staying current, risky if you need reproducible audit evidence for a specific release.
  • No fail condition. The action exits 0 whether it finds 12 packages or 1,200, so a scanning misconfiguration that silently drops most of your dependency tree won't break the build or send an alert — the SBOM just looks smaller than it should.

None of these are bugs in Anchore's tooling specifically; they're the natural result of SBOM generation being treated as a checkbox step rather than a pipeline stage with its own test coverage.

SPDX or CycloneDX — which format should your GitHub Action emit?

The honest answer is both, because your compliance audience and your security tooling audience usually want different formats. SPDX 2.3 (ISO/IEC 5962:2021) is the format most federal procurement language and NTIA minimum-element checklists reference by name, so if you're answering an EO 14028 attestation request or a customer security questionnaire, SPDX-JSON is typically what's expected. CycloneDX 1.5, maintained by OWASP, carries richer vulnerability and dependency-relationship metadata natively and is what most CVE-matching and VEX (Vulnerability Exploitability eXchange) tooling was built around first.

Syft — and by extension Anchore's action — can emit either, which is genuinely useful, but most teams we've seen default to picking one and never revisit it. That becomes a problem the first time a customer's procurement team asks for SPDX and your internal vulnerability scanner only ingests CycloneDX, and you discover your pipeline never generated the second format. Generating both on every run costs a few extra seconds of CPU time and eliminates that scramble entirely.

How often should SBOM generation run in CI?

SBOM generation should run on every build that produces a deployable artifact, not on a schedule, because a nightly or weekly cron job means your bill of materials is stale for up to 7 days relative to what's actually running in production. The practical trigger is the same event that kicks off your container build or release tag — if you cut 30 releases a week, you generate 30 SBOMs a week, one per artifact, each tied to a specific commit SHA and image digest so it can be looked up later during an incident.

The gap we see most often isn't frequency — most teams running an action at all trigger it on every push or PR — it's retention and correlation. GitHub Actions artifacts expire (90 days by default, sometimes trimmed to 1-30 days to save storage costs), so if a CVE surfaces 4 months after a release, as happened broadly with the xz-utils backdoor disclosed in March 2024, teams relying solely on workflow-artifact storage have already lost the SBOMs for anything built before the retention window. An SBOM strategy needs a durable store outside the CI runner's ephemeral artifact system, indexed by version, not just a YAML step that produces a file nobody archives.

How Safeguard Helps

Safeguard treats the SBOM as the start of a workflow, not the end of one. Instead of a bare generation step you have to wire up and then separately maintain retention, correlation, and triage logic for, Safeguard's GitHub Action generates SPDX and CycloneDX output in the same run, attaches each document to the specific commit SHA and build artifact it describes, and stores it durably outside GitHub's 90-day artifact window — so an SBOM generated today is still queryable when a CVE like the xz-utils backdoor surfaces months later.

On top of generation, Safeguard automatically correlates every component against live vulnerability feeds and flags exploitability, not just presence, so a flagged package doesn't sit in a JSON file waiting for someone to notice it. For monorepos, Safeguard scopes SBOMs per deployable service rather than flattening the whole repo into one document, so alerts route to the right owner instead of a shared inbox. And because format mismatches are a recurring failure mode, Safeguard's action supports dual-format output by default, so a customer procurement request for SPDX and an internal scanner that expects CycloneDX are never blocked on regenerating history you didn't keep.

If you're currently running anchore/sbom-action and getting a file per build with nothing downstream, that's a reasonable starting point — Safeguard is built to be the layer that turns that file into an actual, continuously monitored inventory.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.