Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCXII · Container Delivery PipelineSBOMAndScan

SBOM and vulnerability scan — syft and trivy

Advanced⏱ ~27 mingitdocker

What you'll learn

  • Generate an SBOM against the final image with syft and select an output format that matches the downstream consumer
  • Run a vulnerability scan against the image digest with trivy and gate the pipeline on the result
  • Distinguish the SBOM (inventory) from the vulnerability report (CVE matches) and explain why one cannot replace the other
  • Attach both as signed attestations so they survive the lifetime of the digest

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

After the build produces an image digest, two observation stages consume that digest: syft, which generates a Software Bill of Materials, and trivy, which generates a vulnerability report. Both are required; the SBOM without a vuln scan is an inventory without risk context, and a vuln scan without an SBOM is a list of CVEs without the components they attach to. Together, they are the observation that turns a built image into an auditable artefact.

Why two observation stages

flowchart LR
    A["Image digest"] --> B["syft (SBOM)"]
    A --> C["trivy (vuln scan)"]
    B --> D["Signed attestation"]
    C --> D
    D --> E["Stored with digest in registry"]

The SBOM and the vulnerability report answer different questions about the same digest. The SBOM answers: what is in this image? The vulnerability report answers: what known CVEs match what is in this image? A vulnerability report without an SBOM cannot tell you which components carry which CVEs; an SBOM without a vulnerability report cannot tell you which components are exposed. The two are complementary; either alone is incomplete.

Both stages run against the final image, not the source tree. A source-tree SBOM misses the OS packages the base image contributes. A source-tree vulnerability scan misses the same. Most CVEs live in OS packages, not in application dependencies; the OS packages are reachable only by scanning the final image.

Running syft in CI

The dominant SBOM generator is anchore/syft. It supports SPDX JSON, CycloneDX JSON, and a few other formats. The invocation scans a directory:

syft dir:. -o spdx-json > sbom.spdx.json

The dir:. argument scans the current directory. To scan the image the build produced, swap to:

syft registry:app:$COMMIT_SHA -o spdx-json > sbom.spdx.json

The format decision is consumer-driven. SPDX JSON is the right choice when the downstream consumer is license-policy tooling or a regulator that expects SPDX identifiers. CycloneDX JSON is the right choice when the downstream consumer is a vulnerability management platform that ingests CycloneDX’s component graph natively. The conversion cost between formats is non-zero; pick one as the source of truth.

Running trivy in CI

Trivy is the dominant vulnerability scanner for container images. It supports OS package databases (Debian, Ubuntu, RHEL, Alpine), language-specific advisories (npm, pip, gem, …), and IaC scanning for Kubernetes manifests and Terraform:

trivy image --severity HIGH,CRITICAL --exit-code 1 app:$COMMIT_SHA

The --severity HIGH,CRITICAL flag restricts the report to the two severities most teams gate on. The --exit-code 1 flag makes trivy exit non-zero on any matching CVE, which fails the CI step. Without --exit-code 1, trivy prints the report and exits zero, which makes the report advisory rather than gating.

A common production pattern is two scans: a gate scan that fails the build on HIGH or CRITICAL, and a report scan that emits a full report (including LOW and MEDIUM) to a vulnerability management platform for later triage. The two share the same trivy database but produce different signals.

Attaching both as attestations

The SBOM and the vulnerability report must outlive the CI run. The pipeline attaches both to the digest as signed attestations:

cosign attach sbom --sbom sbom.spdx.json app:$COMMIT_SHA
cosign attest --yes --predicate trivy-report.json --type vuln \
  app:$COMMIT_SHA

The two attestations are stored as OCI artifacts at well-known tags, references to the signed digest. A verifier iterating the attestations on a digest can fetch the SBOM, fetch the vuln report, verify each attestation against the signature, and answer both questions (what is in it, what is exposed) without re-running syft or trivy.

What syft and trivy cannot do

Three limits worth naming:

  • They do not verify components. A package at version 1.2.3 in the SBOM may be the upstream package or may be a recompiled binary that identifies itself as 1.2.3. Verification is the signature’s job, not the SBOM’s.
  • They do not capture runtime behaviour. A package loaded dynamically or a configuration file fetched at boot may be missing from the SBOM. Runtime coverage is a behaviour test, not a scanner.
  • They do not say a CVE is exploitable. A CVE matched against the SBOM is a match, not an exploit. The package may not be loaded, may not be reachable, may not be on a vulnerable code path.

Production discipline

  1. Scan the final image, not the source. Source scans miss the OS-layer CVEs that dominate the report.
  2. Gate on severity, not on advisories alone. A “fix available” gate is policy-driven, not technology-driven.
  3. Attach both as signed attestations. CI artifact storage has retention; attestations on the digest have the lifetime of the image.
  4. Refresh the database at build time, not at scan time. Scanning with a stale database produces a report that misses advisories published since.
  5. Track gate thresholds as code. A pipeline that hard-codes --severity HIGH,CRITICAL should track that decision in the repository, not in a runner configuration file.

Cross-course references

  • Containers for Production Sysadmins - Part VI covers SBOM-driven compliance; this lesson is the producer side.
  • This course, Part LIII (ContainerSupplyChain) - LIII-05 covers SBOM generation; this lesson wires it into the pipeline alongside the vulnerability scan.
  • Sigstore project - the attestation model is documented in the in-toto and cosign sections of the Sigstore documentation.

Quiz

Knowledge check · 4 questions

  1. Q1. Why does the SBOM and the vulnerability report both need to scan the *final image*, not the source tree?

  2. Q2. A SBOM uploaded only as a CI artifact with the standard 30-90 day retention is durable for the lifetime of the deployed image, even after the artifact retention expires.

  3. Q3. Name two limits that apply to both syft and trivy: things the tools do not provide that downstream consumers must obtain elsewhere.

  4. Q4. Diagnose a pipeline where syft is configured to scan the source tree and trivy is configured to scan without a severity gate.

    A team configures the pipeline with `syft dir:. -o spdx-json > sbom.spdx.json` and `trivy image --exit-code 0 app:$COMMIT_SHA`. Six months later, a CVE is published in glibc; the audit team asks which running images are affected. The team greps the SBOMs for 'glibc' and finds nothing, because the source tree never contained glibc - the base image did. The trivy reports were stored as CI artifacts with 30-day retention and are gone. The team cannot answer the audit question.

Passing score: 75%. Answers are checked in this browser.