Security
9 min readOctober 7, 2026

SBOM Generation for Containers: Syft, Grype, and in-toto Attestations

CO
Coding Protocols Team
Platform Engineering
SBOM Generation for Containers: Syft, Grype, and in-toto Attestations

Quick answer

An SBOM is a structured, machine-readable inventory of everything inside a container image — not a changelog, not a README. Here's how to generate one with Syft, scan it for known vulnerabilities with Grype, and attach it to the image as a signed in-toto attestation that an admission policy can actually verify.

9 min read · Security

"We have an SBOM for that image" means nothing on its own. An SBOM (Software Bill of Materials) is a structured, machine-readable inventory of every package, library, and dependency inside a container image — its name, version, and license, cross-referenced against vulnerability databases. It is not a changelog, not a README, and not something you can accurately reconstruct by reading a Dockerfile. If you want to know whether a specific log4j version shipped in an image you built eight months ago, an SBOM is how you answer that in seconds instead of re-pulling and re-scanning the image.

This post covers the practical pipeline: generate the SBOM with Syft, scan it for known vulnerabilities with Grype, and attach it to the image as a signed attestation with cosign — so a cluster admission policy can verify "this image has an attached, signed SBOM" before it's allowed to run, not just "this image is signed."


SPDX vs CycloneDX: The Two Formats That Matter

Two SBOM formats dominate in practice, and the difference is about intent, not just syntax:

  • SPDX (Software Package Data Exchange) is a Linux Foundation standard built around license compliance and provenance. It's the format procurement and legal teams expect, and it's the one named explicitly in US federal software supply-chain requirements.
  • CycloneDX was built by OWASP specifically for application security — it has first-class fields for vulnerability data and is the format most security scanners (including Grype) consume most naturally.

In practice: generate CycloneDX if your primary goal is vulnerability scanning, SPDX if you need to satisfy a compliance or license-audit requirement, or both if you don't want to choose. Syft can emit either from the same scan, so there's rarely a reason to pick only one.


Generating an SBOM with Syft

Syft scans a container image (from a registry, a local Docker daemon, or a tarball) or a filesystem and produces an SBOM without needing the image's source code or build process:

bash
1# CycloneDX JSON — most scanner-friendly format
2syft registry:myorg/payments-api:v1.4.0 -o cyclonedx-json > sbom.cdx.json
3
4# SPDX JSON — the compliance-oriented format
5syft registry:myorg/payments-api:v1.4.0 -o spdx-json > sbom.spdx.json
6
7# Generate both from a single scan
8syft myorg/payments-api:v1.4.0 \
9  -o cyclonedx-json=sbom.cdx.json \
10  -o spdx-json=sbom.spdx.json

Syft works by inspecting the image's filesystem layers directly — package manager metadata (dpkg, apk, rpm), language-specific lockfiles and manifests (package-lock.json, go.sum, requirements.txt, compiled Go binaries' embedded module info), and known binary signatures. It doesn't execute the image or need access to the original build environment, which is exactly why it can be run in CI immediately after a build step, or retroactively against an image someone else built years ago.


Scanning the SBOM with Grype

Grype takes an SBOM (or an image directly) and matches every package in it against vulnerability databases (NVD, GitHub Security Advisories, distro-specific feeds). Syft and Grype are a pair, not competitors — Syft answers "what's in here," Grype answers "is any of that known to be vulnerable":

bash
1# Scan a previously generated SBOM
2grype sbom:sbom.cdx.json
3
4# Fail the pipeline if anything high-severity or worse is found
5grype sbom:sbom.cdx.json --fail-on high --output json --file grype-results.json
6
7# Scan an image directly (Grype calls Syft internally if no SBOM is supplied)
8grype myorg/payments-api:v1.4.0 --fail-on critical

