Skip to content

Day 159 — govulncheck & gosec

Month 6 · Week 3 · ⬅ Day 158 · Day 160 ➡ · Journal index

🎯 Learning Objective

Scan a Go project two complementary ways — govulncheck for reachable known vulnerabilities in dependencies/stdlib, and gosec for risky code patterns — and wire both into CI.

📚 Topics

  • govulncheck: reachability-based CVE scanning from the Go vuln database
  • gosec: AST-based linter for insecure code (secrets, weak crypto, SQLi)
  • Where each fits in CI; triaging and suppressing findings honestly

📖 Reading / Sources

📝 Notes

  • govulncheck is reachability-based, which is its superpower: it builds your call graph and only reports vulnerabilities your code actually calls, not every CVE that merely exists in go.sum. Far less noise than naive SCA scanners → [[govulncheck]].
  • It scans both third-party modules and the standard library (a vulnerable Go toolchain version is flagged too). Source data is the curated Go vuln DB.
  • Run modes: govulncheck ./... (source mode, needs the code + reachability) or govulncheck -mode=binary ./app (scan a built binary — great for verifying released artifacts). -format=json for CI parsing.
  • A finding has two tiers: "Vulnerability + your code calls it" (act now) vs "in your module graph but not called" (informational). Prioritize the called ones.
  • gosec is pattern/AST-based: it flags insecure code, e.g. hardcoded credentials (G101), math/rand for security (G404), SQL string concatenation (G201/G202), weak TLS/crypto (G402/G401), command injection (G204), unhandled errors (G104), file perms (G302/G306). It complements govulncheck — different layer (your code vs known CVEs) → [[gosec]].
  • Triage, don't blanket-suppress. For a true false positive, annotate the exact line // #nosec G404 -- non-crypto jitter, reviewed with a reason, or exclude a rule deliberately (-exclude=G104). Silent global suppression hides real bugs.
  • CI placement: run govulncheck on a schedule and per-PR (new CVEs appear without code changes). Gate the build on reachable HIGH/CRITICAL; treat unreachable ones as warnings. gosec runs per-PR on changed code. go vet and staticcheck round out correctness/lint but aren't security scanners.
  • Keep the toolchain itself updated (go version, base images by digest from [[day-150]]) — many stdlib CVEs are fixed simply by bumping Go.
  • These are CLI tools, not library code in your service, so there's no stdlib example — the snippets below are the real invocations.

💻 Code Examples

# Install (module-versioned tools).
go install golang.org/x/vuln/cmd/govulncheck@latest
go install github.com/securego/gosec/v2/cmd/gosec@latest

# Reachable-vulnerability scan of the whole module (source mode).
govulncheck ./...
# Scan a released binary instead of source.
govulncheck -mode=binary ./bin/app
# Machine-readable for CI gating.
govulncheck -format=json ./... > vulns.json

# Security linter over the code; fail CI on any finding.
gosec ./...
gosec -severity=medium -confidence=medium -fmt=sarif -out=gosec.sarif ./...
# GitHub Actions: block PRs on reachable vulns and insecure patterns.
- name: govulncheck
  run: |
    go install golang.org/x/vuln/cmd/govulncheck@latest
    govulncheck ./...
- name: gosec
  uses: securego/gosec@master
  with:
    args: -severity medium ./...

🏋️ Exercises / Practice

Exercise Status Link
(Reinforces Day 156/157 allocation-safe code patterns) exercises/month-06/week-3/

No stdlib runnable example today — govulncheck and gosec are external CLIs run against the repo, shown as the commands above.

🐛 Mistakes Made

  • Panicked at a long go list -m all CVE list from a naive scanner; govulncheck showed only the 2 my code actually reaches. Reachability cut the noise massively.
  • Used // #nosec with no rule id or reason on a real finding. Replaced with a specific // #nosec G404 -- reason after confirming it was non-security randomness.

❓ Open Questions

  • How to keep govulncheck green continuously without alert fatigue — schedule cadence vs per-PR, and auto-PRs for safe bumps (Dependabot/renovate)?

🧠 Active Recall (answer without looking)

  1. Q: Why does govulncheck report fewer issues than a typical SCA tool that reads go.sum?
    A

It's reachability-based: it analyzes the call graph and reports a vulnerability only if your code actually calls the affected symbol. Dependencies that contain a CVE but whose vulnerable function you never invoke are downgraded to informational, eliminating most false-positive noise. 2. Q: govulncheck vs gosec — what different layers do they cover?

A

govulncheck finds known vulnerabilities (CVEs from the Go vuln DB) in your dependencies and the standard library that your code reaches. gosec is a static linter that finds insecure code patterns you wrote (hardcoded secrets, weak crypto, SQL concatenation, math/rand for tokens). One scans the supply chain; the other scans your own source.

🪶 Feynman Reflection

Two different security guards. govulncheck reads the "wanted posters" (the vuln database) and checks whether anyone you actually talk to (your reachable call graph) is on them — ignoring strangers you never contact. gosec is the inspector walking through your own house looking for unlocked doors you left: passwords on sticky notes, a flimsy lock (weak crypto), a window you can climb through (SQL injection).

🕳️ Knowledge Gaps

  • SARIF integration with GitHub code scanning and dedup across runs.
  • Building an SBOM and tying it to vulnerability monitoring over time.

✅ Summary

I can scan a Go project for reachable known vulns with govulncheck (source and binary modes) and insecure code patterns with gosec, triage findings honestly with scoped suppressions, and gate CI on the ones that matter.

⏭️ Next Steps / Prep for Tomorrow

  • Day 160: harden the running service — authentication/authorization and rate limiting.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): govulncheck & gosec scanning (day 159)