Introduction
When a critical vulnerability like Log4Shell or the xz-utils backdoor drops, the first question every security team asks is the same: "Are we affected?" If you can't answer that in minutes, you're stuck grepping through container layers and package-lock.json files while the clock runs.
A Software Bill of Materials (SBOM) solves this ahead of time. It's a structured, machine-readable inventory of every component that makes up a piece of software — direct dependencies, transitive dependencies, OS packages, language runtimes, and their exact versions. Regulators (US Executive Order 14028, the EU Cyber Resilience Act) and enterprise customers increasingly require them, but the real value is operational: an SBOM turns "which of our 40 services ship this vulnerable library" from a multi-day fire drill into a single query.
This guide covers two complementary open-source tools from Anchore:
- Syft — generates SBOMs from container images, filesystems, or archives in formats like SPDX and CycloneDX
- Grype — consumes an SBOM (or scans directly) and matches its components against known-vulnerability databases (NVD, GitHub Security Advisories, distro trackers)
Together they give you a two-stage pipeline: inventory, then assess — which is more auditable and reusable than a scanner that only reports vulnerabilities and discards the component list.
Prerequisites
- A Linux, macOS, or WSL2 shell
- Docker installed if you plan to scan container images
- Outbound internet access (Grype needs to pull its vulnerability database on first run)
- Optional: a GitHub repository with Actions enabled, for the CI section
Step 1: Install Syft and Grype
Both tools ship a single static binary and an official install script:
# Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Verify
syft version
grype versionPackage manager installs are also available (brew install syft grype on macOS, or the anchore/grype and anchore/syft Docker images if you'd rather not install anything locally).
Step 2: Generate an SBOM with Syft
Point Syft at a container image, a local directory, or an archive. Start with a container image:
syft nginx:1.27-alpine -o tableThis prints a human-readable table of every package Syft found — Alpine apk packages, plus anything layered on top (npm, pip, gems, Go modules, JAR files, etc.), each with its detected version and package type.
For machine-readable output, generate SPDX or CycloneDX JSON — the two formats most tooling and compliance frameworks expect:
# CycloneDX (widely supported by vulnerability scanners)
syft nginx:1.27-alpine -o cyclonedx-json=nginx-sbom.cdx.json
# SPDX (preferred for licensing/compliance audits)
syft nginx:1.27-alpine -o spdx-json=nginx-sbom.spdx.jsonYou can also scan a local project directory instead of a built image, which is useful for catching issues before you even build a container:
syft dir:. -o cyclonedx-json=app-sbom.cdx.jsonStep 3: Scan the SBOM for known vulnerabilities with Grype
Grype can scan an image directly, but feeding it a pre-generated SBOM is faster on repeat runs and keeps the "what's installed" and "what's vulnerable" steps decoupled — the SBOM doesn't change between scans, only the vulnerability database does.
# Scan the SBOM you just generated
grype sbom:nginx-sbom.cdx.jsonSample output:
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
libcrypto3 3.3.1-r0 3.3.2-r0 apk CVE-2026-1234 High
libssl3 3.3.1-r0 3.3.2-r0 apk CVE-2026-1234 High
busybox 1.36.1-r15 (none) apk CVE-2025-9821 Medium
To scan an image directly without a separate SBOM step:
grype nginx:1.27-alpineFail a build on high/critical findings — the exit code Grype needs for CI gating:
grype sbom:nginx-sbom.cdx.json --fail-on highStep 4: Wire it into CI/CD
Add an SBOM-and-scan gate to a GitHub Actions workflow so every image build gets inventoried and checked before it ships:
name: SBOM & Vulnerability Scan
on:
push:
branches: [main]
jobs:
sbom-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Generate SBOM
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@v6
with:
sbom: sbom.cdx.json
fail-build: true
severity-cutoff: high
- name: Upload SBOM as build artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.cdx.jsonArchiving the SBOM as a build artifact (or pushing it to an internal registry) means that six months from now, when the next Log4Shell-class CVE hits, you can grep every archived SBOM for the affected package instead of re-scanning every image from scratch.
Verification / Testing
Confirm the pipeline actually catches something real. Anchore maintains an intentionally vulnerable test image:
grype anchore/anchore-engine:v0.9.4 --fail-on critical; echo "exit code: $?"You should see a non-zero exit code and a list of critical/high findings — this confirms --fail-on will correctly break a CI build. Then confirm the happy path with a minimal, well-maintained image:
grype alpine:latest --fail-on critical; echo "exit code: $?"This should exit 0 with zero or near-zero critical findings, proving the gate doesn't false-positive on clean images.
Troubleshooting
- Grype hangs on first run — it's downloading its vulnerability database (several hundred MB). Run
grype db statusto check, or pre-warm it withgrype db update. - Syft finds far fewer packages than expected — some ecosystems require specific manifest files (
package-lock.json,Pipfile.lock,go.sum) to resolve exact transitive versions. If those lockfiles aren't present in the image/directory, Syft can only report what it can see. - "stale database" warnings from Grype — the local vulnerability DB has a TTL (default 5 days). Run
grype db updatein CI as a pre-step, or schedule a daily refresh, so scans don't run against outdated CVE data. - CI gate blocks on a vulnerability with no available fix — use Grype's
.grype.yamlignore rules to suppress a specific CVE with an expiry date and a documented justification, rather than loweringseverity-cutoffglobally. - SPDX vs CycloneDX confusion — if a downstream tool or customer requirement specifies a format, check first; they are not interchangeable, and some scanners (including older Grype versions) only fully support one.
Summary
Syft and Grype split supply-chain security into two clean, composable steps: know what you're running (SBOM generation) and know if it's dangerous (vulnerability matching). Generating SBOMs at build time and archiving them costs almost nothing, but it converts every future "are we affected?" fire drill into a five-minute search instead of a multi-day audit. Start by wiring the CI gate from Step 4 into one repository, tune the --fail-on threshold to your risk tolerance, and expand from there once the false-positive rate settles down.