Block a banned dependency with a signed SBOM
An SBOM is an inventory. On its own it does not stop anything: a file that says your release contains a deprecated package is only useful if something reads it before the release goes out, and only trustworthy if you know which tool produced it and that nobody edited it afterwards.
CI/lock records the SBOM as signed evidence. The sbom attestor captures the
CycloneDX or SPDX file a scanner writes, command-run records the exact
scanner command, and a Rego rule over the SBOM's components decides whether
the release passes. In this session the prompt asks Claude to block any
release of a Go module that still ships github.com/dgrijalva/jwt-go
(unmaintained, CVE-2020-26160), then to migrate the code and show the gate
passing.
TestifySec wrote the prompt and sent it into the session. Claude Code and
cilock ran unedited, and the recording was not changed afterwards.
What happens, step by step
-
Two keys. Claude generates an ed25519 key that signs the evidence and a separate one that signs the policy. Separate keys let you separate the roles: whoever holds only the evidence key cannot change the rules. In the recording Claude holds both, so that separation exists only once the policy key goes to someone else.
cilock policy template --goal sbomis refused because there is no enrolled Pushgate agent, so Claude writes the policy by hand fromcilock policy guide --goal sbom. -
Record the SBOM. Syft runs inside
cilock runwith thesbomattestor:cilock run --step sbom --offline -k keys/evidence.key -a sbom \ -o evidence/sbom-before.att.json -- \ syft dir:. --exclude ./keys --exclude ./evidence -o cyclonedx-json=sbom.cdx.jsonIn the recording Claude spells "no platform" as an empty
--platform-url;--offlineis the documented flag and does the same thing. The SBOM listsgithub.com/dgrijalva/jwt-go v3.2.0+incompatible. -
Write the policy. The
sbomstep must be signed by the evidence key, syft must exit0with exactly the pinned arguments (so a hand-made SBOM cannot stand in), the SBOM must list at least one component, and abanned_jwt_gorule denies any component whose name or Go package URL isgithub.com/dgrijalva/jwt-go. The rule denies when the SBOM cannot be read, instead of passing.cilock policy validate --require-signedpasses on the signed policy. -
The gate blocks today's repo.
cilock verify sbom.cdx.json --offline -p policy/policy.signed.json -k keys/policy.pub -a evidence/sbom-before.att.jsonexits1, and the ban is the only denial:policy was denied due to: banned dependency github.com/dgrijalva/[email protected]+incompatible (unmaintained, CVE-2020-26160): migrate to github.com/golang-jwt/jwt/v5 -
Migrate. Only the import path changes in
token.goandtoken_test.go;go.modmoves togithub.com/golang-jwt/jwt/v5 v5.3.1andgo vetandgo testpass. -
The same policy passes. Claude checks the signed policy's SHA-256 has not changed, records a new SBOM with the same command and verifies it: exit
0. -
Try to get around it. The new SBOM signed with a key the policy does not trust is refused, and the new SBOM checked against the earlier evidence is refused because the file's digest is not a subject of that evidence. Both exit
1.
Why the gate trusts the SBOM
| Question | Answered by |
|---|---|
| Which tool wrote this SBOM, with which arguments? | command-run and the pinned-command rule |
| Was it edited after the scan? | cilock verify sbom.cdx.json hashes the file and requires that digest in the signed evidence |
| Who produced the evidence? | the step's functionary names the evidence key |
| Is the banned package in it? | the Rego rule over components[] |
What the gate does not show is which source the SBOM describes. The release
is verified against the SBOM file, so a clean SBOM from another checkout,
presented with its own matching evidence, would pass; the stale-evidence check
in step 7 only catches an SBOM paired with the wrong evidence. To bind the
inventory to the source you release, verify against the commit: record the
step with the git attestor and verify with the commit as the subject. A
go.sum digest does not do this, because it names only the checksum file and
different source trees and go.mod selections can share one. When you ship a binary, chain the SBOM step to the build
step with artifactsFrom so the inventory is tied to the bytes you release;
see Policy verification. The
sbom attestor documents the fields a rule can read,
and Syft the scanner side.
Run it yourself
The recording used cilock built from main at commit 5b614726 and syft
1.44.0. Keep the evidence key in a secret store, or sign keyless in CI with
the job's OIDC identity; see
Signing and identity. The recording
ran offline, so the signatures carry no timestamp.
Reference generated from the product documentation. Match commands and support details to your installed release.