December 2021. You’re On Pager Duty.
Suddenly, Slack explodes. Log4Shell. A zero-day in log4j. In everything.
You have no idea what’s in your app. Dependencies of dependencies of dependencies, nested three, four, five layers deep. Does your Java microservice use log4j? Probably. Does it use it directly, or only transitively through some library you forgot you imported two years ago? Good luck finding out.
You’re staring at the dependency tree like it’s the final season of Lost, trying to trace causality backwards. Meanwhile, the internet is literally on fire, and your team is guessing.
An SBOM — a Software Bill of Materials — would have answered that question in seconds. A machine-readable list of every package in your app, every version, every hash. Like an ingredients label on a food product. You check what’s in your cereal before you feed it to your kids. Why are we not doing this with code?
What’s a Supply Chain Attack, Anyway?
You might think of supply chain attacks as nation-state stuff — which, fair, SolarWinds in 2020 was nation-state stuff. But they’re not rare. They’re the norm now.
The pattern is simple: attackers don’t break your code. They break someone else’s code that you depend on. You download it, you build it into your application, you ship it to production. Boom. You’re compromised.
Log4Shell was the wake-up call. It wasn’t “hidden” in your app on purpose — it was just there, doing its job, until someone discovered it could do something a little too much like its job. And because log4j is everywhere (it’s a fundamental Java logging library), everyone had to panic-patch simultaneously.
The real question isn’t “how do we prevent supply chain attacks?” It’s “how do we know what we’re shipping?”
That’s where an SBOM comes in.
An SBOM Is Just an Ingredients Label
A Software Bill of Materials is exactly what it sounds like: a comprehensive list of every component, library, and dependency in your software. Package name, version, hash, license, known vulnerabilities.
It’s what you should have been documenting all along but didn’t, because nobody thought to ask until things started exploding.
There are two major SBOM formats: SPDX (older, document-focused, very detailed) and CycloneDX (newer, container-friendly, oriented toward DevSecOps tooling). In practice, CycloneDX is what you’ll use if you’re shipping containers. Grype, the vulnerability scanner we’ll talk about next, prefers CycloneDX. So do most modern CI/CD systems.
Syft: Generate Your SBOM
Syft is a command-line tool from Anchore that generates SBOMs from basically any source: a Docker image, a Kubernetes pod, a directory on disk, an OCI artifact. It’s fast, it’s reliable, and it’s open source.
Install it:
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/binGenerate an SBOM from a Docker image:
syft docker:nginx:latest -o json > nginx-sbom.jsonThe output is a structured JSON file listing every package in that image. For nginx, that’s probably a few dozen packages. For a Node app with hundreds of npm dependencies? Hundreds or thousands of entries.
Here’s what a small chunk looks like:
{ "artifacts": [ { "name": "openssl", "version": "1.1.1w", "type": "apk", "purl": "pkg:apk/openssl@1.1.1w" }, { "name": "libc", "version": "0.7.12", "type": "apk", "purl": "pkg:apk/libc@0.7.12" } ], "source": { "type": "docker", "target": "nginx:latest" }}Each entry has a PURL (Package URL), version, and type. For CI/CD, you pipe that output into your artifact store so you can scan it later (or scan it immediately, right there in the build).
You can also generate SBOMs from directories:
syft /path/to/project -o cyclonedx > app-sbom.xmlOr from a running container:
syft 'docker://containerid' -o json > container-sbom.jsonSyft auto-detects the package managers (npm, pip, cargo, go, maven, etc.) and builds the full dependency tree.
Grype: Scan for Known Vulnerabilities
Now that you have a complete list of what’s in your software, the next step is obvious: are any of these packages known to be vulnerable?
Grype is Anchore’s vulnerability scanner. It takes an SBOM (or scans a container or directory directly) and cross-references every package against the NVD (National Vulnerability Database) and other sources. It tells you which CVEs affect you, their severity, and whether patches exist.
Install Grype:
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/binScan an SBOM:
grype sbom:nginx-sbom.jsonScan a Docker image directly:
grype docker:nginx:latestGrype output looks like this:
✔ Vulnerability DB [updated] ✔ Scanned image ✔ Vulnerability scan
1 package [1 vulnerability]
CRITICAL CVE-2024-1234 openssl 1.1.1w Fixed in: 1.1.1z https://nvd.nist.gov/vuln/detail/CVE-2024-1234
INFO CVE-2020-5678 libc 0.7.12 No fix availableYou can export results in multiple formats:
grype docker:nginx:latest -o json > scan-results.jsongrype docker:nginx:latest -o sarif > scan-results.sarifJSON for parsing, SARIF for GitHub security scanning. Dead simple.
Integrate Into CI
Here’s where it gets powerful: bake this into your build pipeline.
name: SBOM + Scan
on: [push]
jobs: sbom: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Build Docker image run: docker build -t myapp:${{ github.sha }} .
- name: Generate SBOM run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin syft docker:myapp:${{ github.sha }} -o cyclonedx > sbom.xml
- name: Scan for vulnerabilities run: | curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin grype sbom:sbom.xml -o json > scan-results.json
- name: Fail if critical found run: | CRITICAL=$(jq '[.matches[] | select(.vulnerability.severity=="CRITICAL")] | length' scan-results.json) if [ "$CRITICAL" -gt 0 ]; then echo "Found $CRITICAL critical vulnerabilities" exit 1 fi
- name: Upload artifacts uses: actions/upload-artifact@v4 with: name: sbom-and-scan path: | sbom.xml scan-results.jsonEvery build generates an SBOM, scans it, and fails if critical CVEs exist. Your artifacts are stored as evidence. When a new CVE is announced, you can query your artifact history: “do any of our builds from the past year contain this package?”
Attestation and Trust
Once you’re generating SBOMs, you’ll want to prove they haven’t been tampered with. Enter Cosign, a tool from Sigstore that cryptographically signs artifacts.
cosign sign-blob sbom.json --key cosign.key > sbom.json.sigcosign verify-blob sbom.json --signature sbom.json.sig --key cosign.pubYour SBOM is now signed. Consumers can verify it came from your build pipeline, unmodified. It’s a small thing, but it matters in a supply chain security story.
When Grype Finds Something
So Grype screams at you. “CRITICAL CVE in openssl 1.1.1w.”
Now what?
Triage. Not all critical CVEs affect you. Does the vulnerability require specific conditions? A particular API call? Read the CVE details. Maybe your app doesn’t hit that code path.
Upgrade. Usually the answer. Bump the dependency, re-run the SBOM, re-scan. Gone.
Accept the risk. Sometimes you can’t upgrade (legacy system, no patch available, you’re EOL). Document it. Know the risk. That’s better than not knowing.
Patch around it if the package maintainer won’t. Filter inputs, add controls, isolate the affected component. Not ideal, but better than ignoring it.
The point is: you know. You have a paper trail. You made a decision with full information. That’s the whole game.
The Boring Stuff Is Actually the Hard Part
Here’s the thing: SBOMs are now table stakes. Enterprises will demand them. SLSA compliance requires them. The tech is easy — Syft and Grype handle it in seconds.
The hard part is actually doing something when you find a vulnerability. The infrastructure, the testing, the approval process, the risk acceptance. That’s your real work.
But at least you’ll know what you’re shipping. And on the day the next Log4Shell hits, you won’t be guessing.
Generate an SBOM. Scan it. Commit it. Sleep better.