Day 72 — govulncheck & supply chain

Updated

July 30, 2026

Day 72 — govulncheck & supply chain

Stage VII · ~3h
Goal: Run govulncheck on your module, understand call-graph reachability vs “CVE in go.mod”, remediate or document accepted risk, and capture a short supply-chain checklist for CI—on Go 1.26.

Why this day exists

Your code can be perfect while a dependency is not. Supply-chain hygiene is part of ship quality:

  • Known vulnerable modules
  • Pseudo-versions and abandoned libs
  • replace forks nobody audits
  • CI that never re-scans

govulncheck is the official Go vulnerability scanner, integrated with the Go vulnerability database. Day 74’s gate will expect a clean scan or an explicit accepted-risk note. Day 81’s security checklist will still cover your auth bugs—scanner ≠ secure app.


Theory 1 — What govulncheck does

go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck -version   # if supported; else: note binary path + date
govulncheck ./...

It analyzes your module’s code and reports vulnerabilities that are reachable in the call graph when possible—not merely present in the module graph. That reduces noise versus naïve “every CVE in the lockfile” tools.

Useful invocations

govulncheck ./...
govulncheck -show verbose ./...
# JSON for CI parsing when needed
govulncheck -json ./... > vuln.json

Binary mode

go build -o bin/app ./cmd/service
govulncheck -mode=binary ./bin/app

Binary mode helps when source and shipped artifact diverge (build tags, different main packages, stripped paths). Scan what you ship.

Mode Strength
Source (./...) Maps findings to your call sites
Binary Matches the artifact operators run

Theory 2 — Reading a finding

A report typically includes:

  • Vulnerability ID (e.g. GO-20XX-XXXX)
  • Affected module and version range
  • Fixed version (if any)
  • Whether your code calls into the vulnerable symbol/path
  • Links to advisory detail
Situation Action
Reachable + fixed version available Upgrade; re-test
In module graph but not reachable Still prefer upgrade; document if blocked
No fix yet Mitigate (avoid code path), monitor, careful replace/fork
Only test-only dependency Still upgrade if easy; else note scope
False sense of security Scanner ≠ review of your auth bugs

Example narrative (write yours in SECURITY-SCAN.md)

ID: GO-2024-XXXX
Module: golang.org/x/net
Your version: v0.a.b
Fixed in: v0.x.y
Reachable: yes (HTTP/2 related import path)
Plan: go get golang.org/x/net@v0.x.y && go test ./...

Theory 3 — Remediation workflow

go get example.com/vulnerable@v1.2.4
go mod tidy
go test ./...
govulncheck ./...

If an upgrade breaks APIs:

  1. Read upstream changelog / release notes.
  2. Fix compile breaks in adapters first (prefer not to change domain).
  3. Run unit + smoke of main paths.
  4. Re-run govulncheck.
  5. Update SECURITY-SCAN.md with before/after versions.

Partial upgrades

go list -m all | grep vulnerable
go get golang.org/x/net@v0.x.y
go mod tidy

Avoid go get -u ./... as a blind first move on a large graph—prefer targeted bumps for the advisory, then consider broader hygiene.

Toolchain and stdlib

go version
go env GOTOOLCHAIN

Keep the Go toolchain itself updated; stdlib CVEs happen too. Matching this volume’s Go 1.26 baseline means you already practice currency (Day 37 quality pass). If go.mod says go 1.26, CI should use 1.26.x as well.


Theory 4 — Reachability vs lockfile scanners

Scanner style Reports Noise
Lockfile/CVE match Any vulnerable version in graph Higher
govulncheck (source) Prefer reachable symbols Lower
Image OS scanner (Trivy etc.) Distro packages in container Different surface

Use both mindsets in production orgs. For this volume, govulncheck is the required Go-native bar; container image scan is stretch.

Your module code
   → imports package P@v1 (vulnerable func F)
   → if F is unreachable from your main graph, source mode may omit or deprioritize
Binary mode still encodes what was linked.

Theory 5 — CI placement

Recommended Stage VII pipeline shape:

test → vet → staticcheck → govulncheck → build → (image scan optional)

Example GitHub Actions step sketch:

- name: govulncheck
  run: |
    go install golang.org/x/vuln/cmd/govulncheck@latest
    govulncheck ./...

Policy options:

Policy When
Fail on any finding Main branch / release tags
Fail on reachable only Early adoption
Annotated exceptions Time-boxed, with owner + expiry

Script hook

Add to scripts/lint.sh or scripts/gate-vii.sh:

#!/usr/bin/env bash
set -euo pipefail
go test ./...
go vet ./...
staticcheck ./...
govulncheck ./...

Theory 6 — Other supply-chain controls (awareness)

