Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gomod-vex

gomod-vex checks whether a given CVE in a Go module is actually present and reachable, rather than merely being listed as a dependency. Point it at either:

  • a container image (--image) — inspects the shipped Go binaries, or
  • a source repository (--repo) — clones it and runs govulncheck's call-graph reachability analysis.

It is a generic, Go rewrite of the vex_candidates.py triage script from cwayne18/rke2-toolbox: instead of parsing a Trivy scan report, you point it directly at a target, a module, and (optionally) a list of CVEs.

Package/CVE scanners flag a module as vulnerable whenever the module is a dependency, even if the linker dead-code-eliminated the vulnerable package or the vulnerable functions are never reachable. gomod-vex distinguishes those cases so you can produce accurate VEX statements.

How it works

Image mode (--image)

For every Go binary in the image that links the target module, gomod-vex:

  1. Resolves the vulnerable packages from the OSV Go database, keyed by module + the version embedded in the binary's build info.
  2. govulncheck (binary mode) — for non-stripped binaries, a linked but unreachable package is reported vulnerable_code_not_in_execute_path.
  3. pclntab presence test — a Go binary keeps its function-name table even when fully stripped (-ldflags=-s -w). If none of a CVE's vulnerable packages appear in it, the linker eliminated them: vulnerable_code_not_present.

Module versions are read straight from each binary's embedded build info (debug/buildinfo), so no Trivy report or manual version input is required.

Repo mode (--repo)

The repo is cloned (shallow) and analyzed with govulncheck source mode, whose call-graph reachability is authoritative for a source tree — strictly better than the pclntab heuristic (which only exists because shipped binaries are stripped). Each advisory in the dependency graph is classified as reachable (the vulnerable symbol is actually called), not_in_execute_path (imported but unreachable) or not_present (unused). A local checkout path or file:// URL is scanned in place without cloning.

Large repos: source-mode analysis builds a whole-program call graph and can need several GB of RAM. Very large repos (e.g. rancher/rancher) may exhaust memory — govulncheck gets OOM-killed (signal: killed). Give the process more memory (in a container, raise the memory limit, e.g. docker run --memory=8g), scope the scan with --repo-path <subdir>, or fall back to --image mode.

Standard library (--module stdlib)

Go standard-library CVEs are supported in both modes — pass --module stdlib (the name OSV and govulncheck use; --module std is accepted as an alias):

# Image mode: the Go version comes from each binary's build info, and pclntab
# tells you which vulnerable stdlib packages (net/http, crypto/x509, ...) are
# actually linked into each binary.
gomod-vex --image myorg/app:latest --module stdlib --cves CVE-2025-22870

# Repo mode: govulncheck reports stdlib reachability. Results depend on the Go
# toolchain used, so pin it with --go-version to target a specific release.
gomod-vex --repo github.com/rancher/rancher --module stdlib --go-version 1.24.0

In repo mode the stdlib version analyzed is the one of the Go toolchain that runs govulncheck. GOTOOLCHAIN=auto only ever upgrades, so without --go-version a repo is scanned with the newest locally-available toolchain (inside the container image, the base Go version). Pin --go-version to assess a particular release. Note that a pinned older toolchain may be too old to build the latest govulncheck; pair it with GOMODVEX_GOVULNCHECK_VERSION (e.g. v1.1.4) if go run reports a version requirement.

LLM exploitability check (optional, --llm)

For CVEs whose vulnerable code is genuinely linked (image mode) or reachable (repo mode), a GitHub Models chat model gives an advisory likely / unlikely / unknown exploitability verdict.

