Skip to content
Fallow home
All docs pages

Security agent verification

Cut fallow security candidates down to a short list of survivors that a verifier could not dismiss. Fallow itself makes no model calls.

Use this recipe to turn raw fallow security candidates into a shorter list of survivors that you can act on. A coding agent or an external harness checks each candidate and dismisses the ones that the evidence does not support.

fallow security produces deterministic candidates only. It does not call a model, it does not decide exploitability, and it does not report verified vulnerabilities.

Inputs

Use the CLI when you want a standalone JSON file:

fallow security --format json --surface --quiet > fallow-security.json

Use MCP when an agent is already in an edit loop:

{
  "root": "/path/to/repo",
  "surface": true,
  "paths": ["src/routes/login.ts"]
}

The MCP parameters work as follows:

  • surface: true forwards --surface and adds the top-level attack_surface[] entries.
  • gate set to new returns candidates on changed lines. gate set to newly-reachable returns candidates that became reachable from entry points. newly-reachable requires changed_since.
  • paths forwards repeated --file filters. Fallow returns only candidates whose anchor, trace hops, source-trace hops, or unresolved-callee diagnostics match.

Each candidate has a severity field with a review-priority tier (high, medium, or low). The tier is not a verified vulnerability verdict. When reachability.taint_confidence is present, use it to tell arg-level source association apart from the weaker module-level reachability.

Verifier packet

For each security_findings[] item, build one packet. The packet holds the deterministic fallow evidence and the source windows that you collect:

{
  "schema_version": "fallow-security-verifier-input/v1",
  "finding_id": "9a705395d3b50465",
  "severity": "high",
  "candidate": {
    "source_kind": "http-request-input",
    "sink": {},
    "boundary": {}
  },
  "trace": [],
  "taint_flow": null,
  "taint_confidence": "arg-level",
  "reachability_trace": [],
  "attack_surface": null,
  "source_windows": [
    {
      "path": "src/routes/login.ts",
      "start_line": 12,
      "end_line": 52,
      "text": "..."
    }
  ],
  "blind_spots": {
    "unresolved_edge_files": 0,
    "unresolved_callee_sites": 0,
    "unresolved_callee_diagnostics": null
  }
}

Read the source windows from disk after the scan. Keep them out of the fallow output, so that the fallow output stays deterministic, compact, and independent of any model provider.

blind_spots.unresolved_callee_diagnostics can hold a copy of the top-level diagnostics from the fallow output. Copy it when the verifier needs sample locations to follow up on dynamic dispatch. It is bounded metadata and is not proof of a vulnerability.

Prompt contract

Ask the verifier to dismiss candidates unless the supplied evidence supports a real exploit path:

You are verifying one fallow security candidate.

Fallow is a deterministic candidate producer, not a vulnerability oracle.
Use only the supplied candidate, trace, attack-surface entry, and source windows.
Do not assume data flow beyond the provided code.

Check:
1. Is the input attacker-controlled?
2. Does the value reach the reported sink?
3. Is the reported boundary relevant to exploitability?
4. What concrete impact would remain if the candidate is real?
5. Is there an existing defensive control that dismisses it?

Return only JSON matching fallow-security-verdict/v1.

If attack_surface.defensive_boundary.verification_prompt is present, add it to the prompt as an extra question. Do not treat it as a verdict.

attack_surface.defensive_boundary.controls lists control patterns that fallow found in files on the import trace. A listed control does not prove that the control runs before the reported sink or that it protects the same input. Before you dismiss a candidate because of a control, check the execution order and the value flow in the source windows.

A control with callee: "origin-equality-guard" records a strict .origin comparison against a known string, where the mismatch branch returns or throws at once. It does not prove that the object is a built-in URL, that the checked value reaches the sink unchanged, or that redirects are safe. Verify these points in the source. Controls do not remove candidates, and they do not lower the taint confidence or the severity.

Verdict schema

Ask the verifier for a compact JSON verdict:

{
  "schema_version": "fallow-security-verdict/v1",
  "finding_id": "9a705395d3b50465",
  "verdict": "survivor",
  "reason": "The request query value reaches execSync without validation.",
  "impact": "Command injection through the id query parameter.",
  "evidence_checked": {
    "source": true,
    "sink": true,
    "boundary": true,
    "trace": true,
    "source_window": true
  },
  "dismissal_reason": null,
  "fix_direction": "avoid-shell"
}

Allowed verdicts:

  • survivor: the verifier could not dismiss the candidate from the supplied evidence.
  • dismissed: the candidate is not exploitable from the supplied evidence.
  • needs-human-review: the evidence is incomplete, contradictory, or blocked by missing context.

Store the verifier output next to the original fallow JSON, keyed by finding_id. Do not write verdict fields back into the fallow output.

Finding identity migration

A finding ID contains the rule, the project-relative path, the line, and the column. Two sinks on the same line therefore get different IDs. The JSON output and the visualization use the same ID as partialFingerprints["fallowSecurity/v2"] in the security SARIF output. When you select fewer files, the remaining findings keep their IDs.

When you upgrade from line-only IDs, every security finding gets a new ID, including findings that did not collide before. To migrate:

  1. Regenerate the candidate files and the verdicts together.
  2. Refresh the evaluation labels that you key by finding_id.
  3. Keep old candidate and verdict pairs together for replay. Do not match old verdicts to new candidates by path and line alone.

fallow security survivors exits 2 when a verdict names a finding_id that is not in the candidate file. An old verdict file with a new candidate file therefore fails.

Stored review history does not transfer automatically. SARIF tools can close and reopen each security alert once. Fallow no longer emits the legacy fallowSecurity/v1 fingerprint, because that fingerprint can merge separate sinks again.

Render survivors

To show only the candidates that the verifier kept, join the candidates and the verdict file with fallow security survivors:

fallow security --format json --quiet > candidates.json
# (verifier writes verdicts.json keyed by finding_id)
fallow security survivors --candidates candidates.json --verdicts verdicts.json

The command reports summary.unverdicted, so candidates without a review stay visible. To fail CI when a candidate has no verdict, add --require-verdict-for-each-candidate. To see where the structural trace stopped, run fallow security blind-spots. It lists the unresolved callees that the verifier must inspect by hand.

Caveats

Candidate quality depends on how accurate the source and the trace are. Fallow gates HTTP-input source patterns on the receiver, so a broad *.query pattern does not collide with unrelated APIs. Framework-specific request aliases can still need the judgment of the verifier.

reachability.taint_confidence tells arg-level source association apart from module-level source association. Use severity and taint_confidence to order the triage. Before you report a survivor, use the source windows to verify:

  • that an attacker controls the source
  • the value flow
  • the sink behavior
  • the defensive controls

See also