Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

On this page

Triage scanner findings with signed VEX

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)

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:

    FindingAliasVulnerable package
    GO-2022-1059CVE-2022-32149x/text/language
    GO-2026-5970CVE-2026-56852x/text/unicode/norm
    GO-2026-6629CVE-2026-56851x/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:

    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 for the fields a rule reads, govulncheck for the scanner, and 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.