When a critical vulnerability like Log4Shell drops at 11pm on a Friday, the first question every security team asks is: "which of our products use Log4j?" Without an SBOM, answering that question means grep-ing through build files, chasing down dependency declarations in ten different repos, and hoping nobody missed an indirect import. With an SBOM, it's a single query.
A Software Bill of Materials (SBOM) is a machine-readable inventory of every component — library, package, framework, dependency — that went into a piece of software. Think of it as a nutritional label for code: it tells you what's in there, where it came from, and which version you're running.
What an SBOM contains
A complete SBOM includes, for each component:
- Name and version — the package name and the specific version shipped, not a range.
- Supplier — the organization or person who published it.
-
PURL (Package URL) — a standardized identifier like
pkg:npm/express@4.18.3orpkg:maven/org.apache.logging.log4j/log4j-core@2.14.1that tools can resolve without ambiguity. - License — the SPDX identifier of the license under which the component is distributed.
- Hashes — cryptographic checksums (SHA-256 is standard) that let you verify you got what you expected.
- Dependencies — which other components this one depends on, forming a graph rather than a flat list.
Modern SBOMs in the CycloneDX 1.7 format also embed VEX (Vulnerability Exploitability eXchange) data — statements about which CVEs affect your specific build and what your analysis concluded about each one.
A worked example: one component in CycloneDX
The clearest way to understand an SBOM is to look at one entry. Here's what a single component — the express npm package — looks like inside a real CycloneDX 1.7 document:
{
"type": "library",
"bom-ref": "pkg:npm/express@4.18.3",
"name": "express",
"version": "4.18.3",
"purl": "pkg:npm/express@4.18.3",
"licenses": [{ "license": { "id": "MIT" } }],
"hashes": [
{ "alg": "SHA-256", "content": "9d3b..." }
],
"externalReferences": [
{ "type": "vcs", "url": "https://github.com/expressjs/express" }
]
}
Every field earns its place: type distinguishes a library from an application, framework or OS component. purl (pkg:npm/express@4.18.3) is what makes this machine-resolvable — a vulnerability scanner takes that exact string, queries OSV or another advisory database, and gets back CVEs for this specific version, not a fuzzy name match. licenses uses a real SPDX identifier (MIT), not free-text, so a license-compliance tool can categorise it automatically. hashes lets a consumer verify the artifact they actually received matches what the SBOM claims — the same integrity check a checksum file gives you, but standardised and attached to the inventory itself.
A full SBOM is thousands of these entries plus a dependencies section recording the graph — which component requires which — so a scanner can distinguish a direct dependency from one four levels deep in the tree. When VEX is embedded (CycloneDX 1.7), each vulnerability gets its own statement, for example:
{
"vulnerabilities": [{
"id": "CVE-2024-XXXXX",
"affects": [{ "ref": "pkg:npm/express@4.18.3" }],
"analysis": {
"state": "not_affected",
"justification": "code_not_reachable"
}
}]
}
That single VEX statement is the difference between a scanner flagging a CVE and a security team having already recorded — in a machine-readable, auditable way — that this specific build doesn't actually reach the vulnerable code path. Without VEX, every downstream consumer of your SBOM re-does that analysis themselves.
The two main formats: CycloneDX and SPDX
CycloneDX is an OWASP standard designed specifically for security use cases. It's JSON or XML, compact, and well-supported by tools that want to do something with the SBOM — like scanning it for vulnerabilities or generating VEX documents. CycloneDX 1.7 (the current version) includes full dependency graphs, license expressions, external references, compositions, and embedded VEX.
SPDX (Software Package Data Exchange) is a Linux Foundation standard originally designed for license compliance and now widely used in government procurement. It's more verbose and supports more licensing metadata. U.S. government agencies increasingly require SPDX SBOMs from their software suppliers under the May 2021 executive order on cybersecurity.
Both formats are useful and most modern tooling can ingest either. DepWarden accepts both as input and exports CycloneDX 1.7 with embedded VEX.
When an SBOM is required vs when it's useful
When it's required: U.S. federal agencies and their suppliers are increasingly mandated to produce SBOMs. The EU Cyber Resilience Act will require them for CE-marked products. Defense and critical infrastructure contracts commonly specify them. If your customer is a government agency or a large enterprise with a mature vendor security program, you'll be asked for one.
When it's useful regardless:
- Incident response — when a new CVE drops, you query the SBOM rather than auditing every codebase.
- Merger and acquisition due diligence — assess the security and license risk of a target's software before the deal closes.
- License compliance — know every copyleft dependency before you ship, not after legal flags it.
- Change tracking — diff two SBOMs across releases to see exactly what components changed, and audit the new risk.
Common SBOM mistakes
Generating one, once, and calling it done. An SBOM captures a point-in-time inventory. The moment you bump a dependency or add a new one, it's stale — and a stale SBOM that says "no known vulnerabilities" is worse than no SBOM at all, because it's actively misleading. The fix is mechanical: generate the SBOM as a CI/CD build step, not a periodic manual export.
Treating name+version as unambiguous. Two different ecosystems can publish a package with the same name (request exists on both npm and as an unrelated project elsewhere), and a name alone doesn't tell a tool which registry to check. This is exactly why PURLs exist — pkg:npm/request@2.88.2 is unambiguous in a way "request 2.88.2" is not. An SBOM built from name+version pairs instead of PURLs quietly reintroduces the ambiguity the format was designed to remove.
Skipping the dependency graph. A flat component list without the dependencies relationships tells you what is in your software but not how it got there — which means when a transitive vulnerability shows up, you can't answer "which of my direct dependencies do I need to bump to fix this?" without redoing the resolution by hand. A flat SBOM is still useful for inventory purposes, but it can't drive remediation on its own.
Assuming SPDX and CycloneDX are interchangeable in every field. Both describe the same underlying facts, but CycloneDX's security-first fields (VEX, vulnerability references) have no direct SPDX equivalent, and SPDX's deeper license-relationship expressions don't map cleanly back either. If your downstream consumer specifically needs VEX, generating SPDX alone won't satisfy that requirement even though it's a valid SBOM.
Generating and consuming SBOMs
Generating an SBOM from a manifest is what DepWarden does when you scan: it resolves the dependency tree, assigns PURLs, fetches license data, and exports a CycloneDX 1.7 document. Tools like Syft, cdxgen, and the CycloneDX Maven/Gradle plugins can also generate SBOMs directly from build artifacts.
Consuming an SBOM means running it through a vulnerability scanner (DepWarden accepts CycloneDX and SPDX uploads), a license checker, or a policy engine. The round-trip — generate → enrich → scan → VEX → export — is the compliance workflow most organizations need.
The key insight is that an SBOM is most valuable when it's generated automatically in CI/CD and kept up to date with every release. A one-time SBOM is better than nothing; a stale one gives false confidence.
Validating an SBOM before you trust it
An SBOM being well-formed JSON doesn't mean it's complete. Before relying on one — your own, or one a vendor handed you — check three things. First, does the component count look plausible for the project's size? A Java service reporting 12 components when its build log shows 400 resolved artifacts means the generator only captured direct dependencies, not the full transitive tree. Second, do a sample of PURLs actually resolve — pick five components and confirm the ecosystem, name and version in the PURL match a real package at that registry. Third, check whether the dependency graph is present at all, or whether every component is listed with no relationships; a flat SBOM (see above) will validate against the CycloneDX or SPDX schema perfectly while still being far less useful than a graph-complete one.
Provenance and signing
An SBOM answers "what's in this software," but not "can I trust that this SBOM accurately describes what's really running." That's a separate, complementary concern usually handled by signing the SBOM itself (so a consumer can verify it wasn't tampered with in transit) and by attaching build provenance (SLSA-style attestations describing which CI pipeline, which commit, and which build steps produced the artifact the SBOM describes). A signed SBOM with strong provenance is what a mature vendor security review is actually asking for when it requests "your SBOM" — the document alone, unsigned and with no link back to a specific build, is a good start but not the full answer regulators and enterprise buyers are increasingly expecting.
Top comments (0)