| name | loop-cve-audit |
| description | Iterative dependency-CVE remediation loop: scan with ecosystem-native advisory tooling, assess whether each high/critical finding is actually reachable (with call-path evidence, not vibes), fix the highest-risk reachable one with the smallest credible change, re-verify, and repeat. Terminates when no exploitable high/critical CVE remains or every remaining finding has an evidence-backed reachability assessment and an approved risk decision. Use when the user mentions CVEs, vulnerability scanning, dependency security, npm/pip/cargo audit, security patching, or wants a recurring dependency-security loop. |
Loop: CVE Audit
Drive the current repository's dependencies to a defensible security
state, one verified fix at a time. This is a loop skill: it runs in
iterations with explicit terminal states, and it is designed to be
re-run on a schedule (new CVEs appear over time — this loop never
"finishes" permanently).
Loop contract
The loop ends only in one of these states. Every run must report which
one it reached.
| Terminal state | Meaning |
|---|
| CLEAN | No exploitable high or critical CVE remains. |
| ASSESSED | High/critical findings remain, but every one has evidence-backed reachability analysis and a user-approved risk decision. |
| BLOCKED | A precondition failed (no dependency graph, no advisory access, broken build baseline). Reported, nothing changed. |
Hard rules for the whole loop:
- Never hide findings. No adding entries to audit ignore files,
osv-scanner.toml exclusions, --exclude flags, severity threshold
changes, or deleting a dependency's usage just to silence a finding.
The only two ways a finding leaves the list are: a verified fix, or a
user-approved risk decision.
- "Not reachable" is a claim that requires evidence (format below).
It is never the default.
- New CVEs introduced by an upgrade count as findings and go back
into the queue. Verify the scan after every change, not just at the end.
- Preserve unrelated work. Never commit, revert, or stash changes
that existed before the loop started.
Phase 0 — Preconditions (fail fast)
Before touching anything, verify you can actually do the job:
- The repo has a resolvable dependency graph (lockfile or manifest
present:
package-lock.json/pnpm-lock.yaml/yarn.lock,
poetry.lock/requirements.txt, Cargo.lock, go.mod, etc.).
- An advisory-backed scanner runs successfully (see Phase 1).
- The baseline is green: build and tests pass before any change.
If they don't, you cannot attribute regressions to your fixes.
If any of these fail → terminal state BLOCKED. Report exactly what
is missing and stop. Do not scan from memory; do not guess CVEs.
Phase 1 — Baseline scan
Use the ecosystem-native tool, falling back to osv-scanner (covers
all ecosystems via OSV.dev):
| Ecosystem | Primary tool |
|---|
| npm/pnpm/yarn | npm audit --json (or pnpm audit --json) |
| Python | pip-audit (or uv pip audit) |
| Rust | cargo audit |
| Go | govulncheck ./... |
| Any / multi | osv-scanner --lockfile <path> |
Record in the baseline (this anchors the whole run — the goalposts
must not move mid-loop):
- Tool name and version, advisory source, and scan timestamp.
- Full list of high/critical findings: CVE/GHSA id, package, installed
version, direct or transitive, fixed-in version if any.
Low/medium findings are noted in the final report but are not loop
targets — chasing them is how this loop loses focus.
Phase 2 — Reachability triage
For each high/critical finding, determine whether the vulnerable code
is actually reachable in this project. This is the highest-judgment
step and also the loop's cheat vector — declaring things unreachable is
the lazy path to termination, so the evidence bar is strict.
A reachability assessment must contain all of:
- Vulnerable symbol(s): which function/class/behavior the advisory
actually concerns (read the advisory; the package name is not enough).
- Import path: where this project (or a dependency on the path to
it) imports the affected module — cite
file:line, or show the
transitive chain (A → B → vulnerable-pkg).
- Verdict with proof:
- Reachable: a concrete call path from project code to the
vulnerable symbol, or the exploit precondition shown to exist
(e.g. the vulnerable parser is fed request data).
- Not reachable: why the call path cannot exist — the vulnerable
symbol is never invoked (show the search), the feature is disabled,
the dependency is dev/build-only and never ships, or the exploit
precondition is structurally absent.
Prose like "this doesn't appear to be used" without a cited search or
call chain does not qualify and must be treated as reachable by
default. govulncheck does call-path analysis natively for Go — prefer
its verdict there.
Phase 3 — Ranking
Rank the reachable findings by, in order: severity, reachability
confidence, exposure (does it face untrusted input / production?), and
availability of a remediation (patched version exists, upgrade
distance). Work strictly from the top of this list.
Phase 4 — Remediate (one finding per iteration)
For the highest-ranked finding, apply the smallest credible change:
- Prefer, in order: patch-level bump of the affected package →
minor bump → lockfile-level override/resolution to force a fixed
transitive version → direct-dependency upgrade that pulls the fix →
replacing the dependency (ask first).
- One finding per iteration. Do not batch unrelated upgrades — when
verification fails you must know which change caused it.
- Two-attempt cap: if two remediation attempts for the same
finding fail verification, stop trying. Revert to the last green
state and route the finding to the risk-decision path (Phase 6)
with the failed attempts documented.
Phase 5 — Verify
After every change:
- Install/lockfile update completes cleanly.
- Build passes. Tests pass. (Same commands as the Phase 0 baseline.)
- Re-run the scan with the pinned baseline tool and version:
the finding is gone and no new high/critical finding appeared.
Keep the change only if all three hold; otherwise revert and count the
attempt. Then return to Phase 3 with the updated finding list.
Phase 6 — Approval gates
Stop and ask the user (do not proceed on your own) before:
- Major-version or otherwise breaking upgrades.
- Replacing or removing a dependency.
- Anything that changes production config or deployment.
- Accepting risk on any remaining finding. Present the reachability
evidence and a recommendation; the user makes the risk decision.
Final report
Every run ends with, regardless of terminal state:
- Terminal state reached (CLEAN / ASSESSED / BLOCKED).
- Inventory: every high/critical finding from the baseline and its
outcome (fixed in iteration N / risk-accepted / blocked), plus the
low/medium list for awareness.
- Reachability evidence for every finding assessed not-reachable
or risk-accepted, in the Phase 2 format.
- Fixes applied: package, old → new version, and the verification
results (build/test/scan output summary) proving each.
- Remaining risks and recommended follow-ups.
- Scanner name, version, and scan timestamps (baseline and final).
Claims without fresh command output are not proof. Show the re-scan.
Running it on a schedule
This loop is a natural recurring job — new advisories are published
daily against unchanged code. Good triggers:
- Weekly (e.g.
/loop or a cron/scheduled session invoking this skill).
- After any change to a lockfile or dependency manifest.
On a scheduled re-run, diff against the previous run's report: only new
findings (or findings whose risk decision has expired) create work.
Previously approved risk decisions stay approved unless the advisory's
severity or the project's exposure changed — re-litigate those, not
everything.