Skip to main content

Go reachability with golem

Learning objective

After this chapter you will be able to run dep-scan reachability on a Go project, explain how golem produces the slices dep-scan consumes, and predict which modules will be marked Reachable versus merely present in the BOM. You will also know how to keep golem offline and why a warm module cache matters for that mode.

What golem is and when to use it

golem (Go Source Inspector) is the native slicer dep-scan uses for Go. It is shipped prebuilt via cdxgen-plugins-bin and emits a call graph, interprocedural source-to-sink data-flow slices, API endpoints, and symbol-usage evidence with purl attribution. dep-scan converts that report into its shared, purl-keyed reachability pipeline, so a vulnerable module whose functions are actually called (for example pgx.Connect in a SQL-injection CVE) is marked Reachable, while a module that is merely present in the BOM but never called is not. Reachability is on by default for Go once the slices are present, so for most projects you scan with the research profile and read the Reachable insights.

Prerequisites

golem loads packages through golang.org/x/tools/go/packages with full type and SSA information, so it needs a local Go toolchain to operate.

  • Go toolchain (go) on PATH, so golem can load the module's packages.
  • cdxgen and dep-scan, installed by npm install -g @cyclonedx/cdxgen and pip install owasp-depscan.
  • The golem binary, resolved automatically (see below) or supplied explicitly.
  • A warm module cache (GOMODCACHE) if you intend to run fully offline.

How dep-scan runs golem

Unlike the rusi and dosai paths, dep-scan invokes golem directly when a Go project is detected (-t go) and reachability is on. golem requires the local Go toolchain to load packages, and dep-scan always passes --include-all-flows so that dependency-internal flows, which are precisely where the CVE-reachability signal lives, are retained rather than pruned.

The Go data path is shorter than the cdxgen-mediated Rust and .NET paths, because dep-scan drives golem itself.

  dep-scan ──runs (--include-all-flows)──▶ golem ──▶ report

converted to

go-reachables.slices.json ──▶ reached_purls
(golem needs `go` on PATH; --go-analyzer-network offline sets GOPROXY=off)

dep-scan resolves the golem binary the same way cdxgen does. It checks the GOLEM_CMD environment variable, then DEPSCAN_GOLEM_BINARY, then golem on PATH, then the bundled cdxgen-plugins-bin layout at <pluginsDir>/golem/golem-<platform>-<arch>. When cdxgen spawns, dep-scan propagates GOLEM_CMD and CDXGEN_PLUGINS_DIR to the subprocess so both sides use the same binary. If golem or the Go toolchain cannot be found, Go reachability is skipped with a warning and never a crash, and other languages are unaffected.

Example invocations

The hermetic fixtures under test/data/golem/repos/ let you reproduce the reachable-versus-present contrast offline. The real-cve-app fixture vendors two real, historically vulnerable modules, github.com/gorilla/mux and github.com/satori/go.uuid, and its main calls uuid.NewV4() from the satori module.

# Reachable case: satori/go.uuid is imported and called
depscan -i ./test/data/golem/repos/real-cve-app -o ./reports -t go --profile research --explain

# Point dep-scan at a specific golem binary
DEPSCAN_GOLEM_BINARY=/path/to/golem depscan -i ./test/data/golem/repos/real-cve-app -o ./reports -t go --profile research --explain

# Forbid network access during analysis (requires warm module cache)
depscan -i ./test/data/golem/repos/real-cve-app -o ./reports -t go --go-analyzer-network offline --explain

The control is unreachable-app, which declares github.com/example/vuln-lib in go.mod through a local replace directive but never calls its ProcessQuery function.

# Present-but-unreachable case: vuln-lib is declared but never called
depscan -i ./test/data/golem/repos/unreachable-app -o ./reports -t go --profile research --explain

Reachability is on by default (--reachability-analyzer FrameworkReachability). Switch to SemanticReachability only when you also want reached services and endpoints attributed.

Analysis walkthrough

