SBOMs
Every hardened image comes with a CycloneDX SBOM. This page covers what it covers, how to get it, and the one rule you have to follow to keep it honest.
What it covers
The SBOM builder (lib/sbom, ADR-0036) wraps bombon to produce a complete
inventory:
- The Nix runtime closure — every store path the image needs at runtime, the same walk sbomnix did (ADR-0015).
- The language-ecosystem dependencies that Nix vendors into a single store path and that a closure-only walk therefore misses: Go modules plus the Go stdlib, Rust crates, Ruby gems, Python wheels/sdists, and yarn/npm packages.
That second half is the part that matters for vulnerability matching. A CVE against a specific gem, crate, module, or wheel only shows up if that dependency is named in the SBOM — a closure-only SBOM can’t match it, so the scan gate would silently miss it. See VEX & vulnerability scanning.
How to get it
In pure Nix, the SBOM hangs off the image’s passthru:
$ nix build .#my-service-image.passthru.sbom
mkHardenedImage (and therefore mkSecureArtifact) attaches it by default
(sbom = true). Set sbom = false to skip it, in which case passthru.sbom
is absent.
In CI, the component’s hardened:sbom job builds passthru.sbom. Under
slsa-l1 it substitutes from the build job’s cached closure where it can, and
builds or fetches anything missing. Under slsa-l3 it runs on the shared
runner with --max-jobs 0 (substitute-only — the closure comes from the offline
build’s Attic push, never a rebuild), copies out the *.cdx.json, and — when
sign = true — signs it. See
Integrate an external project.
Auto-detection vs the sbomEcosystems override
The builder auto-detects go and rust from robust markers on the finished
derivation (goModules, cargoDeps) and consumes them without rebuilding.
Ruby and yarn/npm can’t be auto-detected — their inventory lives in a
gemset.nix or a yarn.lock, not in anything readable off the built
derivation. For those you declare the ecosystem explicitly with
sbomEcosystems, which is threaded onto the package before imaging:
hb.mkHardenedImage {
name = "my-rails-app";
version = "3.1.0";
package = myRailsApp;
sbomEcosystems = [
{ kind = "ruby"; gemset = ./gemset.nix; }
{ kind = "yarn"; yarnLock = [ ./yarn.lock ]; }
];
}
The no-IFD vendored-path rule
Every lockfile, gemset, or yarn.lock path in sbomEcosystems must be a
vendored relative path (./gemset.nix, ./yarn.lock) — never an interpolated
read out of a fetched source like "${src}/yarn.lock". Reading a lock file out
of a fetched derivation is an import-from-derivation, which nix flake check
cannot evaluate and the repo’s no-ifd-lockfile lint rejects. Copy the
lock/gemset into your repository and reference it relatively.
This is the same discipline the Rust cargoLock, Python lock, and Ruby
gemset args follow throughout the ecosystem emitters — see those pages for the
per-ecosystem detail.
Reference
The design and rationale (why bombon, why our own consuming emitters rather than bombon’s rebuild-the-package emitters) are in ADR-0036.