What Is an SBOM#

A Software Bill of Materials is a machine-readable inventory of every component in a software artifact. It lists packages, libraries, versions, licenses, and dependency relationships. An SBOM answers the question: what exactly is inside this container image, binary, or repository?

When a new CVE drops, organizations without SBOMs scramble to determine which systems are affected. Organizations with SBOMs query a database and have the answer in seconds.

SBOM Formats#

Two formats dominate the ecosystem. Both are standardized, machine-readable, and widely supported by scanning tools.

SPDX (Software Package Data Exchange)#

SPDX is an ISO standard (ISO/IEC 5962:2021) originally developed by the Linux Foundation. It focuses on license compliance and software composition. SPDX supports JSON, YAML, RDF, and tag-value serialization.

Key fields: package name, version, supplier, download location, checksums, license declared, license concluded, relationships between packages.

Use SPDX when license compliance is a primary concern, when supplying SBOMs to US government agencies (NTIA minimum elements align with SPDX), or when your toolchain already produces SPDX output.

CycloneDX#

CycloneDX is an OWASP standard designed specifically for security use cases. It supports JSON and XML serialization and has native support for vulnerability data, services, and machine learning model components.

Key fields: component name, version, type (library/framework/application/container/firmware), purl (package URL), hashes, licenses, vulnerabilities, dependency graph.

Use CycloneDX when vulnerability management is the primary goal, when you need to embed vulnerability data directly in the SBOM, or when your scanning tools produce CycloneDX natively.

Practical Comparison#

Both formats contain the essential data. The tooling you already use should drive the choice. Syft, trivy, and grype all support both formats. If you have no existing preference, CycloneDX is slightly more ergonomic for security workflows because its vulnerability extension is first-class rather than bolted on.

SBOM Generation with Syft#

Syft from Anchore catalogs packages from container images, filesystems, and archives. It detects packages across ecosystems: OS packages (apk, apt, rpm), language packages (npm, pip, go modules, maven, cargo), and file metadata.

Scanning Container Images#

# Generate CycloneDX JSON from a remote image
syft registry.example.com/myapp:v2.1.0 -o cyclonedx-json > myapp-v2.1.0.cdx.json

# Generate SPDX JSON
syft registry.example.com/myapp:v2.1.0 -o spdx-json > myapp-v2.1.0.spdx.json

# Scan a local image (Docker daemon)
syft myapp:latest -o cyclonedx-json > myapp-local.cdx.json

Scanning Source Directories#

# Scan a project directory before building
syft dir:./src -o cyclonedx-json > src-sbom.cdx.json

# Scan a specific archive
syft file:./myapp-v2.1.0.tar.gz -o spdx-json > archive-sbom.spdx.json

Filtering and Scoping#

# Only catalog OS packages (skip language packages)
syft registry.example.com/myapp:v2.1.0 -o cyclonedx-json --select-catalogers os

# Include file metadata for deeper analysis
syft registry.example.com/myapp:v2.1.0 -o cyclonedx-json --select-catalogers all

SBOM Generation with Trivy#

Trivy generates SBOMs and scans for vulnerabilities in a single tool. This makes it efficient for pipelines that need both outputs.

# Generate CycloneDX SBOM
trivy image --format cyclonedx --output myapp.cdx.json registry.example.com/myapp:v2.1.0

# Generate SPDX SBOM
trivy image --format spdx-json --output myapp.spdx.json registry.example.com/myapp:v2.1.0

# Scan a filesystem
trivy fs --format cyclonedx --output repo.cdx.json ./

# Scan and generate SBOM from a Dockerfile (pre-build analysis)
trivy config --format json ./Dockerfile

Vulnerability Scanning Workflows#

SBOM generation and vulnerability scanning are complementary but distinct steps. Generate the SBOM once, then scan it repeatedly as new CVEs are published.

Grype: Scan SBOMs for Vulnerabilities#

Grype from Anchore scans SBOMs directly, so you do not need to re-catalog the image:

# Scan an SBOM file
grype sbom:./myapp.cdx.json

# Output as JSON for pipeline processing
grype sbom:./myapp.cdx.json -o json > vulnerabilities.json

# Fail the pipeline on critical or high vulnerabilities
grype sbom:./myapp.cdx.json --fail-on high

# Scan a container image directly (generates SBOM internally)
grype registry.example.com/myapp:v2.1.0

Trivy: Scan from SBOM#

# Scan a CycloneDX SBOM
trivy sbom myapp.cdx.json

# Scan with severity filtering
trivy sbom --severity CRITICAL,HIGH myapp.cdx.json

# Output as JSON
trivy sbom --format json --output results.json myapp.cdx.json

Continuous Scanning#

Vulnerability databases update daily. An image that was clean yesterday may have critical CVEs today. Set up scheduled scans against stored SBOMs:

