Why another SAST tool
Most tools land on one of two extremes. Anvesha is a deliberate bet on a third path.
Pure pattern-matching
Fast and cheap, but blind to context — huge false-positive volumes, and structurally unable to catch bugs with no syntactic signature at all (a missing ownership check is valid code; there's nothing to match against).
Hand everything to an LLM
Genuinely context-aware, but slow, expensive, and unverifiable at scale — every finding is only as trustworthy as one model's say-so.
Anvesha's bet
Do everything deterministic and non-hallucinating with real static analysis. Reserve AI reasoning for what's genuinely contextual — ambiguous semantic judgments, and adversarial verification of an already-narrow set of candidates.
No proof, no finding
Enforced in code, not just policy — the Finding model
raises on construction if it has zero citations. Every result is a
graph path with exact line evidence.
How it works
Everything is either building the Weave (the graph), walking it, or diffing it against a prior version — not separate subsystems per feature.
Graph-Build deterministic+ AI for the ambiguous cases
tree-sitter parses the codebase into a real syntax tree; an
original per-language walker turns it into the Weave — nodes are
functions, variables, call sites, parameters; edges are calls and
data movement. A small deterministic catalog tags obvious
source/sink/sanitizer patterns by name (request.args →
source, cursor.execute → sink, escape/
sanitize → sanitizer). Anything the catalog can't
classify — but that's reachable from a detected entry point —
gets one focused question sent to an LLM.
Path-Reasoning deterministic+ AI for unrecognized sanitizers
A graph walk finds candidate paths from a tainted source to a sensitive sink, checking deterministically for known sanitizers along the way — including distinguishing a parameterized query from a string-built one via argument position, not just presence of a call. An unrecognized intermediate call gets one question sent to an LLM: does this actually neutralize the value for this sink?
Adversarial Consensus Verification AI, deliberately
Every surviving candidate is independently reviewed by two personas working from the same evidence — a prosecutor who must build a concrete exploit argument, and a defender who must actively hunt for a reason it isn't exploitable. Only agreement confirms a finding. Disagreement is kept, not discarded, so you can read why reviewers split.
CLI reference
Everything runs through one command today.
uv sync
uv run anvesha survey path/to/project --format markdown
| Flag | Default | What it does |
|---|---|---|
--format | json | Output as json or markdown. |
--out <path> | stdout | Write the report to a file instead of stdout. |
--fail-on <level> | none | Exit 1 on a qualifying finding. When AI verification ran, only confirmed findings qualify; otherwise any finding does. |
--ai / --no-ai | --ai | Enable/disable all LLM-assisted phases. Auto-falls back to --no-ai behavior if claude isn't on PATH. |
--llm-model <name> | CLI default | Override the model used for LLM-assisted phases. |
--llm-budget <n> | 50 | Max LLM calls for Phase 1 labeling and Phase 3 resolution, each. |
--consensus-reviewers | 2 | 2 (unanimous required) or 3 (majority, adjudicator tiebreaks). |
--concurrency <n> | 4 | Max concurrent Phase 4 verification calls. |
The default LLM backend is your own authenticated claude
CLI session — no separate API key needed. It's invoked with
--restricted (no tool access), the prompt piped over
stdin, and --json-schema for schema-validated structured
output — see Architecture.
Architecture
One graph model, a pluggable adapter per language, and a narrow LLM-provider interface used only where reasoning actually needs it.
anvesha/ ├─ weave/ # the graph: model, diff, scoping, (de)serialize, on-disk store ├─ adapters/ # pluggable per-language parsing — one adapter today: Python/tree-sitter ├─ tracing/ # Phase 3: the deterministic source→sink path walker + sanitizer check ├─ reasoning/ # the LLM layer — provider abstraction + Phases 1, 3-ambiguous, 4 │ ├─ provider.py # LLMProvider interface — one method: ask_structured(...) │ ├─ providers/ # claude_cli.py (default) + offline.py (--no-ai fallback) │ ├─ phase1_labeler.py │ ├─ phase3_resolver.py │ └─ phase4_verifier.py ├─ findings/ # the Finding model — citations are non-negotiable ├─ report/ # JSON + Markdown output └─ cli.py # `anvesha survey ...`
Adding a language is meant to be "write another adapter," not
"redesign the core" — a LanguageAdapter only needs to
produce Node/Edge objects; nothing downstream
(the walker, the reasoning phases, reporting) is language-specific.
Adding an LLM backend is the same shape: a new
LLMProvider implementation, nothing upstream changes.
Status & roadmap
Early and under active development.
Built
- Graph-Build, Path-Reasoning, and Adversarial Consensus Verification — the full loop
- Python adapter (tree-sitter based)
claude-CLI LLM provider + offline fallback- JSON & Markdown reporting
Not yet built
- Risk-lens overlays (business-impact, data-sensitivity, adversary-technique, agentic-risk, org-policy)
- Business-logic / access-control reasoning pass
- Environment-inference-based prioritization
- Retest & PR-review CLI commands
- SARIF export, deterministic auto-fix
- Additional language adapters