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.createdis fixed to2026-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.jsonand nothing else. No network. Nonode_modules. No calls tonpm. The bytes you get in Berlin equal the bytes you get in Denver.
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
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)