# govulncheck integration

Source: https://www.testifysec.com/docs/cilock/tools/govulncheck

Scan Go modules for vulnerabilities with reachability via govulncheck under cilock — the SARIF report becomes a signed v0.3 attestation parsed by the rookery sarif attestor.

| Upstream | [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) · The Go Authors · BSD-3-Clause |
| --- | --- |
| Category | vulnerability-scan (primary) |
| Catalog source | attestor-backed (ships a native cilock attestor) |
| Predicate type | `https://aflock.ai/attestations/govulncheck/v0.1` |
| Recommended trace | off — no syscall tracing needed |
| Detected when | - preargv\_prefix: govulncheck<br>- postexec\_observed\_argv\_prefix: govulncheck<br>- postfile\_exists: go.mod<br>- postproduct\_glob: govulncheck\*.json \| \*\*/govulncheck\*.json |

Confirm CI/lock detects it:

```bash
cilock plan --format=json -- govulncheck [...]
```

The facts in this box are generated from the CI/lock binary's own catalog (`cilock tools list`). Do not hand-edit — run `npm run gen:catalog`.

 

## You already know how to run govulncheck. Here's what cilock adds.

 

On its own, `govulncheck` writes a report file. That's an artifact you have to remember, can't prove came from a specific build, and can't tie back to the artifact it was scanning. Cilock wraps the same command and turns the report into a **signed, linked attestation** in your supply chain graph.

 

### What cilock adds

 

**Signed evidence.** govulncheck's reachability claims are signed by the CI workflow. "This vulnerability is not exploitable in our binary" becomes a defensible audit statement, not a hand-wave.

 

**Linked to the artifact.** Each govulncheck attestation links to the Go module materials, the binary it analyzed, and the git commit — auditors trace reachability claims back to the actual artifact.

 

**Policy at the deploy gate.** Policy: "vulnerability findings without a call-trace are advisory; findings WITH a trace block the deploy." Cilock verify reads the reachable flag.

 

**Central audit.** When the next Spring4Shell happens, you can prove which Go services are reachably affected vs imported-but-safe in minutes, not weeks.

 

### From code to prod, end-to-end

 

```text
     git commit (git attestor)
           ↓ subject digest matches
     CI run (github attestor: OIDC, run id)
           ↓ same run produces
     govulncheck scan (this attestor: signed findings)
           ↓ back-refs the artifact
     image / SBOM / binary it scanned
           ↓ same digest gates
     cilock verify policy → allow/deny deploy
```

 

Each arrow is a cryptographic subject digest match. No human reads logs, no copy-paste between systems. The same digest links the scan to the artifact to the deploy decision.

 

## Validated invocation

 

cilock invokes govulncheck directly so `command-run/v0.2` records the literal govulncheck argv. govulncheck has no `-o`/`--output` flag (it writes SARIF to stdout), so a single shell redirect routes its output to a file the product attestor can hash — that's not the `cp` antipattern, it's the only way govulncheck exposes file output today:

 

```bash
cilock run --step govulncheck-scan \
  --signer-file-key-path key.pem --outfile attestation.json \
  --attestations sarif,environment,git \
  -- sh -c 'govulncheck -format sarif ./... > govulncheck.sarif'
```

 

The wrapped `sh -c` records `["sh","-c","govulncheck -format sarif ./... > govulncheck.sarif"]` in command-run — the literal govulncheck argv is right there in the captured envelope, not hidden behind a `cp` of a file produced outside cilock's view. When [`golang/vuln`](https://github.com/golang/vuln) adds a file-output flag, the shell wrapper can be dropped.

 

## What gets captured

 

