101% more reported CVEs per day in 2026 than last year.

SBOM

An SBOM should describe the build that shipped

A build-time SBOM is recorded while the build runs, from the traffic that actually came into it. Most SBOM generators instead reconstruct the list afterwards from the lockfile or the output, which misses the hidden dependencies a build fetches on its own. Here is why, and what recording the build changes.

Updated

What a scanner reads versus what the build pulled in A scanner reads the lockfile, which lists direct packages A, B and C and transitive packages D and E, so its SBOM contains only those. CRACI sees the network traffic into the build job, so its recorded SBOM also contains the hidden dependencies no lockfile lists: code downloaded by an install hook, a download by a build script, and the base image layers. A scanner reads the lockfile package-lock.json direct A B C transitive D E Scanned SBOM A, B, C, D, E only what the lockfile lists CRACI sees the traffic into the build A B C D E install hook download build script download base image layers build job on CRACI Recorded SBOM every package it fetched, hidden too Dashed: hidden dependencies, listed in no lockfile

Build SBOM, source SBOM, analyzed SBOM

CISA's guidance on SBOM types sorts SBOMs by when and how they are made. Three of them cover almost every SBOM generator in use:

Type How it is made What it can miss
Source From source files, manifests and lock files, usually by an SCA tool Runtime, plugin or platform components; anything not declared in the files
Build As part of the build that creates the releasable artifact Whatever the build environment cannot see, which is why completeness should be stated
Analyzed After the build, by analyzing an executable, package or container image Components the tool cannot recognize; results depend on heuristics

CISA lists two benefits of a build SBOM: more confidence that the SBOM represents the artifact correctly, because of the information available during the build and CI/CD process, and more trust, because the same build workflow can sign both the SBOM and the artifact. Its listed drawback is that you may have to change the build process to generate one. With CRACI that change is one line, runs-on: craci, and the recording comes with the runner.

Scanning a folder versus recording a build

Most SBOM generators are scanners. You point one at a source tree or a container image, and it works out what it thinks went in by reading manifests, lockfiles, installed packages or binaries. That is useful, and for software you did not build yourself it is often the only option.

The weakness is that a scanner only sees what is in front of it. If no lockfile was committed, if dependencies were not installed before the scan, or if the product ships as a compiled binary, the scanner has less to work with than the build did. A scan that finds no components at all still produces a valid SBOM document, which is the part that catches people out.

CRACI takes a different approach. It generates the SBOM inside its own CI runner, so the artifact shipped is the artifact described. It is not a scan that ran somewhere nearby.

SBOM basics: SPDX, CycloneDX and OpenVEX

What an SBOM records, how the SPDX and CycloneDX formats differ in practice, and what OpenVEX adds about whether a vulnerability actually affects a product.

Petteri Pulkkinen and Erika Marttinen at Kubernetes Community Days Helsinki 2026, 16:50 to 22:05 of SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chains.

What a scan depends on

Syft, Trivy, cdxgen, the Microsoft SBOM Tool, GitHub's dependency graph export and the Socket CLI all work out an SBOM from what they can read. The patterns below follow from how these tools are documented to work.

Tools disagree on what counts as a component

Each generator decides for itself what counts as a component and which files to trust, so the same pinned commit gives each of them a different list.

Some of that is naming. Tools identify the same GitHub Action under different package URL types, so they can list the same thing and still not match. That is part of the problem too: an SBOM that uses a different identifier for a component is harder to match against vulnerability data.

Whether you installed first can matter more than the tool

Express 5.2.1 does not commit a lockfile. Pointed at a clean checkout, a generator that resolves dependencies from lock files or an installed tree has neither in front of it, so none of the npm dependencies declared in package.json reach the document. It can still return a list, made of whatever else it recognizes, such as GitHub Actions and the workflow files that reference them. No tool here is broken.

On an installed copy of the same repository, one Syft flag changes the answer again. Same tool, same tree, same version. By design, a Syft directory scan reports what a project declares, and the cataloger that reads installed packages is not in its default set for directories.

Some software has no manifest to read

The anthropics/claude-code repository at v2.1.259 has no dependency manifest of any kind: no package.json, lockfile or equivalent. What each generator returns for it is assembled from other file types, so the answer says more about the tool than about the project.

