What an SBOM actually contains β€” and why generation alone is useless

A Software Bill of Materials is a structured list of components in a piece of software, analogous to an ingredients list on food packaging. It identifies each component by name, version, supplier, and (in modern formats) a unique package identifier β€” typically a Package URL (purl) like pkg:npm/[email protected]. It records license information, hash digests for integrity verification, and dependency relationships.

What it does not do on its own: identify vulnerabilities. An SBOM is static inventory data. Its security value is entirely dependent on what you do with it after generation. Feeding it to a vulnerability database, enforcing policies against it in CI/CD, and continuously re-evaluating it against newly published CVEs β€” that's the actual security work. The SBOM is the input, not the output.

Analogy: An SBOM without continuous vulnerability matching is like having a detailed inventory of every medicine in a hospital pharmacy β€” accurately catalogued, professionally formatted, stored in a filing cabinet β€” and never checking the recall notices.

Generate SBOM with Syft (CycloneDX JSON) shell
# Generate from a container image
$ syft myapp:latest -o cyclonedx-json > sbom.cdx.json

# Generate from a source directory
$ syft dir:. -o cyclonedx-json > sbom.cdx.json

# Generate in SPDX format (for regulatory submissions)
$ syft dir:. -o spdx-json > sbom.spdx.json

# This only produces the SBOM β€” no vulnerability data yet

CycloneDX vs SPDX: choosing the right format for your use case

Two formats dominate SBOM tooling: CycloneDX (OWASP project) and SPDX (Linux Foundation / ISO/IEC 5962). Both can represent the same component inventory, but they have different strengths.

  • CycloneDX is security-oriented. It has native support for vulnerability information (via the vulnerabilities object), VEX data, service dependencies, and formulation (build environment). Tool support for vulnerability matching (Grype, Trivy, OWASP Dependency-Track) is stronger. Version 1.6 added MLBOM (machine learning) and CBOM (cryptography) extensions.
  • SPDX is the ISO standard (ISO/IEC 5962:2021) and the format required by many US government contracts and the EU Cyber Resilience Act. License compliance tooling is more mature. SPDX 3.0 adds security profiles that bring it closer to CycloneDX's security capabilities.

Practical recommendation: Generate CycloneDX for internal vulnerability management pipelines. Generate SPDX for regulatory submissions and customer-facing documentation. Syft and cdxgen can produce both formats from the same scan.

Matching SBOM data to CVE databases

Vulnerability matching works by taking each component's PURL or CPE (Common Platform Enumeration) identifier from the SBOM and looking it up in a vulnerability database. The main databases are:

  • NVD (National Vulnerability Database) β€” the canonical CVE source. Slow to update (days to weeks lag), inconsistent CPE mapping quality.
  • OSV (Open Source Vulnerabilities) β€” Google-maintained, faster than NVD, ecosystem-specific (supports npm, PyPI, Go, Maven, RubyGems, etc.). Uses package names and version ranges rather than CPEs, reducing false positives significantly.
  • GitHub Advisory Database (GHSA) β€” curated by GitHub, fed into OSV. Strong npm and Python coverage.
Grype β€” vulnerability scan from SBOM input shell
# Scan a pre-generated SBOM (faster than re-scanning the image)
$ grype sbom:./sbom.cdx.json

NAME           INSTALLED  FIXED-IN  TYPE    VULNERABILITY  SEVERITY
express        4.18.1     4.19.2    npm     CVE-2024-29041  Medium
lodash         4.17.20    4.17.21   npm     CVE-2021-23337  High
semver         5.7.1      5.7.2     npm     CVE-2022-25883  High
openssl        3.0.7      3.0.8     apk     CVE-2023-0286   Critical

# Exit code 1 if critical/high findings β€” use as CI gate
$ grype sbom:./sbom.cdx.json --fail-on high

The key operational insight: by scanning the SBOM rather than re-pulling the image or re-running analysis, you can continuously re-evaluate historical releases against newly published CVEs without rebuilding anything. Store SBOM artifacts alongside releases; re-run grype against them daily using the latest vulnerability database.

VEX: communicating exploitability context

Vulnerability Exploitability eXchange (VEX) is a companion standard to SBOM that lets software producers communicate whether a known vulnerability is actually exploitable in their specific product. It addresses the signal-to-noise problem: a vulnerability in a library you depend on may be in code paths you never call, or in a configuration option you don't use.

