Skip to content

go-dependency-audit

Quick Reference

When you need... Jump to
Run a full dependency audit S2 Gates -> S5 Checklist -> S9 Output
Scan for known CVEs S2 Gates -> S5.1 CVE Scanning
Triage a finding / decide urgency S6 — evidence tier, NOT a CVSS guess
Check license risk S5.2 + references/license-compliance.md
Plan a version upgrade S5.3 + references/upgrade-planning.md
Investigate supply chain risk S5.4 + references/supply-chain-security.md
Review go.mod hygiene S5.5 Module Hygiene
Actually apply a fix S1.3 Remediation Boundary

1 Scope & Operating Mode

1.1 In scope

go.mod/go.sum analysis, CVE scanning via govulncheck (primary), license risk triage, outdated dependency reporting, upgrade path planning, breaking change assessment, supply chain posture (proxy, checksum DB, private modules), +incompatible triage, module graph analysis.

1.2 Out of scope

Application code security (use security-review), micro-benchmark performance (use go-benchmark), infrastructure provisioning, container image scanning, runtime behavior analysis, and legal determinations about license obligations — see S5.2: this skill produces evidence and escalation triggers, never verdicts.

1.3 Remediation Boundary (NON-NEGOTIABLE)

An audit is read-only. It observes; it does not repair.

Class Commands Allowed during audit
Read-only probe govulncheck, go list -mod=readonly, go mod graph/verify/why, go mod edit -json, go mod tidy -diff, go version -m, go env <VAR>…, go-licenses check/report/help, git status/diff/log Yes
Mutating go get, go mod tidy (without -diff), go mod edit -require, go work, go env -w, go list -mod=mod, go-licenses save, go install, cyclonedx-gomod -output No — emit as plan
Destructive git checkout, git restore, git reset, rm Never — not even to roll back

allowed-tools pre-approves; it does not forbid. It keeps writes off the auto-approved surface, while gate 2/11's snapshot detects a mutation that happened anyway. Two writes wear the name of a read, and a coarse list lets both through: go env -w rewrites Go's persistent env file rather than printing it, and go list -mod=mod lets package loading update go.mod/go.sum. Readonly is the default since Go 1.16, but GOFLAGS can override it — so every package-loading command here states -mod=readonly. Writes that produce a deliverable (go-licenses save, SBOM, tool installs) are remediation: emitted under S9.7, marked # EMIT in the references, never run. Guarded by regression checks DA008/DA014/DA015 — static checks that keep writes off the auto-approved surface; they cannot stop a write at runtime.

  1. Emit, do not execute. Every fix is delivered as a copy-pasteable command block under S9.7, for a human to run. The skill never runs it.
  2. Never generate a rollback that discards uncommitted work. git checkout go.mod go.sum overwrites unstaged edits with no recovery path. Require a clean worktree instead — git status --porcelain go.mod go.sum empty before any upgrade loop — and stop on failure rather than revert.
  3. Prove read-only-ness. Record git status --porcelain go.mod go.sum at the start and end. If it differs, a probe mutated the module files (e.g. go mod download on Go < 1.18) — say so in S9.8 rather than silently reporting post-mutation state.
  4. Switching to remediation requires the user to ask for it in this turn. "Audit our dependencies" is not authorization to upgrade them.

2 Gates

Gates are checked in order. Each gate declares a class that determines what failure does — this is the only thing that decides stop-vs-continue.

2.1 Gate classes

Class Meaning On failure
BLOCK The audit's subject does not exist or is untrustworthy Stop. Emit no findings. Emit the reason + what would unblock, headed NOT AN AUDIT.
DEGRADE A capability is unavailable; the subject is fine Continue. Enter the matching S4 mode, list the lost coverage in S9.8.
WARN An observation worth reporting Continue at full scope. Record as a finding.

A gate has exactly one class. There is no gate that both stops and degrades.

2.2 Gate table

Order matters. Gates 1–2 touch no Go tooling, so the read-only baseline is captured before anything could disturb it.

