SBOM Testing: Software Bill of Materials with Syft, Grype, and CI Attestation

SBOM Testing: Software Bill of Materials with Syft, Grype, and CI Attestation

Software supply chain attacks have surged in recent years. SolarWinds, Log4Shell, and the XZ Utils backdoor all shared a common thread: attackers compromised components that developers trusted implicitly. A Software Bill of Materials (SBOM) is the foundational tool that lets you know exactly what is inside your software—and whether any of it is dangerous.

This guide covers everything you need to put SBOMs to work: generating them with Syft, scanning them with Grype, attesting them in CI, and integrating them with GitHub's dependency graph.

What Is an SBOM and Why Does It Matter?

An SBOM is a machine-readable inventory of every component in a piece of software: libraries, frameworks, transitive dependencies, operating system packages, and even compiler toolchains. Think of it as a nutrition label for software.

The US government's 2021 Executive Order on Improving the Nation's Cybersecurity mandated SBOMs for software sold to federal agencies. The EU Cyber Resilience Act follows the same logic. But even if you have no government customers, SBOMs give you something invaluable: the ability to answer "are we affected?" within minutes of a new CVE dropping—instead of days.

Two Dominant Formats

CycloneDX is developed by OWASP and is optimized for security use cases. It supports vulnerability data, license information, and service dependencies. The JSON and XML schemas are well-tooled and widely adopted by security scanners.

SPDX (Software Package Data Exchange) is an ISO standard (ISO/IEC 5962:2021) originally developed by the Linux Foundation. It is broader in scope, covering license compliance as well as security. SPDX 2.3 added support for relationships between components that make it useful for complex multi-repo projects.

For most teams starting out, CycloneDX JSON is the pragmatic choice: it is compact, well-supported by Grype and other scanners, and GitHub's dependency submission API accepts it natively.

Generating SBOMs with Syft

Syft is an open-source SBOM generator from Anchore. It can inspect container images, filesystems, archives, and directories, and it supports both CycloneDX and SPDX output.

Installation

# macOS
brew install syft

# Linux / CI
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# Verify
syft version

Generating an SBOM from a Container Image

# CycloneDX JSON (recommended for vulnerability scanning)
syft ghcr.io/myorg/myapp:latest -o cyclonedx-json > sbom.cdx.json

# SPDX JSON
syft ghcr.io/myorg/myapp:latest -o spdx-json > sbom.spdx.json

# Human-readable table (useful for quick inspection)
syft ghcr.io/myorg/myapp:latest -o table

Syft pulls the image, inspects every layer, and identifies packages through multiple catalogers: dpkg for Debian/Ubuntu, rpm for Red Hat-based images, apk for Alpine, and language-specific catalogers for Python (pip, Poetry), Node (npm, yarn), Go (go.mod), Java (Maven, Gradle), and more.

Generating an SBOM from a Local Directory

If you want an SBOM for your source code rather than a built image:

# From the current directory
syft dir:. -o cyclonedx-json > sbom.cdx.json

# From a specific path
syft dir:/path/to/project -o cyclonedx-json > sbom.cdx.json

Inspecting the Output

A CycloneDX JSON SBOM has a straightforward structure:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.4",
  "serialNumber": "urn:uuid:...",
  "version": 1,
  "metadata": {
    "timestamp": "2026-01-15T10:30:00Z",
    "component": {
      "type": "container",
      "name": "myapp",
      "version": "1.2.3"
    }
  },
  "components": [
    {
      "type": "library",
      "name": "express",
      "version": "4.18.2",
      "purl": "pkg:npm/express@4.18.2",
      "licenses": [{ "license": { "id": "MIT" } }]
    }
  ]
}

The purl (Package URL) field is the key: it is a standardized identifier that vulnerability databases use to cross-reference components against known CVEs.

Vulnerability Scanning with Grype

Grype is Anchore's vulnerability scanner. It can scan container images directly, but its real power comes from scanning SBOMs—this means you can scan offline, scan faster, and scan the same artifact multiple times as new vulnerabilities are disclosed.

Installation

# macOS
brew install grype

# Linux / CI
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

Scanning an SBOM

# Scan a CycloneDX SBOM
grype sbom:./sbom.cdx.json

# Scan with SARIF output (importable into GitHub Security tab)
grype sbom:./sbom.cdx.json -o sarif > grype-results.sarif

# Scan and fail on critical/high vulnerabilities
grype sbom:./sbom.cdx.json --fail-on high

Understanding Grype Output

NAME              INSTALLED  FIXED-IN   TYPE  VULNERABILITY   SEVERITY
openssl           3.0.7      3.0.8      deb   CVE-2023-0286   High
libxml2           2.9.14     (none)     deb   CVE-2022-40303  Medium
express           4.18.1     4.18.2     npm   CVE-2022-24999  Medium

The FIXED-IN column is critical: if a fix exists, upgrading the package resolves the vulnerability. If it shows (none), you need to evaluate mitigating controls or consider alternative packages.

Grype Configuration File

Create .grype.yaml at your project root to set consistent policies:

# .grype.yaml
output: table
fail-on-severity: high
ignore:
  # Accepted risk: no fix available, internal service only
  - vulnerability: CVE-2022-40303
    reason: "No fix available; service not internet-facing"
  # False positive: we don't use the affected code path
  - package:
      name: lodash
      version: 4.17.21
    vulnerability: CVE-2021-23337

SBOM Attestation in CI

