Audit Your Software Supply Chain: Generate an SBOM with Syft and Gate CI on a Grype Vulnerability Scan
Wire Syft and Grype into your pipeline to produce a machine-readable SBOM and break the build the moment a high-severity CVE appears in your dependencies.
What you'll build
You'll generate an SPDX 2.3 JSON Software Bill of Materials from a container image using Syft, then wire Grype into a GitHub Actions workflow that fails the build on any high-or-critical CVE found in that SBOM.
Prerequisites
- Docker 24+ running locally
- macOS (Apple Silicon or Intel) or Linux; Windows users should use WSL2
- A GitHub repository with Actions enabled
- Homebrew on macOS, or curl on Linux, for installation
Familiarity with Dockerfiles and CI YAML is assumed. No prior SBOM experience needed.
1. Install Syft and Grype
Both tools are from Anchore. On macOS:
brew install syft grype
On Linux, both install scripts write to /usr/local/bin, which is owned by root, so pass the shell invocation through sudo:
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin
Both install scripts are short and pinned to signed GitHub releases. Inspect them before running if your security policy requires it.
Confirm the installs:
syft --version
grype --version
2. Build a sample image
You need a real image with dependencies to scan. The Dockerfile below installs a package inline, so no external files are required to build it:
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir requests==2.31.0
CMD ["python", "-c", "print('hello')"]
Save that as Dockerfile at your repo root, then build:
docker build -t myapp:latest .
If you don't have a local project, substitute any public image like python:3.11-slim or node:20-slim to follow along.
3. Generate the SBOM with Syft
syft myapp:latest -o spdx-json=sbom.spdx.json
The -o spdx-json=sbom.spdx.json flag writes SPDX 2.3-compatible JSON to that file. Syft pulls the image layers from the local Docker daemon, catalogs every package it finds (OS packages, Python wheels, Go modules, npm packages, and more), and writes the result. It's thorough: even packages installed mid-layer that were later removed show up under --scope all-layers if you need full layer-by-layer visibility.
To see a human-readable summary at the same time:
syft myapp:latest -o spdx-json=sbom.spdx.json -o table
Open sbom.spdx.json. Each entry in the packages array has name, versionInfo, and an externalRefs array containing a PURL (Package URL). That PURL is what Grype uses to look up known CVEs.
4. Scan the SBOM with Grype
grype sbom:sbom.spdx.json
The sbom: scheme prefix is required. Without it, Grype interprets the argument as an image reference and will fail with a pull error. Grype downloads its vulnerability database on the first run (~50 MB), so expect a brief pause. Subsequent runs use the cached DB.
To gate on severity:
grype sbom:sbom.spdx.json --fail-on high
Exit code 0 means no CVEs at high severity or above. Exit code 1 means at least one was found. --fail-on high catches both HIGH and CRITICAL findings.
For machine-readable output alongside the gate:
grype sbom:sbom.spdx.json --fail-on high -o json > grype-results.json
The --fail-on flag controls the exit code independently of -o, so both work together.
5. Wire it into GitHub Actions
Commit the Dockerfile from Step 2 to your repo root. Then create .github/workflows/supply-chain-audit.yml:
name: Supply Chain Audit
on:
push:
branches: [main]
pull_request:
jobs:
audit:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Install Syft
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sudo sh -s -- -b /usr/local/bin
- name: Install Grype
run: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sudo sh -s -- -b /usr/local/bin
- name: Generate SBOM
run: syft myapp:${{ github.sha }} -o spdx-json=sbom.spdx.json
- name: Vulnerability gate
run: grype sbom:sbom.spdx.json --fail-on high
- name: Upload SBOM
if: always()
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.spdx.json
GitHub-hosted runners execute steps as a non-root runner user with passwordless sudo configured, so the sudo sh invocations work without any extra setup. The same issue that requires sudo on a standard Linux desktop applies here; the fix is identical.
A few deliberate choices. permissions: contents: read enforces least privilege since this job never pushes. if: always() on the upload step means you get the SBOM artifact even when the vulnerability gate fails, so you can inspect it offline. Tagging the artifact with github.sha ties every SBOM to the exact commit and image that produced it.
Verify it works
Push to your repo and open the Actions tab. A clean run shows all steps green with the SBOM artifact in the run summary. A run with a critical CVE fails at "Vulnerability gate" with exit code 1 and prints a findings table.
To test the gate locally before committing:
docker pull python:3.11-slim
syft python:3.11-slim -o spdx-json=sbom-test.spdx.json
grype sbom:sbom-test.spdx.json --fail-on high; echo "Exit: $?"
Most *-slim base images carry OS-level CVEs, so you'll likely see a non-zero exit code and a populated findings table.
Troubleshooting
Syft reports zero packages. This usually means you scanned a scratch-based or empty image. Confirm with docker inspect <image>. Distroless images are supported but must be present in the local Docker daemon, not just referenced by digest.
Grype database fails to update in CI. Behind a corporate proxy, configure ~/.grype.yaml with a db.update-url pointing to an internal mirror. To avoid downloading the full database on every run, cache ~/.cache/grype using actions/cache keyed on a daily schedule or the Grype version.
grype sbom:... returns "unsupported scheme" or similar. You're on an older Grype release. Update with brew upgrade grype on macOS or re-run the install script on Linux.
A known false positive is blocking the build. Add an ignore rule in .grype.yaml at the repo root:
ignore:
- vulnerability: CVE-2023-XXXXX
package:
name: libssl
Ignore rules can match on vulnerability, package.name, package.version, package.type, and package.location. Keep your ignore list in version control and review it on a schedule.
Next steps
- Attach the SBOM as a verifiable OCI attestation using
syft attestwith a Cosign signing key, so downstream consumers can verify the SBOM was produced by your pipeline. - Ship SBOMs to a Dependency-Track instance for continuous tracking and policy enforcement across every service you run.
- Add
actions/cachearound the Grype DB directory to cut 20-30 seconds off every CI run once your pipeline frequency picks up.
Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.
Discussion 0
No comments yet
Be the first to weigh in.