September 6, 2022·6 min read·engineeringgosecuritydependenciesanalysis

Govulncheck connects vulnerable functions to application calls

Go's new vulnerability tooling adds call analysis to dependency information. Its launch distinguishes source analysis, binary inspection and the build configuration each result describes.

A module can contain a vulnerable function without every program depending on that module calling the function. A dependency-version match identifies a useful starting point, but it does not explain the application's relationship to the affected code. Go's new vulnerability tooling attempts to supply that missing relationship through static analysis.

Julie Qiu's 6 September announcement introduces a vulnerability database curated by the Go security team and the experimental govulncheck command. The database draws on existing advisories and direct maintainer reports. The command combines that information with an analysis of the program to report vulnerable functions reached through its calls.

I value this direction because a useful security report should help explain the program in front of its maintainer. A module name and a vulnerable version do not provide the same evidence as a path connecting an application function to an affected operation. That path gives the reviewer a specific claim to examine.

Dependency information has several levels

The vulncheck package documentation at launch distinguishes module requirements, package imports and function calls. These are different relationships. A program can require a module without importing every package it contains, and an imported package can contain functions the program does not call.

That distinction is particularly useful for large dependencies. Advisories often identify a library or release because those are convenient distribution units. An application maintainer still needs to know which affected operations the application can invoke. The answer influences urgency and explains the technical reason behind a proposed dependency update.

The package's results retain the portions of the relevant graphs that lead to identified vulnerabilities. Helper functions produce representative call stacks and import chains from those results. This is an evidence-producing design. The result can describe how the analyzer connected the application's code to a vulnerable operation.

There is a difference between reducing irrelevant reports and proving exploitability. A call path establishes a connection in the analysis. Whether an attacker can supply the necessary data or arrange the vulnerable state requires understanding the function's preconditions and the caller's behavior. A tool that explains the path makes that assessment easier without performing all of it.

Source analysis uses the selected build

The govulncheck command documentation at launch specifies a build configuration for source analysis. It uses GOOS and GOARCH, together with the Go command found on PATH. Build tags control which source files participate. Test files require the command's test flag.

This means the result describes a particular program configuration. A Windows-specific dependency path may be absent from a Linux analysis. A test-only dependency may be absent when tests are not included. Changing those inputs can change which vulnerabilities the command reports without changing the module file.

That is a useful property when the goal is to assess an actual target. It also creates an obligation when the project supports several targets. A clean result for one configuration cannot serve as evidence that every supported operating system and architecture has been analyzed. The configuration belongs with the result when it is stored or discussed.

The selected Go version matters for standard library findings as well. The documentation explicitly notes that a vulnerability affecting one toolchain version may not be reported when analysis uses another. Assessing a deployed application therefore requires relating the analyzed configuration to the toolchain used for the deployed artifact.

My preference is to treat configuration as part of a finding's identity. That makes a change in output explainable. It also keeps an analysis performed on a development machine from silently becoming a claim about a differently built production binary.

The first command is deliberately small

The announcement gives installation and invocation commands. As of this launch, the tool is distributed separately so the team can update it frequently while gathering feedback.

go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

The command documentation requires building govulncheck with Go 1.18 or later. Source invocation uses the package-pattern conventions familiar from the go command and runs from the module directory. That makes the initial action understandable without introducing a separate project-description format.

For a reported vulnerability, the ordinary output includes a summary of a call stack. The verbose flag provides more detail resembling a Go panic trace. The summary is useful as an entry point, while the longer path helps a maintainer inspect intermediate calls and identify where application policy meets the dependency.

JSON output provides an integration route for other tools. The launch documentation describes it as a result object corresponding to the vulncheck API. That is a version-specific contract for an experimental tool. A consumer should use the documented launch format instead of assuming that command examples from another stage of development apply.

The same documentation says the command exits successfully when it finds no vulnerabilities and unsuccessfully when it does. That behavior supports automation, but the status alone loses the explanation. Keeping the associated result preserves the affected function and the connection the analysis found.

Binary inspection has less information

Govulncheck can also inspect a binary by accepting its file path. The launch documentation says this mode uses symbol information to identify mentions of vulnerable functions. It does not have the detailed call information used by source analysis, so its reports omit call stacks.

This mode answers a different operational need. A compiled artifact may be available when its full source environment is inconvenient to reconstruct. Inspecting that artifact can connect an assessment to the delivered file. The tradeoff is that function presence is weaker evidence than the source analyzer's call relationship.

The documented limitations say binary inspection can report code that is present but unreachable. At launch it also only reads binaries compiled with Go 1.18 or later. These are properties of the available implementation, not reasons to describe binary inspection as identical to source analysis.

The configuration comes from the binary's build in this mode. That differs from the host-selected configuration used for source. A report needs to make clear which mode supplied its evidence, especially when comparing a locally analyzed source tree with an artifact built elsewhere.

Static analysis has explicit blind spots

The launch documentation says calls through function pointers and interfaces receive conservative analysis. That can produce false positives or inaccurate call stacks. A reported route should therefore be inspected as an analyzer result, particularly when dynamic dispatch introduces more possible targets than the application uses in practice.

Reflection creates a different limitation. Calls performed through reflect are not visible to the static analysis described here. Vulnerable code reachable only that way will not be reported. This is a reason to avoid interpreting silence as a general proof of safety.

These limitations have different directions. Conservative dispatch analysis can report too much, while an invisible reflective call can leave something out. Combining them into a single statement that analysis is imperfect would lose the information a maintainer needs to interpret a concrete result.

The tool also depends on known vulnerability information. Analysis can connect a program to a recorded vulnerable function, but it cannot report an unknown defect simply because the program calls it. Database quality and analysis precision therefore contribute separately to the result.

There is no supported way to silence findings in the launch version. That limitation matters for integrations that expect a suppression file or an exception flag. A workflow consuming these results needs to keep its own review decision distinct from the evidence the experimental command reports, without assuming an undocumented suppression mechanism exists.

Maintainer reports improve the shared evidence

The launch repository overview describes a database client, an analysis API and the command built for developers. That separation allows other tools to consume the information without requiring every integration to reproduce the command's presentation.

The command's documentation also specifies what it sends to the default database. Requests contain module paths, rather than application source or other program properties. GOVULNDB can select another database implementing the documented interface. Those details help explain the network operation performed during a check.

I regard the most useful change as the ability to discuss a vulnerability at the function boundary where the application encounters it. The database supplies the advisory information, while the analyzer supplies a relationship to the selected program. Maintainers can then examine the reported calls and the vulnerable operation's conditions with a more precise starting point than a dependency name alone.