Vulnerability Management

govulncheck in Production Integration

govulncheck is the best vulnerability scanner the Go ecosystem has ever had, but turning it from a demo into a production gate takes more than adding a CI step.

Nayan Dey
Senior Security Engineer
7 min read

govulncheck went GA in September 2022, reached v1.0.0 on 13 July 2023, and is now the default tool recommended by the Go team for finding known vulnerabilities in Go code and binaries. It is better than most scanners in this ecosystem because it does not just look at your dependency list. It walks the call graph and tells you whether any vulnerable function is actually reachable from your code. That is a big deal for signal-to-noise ratio.

But integrating it in production is not as simple as adding one line to a CI config. I have rolled it out at a few organizations, and each time there were rough edges that took real effort to smooth. This post is about what those edges look like and how to handle them.

What does govulncheck actually check?

govulncheck reads the Go vulnerability database at vuln.go.dev, maintained by the Go security team. Each entry is a YAML document that identifies a vulnerable module, the affected version ranges, the symbols (functions, methods, types) introduced by the vulnerability, and the fixed version. When you run govulncheck ./... in a source tree, the tool resolves your module graph, compares versions against the database, and then does static analysis to see if your code actually calls any affected symbol.

The output has two levels of finding. A module-level finding says "you depend on a vulnerable version but nothing in your code calls the affected symbol." A call-stack finding says "here is the specific line in your code that transitively reaches the vulnerable function." The second is what should wake you up at night. The first is something to plan around.

Binary mode versus source mode

govulncheck can scan source with govulncheck ./... or scan an already-built Go binary with govulncheck -mode=binary ./myapp. Binary mode is useful when you receive binaries from upstream or want to scan release artifacts. It is less precise because the call graph has to be reconstructed from symbol tables, but it works even when source is not available.

In CI I run both. Source mode in PR builds catches issues early. Binary mode against the final release artifact catches anything that the release pipeline might have changed, like a different -tags selection or a rebuild with different flags.

How do you gate merges on govulncheck?

The naive approach is to fail the build whenever govulncheck finds any call-stack finding. In a greenfield project this works. In a legacy codebase with a hundred dependencies, it will paralyze the team on day one. I suggest a graduated rollout.

Start with module-level findings reported but not gating. This gives visibility without blocking work. After a couple of weeks, turn on gating for call-stack findings above a severity threshold. The Go vulnerability database does not include CVSS by default, but most entries alias to a CVE that does, and you can enrich them from the NVD. High and critical first, medium later. Finally, once the team is comfortable, gate on any call-stack finding regardless of severity.

During the Log4j-era panic in December 2021 and January 2022, many Go teams discovered that their dependency lists were not what they thought. Log4j was Java, but similar issues have surfaced in Go. CVE-2022-29526 in golang.org/x/sys affected Unix permission checks and landed in the Go vuln database as GO-2022-0493. A team running govulncheck with call-stack gating caught it on day one. A team running nothing caught it when someone read a blog post.

What about false positives?

govulncheck has fewer false positives than most scanners because of its reachability analysis, but it is not perfect. The most common class of "false" positive is when a vulnerable symbol is referenced in a code path that is never actually exercised in production, such as a test helper or a debug-only flag.

You can suppress findings in two ways. The supported way is to upgrade the vulnerable dependency, which is almost always the right answer. The less supported way is to ignore a specific finding in your CI scripting. I avoid blanket ignores. If a finding has to be ignored, I require a justification in a file that is reviewed like code and has an expiry date. That keeps the ignore list from growing into an untouchable monument.

Handling the vuln database cadence

The Go vulnerability database is updated continuously. New entries appear when the Go security team publishes them, which is usually within a few days of the upstream fix. If you run govulncheck once a day in CI, you will catch new issues within a day. If you run it only on push, you might miss issues that are disclosed between pushes.

I run a nightly scan on every production branch, independent of pushes, and open tickets for any new findings. Since govulncheck is fast, this adds little cost. The nightly scan also catches cases where a newly disclosed vulnerability suddenly affects code that has not changed in months.

Does it work on modules you do not own?

Yes. govulncheck can scan third-party Go programs if you have the binary or a fetchable module. I have used it to scan vendor binaries as part of acquisition due diligence. It will not find vulnerabilities that are not yet in the Go vuln database, so it is not a complete picture, but it is a fast first pass.

How about performance at scale?

For a typical microservice with a hundred dependencies, govulncheck runs in a few seconds. For a large monorepo with thousands of packages, it can take minutes. In one rollout I worked on, scanning the entire monorepo took about four minutes on a 16-core runner, which was acceptable. Sharding by top-level package gets you below a minute if you care.

Caching helps. The vuln database download is cached locally; ensure CI runners retain $GOMODCACHE and the vuln DB cache between runs so you are not re-downloading every time.

Is govulncheck enough?

No, and the Go team would not claim it is. govulncheck only knows about vulnerabilities in the Go vuln database. Many CVEs against Go projects take time to land there. Supplement with a software composition analysis tool that reads from OSV, NVD, and GHSA, and cross-check the results. Reachability analysis is still the strongest signal, but breadth of database matters for completeness.

What about cgo and non-Go dependencies?

govulncheck does not analyze C code linked via cgo. If your Go binary embeds a vulnerable C library, you will not find out from govulncheck. You need a separate SCA pass for the C world. This is a gap worth being explicit about with the team. It is not a criticism of govulncheck, just a boundary of what it can do.

How Safeguard Helps

Safeguard runs govulncheck across every Go repository in your organization on a continuous schedule and normalizes the findings into a single inventory that includes call-stack reachability, CVSS enrichment from NVD, and KEV status from CISA. When a new entry lands in the Go vuln database, Safeguard re-evaluates every project that may be affected and opens remediation tickets with the exact fixed version. Policy gates let you require zero reachable call-stack findings above a chosen severity before a deploy can proceed, and the triage UI tracks ignores with expiration dates so no suppression lives forever.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.