Severity levels, lowest to highest: negligible, low, medium, high, critical. With --fail-on, Grype exits with code 2 when a finding at or above that threshold exists, 0 when the scan is clean — that exit code is what a CI step actually gates on. A .grype.yaml file lets you set the same threshold plus persistent ignore rules (for a CVE you've assessed as not exploitable in your context) without repeating flags on every invocation.

Don't fail every build on every finding. A --fail-on low gate on a large base image will fail constantly on low-severity CVEs with no realistic exploit path, training your team to ignore the gate entirely. Start at high or critical, and use ignore rules for specific, reviewed exceptions rather than lowering the global threshold.


Attaching the SBOM as a Signed Attestation

A plain SBOM file sitting in CI artifacts proves nothing to the cluster — anyone could hand you a fabricated one. cosign attest solves this by wrapping the SBOM in an in-toto attestation, signing it, and publishing it alongside the image in the registry (via Sigstore's keyless signing or a stored key):

bash
1# Attest the CycloneDX SBOM, bound to the image's digest
2cosign attest --type cyclonedx \
3  --predicate sbom.cdx.json \
4  myorg/payments-api@sha256:a1b2c3d4...
5
6# Or SPDX JSON
7cosign attest --type spdxjson \
8  --predicate sbom.spdx.json \
9  myorg/payments-api@sha256:a1b2c3d4...

Two details that matter in practice:

  1. Attest by digest, not by tag. myorg/payments-api:v1.4.0 is mutable — the tag can be repointed to a different image tomorrow. @sha256:... is immutable. An attestation bound to a tag is an attestation about nothing in particular.
  2. --type is a predicate type, not a free-text label. Cosign's accepted values include spdx, spdxjson, cyclonedx, slsaprovenance, vuln, and openvex — use the one that matches what you actually generated, since verification tooling checks the predicate type explicitly.

Verification at admission time uses the same mechanism in reverse:

bash
cosign verify-attestation \
  --type cyclonedx \
  --certificate-identity-regexp "https://github.com/myorg/.*" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  myorg/payments-api@sha256:a1b2c3d4...

A Kyverno ClusterPolicy or OPA Gatekeeper constraint can run this check (via Kyverno's built-in verifyImages attestation support, or a Gatekeeper external-data provider) before the admission controller allows the Pod to be created — rejecting any image that lacks a validly signed SBOM attestation from your pipeline's identity, not just rejecting unsigned images outright.


Server & SSH Hardening Checklist

The firewall, SSH, fail2ban, and update baseline every internet-facing Linux box should pass. Plain Markdown you can run down in an afternoon.

Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.

Where This Fits in a CI Pipeline

The full sequence, in order:

1. Build the image
2. syft <image> -o cyclonedx-json=sbom.cdx.json      # Generate SBOM
3. grype sbom:sbom.cdx.json --fail-on high            # Scan, gate the build
4. Push the image to the registry
5. cosign sign <image>@<digest>                       # Sign the image itself
6. cosign attest --type cyclonedx --predicate sbom.cdx.json <image>@<digest>  # Attach SBOM
7. (At deploy time) Admission policy verifies the attestation before allowing the Pod

Steps 2-3 happen before push; steps 5-6 happen after, since both signing and attestation need the final image digest, which only exists once the image is pushed to a registry. Running them in the wrong order is the most common reason teams end up with a signed image that has no matching attestation, or vice versa.


SBOM vs SLSA Provenance: Different Questions

These get conflated constantly, but they answer different questions:

  • SBOM answers: what's inside this artifact? (packages, versions, licenses)
  • SLSA provenance answers: how was this artifact built? (which source commit, which CI system, which build steps, was the build process itself tamper-resistant)

A perfectly clean SBOM with zero known vulnerabilities tells you nothing about whether the image was built by your legitimate pipeline or by someone who compromised a build step. They're complementary controls, not substitutes — mature supply-chain security posture uses both: SBOM + vulnerability scanning for "what's in here and is it safe," SLSA provenance for "can I trust how this was produced."


Frequently Asked Questions

Do I need both SPDX and CycloneDX, or can I pick one?

Pick one unless you have a specific reason not to. CycloneDX if vulnerability scanning is the primary goal (it's what most scanners, including Grype, are built around). SPDX if you have a compliance or license-audit requirement that names it explicitly. Syft generates both from a single scan at near-zero extra cost, so "generate both" is a reasonable default if you're unsure which a downstream consumer will want.

Does Grype replace a dedicated container scanner like Trivy?

They overlap significantly — both match package inventories against vulnerability databases, and either can generate its own SBOM internally or consume one from Syft. The practical difference is ecosystem fit: Grype pairs natively with Syft's SBOM output and the Anchore/Sigstore tooling, while Trivy is a single all-in-one binary that also covers IaC misconfiguration scanning and secret detection. Many teams run Syft+Grype specifically because they want the SBOM as a durable artifact (for attestation, compliance, and future rescanning), not just a pass/fail scan result.

Can I rescan an old SBOM for new vulnerabilities without re-pulling the image?

Yes — this is one of the main points of generating and storing the SBOM in the first place. grype sbom:old-sbom.cdx.json scans the stored package inventory against today's vulnerability database, so you can answer "is this image I built eight months ago affected by the CVE that just got disclosed" without touching the registry or the original build environment at all.

What happens if the admission policy can't reach the registry to verify the attestation?

This is a real production failure mode worth planning for explicitly: decide upfront whether a verification failure (registry unreachable, OIDC issuer down) fails open (allow the Pod, log a warning) or fails closed (block the Pod). Most teams fail closed for production namespaces and fail open for lower environments, but that's a deliberate policy choice to make, not a default to discover during an incident.


For general container image hardening beyond SBOMs specifically, see Container Image Security: Supply Chain from Build to Production. For the broader toolset — Cosign image signing, SLSA provenance, and Kyverno/OPA policy enforcement working together — see Supply Chain Security Tools for Kubernetes: What to Use and When. For what happens when a build pipeline's own artifacts leak instead of being properly scoped, see CI/CD Supply Chain Security: How Source Code Leaks Happen.

Building an SBOM-and-attestation pipeline for your CI/CD system, or trying to pass a supply-chain security audit? Talk to us at Coding Protocols — we help platform teams wire up SBOM generation, vulnerability gating, and signed attestations without turning every build into a 20-minute compliance ritual.

Official References

  • Syft — SBOM generation CLI and supported formats
  • Grype — vulnerability scanning against SBOMs and images
  • Sigstore Cosign documentation — signing, attestation, and keyless verification

Was this article helpful?

Be the first to rate this article

Related Topics

SBOM
Supply Chain Security
Containers
Syft
Grype
Security

Found this useful? Share it.

Practice this

Related tools

Read Next

Want this running in production, not just on paper?

We're a hands-on DevOps consultancy — Kubernetes, CI/CD, and cloud infrastructure.

Explore Our Services