Go's minimal-runtime, statically-compiled binaries make it a favorite for building fast, portable services—but that same dependency graph of modules and transitive packages is exactly where known vulnerabilities hide. This govulncheck tutorial walks through scanning a real Go codebase for known vulnerabilities using Go's official vulnerability database, from a first local scan to wiring the check into continuous integration. By the end, you'll be able to run govulncheck against source and compiled binaries, interpret its call-graph-aware findings so you're not chasing dependencies you never actually invoke, and fold the results into a repeatable go security tooling workflow. Whether you're auditing a legacy service or hardening a new one before launch, these steps give you a fast, low-noise way to catch real exposure instead of drowning in false positives from every dependency in your go.sum.
Step 1: Install govulncheck
govulncheck is maintained by the Go team and ships as a standalone CLI, not a built-in go subcommand, so you install it like any other Go tool:
go install golang.org/x/vuln/cmd/govulncheck@latest
Confirm it landed in your GOPATH/bin (usually ~/go/bin) and that directory is on your PATH:
govulncheck -version
If you get a "command not found" error, add $(go env GOPATH)/bin to your shell's PATH and re-open your terminal. Pinning a specific version instead of @latest (e.g., @v1.1.3) is worth doing in CI so scans stay reproducible between runs rather than silently picking up new database logic on a random Tuesday.
Step 2: Run a Baseline Scan Against Your Module
From the root of your Go module (wherever your go.mod lives), run:
govulncheck ./...
This tells govulncheck to analyze your entire module, not just the current package. It pulls the latest advisory data from the Go vulnerability database (vuln.go.dev) and cross-references it against every module in your dependency graph—the core of any golang dependency audit. Unlike a simple version-matching scanner, govulncheck performs static call-graph analysis: it checks whether your code actually calls the vulnerable function, not just whether the vulnerable package is present in go.sum. That distinction matters enormously in practice, since most Go projects import far more code than they exercise.
A clean run looks like this:
No vulnerabilities found.
A finding looks like this:
Vulnerability #1: GO-2023-1571
Denial of service via crafted Accept-Language header in golang.org/x/text
More info: https://pkg.go.dev/vuln/GO-2023-1571
Module: golang.org/x/text
Found in: golang.org/x/text@v0.3.7
Fixed in: golang.org/x/text@v0.3.8
Example traces found:
#1: main.go:45:2: main.main calls language.Parse
Notice the "Example traces" section—this is the reachability proof that separates govulncheck from noisier go vulnerability scanning tools.
Step 3: Scan Compiled Binaries, Not Just Source
Source scanning is great during development, but production risk lives in what you actually ship. Govulncheck can analyze a compiled binary directly, which is especially useful for auditing artifacts built by someone else's CI pipeline or a base image you didn't build yourself:
go build -o myservice ./cmd/myservice
govulncheck -mode=binary ./myservice
Binary mode reads the embedded module information Go compiles into every binary and checks it against the vulnerability database. It can't do full call-graph reachability analysis the way source mode can (the symbol table doesn't preserve every call site), but it still tells you which vulnerable modules were compiled in—valuable when you're auditing third-party binaries or verifying a release artifact before it ships.
Step 4: Narrow Down and Prioritize Findings
On a large, older codebase, your first scan may return more results than you can triage in one sitting. Use -json output to pipe results into other tooling or a spreadsheet for tracking:
govulncheck -json ./... > govulncheck-report.json
Each entry includes the OSV ID, affected module, the fixed version, and whether a call trace was found. Prioritize in this order:
- Findings with a call trace — these are exploitable in your actual code path.
- Findings affecting internet-facing packages — HTTP parsing, TLS, serialization libraries.
- Findings with a trivial fix — a patch version bump costs you nothing; do those immediately.
- Findings with no trace and an internal-only affected function — track them, but they're lower urgency.
This triage step is what turns a raw scanner output into an actual go security tooling program rather than a wall of red text nobody reads twice.
Step 5: Update Dependencies to Resolve Findings
Most fixes are a version bump away. Go modules make this straightforward:
go get golang.org/x/text@v0.3.8
go mod tidy
govulncheck ./...
If the fix requires a major version bump with breaking changes, check the module's changelog before upgrading blindly, and re-run your test suite alongside govulncheck. For transitive dependencies you don't import directly, go mod why -m <module> shows you the import chain so you know whether bumping a direct dependency will pull in the patched transitive version automatically.
Step 6: Automate the Scan in CI
A one-off scan catches what's vulnerable today; it won't catch what becomes vulnerable next month when a new CVE lands against a module you already depend on. Add govulncheck to your pipeline so every pull request and every scheduled run re-checks against the live database. Here's a minimal GitHub Actions job:
name: govulncheck
on:
push:
pull_request:
schedule:
- cron: "0 6 * * 1" # weekly, catches newly disclosed CVEs
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Run govulncheck
run: govulncheck ./...
The scheduled weekly run matters as much as the pull-request trigger: it's how you catch vulnerabilities disclosed after the code was already merged, without waiting for someone to touch that file again.
Verifying Your Setup: Troubleshooting This Govulncheck Tutorial
A few issues come up often enough to call out directly:
- "no findings" but you expected some — verify you're not running against a stale
vendor/directory. Govulncheck respects vendoring, so rungo mod vendorafter updating dependencies, or use-mod=modto bypass vendor mode temporarily. - Scan takes a long time on a large monorepo — scope the scan to changed packages in CI with
govulncheck ./path/to/changed/...for fast pull-request feedback, and reserve the full./...scan for the scheduled nightly/weekly job. - Findings for modules you don't think you use — remember govulncheck analyzes the full build graph, including test-only and tooling dependencies declared in
go.mod. Checkgo mod graphto trace how the module got pulled in. - CI fails intermittently — this usually means the vulnerability database updated mid-run, or a transient network issue reaching vuln.go.dev. Pin the govulncheck version and add a retry step rather than disabling the check.
- Version confusion between
go vet's vulncheck integration and the standalone binary — the standalonegovulncheckCLI is the actively maintained path and supports binary mode and JSON output; prefer it over older embedded integrations.
If govulncheck ./... completes with either a clean report or a JSON file you can act on, and your CI job fails the build on new findings, your setup is verified and repeatable.
How Safeguard Helps
Govulncheck is an excellent, purpose-built starting point for a golang dependency audit, but most engineering organizations run more than one Go service, in more than one repository, with dependency graphs that shift every day. Safeguard extends this same reachability-aware philosophy across your entire software supply chain: instead of a single repo's point-in-time scan, Safeguard continuously tracks every Go module (and every other language and package ecosystem you ship) across all your repositories, correlates new CVE disclosures against your actual dependency graph, and tells you which services are truly exploitable versus which merely import an affected package without ever calling the vulnerable function.
Where govulncheck stops at "here's a vulnerability report for this run," Safeguard turns that signal into a managed workflow—automatically opening remediation tickets with the exact fixed version, tracking SLA-based deadlines by severity, and giving security and platform teams a single dashboard across every repo instead of a folder of JSON files scattered across CI logs. For teams that have already adopted govulncheck as their go vulnerability scanning baseline, Safeguard is the layer that makes that signal organization-wide, auditable, and provable for compliance frameworks like SOC 2—so scanning stops being a per-repo habit and becomes a continuously enforced supply chain security program.