# Block a banned dependency with a signed SBOM

Source: https://www.testifysec.com/docs/cilock/use-cases/ban-a-dependency/

A Claude Code session records a CycloneDX SBOM as signed evidence with cilock, writes a policy that refuses any release still shipping github.com/dgrijalva/jwt-go, shows it blocking the repo, migrates to golang-jwt/jwt/v5 and shows the same policy passing.

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.

 

How this was recorded

TestifySec wrote the prompt and sent it into the session. Claude Code and `cilock` ran unedited, and the recording was not changed afterwards.

 

A Claude Code session recording a signed CycloneDX SBOM with cilock and syft, writing a policy that bans github.com/dgrijalva/jwt-go, showing it blocks the repo, migrating to golang-jwt/jwt/v5 and showing the same policy passing

[Download terminal recording (.cast)](https://www.testifysec.com/media/docs/recordings/sbom-gate.cast)

 

## What happens, step by step

 

1. **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 sbom` is refused because there is no enrolled Pushgate agent, so Claude writes the policy by hand from `cilock policy guide --goal sbom`.
 2. **Record the SBOM.** Syft runs inside `cilock run` with the `sbom` attestor:
   
    
   
   ```bash
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.json
```
   
    
   
   In the recording Claude spells "no platform" as an empty `--platform-url`; `--offline` is the documented flag and does the same thing. The SBOM lists `github.com/dgrijalva/jwt-go v3.2.0+incompatible`.
 3. **Write the policy.** The `sbom` step must be signed by the evidence key, syft must exit `0` with exactly the pinned arguments (so a hand-made SBOM cannot stand in), the SBOM must list at least one component, and a `banned_jwt_go` rule denies any component whose name or Go package URL is `github.com/dgrijalva/jwt-go`. The rule denies when the SBOM cannot be read, instead of passing. `cilock policy validate --require-signed` passes on the signed policy.
 4. **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.json` exits `1`, and the ban is the only denial:
   
    
   
   ```text
policy was denied due to: banned dependency github.com/dgrijalva/jwt-go@v3.2.0+incompatible
(unmaintained, CVE-2020-26160): migrate to github.com/golang-jwt/jwt/v5
```
 5. **Migrate.** Only the import path changes in `token.go` and `token_test.go`; `go.mod` moves to `github.com/golang-jwt/jwt/v5 v5.3.1` and `go vet` and `go test` pass.
 6. **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`.
 7. **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](https://www.testifysec.com/docs/cilock/concepts/policy-verification). The [`sbom` attestor](https://www.testifysec.com/docs/cilock/attestors/sbom) documents the fields a rule can read, and [Syft](https://www.testifysec.com/docs/cilock/tools/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](https://www.testifysec.com/docs/cilock/concepts/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.