Consider the real-cve-app source. Its main imports github.com/satori/go.uuid and calls uuid.NewV4(), so the satori module is on a real call path. golem emits a call-graph edge and, where applicable, a data-flow slice that carries the versioned purl pkg:golang/github.com/satori/go.uuid@v1.2.0, which the converter reconciles onto the BOM component. That purl lands in reached_purls, so any advisory affecting satori/go.uuid@v1.2.0 is tagged Reachable and depscan:prioritized. The gorilla/mux module, by contrast, is imported with a blank import (_ "github.com/gorilla/mux") and never called, so it does not earn a reachable flow on call grounds; its treatment depends on whether the blank import itself is enough to satisfy your triage policy, which is a useful conversation to have with your team.

A representative slice (drawn from the committed test/data/go-reachables.slices.json fixture, trimmed for readability) shows the shape dep-scan consumes, including an endpoint and a data-flow into pgx.Connect:

{
"flows": [
{
"code": "main.main",
"parentFileName": "main.go",
"tags": "call-graph, go, call-graph"
},
{
"code": "pgx.Connect",
"parentFileName": "main.go",
"lineNumber": 25,
"isExternal": true,
"tags": "pkg:golang/github.com/jackc/pgx/v4@v4.18.1, call-graph, go, call-graph"
}
],
"purls": ["pkg:golang/github.com/jackc/pgx/v4@v4.18.1"]
}

The versioned purl in both the tags string and the purls array is what the reconciler matches against the BOM component. Without that version, the flow would be evidence of a call but not evidence that the affected version is the one installed.

The contrast with unreachable-app is precise. That fixture declares github.com/example/vuln-lib through a replace directive but its handleHealth handler only writes "ok" and never calls vulnlib.ProcessQuery. golem produces no call-graph edge and no data-flow slice touching vuln-lib, so the converter emits no flow carrying its purl, reached_purls stays empty for that module, and the finding never earns the Reachable insight. The module is present in the BOM and would appear in the full Vulnerability Disclosure Report, but it carries depscan:prioritized=false, which is the triage signal to deprioritize it.

Safety and network behavior

golem does not run the program and does not run go:generate. Package loading uses the local Go toolchain, so module downloads can occur depending on your Go environment, which matters when you scan untrusted repositories. dep-scan defaults to GOFLAGS=-mod=readonly to prevent go.mod rewrites on untrusted repos, so a scan will not silently mutate the module file. Use --go-analyzer-network offline to forbid all downloads; that flag sets GOPROXY=off, which requires a warm module cache (GOMODCACHE) so golem can resolve the packages it needs without touching the network. The offline mode is the right choice for CI on untrusted input, provided the image carries the modules golem will load.

Reading the artifacts

Three files in the reports directory tell the story. The go-reachables.slices.json holds the converted slices; check here first if no module is Reachable, because an empty or purl-less file explains the silence. The <base>.vdr.json carries the depscan:prioritized property and the depscan:insights text ("Reachable", "Endpoint-Reachable") per vulnerability; filter the vulnerabilities array on those properties to reconstruct the prioritized set, as described in the VDR guide. When you generate a VEX document with --csaf, a reached module becomes known_affected while a present-but-unreached module becomes known_not_affected carrying vulnerable_code_not_in_execute_path, as covered in the CSAF VEX guide.

Troubleshooting

If golem reports that it cannot load packages, the usual cause is a missing Go toolchain or a cold module cache under offline mode. Confirm go is on PATH, and for --go-analyzer-network offline runs make sure GOMODCACHE contains the modules the project imports. If slices are generated but no module is Reachable, check that the BOM carries versioned pkg:golang purls; a vendored module without a resolved version can defeat the reconciler. If golem itself is not found, set GOLEM_CMD or DEPSCAN_GOLEM_BINARY to a known-good binary and rerun with SCAN_DEBUG_MODE=debug to see the resolution log.

Summary

golem gives Go the same reachable-versus-present semantics every other language enjoys in dep-scan, through a converter that reconciles native call-graph, endpoint, and data-flow slices onto the BOM's versioned purls. The real-cve-app fixture shows a real historically vulnerable module (satori/go.uuid) marked Reachable because uuid.NewV4() is called, while unreachable-app shows a module that is merely declared and therefore never reached. Keep GOFLAGS=-mod=readonly for untrusted repos, reach for --go-analyzer-network offline when the cache supports it, and read the prioritized set from the VDR or the CSAF status from the VEX. The neighboring chapters Rust reachability and .NET reachability cover the other native slicers, and the Framework reachability chapter explains the engine these slicers feed.