Software Bill of Materials (SBOM) in Practice

Software Bill of Materials (SBOM) in Practice - article cover image Containers & DevSecOps
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

A Software Bill of Materials (SBOM) is a formal, machine-readable inventory of every component — direct and transitive dependencies, libraries, and sometimes build tools and container base layers — that make up a piece of software, along with each component’s version, supplier, and license. The two dominant standard formats are CycloneDX (originated in the OWASP ecosystem, JSON or XML) and SPDX (originated in the Linux Foundation, tag-value, JSON, or RDF), both of which are now ISO-standardized and widely tool-supported. An SBOM answers a question that became impossible to answer by memory once modern applications routinely pull in hundreds or thousands of transitive dependencies: exactly what is running in production, right now, down to the version.

SBOMs moved from a nice-to-have to a compliance requirement after a series of high-profile supply-chain incidents — most notably the SolarWinds breach and the Log4Shell vulnerability in Log4j — demonstrated that organizations frequently could not answer “are we affected” in the hours after a critical CVE dropped, because no inventory of embedded and transitive dependencies existed. US Executive Order 14028 subsequently made SBOM a procurement requirement for software sold to federal agencies, and it has since become a standard deliverable in commercial software supply-chain due diligence.

Attack Prerequisites

Framed from the defender’s side, producing an SBOM that is actually useful (rather than a compliance checkbox nobody consults) requires:

  • A generator that inspects the actual build artifact, not just the manifest — a lockfile alone misses vendored code, statically linked binaries, and container base-image layers that never appear in package.json/requirements.txt.
  • Coverage of transitive dependencies, not just direct ones — the majority of exploitable components in a typical application are transitive, several levels deep, and invisible without full dependency-graph resolution.
  • A process to regenerate on every build, since an SBOM generated once at release time goes stale the moment a dependency is bumped; treat it as a build artifact, not a point-in-time document.
  • A consumer — the SBOM must feed into something that acts on it (a vulnerability scanner, a license-compliance check, an incident-response query tool); an SBOM nobody queries provides no security value regardless of format correctness.

How It Works

SBOM generation happens at one of several points, each with different accuracy trade-offs. Manifest-based generation reads package.json, requirements.txt, pom.xml, or go.mod and their lockfiles — fast and language-native, but blind to anything not declared through that ecosystem’s package manager, such as a manually vendored .jar or a statically compiled C library. Binary/filesystem scanning (what tools like Syft and Trivy primarily do) inspects the actual built artifact — a container image, a compiled binary, a filesystem — looking for package-manager metadata *and* known file signatures for libraries with no manifest trace at all, which is why it is the preferred method for container images: it reflects exactly what will run, including OS packages from the base image that no application manifest would ever list.

Once generated, an SBOM’s value comes almost entirely from what consumes it. A vulnerability scanner (Grype, Trivy, OSV-Scanner) matches each component-version pair in the SBOM against vulnerability databases (the NVD, GitHub Advisory Database, OSV) to flag known-vulnerable components — this is the workflow that answers “are we affected by the new CVE” in minutes instead of days, by querying an existing SBOM inventory rather than re-scanning every codebase from scratch. A license-compliance tool matches declared licenses against an organizational policy (flagging GPL in a proprietary product, for instance). And in incident response, a queryable SBOM inventory across the whole fleet turns “which of our hundred services embed the vulnerable library” into a single query instead of a multi-day manual audit.

The two standard formats differ in emphasis but both express the same core graph: components, their identities (name, version, and ideally a PURL — package URL — and/or CPE for unambiguous cross-tool matching), relationships (this component depends on that one), and metadata (license, supplier, hashes). CycloneDX leans toward security tooling workflows (it has first-class vulnerability and VEX — Vulnerability Exploitability eXchange — extensions); SPDX leans toward license/provenance and originated in compliance tooling. Most organizations standardize on one to avoid format-conversion friction across their tool chain.

Practical Example / Configuration

Generating a CycloneDX SBOM for a container image with Syft:

syft packages myorg/checkout-service:3.2.0 -o cyclonedx-json > sbom.cdx.json
Bash

A trimmed excerpt of the resulting CycloneDX JSON, showing one component entry with the identifying fields a scanner needs — PURL, version, and license:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
  "components": [
    {
      "type": "library",
      "name": "log4j-core",
      "version": "2.14.1",
      "purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1",
      "licenses": [{ "license": { "id": "Apache-2.0" } }],
      "hashes": [{
        "alg": "SHA-256",
        "content": "5d70a9a5a76ec5e6a..."
      }]
    }
  ]
}
JSON

Scanning that same SBOM for known vulnerabilities with Grype, which reads the file directly rather than re-scanning the image, so it runs in seconds and can be re-run against a refreshed vulnerability database without touching the artifact again:

grype sbom:./sbom.cdx.json --output table

# NAME        INSTALLED  FIXED-IN  TYPE  VULNERABILITY   SEVERITY
# log4j-core  2.14.1     2.17.1    java  GHSA-jfh8-c2jp  Critical
Bash

Walkthrough / Implementation

Wiring SBOM generation into a CI pipeline as a build-time artifact, attached to every release, with a scan gate:

# .github/workflows/sbom.yml
jobs:
  sbom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myorg/checkout-service:${{ github.sha }} .
      - name: Generate SBOM
        run: syft myorg/checkout-service:${{ github.sha }} \
               -o cyclonedx-json=sbom.cdx.json
      - name: Scan SBOM for known vulns
        run: grype sbom:./sbom.cdx.json --fail-on high
      - name: Attach SBOM to release
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.cdx.json
YAML

With every release producing and archiving an SBOM, answering “are we affected” for a newly disclosed CVE becomes a fleet-wide query rather than a manual audit:

# Query every archived SBOM across all services for a specific package
for f in sboms/*.cdx.json; do
  jq -r --arg n "log4j-core" \
    '.components[] | select(.name==$n) | "\(input_filename): \(.version)"' "$f"
done
Bash

Note: An SBOM only reflects the version and license, not whether the vulnerable code path in a flagged component is actually reachable from your code. Pair SBOM-based scanning with VEX (Vulnerability Exploitability eXchange) statements to record “affected but not exploitable” determinations, so the same known-vulnerable component does not re-trigger the same manual triage on every scan.

Note: Container-image scanning and manifest-based scanning give different results and both matter: a manifest-based scan misses OS packages baked into the base image (which is how a huge share of container CVEs actually enter production), while an image scan alone can miss build-time-only dependencies already stripped from the final layer.

Detection and Defense

Treat SBOM as infrastructure, not a document:

  • Generate on every build, from the built artifact (container/binary), not only from source manifests, and archive it alongside the release.
  • Gate releases on a vulnerability scan of the SBOM (Grype/Trivy/OSV-Scanner) with a severity threshold, not just a one-time audit.
  • Make the SBOM inventory queryable across the whole fleet so a new CVE can be answered fleet-wide in minutes, not per-service manual checks.
  • Adopt VEX to record exploitability determinations and avoid re-triaging the same non-exploitable finding on every scan.
  • Track license fields alongside security fields — the same inventory answers both compliance and vulnerability questions from one source of truth.

Real-World Impact

The Log4Shell disclosure (Log4j, December 2021) is the canonical case study: organizations with an existing SBOM inventory could query “who has log4j-core between 2.0 and 2.14.1” in minutes, while organizations without one spent days or weeks manually grepping build artifacts and asking every team to self-report. That gap is what drove SBOM from a niche compliance artifact into a baseline expectation, reinforced by US Executive Order 14028’s procurement requirements and increasing adoption of SBOM delivery clauses in commercial software contracts.

Conclusion

An SBOM is only as useful as the pipeline around it: generated from the real build artifact, on every build, feeding a vulnerability scanner with a gate, and queryable across the whole fleet when the next Log4Shell-scale disclosure lands. Treat it as continuously regenerated build infrastructure, not a document produced once for an audit checkbox.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL