DevSecOps & Supply Chain Security

SBOMs & Provenance

18 min Lesson 7 of 28

SBOMs & Provenance

A software supply chain attack does not break into your code — it compromises something your code trusts: a dependency, a build tool, a base image, or a CI runner. The SolarWinds breach, the XZ utils backdoor, and the event-stream npm incident all share this pattern. The industry response is a trio of interlocking standards: Software Bill of Materials (SBOM), provenance attestations, and SLSA supply-chain levels. Together they answer three questions every security team must be able to answer at 2 a.m. after a zero-day drops: What is in our software? Where did it come from? Did anything tamper with the build?

SBOMs: CycloneDX vs SPDX

An SBOM is a machine-readable manifest — a complete inventory of every component in a piece of software, including transitive dependencies, with version, license, and vulnerability identifiers. Two formats dominate the ecosystem:

  • CycloneDX — designed by OWASP specifically for security use cases. Supports JSON, XML, and Protobuf. First-class support for vulnerabilities, services, formulas (how the software was built), and attestations. The de-facto standard in pipeline tooling: Syft, Trivy, cdxgen, and the GitHub Dependency Submission API all produce CycloneDX.
  • SPDX — the ISO/IEC 5962:2021 standard, originally from the Linux Foundation compliance community. Tag-value, JSON, YAML, and RDF formats. Stronger license expression syntax via SPDX license IDs. Mandated by US Executive Order 14028 and the EU Cyber Resilience Act. The NTIA minimum-elements definition is SPDX-aligned.

In practice, large organizations generate CycloneDX for security tooling (feeding into Dependency-Track, Grype, OSV-Scanner) and SPDX for legal and compliance hand-offs. Syft can produce both formats from a single scan.

NTIA minimum elements (US EO 14028): A valid SBOM must include supplier name, component name, version, unique identifier (PURL or CPE), dependency relationships, author, and timestamp. Any SBOM missing these cannot be used for compliance assertions.

Generating SBOMs in CI

The best time to generate an SBOM is immediately after a container image is built, before it is pushed to the registry. At that moment the build environment knows exactly what went into the image. Syft is the most widely adopted generator; cdxgen handles polyglot monorepos; Trivy combines SBOM generation with vulnerability scanning in a single pass.

# Install Syft curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \ | sh -s -- -b /usr/local/bin # Generate CycloneDX JSON SBOM from a built container image syft ghcr.io/myorg/myapp:sha-abc123 \ -o cyclonedx-json=sbom.cdx.json # Generate SPDX JSON SBOM from source tree (reads lock files, manifests) syft dir:. -o spdx-json=sbom.spdx.json # Attach SBOM as a signed OCI attestation with cosign (Sigstore keyless) cosign attest --predicate sbom.cdx.json \ --type cyclonedx \ ghcr.io/myorg/myapp:sha-abc123 # Verify the attestation later (in admission or CD pipeline) cosign verify-attestation \ --type cyclonedx \ --certificate-identity-regexp \ "https://github.com/myorg/myapp/.github/workflows/.*" \ --certificate-oidc-issuer \ "https://token.actions.githubusercontent.com" \ ghcr.io/myorg/myapp:sha-abc123

Provenance Attestations

An SBOM tells you what is in an artifact. A provenance attestation tells you how it was built — which source commit, which CI system, which runner, which build commands, and crucially, whether any of those inputs are trusted. Provenance is a signed document that links a build output to its inputs in a tamper-evident way.

The emerging standard is in-toto attestations with the SLSA Provenance predicate. GitHub Actions generates SLSA provenance automatically when you use the actions/attest-build-provenance action. The attestation is stored in the Sigstore transparency log (Rekor) and pinned to the OCI image digest — not a tag, which is mutable, but the immutable content hash.

Always pin to digest, never to tag. An image tagged :latest can be silently replaced. An image referenced as sha256:abc123... cannot. Your SBOM and provenance attestation must reference the digest; otherwise they can be detached and reattached to a different, potentially malicious image.

SLSA Supply-Chain Levels

SLSA (Supply-chain Levels for Software Artifacts, pronounced "salsa") is a Google-originated framework that defines four assurance levels for build integrity. Each level adds requirements that make tampering progressively harder to hide.

