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.
- 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.
- Never generate a rollback that discards uncommitted work.
git checkout go.mod go.sumoverwrites unstaged edits with no recovery path. Require a clean worktree instead —git status --porcelain go.mod go.sumempty before any upgrade loop — and stop on failure rather than revert. - Prove read-only-ness. Record
git status --porcelain go.mod go.sumat the start and end. If it differs, a probe mutated the module files (e.g.go mod downloadon Go < 1.18) — say so in S9.8 rather than silently reporting post-mutation state. - 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
gocommand. A baseline taken aftergo listcannot prove the audit was read-only —go listis 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.sumis legal when the module has no dependencies or every requirement is redirected by a localreplace. Decide by what is required, not byls: 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 --helpprints 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:
- Load
references/multi-module.mdand follow it. Do not improvise. - Snapshot all manifests at once, still before any
gocommand:git status --porcelain -- '**/go.mod' '**/go.sum' - 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¶
govulncheck ./...in source mode is primary — it traces the call graph, so it reports whether your code can actually reach the vulnerable symbol.- The
-scanlevel decides what "found" means —symbol(default) reports reachable symbols,packageimported packages,modulerequired versions. Lowering it raises noise and forfeits reachability. - Exit code is the CI contract, and
-format jsonbreaks it — text mode:3found at scan level,2invalid usage,1error,0clean.-json/-format sarif/-format openvexexit 0 regardless of findings, so a CI job gating on$?after them never fails. - govulncheck reports no CVSS score — see S6.1. Priority comes from the evidence tier, not from a severity number the tool never emitted.
- Test files are excluded by default —
-testdefaults to false, so test-only dependencies are not analyzed unless you pass-test. - 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.
- 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.
- Use the scanner's own vocabulary —
go-licensestypes areforbidden,restricted,reciprocal,notice,permissive,unencumbered,unknown;--disallowed_typesdefaults toforbidden,unknown. Reporting in the tool's terms keeps the output auditable and version-stable. - 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: 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 Summarycritical = 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.**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.{"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}}