SBOM: Software Bill of Materials and Why You Should Care About Your Dependencies

Knowing what is actually in your software before someone else tells you

Every time a serious supply-chain vulnerability lands, the same scramble begins. Someone in a chat channel asks “are we affected?” and the honest answer, for most teams, is “give us a few days and we’ll tell you.” That few days is the gap an SBOM is meant to close. A Software Bill of Materials is just an inventory — a machine-readable list of every component that went into a build — but having one ready before the panic is the difference between an afternoon and a fortnight.

Advertisement

Strip away the acronym and an SBOM is a manifest. For each thing you ship, it lists the packages, libraries, and transitive dependencies that made it in, ideally with versions, licences, and cryptographic hashes. Two formats dominate: SPDX (an established Linux Foundation standard, good for licence compliance) and CycloneDX (born in the security community, strong on vulnerability tooling). Both are interchangeable enough that most tools can convert between them, so don’t agonise over the choice — pick CycloneDX if security is your driver.

The crucial word is transitive. Your package.json lists maybe forty direct dependencies. The actual dependency graph is closer to nine hundred packages, and the thing that bites you is almost always five layers deep, pulled in by a library you’ve never heard of. An SBOM flattens that graph into something you can grep.

You don’t write an SBOM by hand. You generate it from a build artefact. Syft (from Anchore) is the tool I reach for because it understands containers, filesystems, and most language ecosystems without configuration:

# Inspect a built image and emit CycloneDX JSON
syft myapp:1.4.0 -o cyclonedx-json > sbom.json

# Or scan a project directory directly
syft dir:. -o spdx-json > sbom.spdx.json

The output is verbose, but the shape is simple — an array of components, each with a purl (package URL) that uniquely identifies it:

{
  "components": [
    {
      "type": "library",
      "name": "log4j-core",
      "version": "2.14.1",
      "purl": "pkg:maven/org.apache.logging.log4j/[email protected]",
      "licenses": [{ "license": { "id": "Apache-2.0" } }]
    }
  ]
}

That purl is the magic. It’s a standardised identifier that vulnerability databases key off, which means an SBOM and a scanner together can answer “are we affected?” in seconds.

An SBOM on its own is just a list. The value comes from feeding it to something that knows about CVEs. Grype pairs naturally with Syft and reads the SBOM directly, so you scan the inventory rather than re-scanning the artefact:

# Scan the SBOM we already generated
grype sbom:sbom.json --fail-on high
NAME        INSTALLED  FIXED-IN  TYPE  VULNERABILITY   SEVERITY
log4j-core  2.14.1     2.17.1    java  CVE-2021-44228  Critical

The --fail-on high flag is what makes this useful in CI: the pipeline goes red when a high-or-worse vulnerability appears in something you actually ship. Generate the SBOM at build time, store it as an artefact alongside the image, and you’ve got a permanent record of what each release contained. When a new CVE drops months later, you scan the stored SBOMs — no need to rebuild ancient images.

The pattern that has served me best: generate, store, scan, gate. Here’s the gist in a GitHub Actions step, though the same three commands work anywhere:

- name: Generate and scan SBOM
  run: |
    syft "ghcr.io/acme/api:${GITHUB_SHA}" \
      -o cyclonedx-json > sbom.json
    grype sbom:sbom.json --fail-on high
- name: Attach SBOM to release
  uses: actions/upload-artifact@v4
  with:
    name: sbom-${{ github.sha }}
    path: sbom.json

If you sign images with cosign, attach the SBOM as an attestation so it travels with the artefact and can be verified by whoever pulls it. That turns the SBOM from a file on a CI runner into a provable claim about your release.

SBOMs are inventories, not lie detectors. They tell you what’s present, not whether it’s reachable or exploitable in your particular usage — a vulnerable function you never call still shows up as a finding, which generates noise. That’s what VEX (Vulnerability Exploitability eXchange) documents are meant to address, by recording “yes it’s there, no it’s not exploitable, here’s why,” but VEX tooling is still maturing and the discipline of maintaining it is real work. SBOMs also only capture what the generator can see; a binary blob vendored without metadata is invisible, and dynamically loaded plugins slip through.

For anyone shipping software to customers, regulated industries, or government — yes, and increasingly it’s non-negotiable as procurement requirements catch up. For a hobby project running on your own homelab, generating an SBOM is overkill you’ll never look at. The sweet spot is any team large enough that “are we affected?” can’t be answered from one person’s memory. Adding two commands to a pipeline you already have is close to free, and the first time a big CVE lands and you answer in five minutes instead of five days, it pays for itself. Start with Syft and Grype, gate on high severity, and ignore VEX until the noise actually bothers you.

Advertisement

Related Content

Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.