| Predicate type | Source |
| --- | --- |
| `https://aflock.ai/attestations/environment/v0.1` | host OS, kernel, env vars (sensitive ones obfuscated) |
| `https://aflock.ai/attestations/git/v0.1` | commit hash, branch, tags, dirty status, parents |
| `https://aflock.ai/attestations/material/v0.3` | Merkle root over the Go module before govulncheck runs |
| `https://aflock.ai/attestations/command-run/v0.2` | the literal `sh -c 'govulncheck …'` argv + exit code + ptrace |
| `https://aflock.ai/attestations/product/v0.3` | Merkle root over `govulncheck.sarif` as a real product file |
| `https://aflock.ai/attestations/sarif/v0.1` | the parsed SARIF document (driver = `govulncheck`, rules + results) |

 

## Why this shape

 

| Antipattern (older docs) | Correct shape (this example) |
| --- | --- |
| `cilock run ... -- bash -c "cp govulncheck.sarif govulncheck-product.sarif"` after running govulncheck outside cilock | `cilock run ... -- sh -c 'govulncheck -format sarif ./... > govulncheck.sarif'` |
| `command-run.cmd` records `["bash","-c","cp …"]` — cilock "ran" cp | `command-run.cmd` records `["sh","-c","govulncheck -format sarif ./... > govulncheck.sarif"]` — the literal govulncheck argv is in the envelope |
| The ptrace spy traces `cp`, not govulncheck | The ptrace spy traces govulncheck's syscalls because cilock is its parent (via the shell) |
| Product is a copy of a file govulncheck wrote elsewhere | Product is the file govulncheck wrote inside the wrapped step |

 

