Go modules
Go modules (go.mod + go.sum)
go get foo.dev/bar@v1.2.3
# Coerce a transitive — go.mod:
# require foo.dev/bar v1.2.3
# replace foo.dev/bar => foo.dev/bar v1.2.3
go mod tidy
replace directives are the official mechanism for coerced transitives — they override what’s recorded in dependent modules’ go.mod files. go.sum carries h1: checksums verified at module-cache time; GOSUMDB=sum.golang.org validates against the checksum database. Vendor mode (go mod vendor) bakes the deps into vendor/; the build uses -mod=vendor.
Gotcha: replace is build-local — it doesn’t propagate to consumers of your module, so library authors should publish a fixed release rather than rely on replace.
Developer gotchas — written for people who live in the code
You write Go every day; you only edit go.mod when the linter yells. These are the surprises that catch developers when triaging an SCA finding.
go.modlists a version that isn’t what your build resolves. Go’s Minimum Version Selection (MVS) picks the highest version required across the dep graph. Yourgo.modsaysgolang.org/x/crypto v0.30.0but a transitive requiresv0.31.0; MVS picksv0.31.0.go list -m allshows what’s actually selected. Scanners that read onlygo.modmay flag the wrong version.go.sumis integrity, not pinning. It carries cryptographic checksums for every version your build has ever seen — not a single resolved set. Removing a line fromgo.sumand re-runninggo mod tidybrings it back. The pinning is ingo.mod’srequirelines.replacedirectives are build-local and don’t propagate to consumers. If youreplacea vulnerable transitive in your library and ship it, downstream users still get the original. Areplaceis the right fix for a binary you control; for a library, publish a real version bump.indirectmarkers are stale. A// indirectcomment next to arequireingo.modmeans “no file in this module directly imports it.” Auto-generated bygo mod tidy. Doesn’t mean the artefact is transitively indirect for the current import graph — it’s about whether your code imports it. Refactoring may flip a dep from indirect to direct without changing the version.Vendored builds (
vendor/) freeze code at vendor time.go mod vendorcopies every dep intovendor/. If you bumpedgo.modbut forgotgo mod vendor, the build uses the old code fromvendor/. Scanners that readvendor/show what’s actually compiled; scanners that readgo.modshow what was intended.Init functions execute on import — even from packages you never call.
import _ "github.com/some/driver"runs the package’sinit(). A CVE in an init function is reachable the moment the package is imported, even if you never call any exported function.internal/packages aren’t import-protected againstgo.work. A workspace can include code that imports another module’sinternal/. Scanners flagging code in aninternal/directory may be correct if a workspace setup expanded the import set.Build tags exclude code at compile time.
// +build linuxor//go:build linuxfiles only compile on Linux. A CVE in a Windows-only file isn’t reachable in your Linux build.go list -f '{{.GoFiles}} {{.IgnoredGoFiles}}'shows what’s included.CGO pulls in C libraries that aren’t in
go.mod.import "C"brings in linked C code. CVEs inlibcrypto,libssh2,librdkafkaaren’t visible to Go-only scanners. Grype (with the container scan) catches them;osv-scanneron the source tree doesn’t.go installandgo buildpopulate different caches.~/go/pkg/mod/holds module cache (immutable).~/go/bin/holds installed binaries. A CVE on a CLI tool you ran six months ago may still be in~/go/bin/even though its source is gone.Major versions live at different module paths.
github.com/foo/barandgithub.com/foo/bar/v2are different modules for Go’s resolver. Bumping to v2 is a path change, not a version change.go mod tidywon’t do it for you.-mod=modvs-mod=readonlyvs-mod=vendorchange resolution at build time.GOFLAGS=-mod=readonly(CI default) fails ifgo.modneeds editing.-mod=vendorignoresgo.modand usesvendor/. A CVE ingo.modmay not be in the built binary if-mod=vendoris set andvendor/has older code.
Reachability
go mod why <module>produces the import chain from your main module to a target.go list -deps -json ./... | jqwalks every transitive.go tool callgraph -algo=chafromgolang.org/x/tools/cmd/callgraphproduces a static call graph.- Runtime:
go test -coverprofile=cover.out ./... && go tool cover -html=cover.out.