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 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
vulnerabilitiesobject), 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.
# 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.
{
"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".
- 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.
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:
- Generate a CycloneDX SBOM on every build using Syft or cdxgen. Store it as a signed artifact alongside the release.
- Match vulnerabilities at build time using Grype. Block on high/critical findings with available fixes. Store the vulnerability report.
- Continuously re-scan stored SBOMs daily against the latest OSV/NVD database. Alert on newly published CVEs that affect already-released versions.
- Annotate with VEX for findings that are not exploitable in your context. Track justifications and review them when the component version changes.
- Diff SBOMs on each PR to detect new transitive dependencies. Flag unexpected additions for security review.
- 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 β