Syft SBOM Generation Against Nix Store Paths
Syft captures runtime packages but misses build-time dependencies Nix tracks.

Syft can generate SBOMs against Nix store paths, but the content-addressed structure of the Nix store creates specific behaviors (around dependency closure, runtime vs. build-time components, and package metadata) that teams must understand to produce accurate, useful SBOMs. What follows is a walkthrough of how Syft handles those paths in practice, where its scan holds up, and where a Nix-native tool like sbomnix fills in what Syft leaves out.
The Nix store's content-addressed structure and the challenge it poses for SBOM generation
Every package built by Nix lives at a path shaped like /nix/store/<hash>-name-version/, and that hash is not a checksum slapped on after the fact. By default it encodes the full input graph that produced the package, meaning the source, the patches, the build script, and every dependency all feed into the hash, though Nix also supports a content-addressed mode where the hash derives from the output instead. Two versions of the same tool can coexist at distinct store paths, with no global package database analogous to dpkg status or RPM db existing at a single file location. There is no global package database analogous to dpkg's status file or the RPM database existing at a single file location.
Scale compounds the problem. Nixpkgs, the central package collection for the Nix ecosystem, contains more than 122,000 packages according to Repology data cited by Wikipedia, and a repository that size makes any notion of walking a full dependency closure by hand a nonstarter Nix (package manager) — Wikipedia. Two versions of the same tool can coexist at distinct store paths, with no global package database analogous to dpkg status or RPM db existing at a single file location. They produce different documents, because Nix derivations separate runtime dependencies from build-time ones, and a store path present at runtime may never have included the compiler or build toolchain that produced it in the first place. Flipping that around, a build machine's store can be full of toolchains that never ship anywhere near production. A scan that only looks at what is on disk right now can silently drop build-time dependencies from the record entirely, and if one of those dependencies carried a vulnerability, the SBOM will not know to mention it.
Syft's Nix cataloger behavior when pointed at a store path
Syft ships a dedicated cataloger for the Nix ecosystem, implemented as a concrete Cataloger type in its nix package. The Go package documentation states that the older entry point, NewStoreCataloger, is deprecated in favor of NewCataloger. Using it requires no special flags: point syft at any directory rooted at or containing /nix/store, run syft path/to/dir, and the Nix cataloger activates on its own.
What it actually enumerates is whatever store paths are present on disk at that moment. It reads package name and version straight out of the store path's naming convention and pulls in whatever metadata files happen to sit inside each store path entry. From there, Syft can emit the result in CycloneDX JSON, SPDX JSON, its own Syft JSON, or a human-readable table, covering both dominant SBOM interchange standards. The whole operation runs offline by default: no external API calls, nothing sent anywhere, and everything Syft reports comes from what it finds locally, though a team can opt into network features like remote license lookups or Maven resolution if they want that extra detail. Syft also auto-detects language-level manifests, go.mod, package.json, requirements.txt, and the like, which sometimes live inside Nix store paths, layering language-specific dependency data on top of the Nix-native package entries.
What Syft does not do matters as much as what it does. It does not evaluate Nix expressions, and it does not consult flake.lock to work out what should be present in a fully built closure. It only reports what it finds, not what the derivation graph declares. Syft's output pairs cleanly with Grype: once Syft has produced the SBOM, Grype can scan that document for CVEs without touching the environment again, which lets a team cache the SBOM and re-run vulnerability checks as new CVEs get published.
Accuracy and gaps in Syft's scan-time approach
Syft earns its keep in a specific set of situations. If the goal is inventorying what's actually deployed right now, either a container image built from a Nix derivation or the live /nix/store on a production box, a scan-time approach answers exactly that question. And for container image archives specifically, whether produced by docker image save, podman save, or skopeo copy, Syft handles all three formats and catalogs whatever Nix store entries got baked into the image layers.
The gaps appear the moment the focus shifts from what's here to what should be here, and what was involved in getting it here."" Build-time dependencies, the compilers, the header files, the intermediate tools that never made it into the final runtime closure, are invisible to a scan that only looks at disk contents, and that means the resulting SBOM undercounts the real attack surface from a supply-chain perspective. Syft also treats packages as a flat list rather than a graph: it does not reconstruct which store path depends on which, so there's no way to trace, from the SBOM alone, how a vulnerability in one component might propagate through what depends on it. And while store path names hand over a name and a version for free, the richer metadata that Nixpkgs actually maintains, upstream source URLs, license identifiers, maintainer information, does not always survive the trip from Nixpkgs into the store path itself, which leaves CPE and PURL identifiers missing or incomplete in the final document.
None of this makes Syft's output wrong. It makes it partial in a specific, predictable way. A Syft-only SBOM of a Nix environment checks the compliance box just fine, but a team running an operational vulnerability management program, one that needs to know the entire closure of what got compiled into an artifact, will find it comes up short. The team needs a fast, format-compliant SPDX or CycloneDX document for a compliance handoff or procurement requirement.
How sbomnix approaches Nix dependency enumeration differently
sbomnix takes the opposite starting point. Built specifically for the Nix ecosystem by the Technology Innovation Institute and released under Apache 2.0, it does not scan a filesystem after the fact, it works from Nix's own build machinery. It accepts flake references directly, things like sbomnix., sbomnix github:tiiuae/sbomnix, or sbomnix github:NixOS/nixpkgs/nixos-unstable#wget, and it also accepts raw store paths and result symlinks.
By default, sbomnix scans for runtime dependencies, and getting an accurate answer to that question requires actually building the target, not just inspecting whatever happens to exist on disk. An optional --buildtime flag switches the tool over to the full build-time dependency closure, and this is precisely the gap that a scan-time tool like Syft cannot close on its own. Output comes in CycloneDX JSON, SPDX JSON, and CSV; running sbomnix against a flakeref such as github:NixOS/nixpkgs/nixos-unstable#wget produces sbom.cdx.json, sbom.spdx.json, and sbom.csv in one pass.
There's a caveat worth taking seriously here. Flakeref-based scanning is strongly preferable for supply-chain-quality SBOMs. Given a flakeref, sbomnix pulls in Nixpkgs descriptions, licenses, maintainers, homepage URLs, Package URL identifiers from derivation metadata, and CPE identifiers, turning a name-version list into an auditable SBOM.
sbomnix does not travel alone. The same repository ships nixgraph for querying and visualizing dependency graphs, vulnxscan as a working demonstration of vulnerability scanning with an SBOM as input, and nix_outdated, which flags stale dependencies and ranks them by how many other packages depend on them, a genuinely useful prioritization signal. There is also tooling for producing SLSA provenance attestations from Nix flakes and derivations.
Choosing between Syft and sbomnix for a given Nix SBOM workflow
Neither tool is the universally right answer, and the choice mostly comes down to what the SBOM needs to prove.
Reach for Syft when the target is a container image or OCI artifact built from Nix, since its image-scanning pipeline covers OCI, Docker, and Singularity Image Format archives and does so quickly. It's also the practical choice when a CI pipeline already runs Syft against non-Nix artifacts and adding a separate tool for Nix paths is not worth the operational overhead. If the deliverable is a standard-format compliance SBOM, SPDX or CycloneDX, and nobody has stated a requirement for full build-time closure, Syft's speed and offline operation make it the lower-friction option, with no build step required.
sbomnix earns its place when the primary target is a flake reference, since that's what unlocks the full Nixpkgs metadata enrichment described above.
Syft SBOM generation can be run against Nix store paths. Together they cover both declared intent and runtime reality, which is a distinction neither tool captures alone. Whichever combination a team lands on, the flakeref-versus-store-path choice for sbomnix carries through: feed it a raw store path and the metadata enrichment disappears, leaving an output that looks a lot like what Syft would have produced anyway. A flakeref is always the stronger input. CPE identifiers and PURL fields are required for downstream vulnerability matching. Dependency graph visualization (nixgraph) or SLSA provenance attestation is part of the workflow.
Fitting Nix SBOM generation into a CI/CD pipeline and keeping SBOMs current
There's a gap between generating an SBOM once and generating one that stays useful. Omdia's survey found that 73% of organizations that generate SBOMs say they enable more efficient vulnerability mitigation, yet 86% still find generation challenging, and more than half generate SBOMs only case-by-case. A document produced once for an audit and then filed away cannot answer the question that actually matters day to day: is this system affected by the CVE disclosed this morning?
Wiring Syft into a pipeline is fairly direct. On the GitHub side, an action can generate SBOMs automatically and attach them to a release the moment a release event fires, provided the workflow has actions: read and contents: write permissions set. On GitLab, Syft runs as a job stage against the built container image, with the resulting SBOM stored as a pipeline artifact. Pairing it with Grype in that same pipeline means the SBOM gets generated once and the vulnerability scan re-runs against it repeatedly as CVE databases update, without ever needing to rebuild the image.
For Nix-native pipelines, the better point of insertion is running sbomnix against the flakeref as part of the Nix build step, before the artifact is packaged (this ensures build-time dependencies are captured before they are stripped from the runtime closure). SBOMs can also be attached to container images as signed attestations using Cosign, with Syft generating the document and Cosign handling the signing and attaching, so downstream consumers can verify the attestation without ever re-scanning the artifact themselves.
flake.lock is what makes this whole pipeline durable rather than a one-off exercise. It records the exact commit hash of every external input the flake depends on, so a change to that lock file is a legible, reviewable event, one that can trigger a pipeline to regenerate the SBOM automatically and keep the inventory in sync with the declared dependency graph. That works because of a property Nix guarantees at its core: the same inputs always produce the same store paths, so an SBOM generated from a locked flake is deterministic. Running it twice against the same lock file produces identical output. A diff between two SBOM versions actually means something and can be audited on its own terms.
Reproducible environments' contribution to SBOM accuracy beyond tooling alone
None of the tooling above solves a problem that sits further upstream. Conventional package managers work with version ranges, transitive dependencies that resolve differently depending on what else is installed, and system libraries that vary machine to machine. Two developers on the same project can end up with genuinely different packages installed, so their SBOMs describe different software even though both call it the same project.
Nix's core guarantee cuts that problem off at the source: the same inputs always produce the same outputs, so two developers on the same platform working from the same locked Nix configuration end up with identical store paths, and therefore identical SBOMs. An SBOM generated from a flake-locked Nix environment reflects what actually runs in CI, in staging, and in production, matching those environments exactly rather than approximating them. That drift is not a cosmetic annoyance either. If different team members run different store paths under the same project name, the SBOM generated by one member does not describe what another member ships, and the drift stays invisible until an incident exposes it.
Regulated industries, financial services and healthcare especially, have particular reason to care about this. A content-addressed store path functions as something closer to cryptographic evidence of what software ran in production, rather than a claim someone has to take on faith. Getting there in practice depends on a declarative environment manifest: a flake.nix and its accompanying flake.lock, checked into version control and readable by anyone on the team. That manifest is the thing that makes Nix's reproducibility guarantee an organizational property rather than an individual habit, because it's the single file that CI, new contributors, and production all pull from. Tooling can catalog what's on disk. Only a manifest like that guarantees the disk holds what everyone agreed it should.