Generating an SBOM is only half the job. Attestation proves that a specific SBOM belongs to a specific artifact and was produced by a trusted build process. Without attestation, an attacker could swap your SBOM for one that hides a malicious component.

Full GitHub Actions Workflow

# .github/workflows/sbom.yml
name: SBOM Generation and Scanning

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  packages: write
  id-token: write          # Required for keyless signing
  security-events: write   # Required for SARIF upload

jobs:
  sbom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build container image
        run: |
          docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .

      - name: Push image
        if: github.ref == 'refs/heads/main'
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

      - name: Install Syft
        uses: anchore/sbom-action/download-syft@v0

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: ghcr.io/${{ github.repository }}:${{ github.sha }}
          format: cyclonedx-json
          output-file: sbom.cdx.json
          upload-artifact: true
          upload-release-assets: true

      - name: Scan SBOM with Grype
        uses: anchore/scan-action@v3
        id: grype
        with:
          sbom: sbom.cdx.json
          fail-build: true
          severity-cutoff: high
          output-format: sarif

      - name: Upload SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: ${{ steps.grype.outputs.sarif }}

      - name: Install cosign
        if: github.ref == 'refs/heads/main'
        uses: sigstore/cosign-installer@v3

      - name: Attest SBOM
        if: github.ref == 'refs/heads/main'
        run: |
          cosign attest \
            --predicate sbom.cdx.json \
            --type cyclonedx \
            ghcr.io/${{ github.repository }}:${{ github.sha }}

Submitting to GitHub's Dependency Graph

GitHub's Dependency Submission API lets you feed SBOM data into the native dependency graph, which powers Dependabot alerts and the dependency review action on PRs.

      - name: Submit SBOM to GitHub Dependency Graph
        uses: advanced-security/spdx-dependency-submission-action@v0.1.1
        with:
          filePath: sbom.spdx.json

Or use the official action for CycloneDX:

      - name: Submit Dependencies to GitHub
        uses: mikepenz/sbom-action@v1
        with:
          output: sbom.cdx.json
          token: ${{ secrets.GITHUB_TOKEN }}

Once submitted, the dependency graph at https://github.com/ORG/REPO/network/dependencies shows every component your container image contains—not just what package.json or go.mod declares, but what actually ended up in the image.

Verifying SBOM Attestations

When pulling an image, you can verify its SBOM attestation was produced by your CI pipeline:

cosign verify-attestation \
  --type cyclonedx \
  --certificate-identity-regexp "https://github.com/myorg/myapp/.github/workflows/sbom.yml" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  ghcr.io/myorg/myapp:latest | jq '.payload | @base64d | fromjson | .predicate.components | length'

This command:

  1. Verifies the attestation signature against Sigstore's transparency log
  2. Confirms the identity of the workflow that signed it
  3. Decodes the SBOM payload and counts the components

If the attestation is missing or invalid, the command fails—giving you a hard gate before deployment.

Enforcing SBOM Policy in Kubernetes

Once images have SBOM attestations, you can enforce policy at the cluster level with Kyverno:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-sbom-attestation
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-sbom-attestation
      match:
        any:
          - resources:
              kinds: [Pod]
      verifyImages:
        - imageReferences: ["ghcr.io/myorg/*"]
          attestations:
            - predicateType: https://cyclonedx.org/bom
              conditions:
                - all:
                    - key: "{{ components | length(@) }}"
                      operator: GreaterThan
                      value: 0

Integrating SBOM Checks into Pull Requests

Add a PR comment that surfaces new vulnerabilities introduced by a change:

      - name: Diff SBOM against base branch
        if: github.event_name == 'pull_request'
        run: |
          # Generate SBOM for base branch image
          syft ghcr.io/${{ github.repository }}:${{ github.base_ref }} \
            -o cyclonedx-json > sbom-base.cdx.json
          
          # Compare component counts
          BASE_COUNT=$(jq '.components | length' sbom-base.cdx.json)
          HEAD_COUNT=$(jq '.components | length' sbom.cdx.json)
          
          echo "## SBOM Diff" >> $GITHUB_STEP_SUMMARY
          echo "Base: $BASE_COUNT components | PR: $HEAD_COUNT components" >> $GITHUB_STEP_SUMMARY
          echo "Delta: $((HEAD_COUNT - BASE_COUNT)) components" >> $GITHUB_STEP_SUMMARY

Building an SBOM Culture

Tooling is the easy part. The organizational shift is harder.

Store SBOMs as release artifacts. Every release should include its SBOM in a predictable location—alongside the binary or in the container registry as an attestation. When CVE-2024-XXXX drops at 2am, you need to answer "are we affected?" in five minutes, not five hours.

Scan on schedule, not just on build. New vulnerabilities appear against old code. Add a nightly workflow that scans the SBOMs of your last three production releases against the latest vulnerability databases.

Track SBOM age. An SBOM generated six months ago is stale. Treat it like a perishable. Any image that has not been rebuilt in 90 days should have its SBOM regenerated and rescanned.

Integrate with your ticketing system. Grype's JSON output can be parsed to auto-create tickets for high and critical findings. Don't make humans manually translate scanner output into Jira issues.

SBOMs are not a silver bullet—they cannot prevent vulnerabilities from being introduced. But they dramatically shrink the window between disclosure and response, and they are rapidly becoming a baseline requirement for enterprise customers and regulated industries. Start generating them now, while the tooling is mature and the process is still optional.

Read more

Start now free