Skip to content

find-vulns

find-vulns

findreadhands-off

Use this when: you want a security sweep of existing code

Problem it solves — A diff audit never sees the vulnerability already sitting in main. This hunts existing code boundary-by-boundary against the threat model, dedupes findings to root cause, and verifies each through the blind reviewer before it claims anything.

Find vulnerabilities

Divergent discovery: find ways an attacker reaches the assets in existing code that no open issue currently tracks. Not a diff audit — a hunting expedition framed by the threat model.

Before hunting — load the threat model and the lens

Read docs/security/threat-model.md in full. Extract:

  • The assets (crown jewels: encrypted credentials, Anthropic key, session, task-store integrity).
  • The trust boundaries B1–B8 and their attacker-controlled inputs.
  • The won't-flag corpus — consult it before every finding; a corpus match is excluded, not a finding.
  • The consciously accepted risks — tracked deferrals, not gaps.

This is the denominator. Every finding is relative to it. Judge every finding by the shared protocol in docs/security/review-lens.md — evidence before severity, the exclusion gate, the severity tiers.

Scope

  • Default (working-set): recently-changed files (git diff --name-only main) plus any area the user names ("find vulns in app/Http").
  • --full: the whole codebase. Use deliberately — a full sweep costs more and is meant for pre-go-live or epic-boundary cadence reviews.

Declare the scope at the top of the report.

The hunt — boundary-by-boundary

This is the skill. The goal: find paths where an attacker controlling an input at a trust boundary reaches an asset. Work through each in-scope boundary from the threat model and ask: given what this attacker controls at B-n, what could go wrong in the code that handles it?

Frame as goal + context, not a checklist. The boundary list is the context; the attacker's goal (reach an asset) is the filter. Vuln classes below are illustrative only — anything that connects attacker-controlled input to an asset counts:

  • B1 (webhook): HMAC check bypassed, fail-open on error, replay via non-idempotent handler
  • B2 (credentials): $hidden absent, encrypted attr serialising into a response/log/push
  • B3 (LLM I/O): external content interpolated without @untrusted fence, LLM output trusted for control flow
  • B4 (MCP tools): write tool reachable without gating, tool args from LLM output not validated
  • B5 (v-html): unfiltered markdown/task text piped through v-html or {!! !!} without DOMPurify
  • B6 (auth/session): route outside the auth/verified group, state-changing action with no server-side check
  • B7 (Google): OAuth state/redirect URI, Google-sourced string reaching a prompt unfenced
  • B8 (supply chain): committed secret, known-CVE dependency — the deterministic CI gate's domain; confirm the gate is green rather than re-derive its checks

Orchestration (opt-in, --full only)

A --full sweep's boundaries are disjoint, so it may fan out per docs/agents/orchestration.md: one cheap-model read-only scout per boundary (its attacker-controlled inputs + the assets + the won't-flag corpus as the brief), candidate lists merged per docs/agents/deterministic-merge.md. Root-cause grouping, severity and the blind-reviewer verify stay in-thread — a scout's candidate is a lead, never a finding. Working-set scope stays in-thread; a handful of files doesn't earn the overhead.

Dedupe by root cause

Ten unfenced interpolations of the same untrusted origin = ONE finding with ten locations, not ten findings. Group by the root-cause gap, not by call-site. This prevents a single systemic gap from monopolising severity budget and misleading triage.

Verify via blind-reviewer

Each candidate finding goes to blind-reviewer in the stricter independence mode docs/security/review-lens.md describes (location + category + code only, your reasoning withheld). Consume its verdicts:

  • PASS — confirmed; include in the findings table.
  • FAIL — false positive; drop, but note the count in the report ("N candidates dropped after blind review").
  • CANNOT_VERIFY — its own bucket, surfaced to the human, never dropped.

Contract

ItemValue
Task classdivergent discovery (find-)
ToolsRead, Grep, Glob, Bash, codegraph, blind-reviewer agent
Direct vs forkforks — dispatches blind-reviewer for each candidate finding; --full may fan out per-boundary scouts (above)
Verifiable artifactA verified-findings report (see Output)

Output

Verified-findings table (one row per root cause):

Root causeBoundaryLocationsReachability / attacker-control / blast-radiusSeverityVerifier verdictSuggested fix

CANNOT_VERIFY bucket (findings blind-reviewer couldn't confirm — surfaced, never dropped):

List each with attempted verification + structural reason it couldn't be tested.

Summary line: N confirmed · M CANNOT_VERIFY · P excluded by corpus · Q dropped (false positive)

Pure-read: this skill reports and proposes. Filing issues or applying fixes happens only after the user confirms (surface-then-confirm; the REPORT itself carries no confirm tax — it's what you see, free of charge).

Severity tiers and the evidence-before-severity discipline: docs/security/review-lens.md.

When it MUST run

Mandatorily before the DigitalOcean go-live (deploy epic #116 / #66) — the threat model's review_cadence requires it. Also at each epic boundary as part of that cadence. Default scope at go-live = --full.

How this gets triggered

Description-driven auto-invocation covers the manual trigger. No hook registration needed — this is a deliberate sweep, not a boundary event.