The question you cannot answer without one

When Log4Shell (CVE-2021-44228) dropped in December 2021, every security team on the planet faced the same question: where do we run Log4j? Most teams could not answer it. Not in an hour, not in a day. Some were still finding vulnerable instances months later, buried three dependency levels deep inside vendor appliances they did not even know contained Java.

A Software Bill of Materials (SBOM) exists to make that question answerable in minutes. It is a machine-readable inventory of every component in a piece of software: direct dependencies, transitive dependencies, versions, licenses, and hashes. Think of it as the ingredients label for your codebase.

That is the whole idea. The value is not the document. The value is being able to join that document against a vulnerability database, today and every day after.


Two formats you will actually meet

There are two serious SBOM formats. You do not need to memorize either, but you need to know which one your tooling speaks.

CycloneDX SPDX
Origin OWASP Linux Foundation
Primary focus Security use cases License compliance, now security too
Component identity PURL (package URL) PURL or CPE, plus SPDX IDs
Formats JSON, XML, protobuf JSON, YAML, tag-value, RDF
Extras VEX support, services, dependencies graph Strong license expression language
Typical users AppSec and vuln management teams Legal, OSPOs, large vendors

CycloneDX was built by OWASP with vulnerability management in mind, and it shows: the dependency graph is explicit, and it pairs naturally with VEX (Vulnerability Exploitability eXchange) documents that say "yes, this CVE is in a component we ship, but the vulnerable function is unreachable."

SPDX started life as a license compliance format and became an ISO standard (ISO/IEC 5962). It handles security fine, but its DNA is legal.

In practice: if you are choosing today for vulnerability tracking, CycloneDX is the path of least friction. If a customer or regulator demands SPDX, converters exist and mostly work.


Why regulators keep saying "SBOM"

If SBOMs feel like a compliance fad, that is because three separate regulatory waves pushed them at once.

Executive Order 14028 (US, 2021) came directly out of the SolarWinds compromise. It requires vendors selling software to the US federal government to provide SBOMs, and it made NTIA's "minimum elements" definition the de facto baseline: supplier, component name, version, unique identifiers, dependency relationships, author, and timestamp.

The EU Cyber Resilience Act (CRA) goes further. It requires manufacturers of products with digital elements sold in the EU to maintain an SBOM covering at minimum the top-level dependencies, as part of mandatory vulnerability handling. Main obligations apply from December 2027, and the CRA has teeth: fines up to 15 million EUR or 2.5 percent of global turnover.

DORA does not use the word SBOM directly, but its ICT third-party risk requirements for EU financial entities push in the same direction: you must understand and monitor the risk in software your critical providers ship to you. An SBOM is the practical artifact that makes that monitoring real rather than a questionnaire answer.

The pattern across all three: regulators stopped accepting "we do not know what is inside." Component transparency is becoming a condition of selling software, not a nice-to-have.


How SBOM scanning actually works

Scanning an SBOM against a CVE database is conceptually simple and messy in the details.

Step 1: identify each component. Every entry in the SBOM carries an identifier, ideally a PURL like pkg:maven/org.apache.logging.log4j/[email protected]. PURLs encode ecosystem, namespace, name, and version in one unambiguous string.

Step 2: match against vulnerability data. The scanner joins those identifiers against CVE sources: NVD (which uses CPE identifiers), the OSV database, GitHub Security Advisories, and distro trackers. This is where the mess lives. NVD's CPE strings (cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*) were never designed for package ecosystems, so matching PURL to CPE involves heuristics, and heuristics produce both false positives and misses.

Step 3: check version ranges. A CVE advisory says "affects 2.0-beta9 through 2.14.1, fixed in 2.15.0." The scanner evaluates whether your pinned version falls in the affected range, which requires understanding each ecosystem's versioning quirks (semver, Maven qualifiers, Debian epochs).

Step 4: enrich and prioritize. A raw match list is not a work queue. Each finding needs CVSS for severity, EPSS for exploitation probability, and CISA KEV status for confirmed exploitation in the wild. A CVSS 9.8 with an EPSS of 0.04 percent in a batch job is a different animal from a CVSS 7.5 sitting on the KEV list in your internet-facing API.

This last step is exactly what vuln.mlab.sh does: upload a CycloneDX or SPDX SBOM and get back matched CVEs with CVSS, EPSS, and KEV status side by side, so triage starts from exploitation reality instead of a wall of criticals.


Keeping SBOMs current in CI

A stale SBOM is worse than no SBOM, because it gives you confident wrong answers. The fix is to treat SBOM generation as a build artifact, not a quarterly exercise.

The pipeline shape that works:

# Generate on every build of a release branch
- name: Generate SBOM
  run: syft dir:. -o cyclonedx-json > sbom.cdx.json

- name: Scan SBOM against CVE data
  run: grype sbom:sbom.cdx.json --fail-on critical

- name: Archive with the release
  run: cp sbom.cdx.json artifacts/${VERSION}/

Three rules make this useful:

  1. Generate at build time, from the lockfile or the built image. An SBOM generated from a manifest guesses at resolved versions. One generated from the actual artifact reflects what ships.
  2. Store one SBOM per released version, forever. When the next Log4Shell lands, you need to query what is deployed, which may be three versions behind main.
  3. Rescan old SBOMs on a schedule, not just at build time. New CVEs are published against old components daily. The scan that passed in March says nothing about July. A nightly rescan of every SBOM in production is the mechanism that turns "we shipped clean" into "we are still clean."

That third point is the one most teams miss. The build-time gate catches known-bad dependencies entering. The scheduled rescan catches the far more common case: a dependency that was fine when you shipped it and has a CVE now.


Where SBOMs break

Be honest about the limits, because auditors will not be.

Transitive depth is where the risk hides. Your direct dependencies are maybe 40 packages. The full tree is 1,200. Log4j was almost never a direct dependency; it rode in behind logging facades and vendor SDKs. An SBOM that only covers top-level components (which is all the CRA strictly requires) answers the easy question and dodges the hard one.

False positives are structural, not incidental. CPE matching flags components by name and version with no idea whether the vulnerable code path is reachable, compiled in, or configured on. Expect a meaningful fraction of raw findings to be noise, and plan for VEX statements or documented triage to suppress them. Otherwise engineering learns to ignore the report by week three.

Vendor SBOMs vary wildly in quality. Some are complete and regenerated per release. Others are a one-time PDF export from 2023. Treat a vendor SBOM as a claim to verify, not a fact.

An SBOM sees components, not custom code. Your own SQL injection does not appear in any bill of materials. SBOM scanning complements SAST and pentesting; it replaces neither.

None of this argues against SBOMs. It argues against treating an SBOM as proof of security rather than what it is: an inventory that finally lets you ask the right questions fast.


The next Log4Shell is already in someone's dependency tree, including possibly yours. The teams that answer "where do we run it?" in ten minutes will have generated, stored, and rescanned their SBOMs before they needed them. Start before you need them.