Skip to content

Finding Triage

The agentic scanner applies triage between finding generation and its verifier. This is not one mandatory eleven-step chain shared by every command: gates depend on feature settings, source availability, category, runtime and routing. Some make network requests or model calls. A gate’s accepted label is not a universal reproduction or disclosure verdict.

2026-04-11 ablation results. The stack strictly beats the no-triage baseline on XBOW black-box, is a Pareto tradeoff on white-box (2 flags at limit=50 for 63% fewer findings), and is a no-op on npm-bench. Layer 11 (EGATS) is the one broken layer and is opt-in only (0#116). Numbers: FP Reduction Moat; narrative: 2026-04-11 ablation.

The numbered sections below are a capability inventory, not execution order. Modules live under packages/core/src/triage/; the agentic-scan wiring is in packages/core/src/agentic-scanner.ts. EGATS is discovery orchestration, not a final triage stage. The feature extractor is telemetry/input to gates, not a proof step.

Suppression is guarded. Heuristic rejection paths consult isDisclosureWorthy: protected high-impact/high-severity findings can be held for further verification instead of dropped. That protection is not confirmation. An error, unknown result or missing execution record must not be read as proof that the system is safe.

triage/holding-it-wrong.ts detects documented sink behavior mistaken for a vulnerability, such as treating a file-writing API as arbitrary file write. Enforcement defaults on but ZERO_FEATURE_HOLDING_IT_WRONG=0 disables it. Suppressible matches become info / false-positive and skip later verification; protected findings continue with an explanatory note. Feature extraction still runs for telemetry even when enforcement is off.

triage/feature-extractor.ts builds a 45-element numeric vector from response, payload, evidence and category signals. The agentic scanner records it as triage_features; the default-on evidence gate can suppress findings with evidence_completeness <= 0.5, subject to the same protection against heuristic auto-suppression. Disable enforcement with ZERO_FEATURE_EVIDENCE_GATE=0. The ~77% recall / 16% FPR figure is a historical feature-extractor measurement, not current-target accuracy. See Feature Extractor and Triage Dataset.

triage/oracles.ts dispatches category-specific checks by default on this path, unless routing excludes them. A successful inline check can be reused rather than rerun. Unsupported categories fall through; missing tools, auth or reachable callback infrastructure can prevent a useful result.

CategoryOracleProof
SQLiverifySqliAt least two of boolean response-length difference, timing delta and SQL error signatures
Reflected XSSverifyReflectedXssPlaywright captures the unique token in a dialog; HTML reflection alone is not confirmation
SSRFverifySsrfNonce-matched request to a temporary local collector; the target must be able to reach it
RCEverifyRceProbe command output observed in the response
Path traversalverifyPathTraversalLinux /etc/passwd signature from traversal probes; no Windows equivalent in this oracle
IDOR-like information disclosureverifyIdorNumeric-ID mutation returns distinct nonempty 200 responses; does not establish ownership across identities

Dispatch uses verifyOracleByCategory(finding, target). Its verified bit is category-specific: IDOR’s response-difference heuristic is weaker than a browser execution or callback capture. The scanner may stamp accepted/confidence state; inspect the oracle evidence rather than treating that stamp as uniform proof. Known-category non-confirmation can downgrade severity to low; thrown errors are recorded and do not abort the scan. Test authorization boundaries with known identities before disclosing IDOR.

triage/reachability.tsZERO_FEATURE_REACHABILITY_GATE=1. With source available, a conservative pattern pass inspects paths, entry points and imports. High-confidence unreachable findings can be suppressed subject to the disclosure guard. This is not an exhaustive interprocedural proof of reachability.

Today it’s a zero-dependency grep/pattern pass and deliberately conservative: when it can’t make a confident call it returns reachable: true with low confidence so later stages still run. A tree-sitter interprocedural upgrade is planned.

triage/multi-modal.tsZERO_FEATURE_MULTIMODAL=1. When both source and the foxguard binary are present, 0 runs foxguard on the same code and cross-checks each finding against its SARIF:

  • Both fire → prioritize verification; only sufficiently strong agreement and evidence completeness take the fused auto-accept branch.
  • Only 0 fires → not refutation by itself. Low agreement confidence and incomplete evidence can trigger guarded suppression.
  • Missing tool, scan failure or uncovered file → no independent corroboration.

Even a fused auto-accept label is a triage decision, not a fresh exploit replay.

Terminal window
env ZERO_FEATURE_MULTIMODAL=1 \
0 scan --target https://example.com --scope ./scope.json --repo ./source

triage/pov-gate.tsZERO_FEATURE_POV_GATE=1. The agentic path requires a usable runtime, a finding not already accepted, and routing permission. It uses category-specific oracles (reusing an upstream result when available) or a bounded PoC-generation path.

hasPov: true attaches evidence and boosts confidence. A conclusive negative can downgrade to info; inconclusive results such as unavailable browser/OAST infrastructure are annotated without treating the missing proof as a false positive. Generation of a script is not itself proof that it executed.

triage/structured-verify.ts assesses four questions:

  1. Reachability.
  2. Payload validity.
  3. Impact.
  4. Exploit confirmation.

Despite the final step’s name, each step is a model call with no tools. All steps must return a passing JSON verdict; failure or malformed output short-circuits to rejected. This is evidence assessment, not independent runtime reproduction. The agentic scanner does not run a standalone four-step pass by default; it invokes this module for optional consensus before the tool-using verifier.

ZERO_FEATURE_CONSENSUS_VERIFY=1. The agentic scanner calls verify with three parallel structured passes per candidate. The SDK defaults to a single pass unless votes is supplied. Early resolution can return a majority before all calls settle; it does not guarantee cancellation of their model costs. No per-run seed is set by this implementation. Rejected votes are subject to the disclosure guard; errors fall through to agentic verification.

triage/memories.ts stores false-positive context from human triage. Use 0 triage mark-fp and 0 triage memory to manage feedback. ZERO_FEATURE_TRIAGE_MEMORIES is not a current feature toggle. Memory context can inform verification; it is not independent reproduction evidence.

Scope matching is exact: global, inferred package identity, or target URL/path, within the finding’s category. Default ranking uses token overlap. Opt-in Jev memory assistance reranks up to twelve shortlisted memories and falls back to token ranking when unavailable; it never auto-rejects a finding.

On the native agentic-scan verification path, createScanMemoryStore is wired when ZERO_TRIAGE_FEEDBACK or Jev memory configuration is present. Prepared feedback is scan-local context, not imported into the global memory database. A historical memory in another database is not automatically available to every new run. See advisory evaluation settings.

Terminal window
# Mark a finding FP and remember why
0 triage mark-fp <finding-id> --reason "test fixture, not prod"
# Add a standalone memory
0 triage memory add --finding <id> --reason "sink is harmless helper" \
--scope package --scope-value my-pkg
# List memories
0 triage memory list --scope target

Planned — not implemented. There is no triage/adversarial.ts module and no ZERO_FEATURE_DEBATE flag in the engine. The intent: a prosecutor (finding is real) and a defender (it’s an FP) argue from fresh contexts, and a skeptical judge picks the winner — each seeing only the other’s written arguments, never the research agent’s chain of thought. The design follows the open-source read of Anthropic’s debate paper (arXiv:2402.06782); the point is to keep the two agents’ errors independent.

Its goal is partly served by the hunt cross-family refuter (stages/hunt-cross-family.ts). Its model-family selection is specific to that workflow and available model routes; it is not a guarantee that every scan’s finder and verifier use different model families.

Section titled “11. EGATS — Evidence-Gated Attack Tree Search”

scan --egats opts into beam-search discovery on the native agentic path. It expands an explicit hypothesis tree and uses observed evidence to score branches. It is not a downstream verification stage or part of fp-moat. The historical ablation found a regression on its hard-challenge slice (0#116); it is not a universal performance recommendation.

Env varDefaultStage
ZERO_FEATURE_HOLDING_IT_WRONGon1
ZERO_FEATURE_EVIDENCE_GATEon2
ZERO_FEATURE_REACHABILITY_GATEoff4
ZERO_FEATURE_MULTIMODALoff5
ZERO_FEATURE_POV_GATEoff6
ZERO_FEATURE_PUBLISHABILITY_GATEoff6
ZERO_FEATURE_POC_GEN_STATICoff6
ZERO_FEATURE_CONSENSUS_VERIFYoff8
ZERO_FEATURE_LEARNED_ROUTERoffrouter
ZERO_FEATURE_DYNAMIC_TRIAGEoffrouter

ZERO_FEATURE_TRIAGE_MEMORIES, ZERO_FEATURE_DEBATE, and ZERO_FEATURE_EGATS are not current toggles. EGATS is selected by --egats / config.egats, not an environment flag. See Configuration for feature settings and Features for the separate Jev controls.

fp-moat enables the six gates listed below, not every optional feature. Historical results vary by slice: improved XBOW black-box results, a 0–2 flag cost on white-box, and no change on npm-bench. The reported ~60% reduction in findings accompanied a roughly flat correct-flag count. Re-measure on your target before choosing the preset; enabled gates can still skip missing prerequisites.

Terminal window
0 scan --features fp-moat --target https://example.com --scope ./scope.json
# or, for templated CI:
env ZERO_FEATURE_PRESET=fp-moat 0 scan --target https://example.com --scope ./scope.json

It expands to REACHABILITY_GATE, MULTIMODAL, PUBLISHABILITY_GATE, POV_GATE, POC_GEN_STATIC, and CONSENSUS_VERIFY. Membership lives in packages/core/src/agent/feature-presets.ts and is pinned by test.

A flag you set yourself always wins, so you can ablate one layer:

Terminal window
env ZERO_FEATURE_POV_GATE=0 0 scan --features fp-moat

The preset deliberately omits LEARNED_ROUTER and DYNAMIC_TRIAGE — those decide which layers to skip per finding, so enabling them alongside the moat would suppress the layers you’re trying to measure.

Each layer records a verdict on the finding as it runs. findings show renders it:

Terminal window
0 findings show <id>
Triage provenance:
FP moat NOT engaged: no opt-in moat layer ran for this finding (always-on filters only)
Layers: 3 executed, 5 skipped, 3 unrecorded | 412ms | $0.0000
+ holding_it_wrong executed(pass) — no holding-it-wrong pattern matched
+ evidence_gate executed(pass) — evidence_completeness=0.83 > 0.5
- reachability skipped(skip) — ZERO_FEATURE_REACHABILITY_GATE=0
  • Verdicts stored on each finding determine the displayed provenance. Changing shell flags leaves historical results unchanged.
  • skippedunrecorded. skipped means the layer recorded that it stood down (with the flag or missing precondition named); unrecorded means no verdict exists at all.
  • structured_verify, consensus, and kernel_oracle are currently listed in UNINSTRUMENTED_LAYERS: they do not emit LayerVerdict records. Separate events (such as consensus_verify) may exist, but this provenance summary cannot infer execution from them. They are not silently counted as skipped.

Optional semantic dedupe runs in report post-processing and retains original evidence with additive canonical/cluster mappings. Jev dedupe enables a bounded fast path for exact-location/category pairs only: same location, defect and fix must each score at least 0.98, and insufficient evidence at most 0.02. Competing anchors are ambiguous. Remaining pairs use the existing generative dedupe path. Neither grouping nor incremental ranking is a vulnerability verification or an ecosystem novelty receipt.