A big share of the most-used images on Docker Hub are Go programs: Prometheus, Grafana, Traefik, Caddy, MinIO, etcd, CoreDNS, Loki, VictoriaMetrics, Vault, Consul, ArgoCD, and nearly every exporter. If you run any of them, you've seen their scan reports: dozens of findings on a single tag.
- • Go binaries are usually static — findings in the image's OS packages describe code the program never executes.
- • The Go linker strips uncalled functions —
govulncheckproves which module findings actually made it into the binary. - • Your job isn't to patch all forty findings. It's to sort them — with evidence, not opinion. Three checks do it.
What that list actually is
In a typical Go image report:
- a large part sits in OS packages the program never runs — a Go binary is usually static and doesn't call the image's OpenSSL or libc at all;
- another part is in library code that never made it into the binary — the Go linker strips functions that are never called;
- some findings are in stdlib functions the app doesn't use;
- and a few are real — those get fixed by taking a newer release.
Check 1 — Is the binary static?
Kills most OS-package findings. Pull the binary out of the image and look at it:
docker create --name tmp prom/prometheus && \ docker cp tmp:/bin/prometheus ./prometheus && docker rm tmp file ./prometheus # "statically linked" → never executes the image's OS libraries
If it says statically linked — every finding against openssl, zlib, libc, busybox describes code that ships in the image but is never executed by the application. That's usually the biggest block of the report, ruled out in one command.
Check 2 — Did the vulnerable function make it into the binary?
Kills most module findings:
govulncheck -mode=binary ./prometheus
govulncheck is the Go team's tool. It reports a CVE only if the vulnerable function is actually present in the binary — and since the Go linker drops code that's never called, “present” is a strong signal while “absent” is a proof. In practice this rules out the large majority of module findings: function-level analysis across ecosystems shows fewer than ~10% of dependency vulnerabilities are reachable (Endor Labs).
- Function absent → not exploitable in this binary. Dismissed, with the govulncheck output as evidence.
- Function present → treat as real. Check
go version -m ./prometheusfor the module version, then take the release where it's bumped (or bump and rebuild if it's your image).
govulncheck ./... is even stricter — it builds the real call graph.Check 3 — stdlib findings: check the toolchain version
Findings against stdlib live in the Go toolchain the binary was compiled with. Nothing inside the image can patch them — only a build with a newer Go.
go version -m ./prometheus | head -1 # e.g. "go1.24.2"
- Vulnerable function not in the binary? Check 2 already cleared it.
- It is there, and a newer Go fixes it? Take the vendor's next release — or rebuild with current Go if the image is yours.
- Vendor stopped rebuilding? That tag is unfixable; move to a maintained one.
Write the dismissals down
For each finding you ruled out, record a short VEX statement: not_affected + justification (vulnerable_code_not_in_execute_path) + the command output as evidence. Scanners like Trivy and Grype consume VEX and stop flagging those findings on every future scan — and a security review gets proof instead of “we think it's fine”.
The exceptions
Two cases where you fix immediately, whatever the checks say: the CVE is in CISA KEV (actively exploited) and the service faces the internet — or the vulnerable code is the app's core path.
The result
| Findings block | Check | Outcome |
|---|---|---|
| OS packages (openssl, libc, zlib…) | file | Ruled out if the binary is static |
| Go module dependencies | govulncheck -mode=binary | Most ruled out — function not in binary |
| stdlib / toolchain | go version -m | Sorted by toolchain version |
| The real few | — | Fixed by taking a newer release |
One pass on a typical Go image — and the two or three real findings get fixed by taking a newer release. Repeat on every release.
Or run Go-stack images where this triage is already done and documented: that's what BaseImage ships, re-scanned daily.
Skip the manual triage
Let an agent run it — or get it done for you
A playbook your AI agent can follow to run these checks — or a full evidence-backed report on your own image.