DEV Community

kennedy
kennedy

Posted on

Your SBOM diff is lying to you - how we made SBOM generation byte-for-byte reproducible

If you've ever diffed two SBOMs of the same codebase - before and after a dependency bump, two CI runs, two laptops - you've seen the lie: a wall of changes that aren't changes. Timestamps move. Serial numbers rotate. Keys shuffle. The components didn't move; the document did.

The problem with noisy SBOM diffs

The CycloneDX and SPDX standards let generators stamp created with the wall-clock and serialNumber with a fresh UUID on every run. So two runs over identical inputs produce different bytes - and your "SBOM diff" flags changes that don't exist. Reviewers learn to rubber-stamp the diff, and the one time it hides a real change, nobody looks.

Deterministic generation makes the diff mean something:

same input → same bytes → every difference is a real difference.

How SnapSBOM makes output byte-for-byte reproducible

  • Pinned timestamp - created / creationInfo.created is fixed to 2026-01-01T00:00:00Z, never the wall clock.
  • No serial numbers or UUIDs, anywhere.
  • Canonical ordering + normalized serialization - components sorted stably, keys emitted in a fixed order.
  • Lockfile-only input - it reads package-lock.json and nothing else. No network. No node_modules. No calls to npm. The bytes you get in Berlin equal the bytes you get in Denver.

Terminal demo

But deterministic ≠ correct - the proof

We don't ask you to trust us; we ask you to run the checks:

Check Result
Official CycloneDX 1.6 JSON Schema (via ajv) ✅ valid
Official SPDX 2.3 JSON Schema (via ajv) ✅ valid
Syft 1.55.0 - converts our CycloneDX → SPDX and SPDX → CycloneDX ✅ all 66 components round-trip
Trivy 0.76.0 - vulnerability scan of both our documents ✅ both formats detected, 66/66 packages ingested
@cyclonedx/cyclonedx-npm 6.0.1 cross-check ✅ identical package set, 0 purl differences
Re-run SnapSBOM on the same lockfile ✅ byte-identical
Re-run Syft / cyclonedx-npm on the same input ❌ timestamps and serials churn

Three independent implementations - Syft, Trivy, and cyclonedx-npm - converge on the same 66 packages. Our output is valid because the specification says so, not because we say so.

Reproduce it in 60 seconds

npx snapsbom . --format cyclonedx --output a.json
npx snapsbom . --format cyclonedx --output b.json
diff a.json b.json && echo "byte-identical"

trivy sbom a.json          # ingests our CycloneDX
syft convert a.json -o spdx-json=converted.spdx.json
Enter fullscreen mode Exit fullscreen mode

Honest scope
SnapSBOM guarantees correctness for npm package-lock.json v1-v3 and says so plainly - pnpm, Yarn, and Python are on the roadmap, one parser at a time.
MIT-licensed, written in Rust, single binary.

Top comments (0)