The published npm package is not much different. @anthropic-ai/claude-code declares zero runtime dependencies and eight platform-specific packages that wrap a prebuilt binary. @openai/codex 0.149.1 has the same shape. What runs on the developer's machine is a compiled binary that no manifest reader can see into. This is not a flaw in any one tool. It is a distribution model that after-the-fact scanning cannot describe.

Versions and suppliers

Where two tools find the same package, they do not always agree on its version. One may pin a GitHub Action to a commit SHA while another reports a release tag. A vulnerability lookup keyed on the wrong version gives a confident wrong answer.

The supplier field, one of the NTIA minimum elements, often comes out empty, because most generators have no source for it.

Hidden dependencies: what no scan can see

Every tool above starts from the lockfile or from the build's output and works out what went in. A build fetches more than that. These hidden dependencies are listed in no lockfile, so a scan cannot find them:

  • Install hooks. npm preinstall and postinstall scripts run during the install and can download more code. That is how the Shai-Hulud worm ran.
  • Build scripts. Rust build scripts, among others, download what they need at build time.
  • Base images. A container build pulls a base image and its operating system layers, which no language lockfile lists.
  • Caches and build tools. Packages restored from CI caches and tools installed by setup steps arrive outside the lockfile too.

Scanning the output afterwards does not close the gap. A finished binary, especially compiled code such as Rust, leaves the scanner to guess what went into it.

Two builds, two SBOMs

There is a second reason a scan cannot stand in for the build: most builds are not reproducible. Build the same commit twice, back to back, and there is no guarantee you get the same packages. Package managers resolve and install in whatever order responses arrive, which matters most when there is no lockfile, as with many Python projects, and build steps can include randomness of their own. Byte-for-byte reproducible builds are possible, with tools such as Nix, but they are rare.

So an SBOM generated later, on another machine or from another build, describes a build that may never have shipped. Only the SBOM recorded during the CI build that produced a release proves what is in production.

What recording the build changes

CRACI sees the traffic; scanners do not. It runs your GitHub Actions jobs on its own runners and sees the network traffic coming into each build, so the SBOM lists what the job actually pulled, including transitive and hidden dependencies, rather than what a manifest says it should need.

Caches are where build-time recording gets hard. A package restored from a cache never touches a package registry, so a proxy alone would miss it. CRACI carries dependency evidence with the cache, so packages restored from a cache stay in the SBOM of the job that restored them. See what the GitHub Actions cache hides.

Detected ecosystems include npm, PyPI, RubyGems, Cargo, Go, Nix, OCI, apt, apk and Git sources, plus Yocto and OpenEmbedded download presets. JFrog Artifactory npm and PyPI mirrors work. Declared license metadata is included in the export. SBOMs export in CycloneDX and SPDX. The SBOM generation page covers the full capability.

Seeing the traffic works in the other direction too. A build needs packages from known sources and publishes its output to known places. CRACI's egress policy allows only those and blocks every other outgoing connection immediately, so malware that tries to send your secrets to an attacker's server fails. Because the policy allows what the build needs instead of looking for known-bad hosts, it holds even if you are the first target of an attack nobody has seen before, as with Shai-Hulud. See how an egress policy stops exfiltration.

Container and Docker SBOMs

Container images are where the difference shows most. The usual ways to get a Docker SBOM all analyze the image: Syft and Trivy scan it, docker scout sbom indexes its contents when the image carries no SBOM attestation, and BuildKit's SBOM attestation, switched on with --sbom=true, runs a Syft-based scanner over the final image stage by default. The older docker sbom plugin is deprecated in favor of docker scout sbom.

Each of those reads the finished image, so it sees what is still recognizable in the layers. It does not see what the build fetched along the way. CRACI records the job that builds the image: the images and packages the job pulls from OCI registries and package sources go into the same SBOM as the rest of the build. For an image you did not build, scanning it is still the practical option. See what Docker image SBOMs miss.

Completeness, stated per job