Control Role
go.sum Integrity of downloaded modules
Checksum DB Verified sums for public modules
Module proxy Default proxy.golang.org; privacy via GOPRIVATE
GOPRIVATE / GONOSUMDB Private modules not forced through public sum DB
replace Local forks—audit like first-party code
SBOM / cosign Org-level signing & inventory; optional stretch
Dependabot / Renovate Automated upgrade PRs
Vendoring vendor/ can go stale—re-scan after updates

Private modules sketch

go env -w GOPRIVATE=github.com/yourorg/*
# ensure git credentials work in CI

Theory 7 — Accepted risk template

When you cannot upgrade today:

## Accepted risk

ID: GO-....
Module/version:
Reachable: yes/no
Why not fixed today:
Mitigation (feature flag, avoid path, WAF, etc.):
Owner:
Review by date:
Tracking issue:

Accepted risk without expiry is how permanent holes form. Day 74 gate should reject eternal waivers without dates.


Worked example — clean scan document

SECURITY-SCAN.md:

# govulncheck — day72

Date: YYYY-MM-DD
Go: go1.26.x
govulncheck: <version or install date>
Module: example.com/myservice

## Commands
govulncheck ./...
go build -o bin/app ./cmd/service
govulncheck -mode=binary ./bin/app

## Result
No vulnerabilities found.

## Notes
- Last dependency review: ...
- replace directives: none | listed below
- CI: scripts/lint.sh includes govulncheck

Finding + remediation example

## Finding
ID: GO-....
Module: golang.org/x/net @ v0.a.b
Fixed in: v0.x.y
Reachable: yes (import path ...)

## Remediation
go get golang.org/x/net@v0.x.y
go mod tidy
go test ./...
govulncheck ./... → clean

## Residual risk
None known.

Lab 1 — Install and baseline scan

Suggested workspace: your primary service or CLI module.

  1. Confirm go version is 1.26.x.
  2. Install govulncheck; record version/date.
  3. Run govulncheck ./...; save full output to SECURITY-SCAN.md or attach summary.
  4. Build main artifact; run binary mode.
  5. If clean: still write the clean-scan document (evidence for Day 74).
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
mkdir -p bin
go build -o bin/app ./cmd/...
govulncheck -mode=binary ./bin/app

Lab 2 — Remediate or accept

  1. If findings exist: prefer upgrade path.
  2. Re-test; re-scan.
  3. If blocked: fill accepted-risk template with review date.
  4. Search for replace in go.mod and justify each.
grep -n replace go.mod || true
go list -m all > /tmp/modules.txt
# skim for pseudo-versions on critical path libs

Lab 3 — CI / script integration

  1. Add govulncheck to scripts/lint.sh (or create it).
  2. Sketch Actions (or other CI) step in SECURITY-SCAN.md.
  3. Run the script cold once.
chmod +x scripts/lint.sh
./scripts/lint.sh

Lab 4 — JSON output stretch (optional but useful)

govulncheck -json ./... > vuln.json
# count findings with jq if installed
jq '.. | .osv? // empty | .id' vuln.json 2>/dev/null || true

Document how CI could fail on non-empty findings.

Stretch — image scan awareness

# conceptual — tool must be installed
# trivy image myapp:dev

Note: OS packages in the container are outside govulncheck’s Go module graph. Distroless/static helps shrink that surface (Day 68).


Hour-by-hour suggestion

Time Focus
0:00–0:20 Install tool; baseline source scan
0:20–1:00 Binary scan; interpret findings
1:00–2:00 Upgrade / remediate / accepted risk
2:00–2:30 lint.sh + CI sketch
2:30–3:00 SECURITY-SCAN.md polish

Common gotchas

Gotcha Fix
Old govulncheck DB / binary Reinstall @latest; ensure network to vuln DB
Clean scan ≠ secure app Still do Day 81 checklist
Upgrading everything blindly Read changelogs for major jumps
Ignoring binary mode Scan what you ship
Vendoring stale code Re-scan after vendor updates
Private modules fetch fail GOPRIVATE + CI credentials
Accepting risk forever Expiry date + owner
Only scanning ./cmd Prefer ./... then binary
Committing vuln.json secrets Usually none—but don’t commit env dumps

Checkpoint

  • govulncheck ./... run documented
  • Binary mode run or explicit N/A with reason
  • Findings remediated or explicitly accepted with review date
  • Tool version + Go 1.26 recorded
  • Script/CI hook sketched and executed once
  • Can explain reachability vs lockfile-only scanners
  • replace directives reviewed

Commit

git add .
git commit -m "day72: govulncheck supply chain pass"

Write three personal gotchas before continuing.


Tomorrow

Day 73 — Elective: Kubernetes client-go or deepen the service—choose deliberately and ship one coherent outcome. Bring SECURITY-SCAN.md into the Stage VII gate pack (Day 74).