SBOM Generation and Testing with Syft and Grype

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/syft

Generating 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">| syft

By 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.json

What 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/bin

Scanning 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/project

Output 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 table

The --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:

  1. Generate the SBOM once (or on build)
  2. Re-scan the SBOM against an updated vulnerability database anytime
  3. 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.json

CI/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: high

GitLab 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 natively

Scanning 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.jar

Configuration 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: 120h

Interpreting 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:

  1. EPSS score: Exploitability Prediction Scoring System — probability of exploitation in the wild
  2. Network reachability: Is the vulnerable component in a network-facing path?
  3. Fix availability: Is a patched version available?
# Check if a fix is available
grype nginx:latest --only-fixed

SBOM 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.3

Testing 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 . /app
syft test-image:latest -o spdx-json > test-sbom.json
grype sbom:test-sbom.json
# Should report vulnerabilities in requests 2.6.0

Practical 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.

Read more