Imported from scadastrangelove/rust-in-peace (
.claude/skills/variant-scan/SKILL.md). Install upstream withnpx skills add scadastrangelove/rust-in-peace --skill variant-scan. Copyright stays with the author.
/variant-scan
Three seed-diverse find passes over one target, unioned, then adversarially verified. This is the workhorse that ran every real-OSS campaign (x509-parser, lopdf, zune-jpeg, object, gimli, httparse, quick-xml, miniz_oxide, ciborium, png/image, rmp-serde, ttf-parser, gitoxide). Until now it lived only as a scratchpad workflow script; this skill is its permanent home.
Why three passes and not one. They have different strengths and
converge only on the "big" bug — everything else, each finds alone
(LESSONS.md L21, L25; the design note is docs/variant-analysis.md):
- blind — generic "find security bugs", no class hint. Catches whole bug-classes a threat model didn't anticipate.
- threat-model-first — lenses seeded from
THREAT_MODEL.md§3 (entry points, trust boundaries, the declared defect-origin class). Reaches depth and differential surface a blind pass skips. - CVE/history-seeded — variant analysis ("bugs travel in packs"): for each historical advisory/CVE/RUSTSEC on this crate or a sibling, extract the pattern (not the specific bug) and hunt the same pattern in code paths the original fix didn't cover. Seed from confirmed and refuted prior findings too — control coverage is non-uniform. This is where net-new siblings of already-patched bugs come from.
Run all three by default and union. Dropping one to "save budget" is the one optimization L25 explicitly warns against.
There is a fourth, separate mode — /sast-driven (tool-first: every static
analyser's default rules, clustered into cells and reachability-judged before a
finder reads code). It is deliberately not folded into these three passes:
injecting its hits here would destroy the measurement of what the blind pass
finds on its own. Run it alongside and compare the sets.
Arguments
<target-dir>(required) — source tree to scan.--passes blind,tm,cve— which seed sources to run (default: all three).--seed-cves <file>— a list of advisory IDs / patterns to seed the CVE pass (else derive fromTHREAT_MODEL.md,capabilities.json, and a quick RustSec/GitHub-advisory lookup for the crate + siblings).--focus <area>— restrict all passes to one subsystem (repeatable).
How it runs
Each pass is one invocation of the reference orchestration
find_engine.mjs (for the Workflow tool), with that
pass's lenses:
- Find — N lens-agents in parallel, each emitting
FIND_SCHEMAfindings (bug_class, file, line, symbol, mechanism, reachability_from_entry, poc_sketch, severity, confidence). Corpus isolation (L38 — load-bearing). Each finder works ONLY from the target source + its own lens brief. A finder must NOT read another lens's output, a siblingfindings-*.json, or any on-disk answer key (a plan / PoC / THREAT_MODEL solution) — cross-reading destroys vote independence and invites an over-claim (an earlier run's cve-sonnet glimpsed a siblingcve-opus.json). Give each finder its OWN output path it cannot list siblings from (per-agent dir, not a sharedfindings/), and instruct it: "never open another findings-*.json." Votes are only meaningful when the finders were blind to each other. - Union-of-N dedup — collapse by
bug_class-prefix @ file:symbol, counting votes across lenses. - Verify — per candidate, 3 skeptic lenses (correctness / reachability /
impact), each told to refute and to cite a concrete
where_checked(file:lineof the guard, or of the unguarded path). Disposition:confirmed(≥2 real&&reachable),contested(1),refuted(0 but voted),unverified(no verifier returned — e.g. an infra failure).
Then union the three passes the same way (dedup by the same key across passes; a candidate found by two seed sources is the strongest signal it's real, not a seed artifact).
Without the Workflow tool, run the same shape with plain Task subagents: N
finders → dedup in-message → 3 verifiers per candidate → same disposition rule.
The discipline gate — dispositions are TRIAGE, not verdicts
This is the point of the skill, not a footnote. A confirmed from the vote
panel means "worth your time", never "it's real". Every campaign that skipped
this gate shipped a false positive:
- gitoxide tar-slip — 3/3 unanimous "confirmed HIGH" (and 2/3 on a second
pass). Refuted only by building an independent PoC and running it: the
tarcrate's own..-guard andrawzip's path-normalization, plus anindex_from_treegate upstream, all sat on the path the verifiers never traced. (JOURNAL, gitoxide Stage 3.) - x509 RSA over-claim — "confirmed" by both a finder and a curator layer;
refuted by reading that
asn1-rsrejects the input and the sink re-parses (LESSONS.mdL1/L8, the two-layer over-claim). - gitoxide NTFS
git~1/protect_hfsasymmetries — looked novel; a faithful port of real git's own code once compared against upstreampath.c.
So before any survivor is called real:
- Read the actual verifier text, not the vote count.
find_engine.mjscarriesverifier_reasonsfor exactly this — one skeptic lens is often right and outvoted 2-to-1. - Build an independent PoC (or read the gating call site end-to-end) that exercises the finding through the real entry point on crafted/untrusted input. Construction-via-builder-API is not parse-reachable (L12).
- Verify against the actual shipping target — release AND current default branch, plus the maintainer's own tests/docs — before treating it as reportable (L15/L32).
- Treat
unverified-from-infra-failure as no signal, not tacit refutation — re-run verify or hand-check the highest-severity ones.
Only what survives 1–3 goes downstream to /triage → grade/reattack
(execution-verified) → predisclose.
Output
Per pass and for the union: confirmed / contested / refuted / unverified
lists, each candidate carrying lenses, votes, real_votes, where_checked,
and verifier_reasons. Write VARIANT-FINDINGS.json (+ .md) in the same
shape /triage ingests, with a per-candidate disposition and an explicit
independently_verified: false until step 2 above is done by hand.
Relationship to the rest of the pipeline
/variant-scan is the recall front-end (static, read-only, multi-pass). It
feeds /triage; execution-verification still happens in vuln-pipeline
(grade, the find→fuzz reattack bridge, run_crash_track). It does not
replace /vuln-scan — use /vuln-scan for a quick single-pass focus-area
review, /variant-scan when you want maximum recall and the target has history
worth seeding from.