# Triage scanner findings with signed VEX

Source: https://www.testifysec.com/docs/cilock/use-cases/vex-triage/

A Claude Code session records a govulncheck scan as signed evidence, checks that none of three golang.org/x/text findings reach the code, signs an OpenVEX statement for each with cilock attest vex, and writes a policy that refuses the release when any finding lacks one.

A vulnerability gate that blocks on every finding gets switched off the first time it blocks a release for code you do not run. A gate that lets people wave findings through by hand gets no better. VEX (Vulnerability Exploitability eXchange) is the middle path: a statement, per finding, that says why the finding does not apply, signed by whoever is accountable for that judgment.

 

`cilock attest vex` writes an OpenVEX document and signs it as evidence. It is fail-closed at authoring: `not_affected` needs a justification, `affected` needs an action statement, and unknown statuses are rejected. In this session the prompt asks Claude to record a govulncheck scan for a Go module that pins an old `golang.org/x/text`, check whether any finding reaches the code, write a signed VEX statement for each one that does not, and build a policy that passes only when every finding is covered.

 

How this was recorded

TestifySec wrote the prompt and sent it into the session, and approved one permission prompt (Claude deleting evidence files it had generated, before re-recording them). Claude Code and `cilock` ran unedited, and the recording was not changed afterwards. The session used Go 1.26.9 (`GOTOOLCHAIN`), so the scan reports the module's findings and not the standard library's.

 

A Claude Code session recording a signed govulncheck scan with cilock, writing a signed OpenVEX statement for each unreachable golang.org/x/text finding, and showing the policy refuse the release when one statement is missing

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

 

## What happens, step by step

 

1. **Scan.** govulncheck reports three findings in `golang.org/x/text v0.3.7`, none reachable, all matched at module level with no package or function in the trace:
   
    
   
   | Finding | Alias | Vulnerable package |
   | --- | --- | --- |
   | GO-2022-1059 | CVE-2022-32149 | `x/text/language` |
   | GO-2026-5970 | CVE-2026-56852 | `x/text/unicode/norm` |
   | GO-2026-6629 | CVE-2026-56851 | `x/text/secure/precis` |
 2. **Check the claim before signing it.** Claude does not take "unreachable" on the scanner's word: `go list -deps -test ./...` shows the module pulls in only `x/text/width` and `x/text/transform`. So each statement is `not_affected` with the justification `vulnerable_code_not_present`.
 3. **Record the scan and the statements.** Claude writes the steps into a script, `vex-gate/record.sh`, so they can be re-run. The scan is recorded with the `govulncheck` attestor; each statement is one `cilock attest vex` call:
   
    
   
   ```bash
cilock run --offline --step vulns -k "$KEY" --workload manual -a govulncheck \
  --subjects "$anchor" -o "$out/vulns.att.json" \
  -- sh -c 'govulncheck -json ./... > govulncheck.json'

cilock attest vex --offline -k "$KEY" --workload manual -a environment -s vex \
  --subjects "$anchor" \
  --product pkg:golang/example.com/labels --vuln CVE-2022-32149 --status not_affected \
  --justification vulnerable_code_not_present --author vex-triage \
  --impact-statement "golang.org/x/text/language is not in the build graph of example.com/labels ..." \
  --vex-out vex-gate/vex/go-2022-1059.openvex.json -o "$out/vex-go-2022-1059.att.json"
```
   
    
   
   `$anchor` is `go.sum=sha256:<digest of go.sum>`. Every envelope carries it as a subject, and the release is verified against `go.sum`, so the statements apply to this exact dependency set. When `x/text` is bumped, `go.sum` changes and the old statements stop counting.
 4. **Write the policy.** `cilock policy template --goal vulns --with-vex` needs an enrolled Pushgate agent, so Claude writes the Rego by hand from `cilock policy guide`. The scan command is pinned and must exit `0`. Every finding needs a signed statement that matches its GO id or a CVE/GHSA alias and names the product; only `fixed`, or `not_affected` with a justification, counts. Claude adds one more rule: a "not present" or "not in execute path" statement is refused if the signed scan shows the package imported or the code reachable, so a later import of `x/text/language` cannot hide behind today's statement.
 5. **Verify.** With all three statements, `cilock verify go.sum` exits `0`. With GO-2026-6629's statement left out it exits `1`: `GO-2026-6629 (aliases {"CVE-2026-56851"}) has no signed VEX statement from step "vex"…`. A statement signed with a key the policy does not trust is refused, an `under_investigation` statement in place of the real one is refused, and an `under_investigation` statement next to the valid one is refused too, because a conflicting statement blocks the finding.

 

## A VEX statement is a judgment, not a fix

 

Signing `not_affected` moves the decision to someone accountable and puts it in the evidence. It does not remove the vulnerable code from `go.sum`. The statements in this session say so in their impact statement ("Pending golang.org/x/text bump next sprint"), and binding them to the `go.sum` digest keeps them from outliving the dependency set they were written about.

 

The policy in the recording is hand-written; Claude says so and recommends a review before relying on it. See the [`vex` attestor](https://www.testifysec.com/docs/cilock/attestors/vex) for the fields a rule reads, [`govulncheck`](https://www.testifysec.com/docs/cilock/tools/govulncheck) for the scanner, and [Policy verification](https://www.testifysec.com/docs/cilock/concepts/policy-verification) for how `attestationsFrom` lets one step's rule read another step's evidence.

 

## Run it yourself

 

The recording used `cilock` built from `main` at commit `5b614726` and govulncheck on Go 1.26.9. Signing ran offline with local keys, so the signatures carry no timestamp; add `--timestamp-servers` in CI if you need one.

Reference generated from the product documentation. Match commands and support details to your installed release.