SLSA Supply Chain Levels — L0 to L3 SLSA L0 No guarantees Build: any process Source: unversioned Provenance: none Baseline — most software today SLSA L1 Provenance exists Build script used Source versioned Provenance: unsigned Accidental tampering detectable SLSA L2 Signed provenance Hosted build service Source: version control Provenance: signed Protects against compromised uploader SLSA L3 Hardened build Ephemeral isolated env Two-person review No persistent creds Provenance: unforgeable Protects against compromised build platform Increasing supply-chain integrity →
SLSA levels L0–L3: each level adds build integrity requirements that make supply-chain tampering progressively harder to execute or conceal.

Most GitHub Actions workflows running on ubuntu-latest with OIDC-issued tokens meet SLSA L2 today. Reaching L3 requires an isolated, ephemeral build environment where the build service itself — not the developer — generates and signs provenance, and no persistent credentials are injected into the build. Google's Cloud Build with SLSA L3 builder and GitHub's own SLSA Level 3 container builder (slsa-framework/slsa-github-generator) are the practical paths.

End-to-End Pipeline: SBOM + Provenance in GitHub Actions

name: Secure Build on: push: branches: [main] permissions: id-token: write # required for keyless Sigstore signing contents: read packages: write attestations: write # required for actions/attest-build-provenance jobs: build-sign-attest: runs-on: ubuntu-latest outputs: image-digest: ${{ steps.push.outputs.digest }} steps: - uses: actions/checkout@v4 - name: Build and push image id: push uses: docker/build-push-action@v6 with: push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }} # Generate SLSA provenance attestation (stored in Sigstore Rekor) - name: Attest build provenance uses: actions/attest-build-provenance@v1 with: subject-name: ghcr.io/${{ github.repository }} subject-digest: ${{ steps.push.outputs.digest }} # Generate and attach CycloneDX SBOM - name: Generate SBOM with Syft run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \ | sh -s -- -b /usr/local/bin syft ghcr.io/${{ github.repository }}@${{ steps.push.outputs.digest }} \ -o cyclonedx-json=sbom.cdx.json - name: Attest SBOM uses: actions/attest-sbom@v1 with: subject-name: ghcr.io/${{ github.repository }} subject-digest: ${{ steps.push.outputs.digest }} sbom-path: sbom.cdx.json

Continuous Vulnerability Scanning with SBOM

Generating an SBOM at build time is only half the picture. The SBOM must be continuously re-scanned against updated vulnerability databases after the image is deployed — a component that was clean on Monday may have a published CVE by Wednesday. Dependency-Track is the production-grade platform for this: it ingests CycloneDX SBOMs, maintains its own mirrored vulnerability databases (NVD, OSV, GitHub Advisory), and exposes a REST API and webhooks that integrate into your alerting stack. Feed it your SBOM on every deploy; it handles ongoing monitoring.

Production pitfall — SBOM freshness: Do not store SBOMs only as CI artifacts. CI artifacts expire. Store SBOMs in your OCI registry as attestations (with cosign or actions/attest-sbom) so they travel with the image digest and are always retrievable regardless of how old the image is. When a CVE drops, you need to query "which running images contain libssl 3.0.2?" in seconds — not hunt through expired artifacts.

Querying SBOMs Under Incident Conditions

When a zero-day is announced (e.g., a new Log4Shell-class vulnerability), your first question is blast radius: which services are affected? With a well-maintained SBOM pipeline and Dependency-Track, the answer is a single API call. Without it, you are manually grepping thousands of pom.xml and package-lock.json files across hundreds of repositories under time pressure — a notoriously error-prone process that leads to missed instances. At Google and Netflix scale, SBOM-driven blast-radius queries run in under one second across tens of thousands of service versions.

Adopt PURL (Package URL) as your primary component identifier. A PURL like pkg:npm/%40angular/core@17.3.2 or pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1 is ecosystem-agnostic, human-readable, and understood by every major SCA and SBOM tool. Use PURLs in your SBOMs; avoid CPEs for application dependencies (CPEs were designed for OS packages and are ambiguous for application libraries).

Key Takeaways

  • SBOMs answer what; provenance attestations answer how and from where; SLSA levels measure how trustworthy the build process is.
  • Generate CycloneDX SBOMs from your container images at build time; attach them as signed OCI attestations pinned to the image digest.
  • SLSA L2 is achievable today with standard GitHub Actions; L3 requires an isolated build service (slsa-github-generator or Google Cloud Build).
  • Store SBOMs in the registry alongside the image and feed them into Dependency-Track for continuous CVE monitoring.
  • Under a zero-day incident, a mature SBOM pipeline reduces blast-radius assessment from hours to seconds.