Compares two CycloneDX or SPDX SBOMs to surface net-new, removed, and version-changed components between image or build versions; uses cyclonedx-cli diff for structured output and syft-based generation for the input SBOMs; gates CI on net-new component introduction; enables supply-chain alerting when unexpected dependencies appear across releases. Use when the team needs to detect dependency drift between container image builds, release candidates, or dependency-update branches.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Medium
Suggest reviewing before use
An SBOM diff turns two point-in-time inventory snapshots into a change signal: which components are net-new, removed, or version-changed between image or build versions. It turns an SBOM from a static artifact into a change-control gate.
Per github.com/CycloneDX/cyclonedx-cli, the CycloneDX CLI
provides a first-class diff subcommand that accepts any two CycloneDX
BOMs (XML, JSON, or Protobuf) and emits structured added/removed/modified
output in text or JSON form.
Per github.com/anchore/syft, Syft generates the per-image CycloneDX or SPDX SBOMs that feed the diff step; the two tools compose naturally in CI.
Differentiation vs sibling skills:
syft-generation generates SBOMs from a
single image; it does not compare across versions.cyclonedx-format documents the schema;
it does not run diffs.grype-scanning scans for vulnerabilities
against a single SBOM; it does not detect drift between builds.This skill covers the SBOM-to-SBOM comparison workflow end to end.
Per cdx-cli-gh install options:
# Homebrew
brew install cyclonedx/cyclonedx/cyclonedx-cli
# Docker (no local install required)
docker run cyclonedx/cyclonedx-cli diff sbom-from.json sbom-to.json --component-versions
# Binary - download from releases page
# https://github.com/CycloneDX/cyclonedx-cli/releasesSyft is also required to generate input SBOMs. Per sf-gh:
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
# or: brew install syftPer sf-gh, generate one SBOM per image version in CycloneDX JSON format (the format accepted by cyclonedx-cli diff):
# Baseline image (e.g., the last release)
syft myapp:1.0 -o cyclonedx-json=sbom-v1.0.json
# Target image (e.g., the release candidate)
syft myapp:1.1 -o cyclonedx-json=sbom-v1.1.jsonFor local directory scans (non-container builds):
syft dir:./release-1.0 -o cyclonedx-json=sbom-v1.0.json
syft dir:./release-1.1 -o cyclonedx-json=sbom-v1.1.jsonUse consistent format and source type across both runs so the diff is comparing equivalent inventories.
Per cdx-cli-gh, the diff subcommand syntax is:
cyclonedx diff <from-file> <to-file> [options]Key flags per cdx-cli-gh:
| Flag | Values | Purpose |
|---|---|---|
--from-format | autodetect, json, xml, protobuf | Override format detection for the baseline file |
--to-format | autodetect, json, xml, protobuf | Override format detection for the target file |
--output-format | text (default), json | Output style - use json for CI parsing |
--component-versions | (flag, no value) | Report added, removed, and version-changed components |
Human-readable diff (text output, default):
cyclonedx diff sbom-v1.0.json sbom-v1.1.json --component-versionsMachine-readable diff (JSON output, for CI gate logic):
cyclonedx diff sbom-v1.0.json sbom-v1.1.json \
--component-versions \
--output-format json \
> diff-result.jsonThe --component-versions flag is the signal flag: without it the diff
reports structural BOM changes; with it it reports the dependency inventory
delta (added, removed, modified components).
With --component-versions --output-format json, the JSON output contains
three categories of change per cdx-cli-gh:
Any non-empty "added" list requires review. A component may be:
Removals and version changes warrant the same review, especially in regulated or security-sensitive contexts.
Parse the JSON diff to fail the pipeline when net-new components appear. The minimal gate:
ADDED=$(jq '.added | length' diff-result.json)
if [ "$ADDED" -gt "0" ]; then
echo "::error::Net-new components detected. Review diff-result.json."
jq '.added' diff-result.json
exit 1
fiTo allow a pre-approved set of additions (a known intentional dependency upgrade), pair the count check with an allowlist. The full GitHub Actions gate job and the allowlist-subtraction pattern are in references/ci-workflows.md.
For production monitoring, run a scheduled nightly diff against the last known-good SBOM rather than comparing two build artifacts, alert on any added or removed components, and rotate the known-good baseline only on a clean diff so it advances past passing builds and still catches later regressions. The full nightly-drift GitHub Actions workflow is in references/ci-workflows.md.
Comparing myapp:2.3.1 (baseline) to myapp:2.4.0 (candidate):
syft myapp:2.3.1 -o cyclonedx-json=sbom-2.3.1.json
syft myapp:2.4.0 -o cyclonedx-json=sbom-2.4.0.json
cyclonedx diff sbom-2.3.1.json sbom-2.4.0.json --component-versions --output-format jsonTruncated example JSON output per cdx-cli-gh documented output shape (added/removed/modified keys):
{
"added": [
{"name": "protobuf", "version": "4.25.3", "type": "library"}
],
"removed": [],
"modified": [
{"name": "openssl", "from-version": "3.1.4", "to-version": "3.3.0", "type": "library"}
]
}The team reviews: protobuf is net-new. If it is a known transitive dep of
a gRPC library the team deliberately upgraded, it is added to the allowlist.
If unexplained, the release is blocked pending investigation.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Diff without --component-versions | Reports only structural BOM metadata changes, not dependency inventory delta | Always pass --component-versions |
| Compare SBOMs generated by different tools or formats without normalizing | Format differences appear as false-positive diffs | Use the same generator and --output-format for both runs |
| Treat removed components as low-priority | Removals can indicate silent packaging changes hiding a dep under a new name | Review removed list alongside added list |
| Gate only on count, not content | A single legitimate dep addition will block valid releases permanently | Pair count gate with an allowlist (Step 5) |
Generate SBOMs at different scan depths (dir: vs image) | Inconsistent cataloger scope produces noisy diffs | Match source type across baseline and target runs |
cyclonedx convert before diffing.grype-scanning
to assess whether net-new components carry known CVEs.syft-generation.--platform=linux/amd64 on both Syft runs; a combined diff of mixed-arch
SBOMs will conflate platform-specific inventory as change.syft-generation - generate the input SBOMscyclonedx-format - CycloneDX schema referencegrype-scanning - vuln-scan net-new components
after the diff gate passes