Go's dependency model is unusual among package ecosystems: instead of letting a resolver pick the newest compatible version of everything, Go uses Minimal Version Selection to compute a single, deterministic version of every module in a build. That single detail changes how any software composition analysis (SCA) tool has to approach scanning — there's no nested node_modules-style tree with five copies of the same library at five versions, just one flat, provable answer. Snyk Open Source is one of the more widely deployed SCA tools for Go, integrated into snyk test, snyk monitor, and snyk sbom. This post walks through, based on Snyk's public documentation, how it reads go.mod and go.sum, when it shells out to the Go toolchain versus parsing manifests directly, and how Go's own resolution algorithm shapes what a vulnerability report can and can't tell you.
What files does Snyk actually read in a Go project?
Snyk's Go support centers on two files: go.mod, which declares the module's own path, its Go version, and its direct and (since Go 1.17) many indirect requirements, and go.sum, which records cryptographic checksums for every module version the build needs, direct or transitive. Per Snyk's documented CLI behavior, snyk test run from the root of a Go module looks for go.mod as the entry point and expects go.sum to be present and consistent with it; a go.mod without a corresponding go.sum can produce an incomplete or failed scan, since Snyk needs the resolved checksums to be confident which exact versions ended up in the build. Snyk also supports --all-projects, which walks a repository for multiple go.mod files — relevant for monorepos that contain several independently versioned Go modules rather than one at the repo root.
How does Snyk build the actual module dependency graph?
Snyk's documentation describes two paths, and which one runs depends on whether a working Go toolchain is available in the scanning environment. When Go is installed and the project builds, Snyk shells out to Go's own tooling — effectively the same mechanism behind go list -m all and go mod graph — to get the fully resolved, build-accurate module graph, including every indirect dependency and the exact version Go's resolver selected for each. When no Go toolchain is available (for example, in some CI containers or IDE-integrated scans), Snyk falls back to statically parsing go.mod and go.sum without invoking go commands. This static mode is more portable but is explicitly less capable of fully reconstructing edge-level graph relationships than a live go list invocation, because a manifest alone doesn't always encode which module introduced which indirect requirement the way a resolved build graph does. Snyk's guidance is that scanning from an environment where the project can actually be built produces the most accurate dependency tree.
Why does Go's Minimal Version Selection matter for vulnerability reporting?
MVS matters because it guarantees exactly one version of each module per build, which is what lets Snyk report a single, unambiguous "this version is in your build" answer instead of a range of possibilities. Go's resolution algorithm, described by the Go team since the module system's introduction in Go 1.11 (2018) and formalized in Russ Cox's "Minimal Version Selection" writeup, works by taking the highest version requirement declared by any module in the build list for a given dependency — not the newest version available upstream. If your go.mod requires github.com/example/lib v1.2.0 and a transitive dependency requires v1.4.0, Go selects v1.4.0: the minimum version that satisfies every stated requirement, not the maximum available. This means a project can be sitting on an old, vulnerable release of a transitive dependency simply because no module in the graph ever asked for anything newer, even if a patched version has existed upstream for months. Snyk's remediation advice for Go reflects this: fixing a transitive vulnerability frequently requires bumping the direct dependency that pulls in the vulnerable transitive one, or adding an explicit require line in go.mod to force a higher minimum version, rather than simply asking for "the latest" as you might with npm audit fix.
Does Snyk account for replace, exclude, and vendor directives?
Yes — Snyk's documentation states that it honors go.mod directives that alter resolution, including replace (which swaps one module path or version for another, often used to point at a fork or a local path) and exclude (which removes a specific version from consideration). Since these directives change what actually ends up in the build, ignoring them would produce a graph that doesn't match reality; Snyk's approach of preferring live go tooling when available is partly what lets it honor these correctly, since go list/go mod graph apply the directives natively as part of normal Go resolution. Vendored dependencies (a vendor/ directory populated by go mod vendor) are also a supported scenario, since Go 1.14 made vendor-mode builds a first-class, checksum-verified path — Snyk's scan needs to reconcile the vendored copies with what go.sum and go.mod declare rather than silently trusting whichever files happen to be on disk.
How does Snyk handle private modules and Go workspaces?
Private and internal Go modules require the same environment configuration for Snyk that they would for a normal go build: Snyk's documentation points to setting GOPRIVATE, GONOSUMCHECK, or GOFLAGS and ensuring GOPROXY and any required Git credentials are available in the scanning environment, since Snyk relies on the same module-fetching machinery Go itself uses when it shells out to the toolchain. Without correct proxy and auth configuration, a private module reference in go.mod can cause the live-resolution path to fail, pushing Snyk toward the more limited manifest-parsing fallback. Go workspaces (go.work, introduced in Go 1.18 in March 2022) let a repository coordinate multiple modules during local development without editing each one's go.mod; Snyk's Go support has extended over time to recognize multi-module project layouts, though the most reliable results still come from scanning each module's own go.mod/go.sum pair directly rather than relying solely on a workspace file to describe the whole graph.
What does a Snyk Go scan actually flag as a finding?
A finding is a match between a resolved module version in your graph — either from live go list output or from go.sum — and a known vulnerability in Snyk's vulnerability database for that module and version range, surfaced with the dependency path showing which direct dependency pulled it in. Because Go's MVS produces one version per module rather than a tree of coexisting versions, a single vulnerable transitive dependency in Go typically shows up as one clean finding with one upgrade path, in contrast to ecosystems like npm where the same vulnerable package might appear nested under several different top-level dependencies at different versions simultaneously. Snyk presents this alongside its "reachable vulnerability" style prioritization on some plans, and supports exporting the resolved graph as an SBOM via snyk sbom --format=cyclonedx1.4+json (or similar CycloneDX/SPDX output), which captures the same module list independent of whether a vulnerability currently exists for it.
How Safeguard Helps
Understanding how any SCA tool resolves the Go module graph matters because the accuracy of that graph is the ceiling on the accuracy of everything downstream — findings, prioritization, and SBOMs are only as good as the dependency data feeding them. Safeguard's software supply chain security platform is built around that same principle: continuous, build-aware dependency graph generation across ecosystems including Go modules, npm, PyPI, and Maven, so that vulnerability findings are tied to the versions actually shipping in production rather than a best-effort static read of manifest files. Safeguard correlates resolved module graphs with runtime and CI provenance data, flags newly introduced or unexpectedly changed transitive dependencies (a common vector for supply chain compromise), and generates SBOMs that stay in sync as go.mod and go.sum change across commits. For teams running Go services at scale, that means fewer blind spots between what a manifest declares and what actually gets compiled into a binary — visibility that holds up whether your build pipeline has a live Go toolchain available or not.