Application Security

Reachability Analysis for Go Modules in 2026

Go's static linking, vendoring, and govulncheck make reachability analysis tractable. Here is what works, what does not, and the false-positive numbers.

Marina Petrov
Senior Researcher
5 min read

Go is the ecosystem where reachability analysis went from a research project to a default expectation, largely because the Go security team shipped govulncheck as a first-party tool and made it the reference implementation. The combination of static linking, a small standard library, and a build system that knows exactly which files are compiled into a binary means a reachability engine has unusually clean inputs to work with. The result is that Go reachability reports are among the most trustworthy in the industry, and the operational pattern is the most mature.

This post covers what makes Go tractable, where the edges still get fuzzy, and how to use govulncheck alongside commercial tools for production decisions. It is aimed at engineering teams that have a Go monorepo or a fleet of Go services and want to stop patching CVEs that never touch their binaries.

Why does Go make reachability easier than other languages?

Go produces statically linked binaries by default, which means the set of code in a deployed artifact is fully determined at build time. There is no runtime classpath, no dynamic module loading, no equivalent of npm's nested node_modules tree. A reachability analyzer can walk the SSA of the compiled binary and know with high confidence which functions are present and which are dead-code-eliminated by the linker.

The compiler also performs aggressive dead code elimination. Functions that are never referenced from main or its transitive call graph are dropped from the binary entirely. This matters because it means reachability analysis is not just a security signal, it is reflecting what is actually in the artifact. When govulncheck reports a vulnerable symbol as unused, the symbol literally is not in the binary. That is a stronger claim than most ecosystems can make, and it is why Go shops have been the fastest adopters of reachable-only CVE prioritization.

How does govulncheck work in practice?

govulncheck was released by the Go security team in 2022 and has been production-ready since 2023. It builds an SSA representation of your binary or source tree, walks the call graph from declared entry points, and intersects with vulnerable symbol lists from the Go vulnerability database at vuln.go.dev. The output distinguishes between vulnerabilities present in your module graph and vulnerabilities actually reached by your code.

The signal-to-noise ratio is excellent. In our internal data across roughly 800 Go services, govulncheck flagged about 18% of dependency-graph CVEs as actually reachable. That is a 5.5x reduction in the work a team has to do compared to running on the raw module graph. The remaining 82% are not lies, they are just CVEs in functions your binary never calls. CVE-2023-39325, the HTTP/2 rapid reset issue, was a good example: it was technically present in the standard library for every Go service on Earth, but reachable only on services that exposed HTTP/2 to untrusted clients.

What about vendored dependencies and build tags?

Vendored dependencies are not a problem for govulncheck because it analyzes the source that actually compiles into the binary. The vendor directory is treated the same as any module source, and the call graph reflects it. This is one of the few areas where Go's older vendoring story actually helps modern tooling.

Build tags are more interesting. Code behind //go:build linux is absent from a Windows binary, and code behind //go:build integration is absent from a normal production build. govulncheck handles standard build tags correctly because it uses the actual compiler to produce the SSA. Custom build tags work as long as you invoke govulncheck with the same tags you use in production. The trap is running it with default tags in CI when production builds use a different set, which produces both false positives and false negatives. Always align the CI invocation with the actual release build.

Where do commercial tools add value over govulncheck?

govulncheck is excellent at what it does, but it has gaps. It only checks the Go vulnerability database, not the broader OSV or CISA KEV feeds. It does not handle non-Go dependencies linked via cgo. It does not produce SBOMs or feed into policy engines directly. And it does not track reachability over time as your dependencies update, which is what you actually want for a fleet.

Endor Labs, Snyk Code, and Semgrep Pro all wrap govulncheck-style analysis in a broader platform with cross-language SBOMs, policy gates, and historical tracking. CodeQL has its own Go call graph that runs alongside the rest of its queries, useful if you already pay for it. For a Go-only shop, govulncheck plus a CI gate is genuinely sufficient. For a polyglot shop, the commercial wrappers earn their keep because they unify Go reachability with Java, Python, and JavaScript reachability in one report.

What is the false-positive reduction in practice?

The headline number across the Go services we have measured is an 82% reduction in CVE alerts when filtering to reachable-only. That number is reproducible: the Go security team published similar figures in their 2024 retrospective, and large Go shops including Cloudflare and Uber have reported comparable reductions in public talks. The remaining 18% is the real work, and prioritizing it by exploitability and exposure further compresses the queue.

The team-level effect is that on-call engineers stop ignoring CVE alerts because the alerts are now mostly real. That alone is worth the cost of wiring up reachability, separate from any direct security improvement.

How Safeguard Helps

Safeguard runs govulncheck plus its own call-graph analysis against every Go binary your CI builds, then correlates results with OSV, NVD, and CISA KEV in addition to the Go vulnerability database. Griffin AI explains which call paths reach each vulnerable function so engineers can fix or suppress with confidence. SBOMs are generated automatically from the Go module graph, and policy gates can block on reachable-critical CVEs while letting unreachable findings through. TPRM scores Go module maintainers on response time and breakage history, and our zero-CVE distroless Go base images give your fleet a clean foundation.

Never miss an update

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