Reading .Drv Files for SBOM Component Identification
Nix's.drv files provide the complete, tamper-evident dependency record SBOMs actually need.

A.drv file answers the question every SBOM tool eventually has to face: what, exactly, went into building this thing? It's the low-level build instruction Nix writes to the store once a Nix expression gets evaluated, roughly the same role a.o file plays in C compilation. Everything a build needs, the compilers, the shared libraries, the exact patch sets, gets pinned down in that file before a single line of code compiles. That precision is the whole case for reading.drv files directly when building a software bill of materials, instead of trusting a summary of what a package is supposed to contain. Reading the.nix expression instead of the.drv is a mistake SBOM tooling can fall into, and it's a reason bills of materials can misrepresent what actually got built.
Every.drv carries a fixed set of required fields. It names a system target such as x86_64-linux, a path to the builder executable (often just /bin/sh), a list of string arguments passed to that executable, and a key/value map of environment variables. Put together, those four fields describe an exec syscall. That's it. That's how Nix actually runs a build under the hood. Each.drv gets stored at a hashed path, something like /nix/store/m2nb4d0pfydr8bq5ww1yqbrkvvf18zbl-perl-5.36.0.drv, and any change to the derivation, a version bump, a patch, a flag, produces a new hash and a new path. Derivations reference each other too, either by path or by named output, and those references chain into a directed acyclic graph. Because every path in that graph embeds a hash of its own contents and inputs, the whole structure is a Merkle tree. That property, more than any single feature, is what makes the.drv graph a complete and tamper-evident record of a dependency graph. Change one input anywhere in the tree and the hash changes all the way up. Nothing gets swapped out without leaving a trace.
How the.drv closure captures build-time versus runtime dependencies, and why the difference matters for SBOMs
Nix works with two closures, and mixing them up is the second most common mistake in this space. The build-time closure is the complete set of store paths Nix needs to reproduce a build: compilers, build tools, standard libraries, bootstrap infrastructure, all of it. Computing this closure only takes evaluating the.drv. The package doesn't need to be built first, which matters for anyone trying to generate SBOMs early in a pipeline instead of waiting on a slow build to finish.
The scale catches people off guard almost every time. Even a minimal hello-world C program pulls in over 150 packages once you count gcc, stdenv, glibc, bash, and their transitive dependencies. A one-line "hello world" carries a dependency footprint most developers never see, because traditional package managers hide the toolchain behind an assumption that it's already sitting on the machine. Nix doesn't get to make that assumption. It has to name everything, every time.
The runtime closure is narrower: the subset of build-time dependencies whose store paths actually show up in the built output, found by scanning the output after the build finishes. That "after" is unavoidable. Runtime dependency information only exists once the build has run, so unlike the build-time closure, there's no way to get a runtime SBOM without building the target first.
This split has a direct SBOM consequence, and picking the wrong one for the job hands someone a document that answers a question nobody asked. A build-time SBOM, pulled from the.drv closure, documents every tool used to compile the software, including things that never ship in the final artifact. A runtime SBOM documents only what the final artifact needs to run. An auditor checking for vulnerable build tooling wants the build-time view; a security team scanning a running container wants the runtime view, and handing either of them the wrong document wastes their time and yours. Static SBOMs generated outside this model, by scanning source repos or lockfiles without walking an actual build graph, routinely miss transitive dependencies, silent package substitutions, or components pulled from a compromised registry. The.drv closure removes that ambiguity by construction, because the graph it describes is the graph Nix actually built from, not a guess at one.
The regulatory and standards context that makes precise SBOM components a requirement, not a nice-to-have
None of this would matter much if nobody asked for it. Regulators increasingly do. U.S. Executive Order 14028 set the expectation that SBOMs would accompany software sold to the federal government, and that expectation turned into concrete procurement language through subsequent NIST, NTIA, and OMB guidance. EO 14144, signed in January 2025, along with OMB Memorandum M-26-05 in January 2026, gave federal agencies more room to decide how strictly to enforce these requirements, though many agencies still require SBOMs and attestations as part of procurement.
The baseline itself changed too. CISA's 2026 minimum elements replaced the NTIA's 2021 baseline on July 29, 2026, adding ten new required data fields on top of the 2021 version, including component hashes, license data, and metadata about the SBOM document itself. The update runs deeper than cosmetics. Component hashes and provenance metadata are exactly the kind of information a.drv graph already encodes by design, since every store path is a hash of its own build inputs. A regulatory requirement and a technical capability line up here almost perfectly, and treating them as separate problems is a mistake.
Outside a certain region, one jurisdiction's Cyber Resilience Act makes SBOMs a legal requirement for products with digital elements, and its NIS2 Directive requires supply chain security measures that SBOMs help demonstrate. The FDA requires SBOMs for what it calls "cyber devices" under Section 524B of the FD&C Act, and strongly recommends them for other software-containing medical devices.
Three formats dominate: SPDX, CycloneDX, and SWID tags. CycloneDX sits at version 1.7 as of October 2025, adopted as ECMA-424 2nd Edition in December 2025, the last release expected in the 1.x line, and it stays backward compatible with 1.4 through 1.6. It has also grown well past software SBOMs into hardware (HBOM), services (SaaSBOM), machine learning (ML-BOM), cryptography (CBOM), and attestations (CDXA). CycloneDX has wide adoption in vulnerability management tooling. SPDX has wide adoption in license-compliance workflows, and GitHub generates SPDX natively straight from its dependency graph. Neither format is going away, and most tools that read.drv files output both, so picking one over the other rarely means giving anything up.
Why.drv-level analysis is more accurate than reading.nix expressions, and what it costs
Nix SBOM tools generally operate at one of two levels, and the trade-off between them shapes almost everything else in this space. At the.nix level, a tool reads the Nix language expression before it's evaluated. That gives access to the meta block, descriptions, licenses, maintainers, homepage URLs, none of which survives into the evaluated derivation. At the.drv level, a tool reads the store derivation after evaluation. That gives the complete dependency graph, but the meta block is already gone.
Don't trust a tool that stops at the.nix level. It has a real blind spot: it can't reliably find dependency relations built through string interpolation, a pattern that shows up constantly in real Nix code. A derivation reference buried inside an interpolated string just isn't visible to a tool parsing the expression syntactically. So many.nix-level tools fall back to parsing.drv files anyway to catch these cases, which quietly gives up the metadata advantage that was the whole reason to work at the.nix level in the first place.
The.drv level has its own cost. Since the metadata is gone, reconstructing it means a second lookup, usually against nixpkgs metadata, and that lookup isn't always possible if nobody knows which nixpkgs revision produced a given store path. Sandboxed SBOM generation closes part of this gap: per the cyberus-technology/nix-sbom-helper project, an SBOM generated inside the sandbox is the most accurate available, because it uses the exact.drv files from the exact derivations that produced the inputs in question, not a reconstruction after the fact.
Chase graph accuracy, chase metadata richness, or pay the cost of doing both. That's the design question every tool in this space has to answer one way or another, and the honest answer is that.drv-level accuracy should win whenever the two conflict, because a rich but wrong dependency list is worse than a sparse but correct one. There's a further wrinkle that neither level fully solves on its own. Language ecosystems like Rust and Go vendor their own dependencies, meaning those dependencies aren't Nix derivations at all and don't show up in the.drv graph. Handling them takes a separate mechanism regardless of which level a tool works at.
The current tooling landscape for extracting SBOM components from.drv files
A handful of open-source projects have staked out different positions on these trade-offs.
tiiuae/sbomnix (162 GitHub stars as of October 2025) is less a single tool than a suite: sbomnix for SBOM generation, nixgraph for dependency graph visualization, nixmeta, vulnxscan for vulnerability scanning, provenance for generating SLSA v1.0 compliant attestation files for any flake or derivation, nix_outdated for tracking outdated dependencies by downstream impact, and Repology.org clients (repology_cli, repology_cve). It works at the.drv level and attempts metadata reconstruction, supports pruning to build-time or runtime scope via a --buildtime flag, and accepts flake references, store paths, or result symlinks as targets. Flake references unlock nixpkgs metadata enrichment; store paths skip it. Output covers both CycloneDX and SPDX, and sbomnix publishes SBOMs for its own releases as release assets. The project came out of the Ghaf Framework at tiiuae.
nikstur/bombon generates CycloneDX v1.5 SBOMs for Nix packages and targets compliance with Germany's BSI Technical Guideline TR-03183 and EO 14028. Its buildBom function accepts extraPaths for pulling in additional store paths (useful for container images that discard references), a boolean includeBuildtimeDependencies, and regex-based excludes. Bombon handles vendored dependencies through a passthruVendoredSbom mechanism: .rust runs cargo-cyclonedx, .npm covers npm dependencies fetched via fetchNpmDeps/npmConfigHook/buildNpmPackage, and .go pulls Go modules straight from build info baked into compiled binaries. Notably, the Go vendored SBOM path doesn't accept includeBuildtimeDependencies, since it's generated from information already inside a compiled binary rather than from the derivation graph.
louib/nix2sbom is a command-line utility that extracts SBOMs from Nix derivations, outputting both CycloneDX and SPDX. It reconstructs package hierarchy, identifies source origins including git URLs and mirrors, and tracks patches applied during the build. The stated goal is a transparent view of the supply chain inside a Nix environment.
tweag/genealogos generates Nix SBOMs using a separate tool called nixtract for dependency introspection, producing a hierarchical representation of build and runtime dependencies together.
cyberus-technology/nix-sbom-helper produces SBOM output for both standard Nix and flakes through a buildSbom function, outputting CycloneDX JSON, SPDX JSON, and CSV all at once. A run against hello-2.12.2 generates all three formats in a single pass. As already noted, its sandboxed generation mode is what the project's own documentation calls the most accurate option, since it draws from the exact.drv files behind the exact derivations used.
Across all five, the same trade-off from the previous section keeps showing up in different clothes. sbomnix leans toward metadata reconstruction through flake references. bombon leans toward compliance-format precision and explicit vendored-dependency bridges. nix2sbom leans toward provenance detail like patch tracking. genealogos leans toward hierarchical clarity between build and runtime scope. nix-sbom-helper leans hardest into sandbox-verified accuracy. None of them make the trade-off disappear, and none should be picked on reputation alone. Pick based on which side of the trade-off your audit actually needs.
What.drv files alone cannot prove, and where provenance attestation fills the gap
A derivation hash proves one thing clearly: consistency with a particular Nix evaluation. Same inputs, same.drv, same hash, every time. What it does not prove is that the evaluator, the builder, or the local toolchain that ran the build actually behaved honestly. A compromised build host can still tamper with the build process, or swap the output after evaluation but before it lands in the store. The hash alone can't catch that, and treating a matching hash as proof of a clean build is the third mistake worth naming here.
The point is worth stating directly: Nix offers strong input and environment control and can serve as part of a broader provenance strategy, but it does not by itself provide hardware-rooted evidence that a specific artifact came from a specific build execution. The.drv graph tells you what should have happened. It doesn't, on its own, tell you what did happen on the machine that ran it.
Sandboxing narrows that gap somewhat. Nix's build sandbox blocks network access and filesystem access outside the store, which stops a build from quietly pulling something in from the host machine that isn't declared as an input. That's a real supply chain security property, even without any hardware-level attestation behind it. SLSA provenance attestation, of the kind sbomnix's provenance tool generates in SLSA v1.0 compliant form, addresses what's left: it records source, dependencies, toolchain, and build recipe, and binds all of it to a specific, verifiable build execution.
Put the three together, the.drv closure for component completeness, the sandbox for build isolation, and SLSA attestation for execution integrity, and that's what a genuinely complete supply chain claim requires. Any one of the three alone is a partial answer dressed up as a full one. Reproducible builds push this further still, by gathering source materials into a full "source closure" that makes an independent, offline rebuild possible. That's a strong, independently verifiable form of supply chain integrity, but it comes at a cost: the target actually has to be built somewhere to prove it, not just evaluated.
Putting it into practice: reading.drv files systematically across the software lifecycle
Timing matters more than it looks. Build-time SBOMs can be generated right at evaluation, with no build required, since the.drv closure is fully known the moment the derivation is instantiated. Runtime SBOMs have to wait until after the build completes, since that closure is a subset found by scanning the actual output. The build-time closure is always a superset of the runtime one: everything that touched the build, versus what actually shipped.
In CI/CD, this timing works in a team's favor. A.drv file exists after evaluation and before realization (the actual build step), so generating a build-time SBOM at that point adds essentially no extra overhead to the pipeline. Onboarding runs on the same mechanics: a reproducible environment locked to a flake.lock means every developer machine and every CI runner evaluates the identical derivations, which means they produce identical.drv files, which means SBOM consistency falls out of environment consistency instead of needing separate enforcement. The same logic covers AI coding agents operating inside a Nix-managed environment, too. Their dependency footprint gets captured by the same.drv graph and checked by the same tooling as anything a human developer touches, since the environment itself doesn't tell the difference between the two.
The vendored dependency problem doesn't go away on its own, and skipping it is the fourth mistake. Rust, Go, and JavaScript projects need their own ecosystem-native tools, cargo-cyclonedx, cyclonedx-gomod, npm's own SBOM support, bridged into the Nix-level SBOM through something like bombon's passthruVendoredSbom mechanism. Leave this step out and the resulting bill of materials has a real hole in it, since vendored code by definition isn't a Nix derivation and never shows up in the.drv graph on its own.
Format choice, in practice, can mostly wait. If vulnerability management and triage based on a vulnerability exchange format is the main use case, CycloneDX 1.7 fits more directly with the tooling built for that job. If license compliance or GitHub-native dependency review matters more, SPDX is the stronger fit. Since most of the tools covered here output both formats from the same scan, the decision doesn't need to happen upfront.
Environment drift is an SBOM accuracy problem, not some separate operational headache to file away for later. If development, CI, and production aren't pinned to the same derivations, the SBOM generated at build time may simply not describe what's running in production, no matter how carefully it was built. Reproducible environments aren't a nice-to-have layered on top of SBOM tooling. They're the precondition for the SBOM being true in the first place. And that's exactly where the 2026 requirements for component hashes and related metadata meet the.drv graph on its own terms: a hash-addressed, tree-structured dependency tree satisfies the requirement almost by accident, because that's simply what a complete and tamper-evident build record looks like.