The `sh -c` wrapper is a tool-output limitation — govulncheck (as of v1.x) has no `-o` / `--output` flag and writes SARIF only to stdout. A single shell redirect routes its output to a file the product attestor can hash. This is NOT the cp antipattern — once [`golang/vuln`](https://github.com/golang/vuln) ships a file-output flag, the wrapper can be dropped.

 

## Attesting the `-json` stream: run govulncheck inside `cilock run`

 

The `govulncheck` attestor (`https://aflock.ai/attestations/govulncheck/v0.1`) reads govulncheck's own `-json` stream instead of SARIF. It only attests a scan that ran inside the same `cilock run`:

 

```bash
cilock run --step vulns -a git -a govulncheck \
  -- sh -c 'govulncheck -json ./... > govulncheck.json'
```

 

The `-json` stream has no record that marks the end of a scan, so a stream alone cannot show that the scan finished. The attestor needs two things: the stream reaches govulncheck's `Checking ... against the vulnerabilities...` record (govulncheck v1.1.1 or later), and the collection's `command-run` shows the wrapped command exited 0. If either is missing, the attestor refuses the scan. The run exits 1, and the signed collection has no `govulncheck` predicate, so a failed scan is never stored as zero findings. These are refused:

 

- The wrapped command exited non-zero. This includes runs with `--ignore-command-exit-code`.
 - Under `--trace`, a traced `govulncheck` process exited non-zero, even when a wrapper such as `|| true` exited 0.
 - The step produced more than one completed `-json` stream. The attestor signs one report per step and has no safe way to choose between them, so write one stream per step.
 - The collection has no `command-run`, so nothing shows how govulncheck exited. cilock-action's `action-ref` mode records `github-action` instead of `command-run`. Run govulncheck through the action's `command` input.

 

`cilock attest -a govulncheck` does not attest a `govulncheck.json` written before the step. `cilock attest` wraps a no-op command, so that file is not a product of the step, and the attestor is skipped. Run the scan inside the step.

 

Without `--trace`, cilock sees only the wrapped command's exit status. A wrapper that throws away govulncheck's status (`govulncheck -json ./... > govulncheck.json || true`) hides a failed scan.

 

## Validate it locally

 

List the predicate types emitted into the Collection:

 

```bash
jq -r '.payload' attestation.json | base64 -d | jq '.predicate.attestations | map(.type)'
```

 

Expected output:

 

```json
[
  "https://aflock.ai/attestations/environment/v0.1",
  "https://aflock.ai/attestations/git/v0.1",
  "https://aflock.ai/attestations/material/v0.3",
  "https://aflock.ai/attestations/command-run/v0.2",
  "https://aflock.ai/attestations/product/v0.3",
  "https://aflock.ai/attestations/sarif/v0.1"
]
```

 

Confirm `command-run.cmd` carries the literal shell-redirect argv (proof the cp antipattern is gone):

 

```bash
jq -r '.payload' attestation.json | base64 -d \
  | jq '.predicate.attestations[] | select(.type=="https://aflock.ai/attestations/command-run/v0.2") | .attestation.cmd'
# ["sh","-c","govulncheck -format sarif ./... > govulncheck.sarif"]
```

 

## Validated example

 

The exact invocation above runs end-to-end against real infrastructure in CI. The signed envelope + real predicate excerpt is at [`aflock-ai/attestor-compliance-examples/tool-govulncheck-sarif`](https://github.com/aflock-ai/attestor-compliance-examples/tree/main/tool-govulncheck-sarif).

 

## FAQ

 

### Does cilock support govulncheck?

 

Yes. Wrap `govulncheck -format sarif ./... > govulncheck.sarif` with `cilock run --attestations sarif,environment,git`. The SARIF report becomes a signed v0.3 attestation under `https://aflock.ai/attestations/sarif/v0.1`, the literal govulncheck argv is captured in `command-run/v0.2`, and the report file is hashed into the v0.3 Merkle tree.

 

### Why does cilock wrap govulncheck in `sh -c`?

 

govulncheck (v1.x) has no `-o` / `--output` flag — it writes SARIF only to stdout. A single shell redirect (`> govulncheck.sarif`) routes the output to a file so cilock's `product/v0.3` attestor can hash it. The `command-run` attestor still records the literal govulncheck argv inside the `sh -c` string; this is a tool-output limitation, not the cp antipattern.

 

### What's the difference between govulncheck and Grype / OSV-Scanner?

 

govulncheck does **call-graph reachability** — it flags only the vulnerabilities your code actually calls into, not every vulnerable package in your dependency tree. Grype and OSV-Scanner do lockfile/package-level matching, which catches more (sometimes too much). govulncheck is Go-only; Grype and OSV-Scanner are polyglot.

 

### Can I gate deploys on govulncheck findings under cilock?

 

Yes — write a Rego policy over the captured SARIF predicate. The `sarif/v0.1` predicate carries `runs[].results[].level` (`error` / `warning` / `note`) and `runs[].results[].properties` (which includes govulncheck's reachability annotations). Deny on `level == "error"` or on any finding with a populated call trace. See [verify-in-a-release-gate](https://www.testifysec.com/docs/cilock/guides/verify-in-a-release-gate).

 

## See also

 

- [`sarif` attestor](https://www.testifysec.com/docs/cilock/attestors/sarif) — the underlying ingestion path
 - [Verify in a release gate](https://www.testifysec.com/docs/cilock/guides/verify-in-a-release-gate) — Rego over the captured SARIF
 - [Validated example: tool-govulncheck-sarif](https://github.com/aflock-ai/attestor-compliance-examples/tree/main/tool-govulncheck-sarif) — the upstream README this page mirrors
 - [`golang/vuln` on GitHub](https://github.com/golang/vuln) — upstream govulncheck source
 - [Tools index](https://www.testifysec.com/docs/cilock/tools)

 

---

**This page is generated from the CI/lock tool catalog.** Don't edit it here — the source is [`attestation/detection/docs/govulncheck.doc.md`](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/govulncheck.doc.md) in [aflock-ai/rookery](https://github.com/aflock-ai/rookery). The same catalog powers `cilock tools show govulncheck` in the CLI, so a single edit keeps the terminal and the website in sync.

[Edit this page →](https://github.com/aflock-ai/rookery/edit/main/attestation/detection/docs/govulncheck.doc.md) ·  [Add a new tool →](https://github.com/aflock-ai/rookery/new/main/attestation/detection/docs)

When you open the pull request: if you are an **AI agent**, add the `ai-agent` label. If you are a **human**, add the `human` label.

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