GitHub Models enforces a low per-minute burst limit, so a scan that assesses many CVEs can hit 429 Too Many Requests (sometimes phrased as a Terms of Service / "scraping" notice — that is GitHub's secondary rate limit). gomod-vex mitigates this by:

  • caching verdicts per CVE — in image mode the same CVE linked into many binaries is assessed once and reused;
  • spacing out requests (default 1s between calls); tune or disable this with GOMODVEX_LLM_MIN_INTERVAL (a Go duration, e.g. 2s, or 0 to disable);
  • retrying 429/5xx with backoff, honoring the server's Retry-After (up to two minutes) so a rate-limit window is actually outlasted.

A failed assessment is non-fatal: the finding is still reported (e.g. LINKED), just without an LLM verdict.

Requirements

  • A Go toolchain on PATH (also required at runtime for --repo source analysis). Repo mode builds and runs govulncheck itself via go run with GOTOOLCHAIN=auto, so Go will fetch whatever toolchain the scanned module requires — no manual version matching needed.
  • skopeo on PATH — image mode
  • git on PATH — repo mode (unless scanning a local path)
  • govulncheck on PATH — used only in image (binary) mode; optional. Repo mode does not need it preinstalled.
  • Network access for --repo (to clone, download the module graph, and fetch a toolchain if the module needs a newer Go than is installed)
  • GITHUB_TOKEN (or GH_TOKEN) when using --llm (or --gist; that also needs gist scope)

Install

go install github.com/cwayne18/gomod-vex@latest

Or build from source:

git clone https://github.com/cwayne18/gomod-vex
cd gomod-vex
go build -o gomod-vex .

Container image (GHCR)

A self-contained image bundling skopeo, git, govulncheck and a Go toolchain (so both image and repo modes work) is published to ghcr.io/cwayne18/gomod-vex on every push to main and every v* tag:

docker run --rm ghcr.io/cwayne18/gomod-vex:latest \
  --image rancher/hardened-coredns:v1.14.6 \
  --module golang.org/x/net --cves CVE-2023-39325

Pass a token through the environment to enable --llm:

docker run --rm -e GITHUB_TOKEN ghcr.io/cwayne18/gomod-vex:latest \
  --image myorg/myapp:latest --module golang.org/x/crypto --llm

Usage

gomod-vex --image REF  --module PATH [--cves LIST] [flags]   # image mode
gomod-vex --repo  REPO --module PATH [--cves LIST] [flags]   # repo mode

Check two specific x/net CVEs in an image:

gomod-vex \
  --image rancher/hardened-kubernetes:v1.30.1-rke2r1 \
  --module golang.org/x/net \
  --cves CVE-2023-39325,CVE-2023-44487

Check a CVE against a source repo via reachability analysis:

gomod-vex \
  --repo github.com/rancher/rancher \
  --module golang.org/x/net \
  --cves CVE-2023-45288

--repo accepts github.com/owner/repo, a full clone URL, a bare owner/repo (assumed GitHub), or a local checkout path / file:// URL. Use --ref for a branch, tag or commit and --repo-path for a module in a subdirectory.

Check every advisory known for x/crypto, as JSON, with the LLM layer:

export GITHUB_TOKEN=...    # a token with models:read
gomod-vex \
  --image myorg/myapp:latest \
  --module golang.org/x/crypto \
  --llm --format json

Flags

Flag Default Description
--image Container image to inspect (mutually exclusive with --repo)
--repo Git source repo to analyze via govulncheck source mode
--ref (default branch) Branch, tag, or commit to check out for --repo
--repo-path . Module subdirectory within --repo to scan
--module (required) Go module import path to evaluate (or stdlib for the standard library)
--cves (all) Comma-separated CVE / GHSA / GO ids; empty checks every advisory for the version
--cves-file File with one id per line (merged with --cves; # comments allowed)
--version (auto) Override the module version (image mode) instead of reading build info
--go-version (auto) Pin the Go toolchain for --repo analysis, e.g. 1.24.0 (useful with --module stdlib)
--os / --arch linux / amd64 Image platform variant to pull (image mode)
--llm false Consult a GitHub Models LLM on genuinely-affected CVEs
--llm-model openai/gpt-4o GitHub Models model id for --llm
--format text text or json
--out (stdout) Write output to a file
--gist false Also upload the output to a public GitHub gist and print its URL (needs a token with gist scope)
--gist-secret false With --gist, create a secret (unlisted) gist instead of a public one
--quiet false Suppress progress logging on stderr

Exactly one of --image or --repo is required.

--gist uploads whatever would otherwise be printed (respecting --format) to a gist using the same GITHUB_TOKEN / GH_TOKEN as --llm; the token needs gist scope. The gist URL is printed to stdout after the report. It composes with --out (the report is written to the file and uploaded).

Output statuses

Status Meaning Suggested VEX justification
not_present Vulnerable package absent (pclntab / govulncheck source) vulnerable_code_not_present
not_in_execute_path Linked/imported but govulncheck marks it unreachable vulnerable_code_not_in_execute_path
linked Vulnerable package genuinely linked, image mode (real finding) (none — treat as affected)
reachable Vulnerable symbol is called, repo mode (real finding) (none — treat as affected)
undetermined No mapping for the id at this version (manual review)

In image mode, when OSV publishes no package-level import paths for an advisory (e.g. some GitHub-only GHSA records), presence is asserted at module granularity instead; these are coarser, so validate before transferring.

Caveats

  • The LLM verdict is advisory only. Never auto-file a VEX statement solely on an LLM verdict; it supplements, and does not replace, the deterministic checks.
  • pclntab matching (image mode) is a heuristic. It is deliberately conservative (a genuinely-linked package is never reported absent), but validate candidates before publishing VEX.
  • Repo mode needs a Go toolchain, git and network access at runtime. It runs govulncheck via go run from inside the target module with GOTOOLCHAIN=auto, so Go automatically fetches a newer toolchain when the scanned module requires one. Override the govulncheck version with GOMODVEX_GOVULNCHECK_VERSION if needed.

License

MIT — see LICENSE.

About

Check whether Go-module CVEs are actually present/exploitable in a container image's binaries (pclntab + govulncheck + optional LLM)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages