Testland
Browse all skills & agents

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-diff
View source

sbom-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:

  • 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.

When to use

  • A container image was rebuilt (base image update, dep update, CI pipeline change) and the team needs to know what changed in the dependency inventory.
  • A release candidate branch must pass a "no unexpected new transitive dependencies" gate before merge.
  • A scheduled nightly diff monitors a production image tag for supply-chain drift vs the last known-good SBOM.
  • Post-incident: an unexpected library appeared in a running container and the team needs to pinpoint when it was introduced.

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/releases

Syft 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 syft

Step 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.json

For 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.json

Use 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):

FlagValuesPurpose
--from-formatautodetect, json, xml, protobufOverride format detection for the baseline file
--to-formatautodetect, json, xml, protobufOverride format detection for the target file
--output-formattext (default), jsonOutput 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-versions

Machine-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.json

The --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):

  • Added - components present in the target BOM but not the baseline. These are net-new dependencies introduced in this image version.
  • Removed - components present in the baseline but absent from the target. Removals may indicate intentional cleanup or an unexpected packaging change.
  • Modified - components present in both, but with a version change. A modified entry contains the old and new version values.

Any non-empty "added" list requires review. A component may be:

  • An expected transitive dep of a package the team intentionally upgraded
  • A net-new package pulled in by a base image update
  • An unexpected addition (supply-chain risk, malicious dep injection, misconfigured build)

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
fi

To 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 json

Truncated 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-patternWhy it failsFix
Diff without --component-versionsReports only structural BOM metadata changes, not dependency inventory deltaAlways pass --component-versions
Compare SBOMs generated by different tools or formats without normalizingFormat differences appear as false-positive diffsUse the same generator and --output-format for both runs
Treat removed components as low-priorityRemovals can indicate silent packaging changes hiding a dep under a new nameReview removed list alongside added list
Gate only on count, not contentA single legitimate dep addition will block valid releases permanentlyPair count gate with an allowlist (Step 5)
Generate SBOMs at different scan depths (dir: vs image)Inconsistent cataloger scope produces noisy diffsMatch source type across baseline and target runs

Limitations

  • cyclonedx-cli diff operates on CycloneDX format (XML, JSON, Protobuf) per cdx-cli-gh (opens in new window); SPDX-format SBOMs require conversion first via cyclonedx convert before diffing.
  • The diff output reflects component-level inventory changes, not vulnerability changes. Pair with grype-scanning to assess whether net-new components carry known CVEs.
  • Syft inventory accuracy affects diff signal quality: if Syft misses a component in one scan but not the other, the diff will misattribute the delta. See the accuracy-validation section in syft-generation.
  • For multi-arch images, diff each platform separately using --platform=linux/amd64 on both Syft runs; a combined diff of mixed-arch SBOMs will conflate platform-specific inventory as change.

References

  • cdx-cli-gh (opens in new window) - cyclonedx-cli repository: install, subcommand reference, diff flags, output format documentation
  • sf-gh (opens in new window) - Syft repository: SBOM generation, output formats, source types
  • references/ci-workflows.md - full CI-gate + nightly drift workflows
  • syft-generation - generate the input SBOMs
  • cyclonedx-format - CycloneDX schema reference
  • grype-scanning - vuln-scan net-new components after the diff gate passes

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
    fi

To 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.json

The "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.