← SBOM Diff Check

Why did bumping one package show up as dozens of changes?

You edited one line in package.json, requirements.txt, or go.mod, rebuilt, and the diff reports dozens of added, removed, and version-changed packages. That is usually the SBOM working correctly, not a sign anything went wrong — here is why the cascade happens and how to check it is the expected kind.

The SBOM records the whole install, not your one-line edit

syft, docker sbom, trivy, and cdxgen all walk the resolved dependency tree and record every package that actually ends up installed — direct and transitive alike. Bumping one direct dependency's version can change which versions its own dependencies resolve to, because most package managers re-resolve the whole tree rather than touching only the line you edited. A single version bump on a popular package can easily carry twenty or thirty transitive packages along with it, each showing up on this page as its own added, removed, or version-changed row.

This is not specific to this tool. A line-based diff of the raw SBOM JSON files would show the same cascade, just buried under the serial number and timestamp noise this tool already strips out — see how this compares to diffing the raw files by hand.

Telling an expected cascade from a real surprise

The question worth asking is not "how many rows changed" but "does every row trace back to the one thing I actually changed." Work it in this order:

  1. List what you actually edited — the one or few direct dependencies whose version constraint you changed.
  2. For each name the diff reports as added, removed, or version-changed, check whether it is a dependency (direct or transitive) of what you edited, using your package manager's own tree command (npm ls <pkg>, pip show <pkg>, go mod why <pkg>, or equivalent).
  3. If every row traces back to your edit, the cascade is expected — your lockfile or resolved tree changed exactly the way a dependency bump should.
  4. A row that has nothing to do with what you changed — a package your manifest never named and no changed dependency pulls in — is the one worth investigating: a stale lockfile committed by accident, a build environment that drifted, or a substitution attack are all things that would produce exactly that shape of finding.

Wiring --fail-on-change into CI without training people to ignore it

sbom_diff.py's --fail-on-change flag exits 2 the moment anything differs between the two SBOMs you give it — it does not distinguish an expected cascade from a real surprise, because it cannot see which dependency you meant to bump. Two shapes of pipeline get real value out of it, and one does not:

python3 sbom_diff.py old.json new.json --fail-on-change  # exit 2 if anything differs

For the second shape, run without --fail-on-change and read the report as a diff to review, the same way you would read this page's table — not as a gate.

Common questions

Does this mean my dependency bump broke something?

Not by itself. A version-changed row means the resolved version differs between the two SBOMs, not that anything is broken — most of these rows are transitive packages whose version your package manager chose on your behalf when it re-resolved the tree, not lines you edited or reviewed.

How do I tell an expected cascade from a real supply-chain surprise?

Check whether every added, removed, or changed name is a dependency — direct or transitive — of the one package you bumped, using your package manager's own dependency-tree command. If every row traces back to that one edit, the cascade is expected. A name that has nothing to do with what you changed is the one worth investigating.

Should I wire --fail-on-change into my CI pipeline?

Only where no dependency change is ever expected, such as a build meant to be pinned or vendored. Wired into an ordinary CI pipeline that rebuilds after every dependency bump, --fail-on-change fails on the exact commits meant to change dependencies, which trains a team to ignore the gate rather than read it.

Why does the SBOM list packages I never installed directly?

Because syft, docker sbom, trivy, and cdxgen all record the full installed dependency tree, not just what your package.json, requirements.txt, or go.mod names directly. That is the SBOM doing its job — a build's actual attack surface is every package that ends up installed, not only the ones a human typed into a manifest.

Want the tool itself, or how it compares to the official CLI's own diff command? See SBOM Diff Check, or how this compares to cyclonedx-cli diff.