# Cron job or scheduled CI pipeline
# Re-scan all stored SBOMs against the latest vulnerability database
for sbom in /var/sboms/*.cdx.json; do
  grype sbom:"$sbom" -o json >> /var/reports/daily-scan-$(date +%Y%m%d).json
done

CVE Prioritization#

Not every CVE requires immediate action. Prioritize based on four factors.

1. Reachability#

Is the vulnerable code actually invoked in your application? A critical CVE in a library you include but never call is lower priority than a medium CVE in a function your application executes on every request. Reachability analysis tools like govulncheck (Go) and npm audit signatures (Node.js) help determine this.

# Go: check if vulnerable functions are actually called
govulncheck ./...

# Node.js: audit with reachability context
npm audit --omit=dev

2. Exploitability#

Check EPSS (Exploit Prediction Scoring System) scores. A CVE with CVSS 9.8 but EPSS 0.01% is far less urgent than a CVE with CVSS 7.5 and EPSS 85%. EPSS predicts the probability of exploitation in the next 30 days.

Check KEV (Known Exploited Vulnerabilities) from CISA. If a CVE is on the KEV list, it is actively exploited in the wild and must be patched immediately regardless of other factors.

3. Exposure#

Is the vulnerable component in a public-facing service or an internal batch job? A CVE in a library used by your internet-facing API gateway is higher priority than the same CVE in an internal metrics collector.

4. Fix Availability#

Is there a patched version available? If not, determine whether a workaround exists or whether the component can be replaced. Do not waste cycles on CVEs with no available fix unless the risk justifies removing the component entirely.

Prioritization Matrix#

Priority 1 (patch within 24h): On KEV list OR (EPSS > 10% AND reachable AND public-facing)
Priority 2 (patch within 7 days): CVSS >= 9.0 AND reachable AND fix available
Priority 3 (patch within 30 days): CVSS >= 7.0 AND reachable AND fix available
Priority 4 (next maintenance cycle): CVSS >= 4.0 OR unreachable high-severity
Accept risk: CVSS < 4.0 AND unreachable AND no known exploit

Remediation Tracking#

Discovering vulnerabilities is useless without a system to track remediation.

Structured Remediation Workflow#

  1. Triage: Scanner finds CVE. Determine reachability, exposure, and fix availability. Assign priority.
  2. Assign: Create a ticket with CVE ID, affected component, affected images, priority, and SLA deadline.
  3. Fix: Update the dependency, rebuild the image, regenerate the SBOM.
  4. Verify: Re-scan the new SBOM to confirm the CVE is resolved. Ensure no new CVEs were introduced.
  5. Deploy: Push the patched image through the standard deployment pipeline.
  6. Close: Mark the ticket resolved. Record time-to-remediation for metrics.

Tracking Metrics#

Measure these over time to demonstrate improvement:

  • Mean time to remediation (MTTR) by severity level
  • Count of open CVEs by priority, tracked weekly
  • SLA compliance rate: percentage of CVEs fixed within the priority window
  • New CVE introduction rate: how many new CVEs appear per image build

CI/CD Pipeline Integration#

GitHub Actions Example#

name: SBOM and Vulnerability Scan
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * *'  # Daily re-scan

jobs:
  sbom-and-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Generate SBOM with syft
        uses: anchore/sbom-action@v0
        with:
          image: myapp:${{ github.sha }}
          format: cyclonedx-json
          output-file: sbom.cdx.json

      - name: Scan SBOM for vulnerabilities
        uses: anchore/scan-action@v4
        id: scan
        with:
          sbom: sbom.cdx.json
          fail-build: true
          severity-cutoff: high

      - name: Upload SBOM as artifact
        uses: actions/upload-artifact@v4
        with:
          name: sbom-${{ github.sha }}
          path: sbom.cdx.json

      - name: Upload scan results to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: ${{ steps.scan.outputs.sarif }}

Storing SBOMs in OCI Registries#

Attach SBOMs to container images as OCI artifacts so they travel with the image:

# Attach SBOM using ORAS
oras attach registry.example.com/myapp:v2.1.0 \
  --artifact-type application/vnd.cyclonedx+json \
  sbom.cdx.json

# Attach SBOM using cosign
cosign attest --predicate sbom.cdx.json \
  --type cyclonedx \
  registry.example.com/myapp:v2.1.0

# Retrieve the attached SBOM later
cosign verify-attestation --type cyclonedx \
  --key cosign.pub \
  registry.example.com/myapp:v2.1.0 | jq -r '.payload' | base64 -d

Admission Control: Block Unscanned Images#

Combine SBOM attestation with Kyverno to block images that lack an SBOM attestation:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-sbom-attestation
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-sbom
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestations:
            - type: https://cyclonedx.org/bom
              attestors:
                - entries:
                    - keys:
                        publicKeys: |-
                          -----BEGIN PUBLIC KEY-----
                          ...
                          -----END PUBLIC KEY-----

SBOMs are not a checkbox exercise. They are the foundation of a vulnerability management program that can answer “are we affected?” within minutes of a new CVE disclosure. Generate SBOMs at build time, store them alongside artifacts, scan them continuously, and track remediation with SLAs tied to real risk.