sbom-diff
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.
Install with skills.sh (any agent)
npx skills add testland/qa --skill sbom-diffsbom-diff
Overview
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 (opens in new window), 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 (opens in new window), 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:
This skill covers the SBOM-to-SBOM comparison workflow end to end.
When to use
Step 1 - Install cyclonedx-cli
Per cdx-cli-gh (opens in new window) 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 (opens in new window):
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
# or: brew install syftStep 2 - Generate the two SBOMs
Per sf-gh (opens in new window), 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.
Step 3 - Run the diff
Per cdx-cli-gh (opens in new window), the diff subcommand syntax is:
cyclonedx diff <from-file> <to-file> [options]Key flags per cdx-cli-gh (opens in new window):
| 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).
Step 4 - Interpret the output
With --component-versions --output-format json, the JSON output contains three categories of change per cdx-cli-gh (opens in new window):
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.
Step 5 - CI gate on net-new components
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.
Step 6 - Alerting pattern (nightly drift detection)
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.
Example
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 (opens in new window) 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-patterns
| 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 |
Limitations
References
sbom-diff CI workflows
View source (opens in new window)sbom-diff CI workflows
Full GitHub Actions workflows extracted from sbom-diff. The SKILL.md spine keeps the minimal jq gate; the complete CI-gate job, the allowlist pattern, and the nightly drift-detection workflow live here.
CI gate on net-new components
- name: SBOM diff gate
run: |
ADDED=$(jq '.added | length' diff-result.json)
echo "Net-new components: $ADDED"
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), maintain an allowlist and subtract matches before the count check:
UNAPPROVED=$(jq --rawfile allow allowlist.txt \
'[.added[] | select(.name as $n | $allow | test($n) | not)] | length' \
diff-result.json)Adjust the gate threshold and allowlist policy to the team's change-control requirements; the CI step above enforces zero-tolerance as the strictest form.
Nightly drift detection
For production image monitoring, run a nightly diff against the last known-good SBOM rather than comparing two build artifacts:
jobs:
sbom-drift:
runs-on: ubuntu-latest
schedule:
- cron: "0 2 * * *"
steps:
- name: Generate current SBOM
run: syft myapp:production -o cyclonedx-json=sbom-current.json
- name: Download last known-good SBOM
run: |
aws s3 cp s3://sbom-store/sbom-last-good.json sbom-baseline.json
- name: Diff
run: |
cyclonedx diff sbom-baseline.json sbom-current.json \
--component-versions --output-format json \
> drift-result.json
- name: Alert on drift
run: |
ADDED=$(jq '.added | length' drift-result.json)
REMOVED=$(jq '.removed | length' drift-result.json)
if [ "$ADDED" -gt "0" ] || [ "$REMOVED" -gt "0" ]; then
echo "Supply-chain drift detected"
cat drift-result.json
# Pipe to Slack/PagerDuty/JIRA as needed
exit 1
fi
- name: Rotate known-good on clean diff
if: success()
run: aws s3 cp sbom-current.json s3://sbom-store/sbom-last-good.jsonThe "rotate known-good on clean diff" step ensures the baseline advances only when the image passes the gate, catching regressions introduced in a later build.
Related skills
cyclonedx-format
Reference for the CycloneDX v1.6 software bill of materials (SBOM) specification - OWASP-curated, security-focused format covering software components, services, dependencies, vulnerabilities, formulation, machine learning models, and SaaS BOMs; supports XML / JSON / Protobuf encodings; per-language generators for npm, pip, Maven, Gradle, Go, etc.; integrates with CI via generate + sign + attest workflow. Use when the user asks to generate or write a software bill of materials / SBOM in CycloneDX form, or when the team adopts CycloneDX as its primary SBOM format (preferred for security-focused use cases vs SPDX's licensing focus).
grype-scanning
Scans for vulnerabilities using Anchore Grype: `grype sbom:./sbom.json` / `grype {image}` / `grype dir:./` across OS-package + language-package ecosystems (Alpine / Debian / Ubuntu / RHEL / Amazon Linux / Ruby / Java / JavaScript / Python / .NET / Go / PHP / Rust). `.grype.yaml` per-CVE and per-package ignore rules with mandatory `expires:` dates and reachability justification (the Grype-native suppression path, distinct from standalone VEX document authoring in vex-author); EPSS + KEV + risk-score prioritization; OpenVEX assertion filtering; `--fail-on high/critical` CI gate. Use when the team wants Grype-native vuln scanning, or pairs with Syft (syft-generation) for an SBOM-driven workflow.
spdx-format
Reference for the SPDX (Software Package Data Exchange) v2.3 + v3.0 SBOM specification - Linux Foundation-curated, license-focused format covering packages, files, snippets, relationships, license declarations, and (in 3.0) AI / dataset / build / security profiles; supports Tag-Value / JSON / YAML / RDF / Spreadsheet encodings; preferred by US Federal procurement (NIST guidance) and Linux distros. Use when the team's SBOM consumer requires SPDX format (federal procurement, Linux Foundation members, license-compliance focus).
syft-generation
Generates Software Bill of Materials (SBOMs) using Anchore Syft - supports container images / directories / archives across OCI / Docker / Singularity formats; output formats CycloneDX-JSON / SPDX-JSON / Syft-JSON / table / GitHub-JSON; pairs with `grype-scanning` for SBOM-driven vuln scanning. Use when the team needs SBOM artifacts for compliance (US EO 14028, EU CRA, FDA medical-device guidance) or as input to vuln scanners.
trivy-image
Configures and runs Trivy for container image scanning: Aqua Security's all-in-one scanner combining vuln + secret + misconfiguration + license detection in one pass; `trivy image {image}` with --severity HIGH,CRITICAL filter; --format sarif/json (incl. scan-embedded CycloneDX; for standalone SBOM generation see syft-generation + cyclonedx-format); .trivyignore CVE suppression file; --ignore-unfixed for actionable filter; --scanners vuln/misconfig/license/secret toggle. Use when the team wants a single tool covering container image security across multiple dimensions, not for producing a standalone CycloneDX SBOM.
vex-author
Authors and validates OpenVEX documents - produces `not_affected`, `affected`, `fixed`, and `under_investigation` statements with justification codes using `vexctl create`; attaches VEX assertions to container images; outputs `.openvex.json` files consumed on a downstream VEX-filter / vulnerability-prioritization path. Use when a scanner flags a CVE that analysis confirms is not exploitable in your deployment, and a machine-readable `not_affected` assertion is needed to suppress false positives without discarding the finding from the audit trail.