Validating Nix-Generated SBOMs Against CycloneDX and SPDX Schemas
Nix's pure-function model produces SBOMs that need careful schema mapping to validate correctly.

Nix's dependency model already produces some of the most complete dependency data available in any build system, but that completeness only matters if the SBOM it generates actually validates against the schema an auditor or a piece of downstream tooling expects. That's the practical problem this piece works through, and it starts with why the pressure is on right now. On the U.S. side the picture is messier: EO 14028 is still in force, but EO 14306, signed in June 2025, walked back the enhanced attestation mandates that EO 14144 had added, and agencies now have more room to decide how hard to push. OMB Memorandum M-26-05, from January 2026, handed agencies that discretion explicitly, yet in practice many of them are still asking for SBOMs and attestations at procurement time, so the obligation hasn't gone away so much as it's gone quiet.
CISA hasn't sat still either. Its draft update to the minimum elements guidance, published in August 2025, pushes past the 2021 NTIA baseline (supplier, component name, version, unique identifiers, dependency relationships, author, timestamp) and adds fields around provenance and authenticity, tying the SBOM more tightly into cybersecurity workflows generally. South Korea announced its own SBOM requirement for public-sector IT systems in October 2025, targeting 2027, a timeline that aligns with the CRA's.
For Nix teams specifically, the CRA's actual text determines compliance obligations, not the headlines about it SPDX CycloneDX standards – Open Source Security Foundation. Crucially, the SBOM doesn't need to be published. It needs to be produced on request, correctly, for an authority that will actually try to parse it SPDX CycloneDX standards – Open Source Security Foundation. That non-publication clause changes the calculus: nobody's grading this on public perception. But a schema-invalid document handed to a regulator during an audit isn't a paperwork nuisance, it's a finding. A Nix-generated SBOM that fails validation is a liability waiting for the one moment someone asks to see it. It's a liability waiting for the one moment someone asks to see it.
What CycloneDX 1.7 and SPDX 3.0.1 require in 2026
The two schemas a Nix SBOM needs to satisfy have both moved recently, and neither move was cosmetic. CycloneDX is version 1.7 as of October 2025, adopted that December as ECMA-424 2nd Edition, and it's the last release planned in the 1.x line, still backward compatible all the way to 1.4 CycloneDX vs SPDX: Which SBOM Format to Choose in 2026 | Interlynk. Across that line the format quietly stopped being just a software SBOM spec. An API-first CycloneDX 2.0 is the next thing the project has signaled publicly, which matters for anyone deciding how deep to invest in current tooling.
SPDX took a more structural leap. Version 3.0.1 belongs to the 3.0 line that shipped in April 2024, with a 3.1 release candidate already in progress, and it rebuilt the format around an element-based model, packages, files, and snippets as independent nodes linked by relationships, replacing the older document-centric design of the 2.x series CycloneDX vs SPDX: Which SBOM Format to Choose in 2026 | Interlynk. That's a bigger change than it sounds.
None of that changes what a CRA-compliant pipeline actually needs at the field level. Every component needs a name, a version, and a supplier; every component needs at least a SHA-256 hash; licenses need to be expressed as SPDX identifiers rather than free text; dependency coverage needs to reach at least the top level, with transitive coverage treated as best practice rather than a hard floor; and every package needs a PURL. Choosing which schema to generate mostly comes down to the audience on the other end. Security tooling, VEX documents, SCA platforms, and systems like Dependency-Track want CycloneDX. License review, open source disclosure, procurement, and OSPO workflows lean SPDX. Generating both isn't expensive with current tooling, and covering both audiences is usually worth doing, but each should come from the same real build metadata rather than one being converted into the other after the fact, since conversion has a way of quietly dropping fields nobody notices until an auditor does. CycloneDX grew across the 1.x line from a software SBOM format into a general bill of materials standard (HBOM, SaaSBOM, ML-BOM, CBOM, and formal attestations (CDXA)); version 1.6 introduced CBOM and CycloneDX Attestations; version 1.7 added citations for verifiable provenance, patent metadata, and Traffic Light Protocol distribution markers CycloneDX vs SPDX: Which SBOM Format to Choose in 2026 | Interlynk. A critical nuance is that SPDX 2.3 is still widely emitted by build tooling, and the ISO standard (ISO/IEC 5962:2021) codifies SPDX 2.2.1, not 3.0.1 (so when a requirement cites "ISO/IEC 5962:2021," it names 2.2.1, and teams needing SPDX 3.0.1 specifically are in early-adopter territory with limited toolchain support) CycloneDX vs SPDX: Which SBOM Format to Choose in 2026 | Interlynk. Interlynk's format comparison finds that only Microsoft's sbom-tool has meaningful SPDX 3.0 support, and practically, most Nix tooling targets SPDX 2.3 CycloneDX vs SPDX: Which SBOM Format to Choose in 2026 | Interlynk.
Why Nix's dependency model produces structurally different SBOM content
Nix treats packages as pure functions of their inputs. Combined with derivation sandboxing and content-addressed storage, that means provenance is a property of the build itself. That's a genuinely different starting point than most SBOM generation, where a scanner looks at a finished artifact and tries to reconstruct what went into it. Flakes push this further: flake.lock pins the exact commit hash of every external input, so the same lockfile produces the same build, byte for byte, every time. The SBOM is effectively self-validating against the thing it describes.
The scale involved isn't small. Nixpkgs holds more than 122,000 packages as of January 2025, and it keeps a higher share of those packages current than any other package repository tracked in that comparison github.com. That's a lot of surface area for a single SBOM to potentially cover, and getting the schema mapping right affects accuracy across that whole footprint in a way narrower ecosystems don't have to contend with.
The part of Nix's model with no real analogue elsewhere is the split between build-time and runtime closures. The build-time closure is everything referenced in the derivation's.drv file, compilers, build tools, standard libraries, bootstrap infrastructure, and it can be computed just by evaluating the derivation, without building anything. Even a trivial hello-world program in C drags in gcc, stdenv, glibc, and bash as part of that closure. The runtime closure is narrower: it's the subset of those build-time dependencies that the finished output actually references once it's built, and Nix determines this by scanning the built output for store path references. The target has to be built first to compute it. Most build systems don't draw this line at all, so there's no shared convention for how an SBOM should represent it, and that ambiguity becomes a schema-level question the moment someone tries to validate the document: which closure does this SBOM claim to represent, and does the metadata actually say so?
Store path identifiers (/nix/store/<hash>-<name>-<version>) are the canonical Nix reference (they are not PURLs, are not CPEs, and schema validators have no native understanding of them); they must be translated into standard identifiers. And the derivation graph itself carries more metadata than CycloneDX or SPDX fields were designed to receive, so mapping it onto component properties or package fields takes deliberate engineering. The payoff for getting this right is that post-hoc scanners applied to container images or source trees can miss transitive dependencies, unnoticed package substitutions, or components pulled from a compromised registry, and Nix's closure-based approach makes exactly those problems visible, but only when the resulting SBOM is generated correctly and actually passes validation against the schema it targets.
The specific schema compliance friction points Nix SBOM output creates
Most of the friction traces back to the gap between Nix's internal vocabulary and the controlled vocabularies CycloneDX and SPDX expect. PURL generation is the clearest case: Nix store paths don't map to any standard PURL type on their own, and building a valid pkg:nix or pkg:nixpkgs identifier requires knowing which version of nixpkgs actually produced the package. CPE generation runs into a similar wall from a different angle: CPEs expect a structured vendor, product, and version string, and Nix package names don't follow a naming convention uniform enough to map cleanly onto that structure, so tools generating CPEs from Nix metadata end up leaning on heuristics, and heuristics produce false positives and unresolvable identifiers often enough to matter.
The runtime versus build-time closure question isn't just conceptually interesting, it's a concrete compliance risk. CRA minimum elements call for transitive dependency coverage, and a runtime-only SBOM, while smaller and adequate for plenty of security use cases, may not satisfy an auditor who expects the full transitive graph. Whichever closure a team chooses to generate, it has to be declared clearly in the SBOM's own metadata, or the document reads as ambiguous to anyone trying to assess it against a checklist. Hash requirements bring a subtler version of the same mismatch: CRA-compliant pipelines want a SHA-256 hash for every component, and Nix store paths are content-addressed, so they do carry hash information, but that hash encodes the derivation's inputs rather than the SHA-256 of the component artifact considered on its own. A validator looking for a standard hash field in the expected place can flag these as missing or malformed even though the underlying data is, in Nix's own terms, what it should be.
License metadata has its own gap. Schema validators expect SPDX license identifiers drawn from a controlled list, and while Nixpkgs does carry license metadata for most packages, coverage isn't universal, and some packages carry free-form license strings that were never meant to satisfy a strict validator. The supplier field causes a comparable failure: CRA minimum elements and CISA's updated guidance both want a supplier per component, and Nix derivations track maintainer information, which isn't the same thing, so missing or non-standard supplier values are a recurring cause of downstream validation failures. Version strings add a smaller but persistent nuisance, since Nix versions sometimes include commit hashes, date stamps, or build suffixes that are perfectly meaningful inside Nix but can fail a schema's pattern validation or confuse a version-comparison tool downstream. And all of this gets worse at the source: when the SBOM target is a bare store path rather than a flake reference, sbomnix has no way to resolve which nixpkgs version produced the package, so it skips metadata enrichment entirely, and the resulting document is thin enough to fail almost any validator with real expectations.
The Nix SBOM toolchain: what each tool generates, validates, and covers
sbomnix, maintained under the tiiuae/sbomnix repository and released under the Apache License 2.0, is the closest thing to a reference generator for this problem. It accepts either a flake reference or a store path, works out the dependency graph, and exports the result as CycloneDX, SPDX, or CSV. Pointing it at a flake reference pulls in Nixpkgs descriptions, licenses, maintainers, and homepage data, and this path actually produces schema-compliant metadata rather than a bare skeleton. A companion project, ghafscan (also called flakevuln), demonstrates daily automated vulnerability scanning for a Nix flake, packaged as both a reusable GitHub Action and a local CLI. Notably, sbomnix's own releases ship with CycloneDX and SPDX SBOMs in the release assets, which is a reasonable proof that the tool trusts its own output enough to use it on itself.
Nix-SBOM, listed on the CycloneDX Tool Center, takes a different approach for large closures. Built on top of Nixtract, it generates CycloneDX SBOMs directly from Nix expressions without building the derivations at all. That matters practically: building every derivation in a large closure just to describe it is often prohibitively expensive, and skipping that step is a real efficiency gain rather than a shortcut that sacrifices accuracy. FlakeBOM, built by Determinate Systems, generates CycloneDX SBOMs from any flake with fairly thorough metadata and detection of vendored dependencies. Determinate Systems has obtained FedRAMP High Authorization for FlakeHub, which is relevant for federal procurement contexts where agency requirements for SBOMs persist post-EO 14306.
It supports every CycloneDX spec version from 1.4 through 1.7, and its built-in validation suite goes well beyond a basic schema check: it does deep validation of metadata, PURL, bom-ref, and property consistency, and applies compliance rule packs for OWASP SCVS (all 87 controls across L1/L2/L3) and for EU CRA SBOM expectations, covering 8 controls CycloneDX vs SPDX: Which SBOM Format to Choose in 2026 | Interlynk cyclonedx.org. Run as a second pass over sbomnix output, cdxgen is a practical way to gate a document against CRA requirements before it ever reaches an auditor. Its resource command inspects component types against OWASP SCVS maturity models, its report commands let someone extract, filter, list, and summarize component, service, license, resource, and vulnerability data with SQL-like query syntax, outputting in txt, csv, markdown, and JSON, and it supports trim, patch (IETF RFC 6902), and diff operations on BOM documents, useful for fixing specific field failures without regenerating the whole SBOM. The CycloneDX Tool Center lists 314 tools in total, and the overwhelming majority are specific to some other language or platform; these five are the ones that actually matter for anyone working from a Nix build CycloneDX Tool Center | CycloneDX. It supports PURL identifiers exposed through Nix derivation metadata and generates CPE identifiers for identified packages. Its main outputs are sbom.cdx.json (CycloneDX) and sbom.spdx.json (SPDX). Companion tools in the same repository include nixgraph (dependency graph visualization), vulnxscan (vulnerability scanning using generated SBOMs), nix_outdated (identifies and prioritizes outdated dependencies), repology_cli and repology_cve (Repology.org clients), and provenance (SLSA v1.0 compliant provenance attestations for any Nix flake or derivation). cdxgen (CycloneDX Tool Center) is among the tools covered. It runs as a CLI, Node.js library, REPL, or HTTP server; detects languages and package managers; resolves dependencies; and outputs a CycloneDX BOM in JSON. It supports custom JSON schema variants for teams needing organization-specific compliance rules beyond what official schemas require.
A practical generate-validate-fix-publish workflow for Nix SBOM output
Generation starts with a decision that shapes everything downstream: always point sbomnix at a flake reference rather than a bare store path, because only the flake reference lets it resolve the nixpkgs version and pull in the license, maintainer, and homepage metadata that a validator will actually look for. A store path might be faster to hand it, but the resulting SBOM will be thin in exactly the fields CRA compliance cares about.
At the same moment, decide explicitly if the document represents the runtime closure or the full build-time closure, and make that declaration visible in the SBOM's own metadata rather than leaving it implicit. sbomnix defaults to runtime dependencies, and reaching the full build-time closure means adding the --buildtime flag, which has the advantage of not requiring the target to be built first, since the build-time closure can be computed from evaluation alone. Whichever choice gets made, an auditor working from CRA minimum elements is going to ask which closure they're looking at, and a document that doesn't say is a document that invites a dispute.
Validation is where cdxgen and sbom-utility earn their place in the pipeline.
Fixing what validation reveals rarely means starting over. sbom-utility's patch operation, built on the IETF RFC 6902 patch standard, lets a team correct one field, a missing supplier, a malformed hash, a non-standard license string, directly in the BOM document, and its diff and trim commands make it possible to see what changed and keep the document lean. That's a meaningfully different workflow than regenerating the whole SBOM from sbomnix every time a single field fails, and it matters because Nix's rich derivation metadata means a lot of the correct data does exist somewhere in the pipeline; the fix is often translation, not reconstruction.
Publishing, in the CRA's specific sense, doesn't mean putting the document on a public registry. Retention alone is not enough if the document that gets pulled off the shelf hasn't been re-validated since the last dependency bump. Step 1 is to generate from the right target. Teams should decide at generation time whether the SBOM represents runtime or build-.
Sources
- CycloneDX Tool Center | CycloneDX
- SPDX CycloneDX standards – Open Source Security Foundation
- CycloneDX vs SPDX: Which SBOM Format to Choose in 2026 | Interlynk
- GitHub - tiiuae/sbomnix: SBOM, provenance, dependency graph, and vulnerability tools for Nix.
- GitHub - CycloneDX/sbom-utility: Utility that provides an API platform for validating, querying and managing BOM data
- ecma-international.org