A recorded SBOM is only as good as the recording. Instead of presenting every SBOM as complete, CRACI states completeness for each job and each cache, in one of five states:

  • Complete. CRACI observed complete job evidence, and every restored cache was complete.
  • Complete with connections. The requirements passed, but the job used one or more custom network connections, so it is worth reviewing what came through them.
  • Incomplete. CRACI identified a condition that could leave dependencies out of the SBOM.
  • Unavailable. CRACI could not finish evaluating the evidence.
  • Not recorded. The job predates completeness reporting.

Completeness is transitive across caches. If a job restores an incomplete cache, its SBOM is incomplete too, and any cache that job produces carries the incomplete status forward. You know which SBOMs you can rely on and which ones need a second look.

The hard parts: hooks, noise and staying current

Three problems the talk calls out: post-install hooks that fetch things no manifest lists, finding the dependencies that matter in a long list, and keeping the SBOM current, which means generating it in CI on every build.

Petteri Pulkkinen and Erika Marttinen at Kubernetes Community Days Helsinki 2026, 25:39 to 27:22 of SBOMbastic: Getting Ready for Upcoming EU Cybersecurity Regulation in Software Supply Chains.

Provenance through the API

The SBOM is one part of what CRACI records for each job. The API returns SBOMs, network traces and provenance, all recorded from the same build.

Where a scanner is still the right tool

Build-time recording covers software you build. For a third-party image or binary you did not build, a scanner is still the practical choice, and the tools above are capable and widely used. The point is not that scanners are bad. It is that the SBOM for your own releases should come from the build that made them.

A CRACI SBOM also has limits of its own. It covers jobs that run on CRACI runners, which today means GitHub Actions on Linux, with other CI systems on the roadmap.

Choosing between tools? See SBOM tools compared, or see how build-time SBOMs fit CRA compliance work. For Syft, Trivy and cdxgen side by side, see what open source SBOM generators miss.

Build-time SBOMs: frequently asked questions

What is a build-time SBOM?

A build-time SBOM, which CISA calls a build SBOM, is generated as part of building the releasable artifact. Instead of reconstructing the component list from files afterwards, it describes what the build itself used. CRACI records it from the packages each GitHub Actions job actually fetched.

What is the difference between a build SBOM and an analyzed SBOM?

An analyzed SBOM is generated after the build by analyzing an artifact such as an executable or a container image, which generally requires heuristics. A build SBOM is generated during the build, from information the build has. Analyzed SBOMs work without access to the build, which makes them the right choice for software you did not build.

How do I generate an SBOM for a Docker image?

For an existing image, scan it with a generator such as Syft, Trivy or docker scout sbom. BuildKit can also attach an SBOM attestation when you build, by scanning the final image. To record what the build of an image pulled in, run the build on a CRACI runner: the job's SBOM covers the images and packages it fetched.

What are hidden dependencies?

Everything a build fetches that no lockfile lists: code downloaded by npm install hooks, files that Rust build scripts download at build time, and the base image and its operating system layers in a container build. Tools that read the lockfile or scan the output miss them. CRACI records them because it sees the traffic coming into the build.

Why do two builds of the same code give different SBOMs?

Because most builds are not reproducible. The package manager resolves and installs packages in whatever order responses arrive, especially without a lockfile, and build steps can include randomness. Build the same commit twice, back to back, and the second build can contain different packages. Only the SBOM recorded during the build that produced a release proves what that release contains.

Is a build-time SBOM always complete?

Not automatically. A recording is only as good as what it can observe, so CRACI states completeness for every job and every cache: Complete, Complete with connections, Incomplete, Unavailable or Not recorded. You know which SBOMs you can rely on and which need a second look.

Does a build-time SBOM replace SBOM scanners?

For the software you build, yes: the SBOM of your releases should come from the builds that made them. For third-party images and binaries you did not build, a scanner is still the practical choice. SBOM tools compared covers the options.

Which SBOM generator does CRACI use?

None. CRACI does not scan files. A package-aware proxy on its runners records the packages each job fetches from npm, PyPI, RubyGems, Cargo, Go, Nix, OCI, apt, apk and Git sources, plus Yocto and OpenEmbedded download presets, and exports the result in CycloneDX or SPDX.

Compare an SBOM from your own build

Book a demo and run one of your workflows on CRACI. Put its SBOM next to the one your current tool produces and see where they differ.

Book a demo