Static analysis for Go security: govulncheck and friends
A dependency scan that flags every known vulnerability in your module graph is a liability, not a feature. Most reports from naive scanners are noise: thousands of CVEs in libraries you import but whose vulnerable paths you never exercise. Teams learn to ignore the output, and the one real vulnerability gets buried. govulncheck exists because the Go team decided the signal-to-noise problem was worth solving at the toolchain level.
How govulncheck is different
The tool does two passes. First it reads the Go vulnerability database — a curated feed of advisories with affected versions — and matches it against your module graph. Then it does what version-only scanners cannot: it traces the call graph of your program to determine whether any vulnerable function is actually reachable from your code. The result is a report of reachable vulnerabilities only, with call stacks showing how the vulnerable code is reached.
That distinction matters. A version-based scanner says “your go.sum contains a vulnerable module” and stops. govulncheck says “this specific function in this module is reachable from your code, through this call path” — which is the information you need to decide whether to upgrade now or to document the exposure.
Two modes cover the lifecycle:
govulncheck ./...— source mode, analyzes the current module. Use it in CI on every pull request.govulncheck -mode=binary ./path/to/binary— analyzes a compiled binary without source; useful for scanning releases and deployed artifacts.
The friends
govulncheck answers “is any reachable dependency vulnerable?” The layers around it answer different questions:
- go vet — catches suspicious constructs in your own code; ships with the toolchain, so it costs nothing to run.
- staticcheck — a broader analysis maintained outside the core team; finds bugs vet misses, including in error handling.
- gosec — inspects Go source for security smells: hardcoded credentials, unsafe calls, weak TLS settings. Noisier than the others; treat its findings as review prompts rather than verdicts.
- golangci-lint — the meta-linter that runs vet, staticcheck, and more behind one config, with govulncheck pluggable into the same pipeline.
- osv-scanner and Trivy — broader supply-chain scanners covering OS packages and container images; useful when the deployed artifact sits in a context that includes more than Go modules.
Wiring it into a workflow
A workable baseline: run go vet ./... and govulncheck ./... in CI on
every pull request; run staticcheck and gosec on a schedule or at release
time; and scan the built binary with govulncheck -mode=binary before
signing the release. Keep the toolchain current — via the toolchain
directive in go.mod — because each release improves both the analysis and
the matching against the vulnerability database.
The database itself is a moving target. Advisories are published continuously, so a scan that passes today should be re-run on a schedule, not only when a dependency changes.
The payoff of small dependency surfaces
The tools only help if the graph they analyze is worth analyzing. A module with one dependency produces a short, readable govulncheck report; a module with hundreds produces a report nobody reads. Dependency hygiene multiplies the value of every static analysis tool above.
trustless, a credential broker CLI for AI agents, keeps exactly one external module pinned via go.sum — so a govulncheck run covers the entire dependency surface of the tool in a single pass, and the binary-mode scan before its cosign-signed releases covers everything that ships.
If a scanner reports a vulnerability in code your program can never reach, is that a vulnerability you need to fix, or information you need to record?