# Gate Check Class Failure action
1 Module exists Glob("**/go.mod") — filesystem only BLOCK No Go module here — nothing to audit
2 Baseline snapshot git status --porcelain go.mod go.sum — before any go command WARN Note uncommitted module edits; the audit reflects the worktree, not HEAD
3 go.mod is well-formed go mod edit -json parses; module directive present BLOCK Malformed manifest — findings would be fiction
4 Module graph resolvable go list -mod=readonly -m all succeeds DEGRADE -> no-graph mode (see below)
5 Checksums available go.sum covers every non-replaced external requirement DEGRADE -> no-integrity mode (see below — absence alone is not a failure)
6 Checksums verify go mod verify WARN Report as a P1 finding, do not stop — a tampered cache is exactly what an audit exists to surface
7 govulncheck available govulncheck -version DEGRADE -> no-cve mode
8 Vuln DB reachable govulncheck exits 0 or 3, not 1 DEGRADE -> no-cve mode (offline)
9 License tool available go-licenses help lists subcommands DEGRADE -> no-license mode
10 Tidy state go mod tidy -diff (Go 1.23+; skip below that) WARN Report untidy go.mod as a hygiene finding
11 Closing snapshot git status --porcelain go.mod go.sum matches gate 2 WARN A probe mutated the module files — say so in S9.8

Four rationales, each replacing a worse rule:

  • Gate 2 precedes every go command. A baseline taken after go list cannot prove the audit was read-only — go list is one of the things it would have to exonerate.
  • Gate 4 is DEGRADE, not BLOCK. Gate 3 already caught a broken manifest; a failure here is environmental (offline, proxy down, missing credentials, cold cache) — a lost capability, not an untrustworthy subject.
  • Gate 5 is conditional, not "file exists". A missing go.sum is legal when the module has no dependencies or every requirement is redirected by a local replace. Decide by what is required, not by ls: external non-replaced requirements with checksums missing -> DEGRADE; none, or all locally replaced -> N/A, legitimately absent, not a finding; cannot tell (gate 4 already degraded) -> DEGRADE, naming the unresolved graph as the cause.

  • Gate 6 is WARN. An integrity failure is the highest-value output this skill can produce; stopping would suppress the finding the user most needs.

  • Gate 9 uses help, not --help. go-licenses --help prints only the logging flags and never lists commands — a useless liveness probe.

2.3 Multi-module repositories

Gate 1 globs for every go.mod. If it finds more than one, the unit of audit is the module, not the repository — running the gates once in the root audits one module and reports it as though it covered all of them. When >1 is found, before gate 3:

  1. Load references/multi-module.md and follow it. Do not improvise.
  2. Snapshot all manifests at once, still before any go command: git status --porcelain -- '**/go.mod' '**/go.sum'
  3. List every module in S9.1; name any you skipped in S9.8.

Single-module repositories skip this — the gate table runs once.

2.4 Scope classification

Mode Trigger Output contract
Quick "check for CVEs", one named concern S9 subset (9.1, 9.2, 9.3, 9.8, 9.9)
Standard "audit dependencies", pre-release check Full S9
Deep "supply chain review", compliance audit Full S9 + provenance/SBOM

3 Depth Selection

Quick

Single-concern scan. Load no reference files. - Triggers: "run govulncheck", "any CVEs?", "check this dependency" - Coverage: govulncheck scan + S6 triage + immediate remediation plan - Output: the S9 subset above. Do not emit empty License/Supply-Chain sections — omit them and say why in S9.8.

Standard (default)

Full audit across 5 domains. Load govulncheck-patterns.md, license-compliance.md, upgrade-planning.md — one per domain this depth covers. (supply-chain-security.md is Deep-only; multi-module.md loads on the gate-1 trigger regardless of depth.) - Triggers: pre-release audit, "audit our dependencies", quarterly review - Coverage: CVE scan, license risk, outdated report, upgrade assessment, hygiene - Force Standard if: multiple go.mod files, compliance requirements, CI integration

Deep

Comprehensive supply chain review. Load all references. - Triggers: compliance audit, incident response, "supply chain review" - Coverage: all Standard domains + provenance, SBOM, transitive license, proxy config - Force Deep if: regulatory compliance, post-incident, new vendor onboarding


4 Degradation Modes

Each mode is entered by exactly one DEGRADE gate. Modes compose — record all that apply.

Mode Entered by Can still deliver MUST NOT claim
no-graph Gate 4 Direct requirements read from go.mod Anything about indirect dependencies, or that the list is complete
no-integrity Gate 5 Module list, versions, licenses, hygiene Reproducible-build or tamper-detection status
no-cve Gate 7, 8 License, outdated, hygiene, supply chain posture Any CVE status — present, absent, or reachable
no-license Gate 9 CVE, outdated, hygiene, supply chain posture License distribution or compliance posture
no-reachability -scan was not symbol, or binary mode Which modules are affected That any finding is or is not reachable

