Safeguard
Supply Chain•

Why does my SBOM diff show changes when nothing changed?

An SBOM diff full of changes after an empty commit usually means volatile fields, tool drift or ordering noise. Here is how to find the cause.

Safeguard Security Team
4 min read

An SBOM diff shows changes with no code change because some fields are meant to differ on every run, such as serial numbers and timestamps, and because the generator, its settings or the build environment may have shifted. Normalise volatile fields, pin the generator, then re-diff.

Fields that are supposed to change

The CycloneDX specification says every generated BOM should have a unique serial number, even if the contents have not changed. It also records a creation timestamp in the metadata. SPDX has the same idea: a required Created field, and a document namespace that must be a new unique URI when the document is updated.

So a raw text diff of two files will always show lines that differ. A component level diff, such as the one in the CycloneDX command line tool, compares components rather than lines, and avoids most of this. If you diff with a generic text tool, strip these fields first.

Causes that are real changes

SymptomLikely causeHow to confirm
Many components added or removed at onceBase image or build environment changedCompare the image digest used by both builds
Many versions bumpedAn unpinned dependency resolved to newer versionsCheck whether a lockfile exists and is committed
Components appear only in one fileGenerator version or settings differRecord the tool version in both runs
Ordering differs, same contentThe tool does not sort outputSort components by purl before comparing
A whole stage is missingMulti-stage scan scope changedSee the Docker note below

A short triage routine

  1. Compare the serial number and timestamp, and ignore them.
  2. Compare the generator name and version recorded in the metadata.
  3. Compare the lockfile or image digest behind each SBOM.
  4. Sort both component lists by package URL and diff only those.
  5. If differences remain, treat them as real and read them.

The package URL format gives each component a predictable identifier made of a type, optional namespace, name and version. Using it as the sort and match key removes most cosmetic noise.

Docker scope is a common surprise

Docker's documentation states that BuildKit scans only the final stage of a multi-stage build by default, and that earlier stages are included only when you set the scan stage argument. If someone turns that on between releases, the diff will show many added build-time components that were always there. That is a change in measurement, not in software.

Make the diff boring on purpose

Pin the generator version in CI. Generate both files with the same command. Store the generator version beside each file. Then a diff that is not empty means something about the software moved, which is the only thing you want it to tell you.

Safeguard generates SBOMs and scans dependencies, and its policies and gates can act on component changes. You can try a scan on your own project at the free scan page.

Try it yourself

Click through it at your own pace, or open it full screen.

Look at how each component is identified by name and version. That identity is what a diff should match on.

Quick check

Should a changed timestamp count as drift?

No. The timestamp records when the file was created, so it differs on every run by design.

Is a reordered component list a change?

No. Sort by package URL before comparing, and the order stops mattering.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.