SBOM Generation and Testing with Syft and Grype
A Software Bill of Materials (SBOM) is the foundation of modern supply chain security. Without knowing exactly what components your software contains, you cannot know what vulnerabilities you carry. Syft generates SBOMs; Grype scans them for vulnerabilities. Together they form a practical SBOM testing workflow that fits into any CI pipeline.
What Is an SBOM?
An SBOM is a machine-readable inventory of all components in a software artifact — packages, libraries, operating system packages, and their transitive dependencies. Think of it as a nutrition label for software: it tells you exactly what is inside.
SBOMs matter because:
- Vulnerability response: When a new CVE drops (like Log4Shell), you can immediately answer "are we affected?" without manual audits
- License compliance: Know which packages have copyleft or proprietary licenses before shipping
- Regulatory requirements: NIST and the US Executive Order on Cybersecurity require SBOMs for software sold to federal agencies
- Incident response: After a breach, an SBOM lets you quickly identify blast radius
Syft: SBOM Generation
Syft is an open-source CLI and library from Anchore that generates SBOMs from container images, filesystems, and archives.
Installation
# macOS
brew install syft
<span class="hljs-comment"># Linux
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh <span class="hljs-pipe">| sh -s -- -b /usr/local/bin
<span class="hljs-comment"># Docker
docker pull anchore/syftGenerating Your First SBOM
# Scan a container image
syft nginx:latest
<span class="hljs-comment"># Scan a local directory
syft <span class="hljs-built_in">dir:/path/to/project
<span class="hljs-comment"># Scan a tarball
syft archive:image.tar
<span class="hljs-comment"># Scan from stdin
docker save nginx:latest <span class="hljs-pipe">| syftBy default Syft outputs a table of packages. For automation you need a structured format:
# SPDX JSON (widely supported)
syft nginx:latest -o spdx-json > nginx-sbom.spdx.json
<span class="hljs-comment"># CycloneDX JSON
syft nginx:latest -o cyclonedx-json > nginx-sbom.cdx.json
<span class="hljs-comment"># Syft's native format (most detail)
syft nginx:latest -o syft-json > nginx-sbom.syft.jsonWhat Syft Detects
Syft identifies packages from:
- OS packages: APT, RPM, Alpine APK
- Language ecosystems: npm, pip, gem, Maven, Go modules, Cargo, NuGet, Composer
- Java archives: JAR, WAR, EAR with nested JARs
- Binary catalogs: identifies well-known compiled binaries (busybox, curl, etc.)
SBOM Quality
Not all SBOMs are equal. A high-quality SBOM should include:
- Unique identifiers (CPE, PURL)
- Version information
- License data
- Supplier information
- Relationship data (what depends on what)
Syft includes Package URLs (PURLs) by default, which makes matching against vulnerability databases accurate.
# Check what Syft found
syft nginx:latest -o table
<span class="hljs-comment"># Example output:
<span class="hljs-comment"># NAME VERSION TYPE
<span class="hljs-comment"># adduser 3.118 deb
<span class="hljs-comment"># apt 2.2.4 deb
<span class="hljs-comment"># bash 5.1-2+deb11u1 deb
<span class="hljs-comment"># ...Grype: Vulnerability Scanning
Grype is Anchore's vulnerability scanner. It works against container images, SBOMs, or directories.
Installation
# macOS
brew install grype
<span class="hljs-comment"># Linux
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh <span class="hljs-pipe">| sh -s -- -b /usr/local/binScanning for Vulnerabilities
# Scan an image directly
grype nginx:latest
<span class="hljs-comment"># Scan a pre-generated SBOM (faster for repeated scans)
grype sbom:nginx-sbom.spdx.json
<span class="hljs-comment"># Scan a directory
grype <span class="hljs-built_in">dir:/path/to/projectOutput shows each vulnerability with:
- Package name and version
- CVE identifier
- Severity (Critical, High, Medium, Low, Negligible)
- Fixed version (if available)
Filtering and Thresholds
# Only show critical and high
grype nginx:latest --fail-on high
<span class="hljs-comment"># Only show fixable vulnerabilities
grype nginx:latest --only-fixed
<span class="hljs-comment"># Output as JSON for processing
grype nginx:latest -o json > vulnerabilities.json
<span class="hljs-comment"># Output as table (default)
grype nginx:latest -o tableThe --fail-on flag is the key CI integration point: set it to critical to block only critical CVEs, or high to be stricter.
Understanding Grype's Database
Grype pulls vulnerability data from multiple sources:
- NVD (National Vulnerability Database)
- GitHub Advisory Database
- RedHat, Debian, Ubuntu, Alpine security advisories
- Amazon Linux, Oracle Linux advisories
The database is cached locally and updated before each scan (or on-demand with grype db update).
The SBOM-Driven Workflow
The power of separating generation from scanning is that you can:
- Generate the SBOM once (or on build)
- Re-scan the SBOM against an updated vulnerability database anytime
- Store SBOMs as artifacts alongside your builds
# Step 1: Build your image
docker build -t myapp:v1.2.3 .
<span class="hljs-comment"># Step 2: Generate SBOM
syft myapp:v1.2.3 -o spdx-json > sbom-v1.2.3.spdx.json
<span class="hljs-comment"># Step 3: Scan immediately
grype sbom:sbom-v1.2.3.spdx.json --fail-on high
<span class="hljs-comment"># Step 4: Store the SBOM as a build artifact
<span class="hljs-comment"># (attach to release, push to OCI registry, etc.)
<span class="hljs-comment"># Later: re-scan old SBOM against new vuln data
grype db update
grype sbom:sbom-v1.2.3.spdx.jsonCI/CD Integration
GitHub Actions
name: SBOM and Vulnerability Scan
on:
push:
branches: [main]
pull_request:
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: myapp:${{ github.sha }}
artifact-name: sbom-${{ github.sha }}.spdx.json
format: spdx-json
- name: Scan with Grype
uses: anchore/scan-action@v3
with:
image: myapp:${{ github.sha }}
fail-build: true
severity-cutoff: highGitLab CI
sbom-scan:
image: docker:latest
services:
- docker:dind
script:
- docker build -t myapp:$CI_COMMIT_SHA .
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- syft myapp:$CI_COMMIT_SHA -o spdx-json > sbom.spdx.json
- grype sbom:sbom.spdx.json --fail-on high
artifacts:
paths:
- sbom.spdx.json
reports:
# GitLab can parse some security formats nativelyScanning Non-Container Artifacts
Syft and Grype aren't just for containers:
# Scan a Python project
<span class="hljs-built_in">cd /path/to/python-project
syft <span class="hljs-built_in">dir:. -o spdx-json > sbom.json
grype sbom:sbom.json
<span class="hljs-comment"># Scan a Node.js project
syft <span class="hljs-built_in">dir:/path/to/node-project
<span class="hljs-comment"># Scan a Go binary
syft go-binary -o spdx-json
<span class="hljs-comment"># Scan a JAR file
syft myapp.jarConfiguration Files
Both tools support configuration files for consistent settings across environments.
.syft.yaml
# .syft.yaml
output:
- format: spdx-json
file: sbom.spdx.json
- format: table
catalogers:
enabled:
- python-index-cataloger
- python-package-cataloger
- javascript-package-cataloger
- go-module-file-cataloger
- java-gradle-lockfile-cataloger
file:
metadata:
digests:
- sha256.grype.yaml
# .grype.yaml
fail-on-severity: high
only-fixed: false
ignore:
# Ignore specific CVEs with justification
- vulnerability: CVE-2021-44228 # Log4Shell - mitigated at runtime
fix-state: not-fixed
- package:
name: some-internal-lib
version: 1.0.0
language: java
output: table
db:
auto-update: true
validate-age: true
max-allowed-built-age: 120hInterpreting Results
False Positives
Grype may flag vulnerabilities that don't apply to your usage:
- Unused code paths: A vulnerability in a library function you never call
- Platform-specific: A Windows-only CVE in a Linux container
- Mitigated: A CVE mitigated by configuration or WAF rules
Use the ignore list in .grype.yaml to suppress false positives, but always document why:
ignore:
- vulnerability: CVE-2023-XXXX
reason: "We don't use the affected code path - see JIRA-1234"Prioritizing Fixes
Not all critical CVEs are equal. Prioritize based on:
- EPSS score: Exploitability Prediction Scoring System — probability of exploitation in the wild
- Network reachability: Is the vulnerable component in a network-facing path?
- Fix availability: Is a patched version available?
# Check if a fix is available
grype nginx:latest --only-fixedSBOM Storage and Attestation
SBOMs are most valuable when stored alongside build artifacts and signed to prevent tampering.
# Push SBOM to OCI registry alongside image
oras push ghcr.io/myorg/myapp:v1.2.3-sbom sbom.spdx.json
<span class="hljs-comment"># Sign the SBOM with Cosign (see Sigstore post for details)
cosign attest --predicate sbom.spdx.json --<span class="hljs-built_in">type spdxjson myapp:v1.2.3Testing Your SBOM Pipeline
Before relying on SBOM scanning in CI, test that it actually catches vulnerabilities:
# Deliberately use a vulnerable base image for testing
FROM python:3.9.0-slim
# Install a known-vulnerable package
RUN pip install requests==2.6.0 # Known vulnerable version
COPY . /appsyft test-image:latest -o spdx-json > test-sbom.json
grype sbom:test-sbom.json
# Should report vulnerabilities in requests 2.6.0Practical Recommendations
Start with containers: Container image scanning gives the highest signal-to-noise ratio. Every layer of the image is scanned.
Generate SBOMs at build time: Attach them to your release artifacts. Future vulnerability disclosures are queryable against historical builds.
Set realistic thresholds: Starting with --fail-on critical is less disruptive than --fail-on high. Tighten over time.
Review suppression list quarterly: Suppressed CVEs accumulate. Review them — some may have been fixed, some may need revisiting.
Use Grype's JSON output for dashboards: Parse grype -o json to feed vulnerability counts into metrics dashboards or Slack alerts.
SBOMs and Grype scanning are becoming standard practice for any team shipping container workloads. The tooling is mature, the CI integration is straightforward, and the alternative — finding out about Log4Shell after the fact — is far more expensive.