Mark every degraded output inline: # DEGRADED [<mode>]: <what is missing>

Two absolute rules:

  • Never fabricate CVE findings.
  • Never claim "no vulnerabilities" without a scan that completed. A govulncheck exit code of 1 is a failed scan, not a clean one.

5 Dependency Audit Checklist

5.1 CVE Scanning

  1. govulncheck ./... in source mode is primary — it traces the call graph, so it reports whether your code can actually reach the vulnerable symbol.
  2. The -scan level decides what "found" means — symbol (default) reports reachable symbols, package imported packages, module required versions. Lowering it raises noise and forfeits reachability.
  3. Exit code is the CI contract, and -format json breaks it — text mode: 3 found at scan level, 2 invalid usage, 1 error, 0 clean. -json / -format sarif / -format openvex exit 0 regardless of findings, so a CI job gating on $? after them never fails.
  4. govulncheck reports no CVSS score — see S6.1. Priority comes from the evidence tier, not from a severity number the tool never emitted.
  5. Test files are excluded by default — -test defaults to false, so test-only dependencies are not analyzed unless you pass -test.
  6. Transitive findings still need go mod why -m <module> to establish which direct dependency pulls them in — that is the module you actually upgrade.

5.2 License Risk Triage

This skill does not give legal advice and does not decide whether a license is compatible with a project. It gathers the facts a lawyer needs and states which facts trigger escalation. Every copyleft finding routes to legal review.

  1. Report the license, the path, and the trigger conditions — never a verdict. Whether a copyleft obligation attaches turns on facts this skill cannot see: distribution, linkage vs build-tool-only, licence version and exceptions, modification, deployment model. Record the observable; escalate the rest.
  2. Use the scanner's own vocabulary — go-licenses types are forbidden, restricted, reciprocal, notice, permissive, unencumbered, unknown; --disallowed_types defaults to forbidden,unknown. Reporting in the tool's terms keeps the output auditable and version-stable.
  3. Distinguish shipped from not-shipped, and label the evidence grade. Required (go list -m all) < build-dependency (`go list -mod=readonly -deps
    `, after discovering `main` packages — never assume `./cmd/...`) < binary (`go version -m `, which reads the module list the build actually recorded). Say which grade you have; only the last supports the phrase "linked into the shipped binary". 10. **A missing LICENSE file is the highest-signal license finding** — no grant of rights was located. An escalation trigger, not a legal conclusion. 11. **Escalate with the facts attached**: module path, licence identifier and version, `go mod why -m` path, evidence grade for shipping (item 9), and whether the project distributes binaries or runs a network service. ### 5.3 Upgrade Planning 12. **Semver signals intent, not a guarantee.** Patch/minor are *lower risk*, not safe, and **`v0.x.y` carries no compatibility promise at all**. Read the changelog; diff the API surface when there is none. 13. **`+incompatible` is a silent major-version upgrade hazard** — the module published v2+ tags without a module-aware `go.mod`, so the toolchain treats those versions as part of the *same* module as v1.x. MVS can therefore upgrade v1.5.2 straight to v4.1.2+incompatible during a routine `-u`. Plan migration to a `/vN` path. 14. **`go get -u` upgrades far more than the target** — it raises the target and its dependencies. Use `go get @` for precise control, and remember that even a precise `go get` can move *other* modules, because minimal version selection re-solves the whole graph. ### 5.4 Supply Chain Security 15. **go.sum is an integrity anchor, not a lockfile.** It records expected hashes; it does not pin which version is selected — that is `go.mod` + MVS. Commit both; verify with `go mod verify`. 16. **GOPROXY affects availability and privacy, not checksum verification.** Validation is controlled by `GOSUMDB` and disabled per pattern by `GOPRIVATE`/`GONOSUMDB` — `GOPROXY=direct` still verifies. 17. **`GOPRIVATE` for internal modules** — stops internal module paths leaking to the public proxy and checksum database. Shorthand for `GONOPROXY` + `GONOSUMDB`. 18. **Deleted upstream tags break builds** — `proxy.golang.org` caches immutably, so a cached version survives tag deletion. Prefer the proxy over `direct`. ### 5.5 Module Hygiene 19. **Check tidiness without mutating** — `go mod tidy -diff` (Go 1.23+) prints the change and exits non-zero if non-empty. Below 1.23 report the check as unavailable rather than running the mutating `go mod tidy`. 20. **Minimize `replace` directives** — each is technical debt, and a local-path `replace` in a committed go.mod breaks every machine but the author's. 21. **Module-graph cycles are legal in Go and are not, by themselves, a defect.** Modules may require each other; only *package* import cycles are rejected by the compiler. Report a cycle as a WARN-level design smell that widens upgrade blast radius — never as a failed check. 22. **`go.work` is normally not committed** — it encodes one developer's local layout. Exception: a single-repository workspace whose `use` directives are all repo-relative. Check the paths before flagging it. --- ## 6 Triage & Priority Model ### 6.1 The tool gives you evidence, not a score The Go vulnerability database does **not** publish CVSS scores, so govulncheck never prints one. Its report carries the `GO-YYYY-NNNN` ID, aliases (CVE/GHSA), summary, affected ranges, fixed version, and `database_specific.review_status` (`REVIEWED` / `UNREVIEWED`). Both rules are mandatory: - **Never state a CVSS score sourced from govulncheck.** It did not produce one. - Any CVSS must be enriched from a *named external source* keyed on the alias — "CVSS 9.8 (NVD, CVE-2023-44487)" — and recorded in S9.3. With no such lookup the column reads `not retrieved`, never a guess. ### 6.2 Evidence tiers govulncheck groups findings into result sections. The section *is* the evidence. | Section | Meaning | Tier | |---------------------------|--------------------------------------------------------|------| | `=== Symbol Results ===` | A vulnerable symbol is reachable from your call graph | **E1 Called** | | `=== Package Results ===` | You import the affected package; no reachable symbol proven | **E2 Imported** | | `=== Module Results ===` | The module is required at an affected version only | **E3 Required** | `No vulnerabilities found.` = zero findings at any tier. ### 6.3 Priority | Priority | Condition | |----------|-----------| | **P0** | E1 Called, a fix version exists, and the call path is reachable from a network-facing entry point | | **P1** | E1 Called (any other case); **or** `go mod verify` reported a checksum mismatch; **or** an unlicensed dependency is linked into a shipped binary | | **P2** | E2 Imported; **or** a copyleft dependency linked into a shipped artifact and pending legal review; **or** a `+incompatible` direct dependency | | **P3** | E3 Required only; minor-version drift; hygiene findings; EOL library with no current findings | Escalation modifiers — apply, then state the reason: - **No fix version available** — escalate one; remediation is a compensating control, not an upgrade. - **`UNREVIEWED` report** — absence of a symbol-level finding is not proof of unreachability. Hold at the tier reported and note the status. - **Reachability not established** (`-scan module|package`, binary mode, or reflection/`unsafe`/plugin in the path) — `no-reachability` mode. Report the tier obtained; never downgrade on absent evidence. - **Test-only dependency** — de-escalate one, only after `go mod why -m` confirms no non-test path exists. --- ## 7 Anti-Examples Each rule below is binding on its own. Worked WRONG/RIGHT pairs for all six are in `references/anti-examples.md` — load it when an audit is about to do one of these things, or when explaining why not. | ID | Anti-pattern | Rule | |------|-------------------------------------------------|----------------------------------------------------------------------| | AE-1 | Assigning a CVSS score govulncheck never emitted | Report `CVSS: not retrieved`, or cite the external database and alias it came from. Priority comes from the evidence tier. | | AE-2 | Turning a licence observation into a legal verdict | Emit the escalation packet — module, licence, path, linkage, distribution — and route to legal. Never conclude. | | AE-3 | Gating CI on an exit code `-json` always sets to 0 | Gate on the text-mode exit code (3 = found, 1 = broke), or parse findings from JSON with `jq -s`. | | AE-4 | Rolling back with a command that destroys work | Never emit `git checkout`/`restore`/`reset`. Require a clean worktree up front and stop on failure. | | AE-5 | Claiming "no vulnerabilities" from a failed scan | Only exit 0 with `No vulnerabilities found.` supports that claim. Exit 1 means `no-cve`, status UNKNOWN. | | AE-6 | Treating `+incompatible` as harmless | It is the *same* module as v1.x to MVS, so `-u` can cross a major version silently. Track as P2 with a `/vN` migration plan. | --- ## 8 Dependency Audit Scorecard Twelve checks in three tiers, applied after every audit — **load `references/scorecard.md`** for the item list and score them there. A check that could not run because of a DEGRADE gate scores **N/A** and leaves both numerator and denominator; it never counts as a pass. Score each tier as a ratio over its applicable items, because a fixed threshold breaks the moment an item goes N/A:
    critical = passed / applicable   must be 1.00      (0 applicable -> tier N/A)
    standard = passed / applicable   must be >= 0.80
    hygiene  = passed / applicable   must be >= 0.75
    PASS iff every non-N/A tier meets its threshold.
    
    Report ratio and raw counts: `Standard 3/3 (1.00) — 2 items N/A`. In a multi-module audit the repository verdict is the **worst** module's, never an average — an average lets a clean module mask a failing one. --- ## 9 Output Contract Quick mode emits 9.1, 9.2, 9.3, 9.8, 9.9; Standard and Deep emit all nine. An omitted section must be named in 9.8 with the reason — never silently dropped, never emitted empty. Volume: P0/P1 fully detailed, P2 up to 10, P3 summary. ### 9.1 Audit Context Every module audited (path + directory), Go version, direct/indirect counts, worktree state, tool versions, scan timestamp. ### 9.2 Mode & Depth `Quick | Standard | Deep`, plus every active degradation mode from S4 and the gate that triggered it. ### 9.3 CVE Scan Results Command (with `-scan`/`-mode`), exit code, and per finding: GO-ID, aliases, module, evidence tier (E1/E2/E3), fixed version, review status, priority, and CVSS **with its source** or `not retrieved`. ### 9.4 License Inventory Per dependency: licence identifier, scanner classification, shipping evidence grade (S5.2 item 9). Separate escalation table for copyleft/unknown/missing with the item-11 facts attached. No verdicts. ### 9.5 Outdated Dependencies Direct dependencies behind latest, grouped by major/minor/patch drift, with the `v0.x` ones called out as unbounded-risk regardless of the size of the bump. ### 9.6 Supply Chain Posture Actual `go env` values (`GOPROXY`, `GOPRIVATE`, `GONOPROXY`, `GONOSUMDB`, `GOSUMDB`); go.sum status and `go mod verify` result; `replace` inventory. ### 9.7 Remediation Plan Prioritized, **as commands for the user to run** — this skill does not run them. Immediate (P0/P1), short-term (P2), backlog (P3). Each entry: module, current -> target, evidence tier resolved, and precondition (clean worktree, green baseline). ### 9.8 Uncovered Risks What this audit did NOT cover. Mandatory — never empty. Must include every degradation mode, N/A scorecard item, omitted output section, module not audited, and escalation handed to another party. ### 9.9 Machine-Readable Summary
    {"summary":{"pass":true,"modes":["no-license"],
     "tiers":{"critical":{"passed":3,"applicable":3,"ratio":1.0},
              "standard":{"passed":4,"applicable":4,"ratio":1.0,"na":1},
              "hygiene":{"passed":3,"applicable":4,"ratio":0.75}}},
    "counts":{"p0":0,"p1":1,"p2":3,"p3":5},
    "evidence":{"e1_called":1,"e2_imported":3,"e3_required":5},
    "modules":{"direct":12,"indirect":47,"affected":4},
    "scan":{"tool":"govulncheck","mode":"source","scan_level":"symbol","exit_code":3}}
    
    **Scorecard appended**, ratios with raw counts and N/A totals: `Critical 3/3 (1.00) · Standard 4/4 (1.00, 1 N/A) · Hygiene 3/4 (0.75) — PASS` --- ## 10 Reference Loading Guide | Condition | Load | |----------------------------------------------|----------------------------------------| | CVE scanning (Standard+) | `references/govulncheck-patterns.md` | | License risk triage (Standard+) | `references/license-compliance.md` | | Upgrade planning, version migration (Standard+) | `references/upgrade-planning.md` | | Supply chain review (Deep) | `references/supply-chain-security.md` | | About to do — or explain — an S7 anti-pattern | `references/anti-examples.md` | | More than one `go.mod` found (gate 1) | `references/multi-module.md` | | Scoring the audit (S8) | `references/scorecard.md` | Each reference has a table of contents — load the relevant sections, not the whole file. **Tool-version note (G1)**: commands here are verified against `govulncheck v1.1.4`, `go-licenses v2.0.1`, and Go 1.26.1. `go mod tidy -diff` additionally requires Go 1.23+. Check `govulncheck -version` and the local `go` version before relying on a flag. A govulncheck built against a *different* Go than the one on `PATH` fails package loading with exit 1 — that is `no-cve`, and the fix is to rebuild govulncheck, not to report a clean scan.