SBOM
What open source SBOM generators miss
Syft, Trivy and cdxgen are capable, free and widely used. All three build an SBOM from the files you point them at: lockfiles, manifests or installed packages. That is why they disagree on the same repository, and why none of them sees what the build fetched outside those files.
Updated
How to generate an SBOM with each tool
Each tool is one command for a source directory or a container image, with CycloneDX or SPDX output:
# Syft: a source directory or a container image
syft ./my-project -o cyclonedx-json=sbom.cdx.json
syft my-image:latest -o spdx-json=sbom.spdx.json
# Trivy: a filesystem or a container image
trivy fs --format cyclonedx --output sbom.cdx.json .
trivy image --format spdx-json --output sbom.spdx.json my-image:latest
# cdxgen: the current directory, optionally with a project type
cdxgen -o bom.json .
cdxgen -t js -o bom.json . The commands are the easy part. What ends up in the file depends on what each tool reads, and they do not read the same things.
What each tool reads
| Tool | Source directory | Container image | Worth knowing |
|---|---|---|---|
| Syft | A directory set of default catalogers, which reads declared dependencies | An image set of default catalogers, which reads installed packages |
Catalogers are chosen by scan type. syft cataloger list shows which run, and
--select-catalogers adds or removes them.
|
| Trivy | Lockfiles, for Node.js package-lock.json, yarn.lock, pnpm-lock.yaml and bun.lock | For Node.js, package.json files under node_modules | Leaves out development dependencies unless you pass --include-dev-deps. |
| cdxgen | Lock files first, which include dev dependencies; for Node.js it also parses the code for imports | Supported | Set the project type with -t to limit what it looks for. |
Why they disagree on the same repository
Point all three at one commit and you get three different lists. None of them is broken. Each follows its own documented rules:
- No lockfile, no dependencies. Express 5.2.1 commits no lockfile. On a clean checkout, a tool
that resolves dependencies from lockfiles or installed packages has neither, so the npm dependencies declared in
package.jsonnever reach the SBOM. It still writes a valid document with whatever else it recognized. - Installed or declared. Whether you ran an install before the scan can matter more than which tool you use. On an installed copy of the same repository, one Syft cataloger setting decides whether the installed npm packages appear at all.
- Development dependencies. Trivy leaves them out by default and cdxgen includes them, so the same lockfile gives two answers to "what is in this project".
- Naming and versions. Tools identify some components under different package URL types, or pin a GitHub Action to a commit SHA where another reports a tag. A vulnerability lookup keyed on the wrong identifier gives a confident wrong answer.
- No manifest at all. The anthropics/claude-code repository has no dependency manifest, and its npm package wraps a prebuilt binary. What each tool returns says more about the tool than about the project.
What all of them miss: hidden dependencies
The deeper limit is shared. A scanner reads the lockfile or the build's output and works out from that what went in, after the fact. Whatever the build fetched outside those files never reaches the SBOM. These are the hidden dependencies:
- Install hooks. npm
preinstallandpostinstallscripts run during the install and can download more code. That is how the Shai-Hulud worm ran. - Build scripts, such as Rust build scripts that download what they need at build time.
- Base images and their operating system layers in a container build, which no language lockfile lists.
- Packages restored from CI caches, which may not match the current lockfile. See what the GitHub Actions cache hides.
- Build tools installed by setup steps, with their own dependency trees.
- Transitive versions resolved at build time when the lockfile is missing, stale or not used by a frozen install. See transitive dependencies.
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. Commercial SCA scanners work from the same kinds of files and share the same blind spots. CRACI has found vulnerable packages that Snyk and Aikido did not report, because the components were never in what they scanned.
The tools are part of your supply chain too
A scanner runs in your CI with access to your code and often your secrets. In March 2026, Aqua Security's advisory
(CVE-2026-33634) described a malicious Trivy v0.69.4 release and poisoned trivy-action and
setup-trivy tags that stole credentials from CI runners. Pin scanner actions to full commit SHAs, and
check the advisory for the versions you run.
How to check your own SBOM
- Run two generators on the same commit and compare the package URLs they report.
- Check whether the repository commits a lockfile, and whether CI installs from it with a frozen install.
- Scan after the install step as well as before it, and compare.
- Compare the list with what your build log shows being downloaded, including setup steps and cache restores.
If the lists differ, the SBOM you publish depends on which tool you happened to run, not on what you shipped.
Record the build instead
CRACI sees the traffic; scanners do not. It is a GitHub Actions runner that sees the network traffic coming into each build, so its SBOM records every package the job pulled in, at every depth of the tree, including the hidden dependencies and packages restored from CI caches. There is no directory to point at and no cataloger to choose. When a recording could not see everything, for example because a job allowed unmonitored traffic, the SBOM says so with its completeness state.
Recording also matters because builds are rarely reproducible. Build the same commit twice, back to back, and the package manager can resolve and install in a different order, so the second build can contain different packages. Only the SBOM recorded during the build that produced a release proves what that release contains.
Keep Syft or Trivy for third-party images and binaries you did not build. For your own releases, see build-time SBOMs or book a demo to compare a recorded SBOM with your current tool's on one of your repositories.
Open source SBOM generators: frequently asked questions
Which is better for SBOMs, Syft or Trivy?
They are built for different jobs. Syft is a dedicated SBOM generator with many catalogers and signed attestations; Trivy is a security scanner that writes SBOMs as one of its outputs. Both read the files in front of them, so on the same repository the bigger difference is usually whether a lockfile is committed and whether dependencies were installed before the scan.
Why does my SBOM show no dependencies?
Most often because the repository commits no lockfile and nothing was installed before the scan. A generator that reads lockfiles or installed packages then has nothing to read, and it still writes a valid SBOM. Commit the lockfile, or scan after the install step.
Does Trivy include dev dependencies in the SBOM?
Not by default. Trivy's Node.js documentation says it does not report development dependencies unless you pass --include-dev-deps. cdxgen, by contrast, parses lock files and includes dev dependencies. Two tools can disagree on the same project for this reason alone.
Can I trust an SBOM from an open source generator?
For what it read, yes. The question is what it did not read: caches, build tools, downloads, install scripts and binaries without package metadata. Treat a scanned SBOM as a description of the files you pointed the tool at, not of the build that made your release.
What is the best SBOM tool?
For software you build yourself, the most complete SBOM comes from recording the build rather than scanning files. CRACI records every package each GitHub Actions job fetches, including from caches, and has found vulnerable packages that Snyk and Aikido did not report. For third-party images and binaries you did not build, a scanner such as Syft or Trivy is still the practical choice.
Put a recorded SBOM next to your scanner's
Run one GitHub Actions workflow on CRACI and compare its SBOM of fetched packages with what Syft, Trivy or cdxgen reported.
Book a demo