A VEX document contains one or more statements mapping a CVE to a component, with a status from a defined vocabulary:

  • not_affected β€” the product is not affected by the vulnerability (with a justification: code_not_reachable, component_not_present, etc.).
  • affected β€” the product is affected and users should take action.
  • fixed β€” the vulnerability has been remediated.
  • under_investigation β€” the producer is assessing impact.
VEX statement in CycloneDX format json
{
  "vulnerabilities": [{
    "id": "CVE-2021-23337",
    "affects": [{
      "ref": "pkg:npm/[email protected]"
    }],
    "analysis": {
      "state": "not_affected",
      "justification": "code_not_reachable",
      "detail": "Application only uses lodash/get and lodash/set. The template injection function (_.template) that contains CVE-2021-23337 is not imported or called anywhere in this codebase.",
      "responses": ["will_not_fix"]
    }
  }]
}

Consuming VEX data in your pipeline (OWASP Dependency-Track supports this natively) suppresses known-false-positive vulnerabilities from reports while preserving the audit trail of the decision. This reduces scanner fatigue and keeps teams focused on actionable findings.

Policy-as-code gates in CI/CD

The most direct operationalization path: define security policies as code and evaluate them against the SBOM in CI. Policies can encode rules like "no critical CVEs with CVSS score β‰₯ 9.0", "no components with licenses incompatible with our commercial distribution", or "no components that are end-of-life".

GitHub Actions β€” SBOM generation + vulnerability gate yaml
- name: Generate SBOM
  run: syft {{ github.event.repository.name }}:{{ github.sha }} -o cyclonedx-json > sbom.cdx.json

- name: Upload SBOM artifact
  uses: actions/upload-artifact@v4
  with:
    name: sbom-{{ github.sha }}
    path: sbom.cdx.json
    retention-days: 365  # keep for historical re-analysis

- name: Vulnerability gate
  run: |
    grype sbom:./sbom.cdx.json \
      --fail-on high \
      --only-fixed \
      -o json > vuln-report.json

- name: License policy check
  run: ort analyze --input-dir . --output-dir ort-results && ort report --fail-on policy-violation

--only-fixed flag matters: grype without this flag reports vulnerabilities that have no available fix. A gate that blocks on unfixable findings creates a CI deadlock. Fail on vulnerabilities with available fixes; track unfixable ones separately for acceptance review.

Diff-based alerting: detecting new component introductions

Rather than treating each SBOM as an isolated snapshot, diff consecutive SBOMs to detect when new components are introduced. This catches supply-chain incidents in real time: a new dependency added in a PR that doesn't appear in the previous release's SBOM is an anomaly worth investigating.

Python β€” SBOM diff for new component detection python
import json

def load_purls(sbom_path: str) -> set[str]:
    with open(sbom_path) as f:
        sbom = json.load(f)
    return {
        c["purl"]
        for c in sbom.get("components", [])
        if "purl" in c
    }

previous = load_purls("sbom-prev.cdx.json")
current  = load_purls("sbom-curr.cdx.json")

new_components     = current - previous
removed_components = previous - current

if new_components:
    print("New components detected:")
    for purl in sorted(new_components):
        print(f"  + {purl}")
    # Feed new_components to a vulnerability enrichment API
    # and alert if any have known CVEs

The full operational SBOM pipeline

A mature SBOM program connects generation, storage, continuous vulnerability matching, VEX annotation, policy enforcement, and diff alerting into a single workflow:

  1. Generate a CycloneDX SBOM on every build using Syft or cdxgen. Store it as a signed artifact alongside the release.
  2. Match vulnerabilities at build time using Grype. Block on high/critical findings with available fixes. Store the vulnerability report.
  3. Continuously re-scan stored SBOMs daily against the latest OSV/NVD database. Alert on newly published CVEs that affect already-released versions.
  4. Annotate with VEX for findings that are not exploitable in your context. Track justifications and review them when the component version changes.
  5. Diff SBOMs on each PR to detect new transitive dependencies. Flag unexpected additions for security review.
  6. Publish SBOMs to customers and regulators as required. OWASP Dependency-Track provides a hosted API for this.

"An SBOM stored in an artifact registry with no downstream pipeline is a documentation exercise. An SBOM feeding a continuous vulnerability scanner, a policy gate, and a diff alert is a security control."

Automate SBOM generation and vulnerability matching in your pipeline

AquilaX SCA generates CycloneDX SBOMs, matches components against OSV and NVD, and produces VEX-aware reports with prioritized remediation β€” integrated into your existing CI/CD workflow.

Explore SCA scanning β†’