Hybrid SAST + LLM toolkit for local code-security audits.
762 OpenGrep rules across 11 languages (Python, JavaScript/TypeScript, Java, Kotlin, Swift, Go, Ruby, PHP, C#, Dart, C/C++). Covers OWASP Web Top 10 2025 A01--A09 (A10, mishandling of exceptional conditions, has no rules yet), OWASP LLM Top 10, mobile (Android + iOS + Flutter + React Native), IaC (Terraform, Dockerfile, Kubernetes), cloud SDK misuse (AWS/GCP/Azure), SCA, secrets, taint analysis, and native C/C++ binary analysis. 5 WSTG checklists.
Stage 0b auto-generates a STRIDE threat model and asset map (entry points, auth boundaries, data stores, external calls, sensitive data) that enriches every downstream stage. A BM25 linker connects findings to their most-relevant entry point when call-graph tracing falls short. An LLM reasons on the slices the scanner flags. Runs as a Claude Code slash command, or as a standalone CLI against any provider. Local only.
- Python 3.10+
- OpenGrep
- MCP mode: Claude Code
- Standalone mode: an LLM provider (Anthropic, Google, OpenAI, OpenRouter, Ollama, ...)
From PyPI (recommended):
pipx install mantis-sast # the CLI binary is `mantis`
pipx install opengrep
mantis --versionFrom source (for hacking on rules/agents):
git clone https://github.com/TyrusRC/automated-code-examination-mcp.git mantis
cd mantis
./setup.sh # installs opengrep + mantis via pipx
./doctor.sh # verifies the installFor MCP mode (Claude Code), copy agents and the slash command into a target project:
./install.sh /path/to/target/projectThe installer also ships inside the wheel. With a pipx/pip install, mantis doctor
prints resources at <dir>; run the installer from there:
bash <dir>/install.sh /path/to/target/projectIt links rules, scripts, and checklists under the target's .claude/sast-*, and adds a
small allowlist (the exact opengrep scan, .claude/sast-scripts/*.py, and read-only
git commands the pipeline runs) to the target's untracked .claude/settings.local.json,
so unattended /audit runs don't stall on permission prompts. Existing entries are kept.
mantis checks PyPI once per 24h on startup and prints a one-line nudge
if a newer release exists. To check explicitly or upgrade in place:
mantis update --check # query PyPI, bypass the 24h cache
mantis update # upgrade via the same installer used (pipx or pip)Disable the passive check by setting MANTIS_NO_UPDATE_CHECK=1. Editable
installs are skipped (you maintain them via git).
/audit [path] [mode] [--fix] [--lite] [--experimental-toast] [focus:<area>]
| Invocation | Behavior |
|---|---|
/audit |
auto-detect stack, full pipeline |
/audit quick |
ERROR severity, HIGH confidence, triage only |
/audit deep |
every rule, full pipeline |
/audit bugbounty |
exploit-first, gates on entrypoint reachability |
/audit cve |
SCA / lockfile dependency scan only |
/audit mobile | mobile-ios | mobile-android | web | desktop | llm | taint |
scope to a single rule pack |
/audit secrets | iac | cloud |
hardcoded creds / Terraform+Dockerfile+K8s / AWS-GCP-Azure SDK misuse |
/audit deep focus:auth |
full pipeline, deep review only on auth-tagged findings |
/audit --fix |
apply patches in a worktree, re-verify |
/audit --lite |
skip slicing and deep review |
/audit bugbounty --experimental-toast |
also run the ToAST hunter on confirmed slices |
focus:<area> accepts: auth, crypto, injection, storage, network,
webview, secrets, privacy, ipc, business-logic, prompt-injection.
mantis init [path] # scaffold .mantis.yaml + .env.example
mantis doctor [path] # probe install, scanner, agents, keys, config
mantis audit [path] [mode] [focus:<area>] [options]
mantis history [path] # list past runs
mantis show [ref] [--path .] # print a past report (ref: id, prefix, latest, -N)
mantis diff [a] [b] [--path .] # unified diff between two runs
mantis update [--check] # query PyPI, upgrade in place
audit options worth knowing:
| Flag | Effect |
|---|---|
--since <ref> |
scan only files changed vs. a git ref (main, HEAD~1, uncommitted, staged) |
--format md|json|sarif|all |
write structured output alongside the markdown report |
--no-cache |
bypass the per-file SAST cache and the LLM response cache (~/.cache/mantis/llm) |
--skip-llm |
SAST + inventory only; no provider needed |
--lite |
skip slicing + deep review |
--fix |
apply patches in a worktree, re-verify |
--fail-on low|medium|high|critical |
exit non-zero if any unsuppressed finding is at or above this severity (pre-commit / local gate) |
--engines <list|all|none> |
SAST engines run alongside OpenGrep by default (installed engines matching the tree, incl. trivy/grype): bandit, gosec, njsscan, eslint-security, checkov, trivy, grype. Pass a list to restrict, all for every installed engine, or none to disable. |
--engines-offline |
run the network SCA engines (trivy/grype) in offline-DB mode. trivy/grype fetch a vulnerability database (network); every other engine is fully local. |
--pack <name> |
run exactly this pack (repeatable), bypassing mode + inventory |
--decompiled |
detect the stack from source extensions alone (no build files) |
audit mode is positional and matches the MCP slash form:
mantis audit quick, mantis audit deep focus:auth, mantis audit . web --since main.
Inventory keys off build files (build.gradle, lockfiles, AndroidManifest.xml).
A decompiled tree — jadx Java/Kotlin, Ghidra pseudo-C, e.g. what
chimera emits when it unpacks a binary —
has source but none of those, so plain inventory would fall back to the fast
pack. Two knobs fix that:
# Best-effort: detect the stack from source extensions alone
mantis audit ./decompiled_out --decompiled --skip-llm
# Precise: you already know the platform, so name the packs
mantis audit ./jadx_sources --pack mobile-android --pack secrets --skip-llm
mantis audit ./ghidra_pseudo_c --pack secrets --skip-llmProgrammatically (SAST-only, no provider needed) — the seam a tool like chimera calls after decompiling a binary to a source tree:
from mantis import audit
findings = audit("jadx_sources", packs=["mobile-android", "secrets"])
findings = audit("decompiled_out", decompiled=True) # auto stack detection
# each finding: {rule_id, severity, confidence, path, start_line, end_line,
# message, metadata (cwe/owasp/masvs...), verdict}The native pack (23 rules) covers decompiled C/C++ from Ghidra, r2ghidra, or IDA Hex-Rays — the chimera RE→SAST bridge:
- decompiled-c-vulns (16 rules): buffer overflows (
gets,strcpy,sprintf,scanf), format string (printf(var)), command injection (system(var),popen), use-after-free, hardcoded credentials, insecure random (rand/srand), integer overflow, TOCTOU race conditions - malware-patterns (7 rules): process injection (
WriteProcessMemory+CreateRemoteThread), shellcode execution via callback (EnumSystemLocalesA,GrayStringA), dynamic API resolution (PEB walk +GetProcAddresshash), anti-debug (IsDebuggerPresent,CheckRemoteDebuggerPresent), registry persistence (RegSetValueExon Run keys), keylogging (GetAsyncKeyState/SetWindowsHookEx), suspicious network (InternetOpenA+HttpSendRequestchains) — all tagged with MITRE ATT&CK technique IDs
# CLI
mantis audit ./ghidra_output --pack native --skip-llm
mantis audit ./r2_decompiled --pack native --language c --skip-llm
# Programmatic (chimera integration)
from mantis import audit
findings = audit("decompiled_output", packs=["native"], language="c")
findings = audit("decompiled_output", packs=["native", "secrets"], language="c", timeout=120)The language parameter tells mantis the source language directly (skipping
extension-based detection), and timeout caps the OpenGrep run — both are
useful for chimera's automated decompile→audit pipeline.
Reports land under <target>/.mantis/runs/<ts>-<sha>.md with stable pointers at
<target>/.mantis/latest.md and <target>/security-audit-report.md. With --fix,
patches are applied in ../<repo>.audit-fix-<short>/; the working tree is never modified.
A patch is kept only if the original rule fires fewer times on the patched file and
the full pack reports nothing new on the changed files; otherwise it is reverted.
Drop a .mantisignore at the target root to silence known-but-accepted findings.
Suppressed findings never reach the LLM. The short rule id is enough; the matcher
also accepts the fully-qualified scanner id or a fnmatch glob.
- rule_id: iac-k8s-host-network
path: "k8s/cni/**"
reason: hostNetwork required for our CNI in this cluster
- rule_id: iac-*
path: "tests/fixtures/**"
reason: deliberately vulnerable test fixtures
- rule_id: secret-generic-password-assignment
path: "docs/examples/**"Same-function findings (12 SQLi calls in one helper) are folded into one
representative before triage so the deep-reviewer doesn't get 12 near-identical
calls; the report shows (+N similar in same function) on the rep.
.mantis.yaml in the target project (see .mantis.example.yaml):
provider: google # anthropic | google | openai | openrouter | ollama
models:
fast: gemini-2.5-flash-lite
mid: gemini-2.5-flash
deep: gemini-2.5-pro
budget:
max_findings: 200
max_deep_calls: 50
sast_bin: opengrep # the only supported scanner
triage:
mode: single # single | dualEnvironment overrides: MANTIS_PROVIDER, MANTIS_MODEL_FAST,
MANTIS_MODEL_MID, MANTIS_MODEL_DEEP, MANTIS_API_BASE,
MANTIS_SAST_BIN, MANTIS_CONFIG, MANTIS_TRIAGE_MODE.
API keys come from the provider's standard env var:
ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENAI_API_KEY,
OPENROUTER_API_KEY. Ollama needs no key; set api_base: http://localhost:11434.
Model names are read verbatim from this file. None are hardcoded in the toolkit.
0 inventory detect stack, lockfiles, entrypoints
0b asset map regex-scan for entry points, auth middleware, data stores,
external calls, sensitive data across all supported languages;
feeds triage context, reachability, slice annotations, and the
auto-generated STRIDE threat model in the report. When
tree-sitter is installed, also builds a context graph with
typed call edges (EXTRACTED/INFERRED) for precise multi-language
reachability and hop-count-to-entry-point in triage context
1 static wide opengrep with the selected pack and --taint-intrafile
(cross-function flows within a file; SCA lockfile and secrets
rules are packs here), plus the extra engines
(bandit/gosec/njsscan/eslint-security/checkov/trivy/grype)
when installed; opt out with --engines none
5 triage fast tier per finding (single or dual chain) — receives
asset context (nearest entry point, auth status, nearby stores);
an unparseable or failed verdict is sliced like NEEDS-DEEP and
marks the run incomplete, never silently passes --fail-on
5b haruspex optional advisory TP-likelihood ranking (MANTIS_HARUSPEX_URL)
6 slice depth-limited callgraph, entrypoint reachability (asset map
extends reachability to non-Python languages); BM25 fallback
links findings to entry points when grep-based callers miss
7 deep review deep tier on TRUE / NEEDS-DEEP slices whose reachability is
yes or unknown (yes only in bugbounty), highest severity first
under the budget — slice text includes ASSET_CONTEXT block
7b toast experimental 3-sample hunt for rule-less bugs (bugbounty,
--experimental-toast)
8 fix patch in worktree; rule hit count must drop and the pack
must report nothing new on changed files (only with --fix)
9 report write <target>/.mantis/runs/<ts>-<sha>.md (+ latest.md symlink);
includes auto-generated STRIDE threat model section
Stages 6–8 are conditionally skipped based on mode and flags. Triage and slicing run in isolated contexts to keep the orchestrator's window small.
The asset map scans the target tree in a single pass per file, matching language-specific regex patterns for:
| Asset kind | Examples |
|---|---|
| Entry points | Flask/FastAPI/Django routes, Express/Koa handlers, Spring @*Mapping, Go http.HandleFunc, Rails routes, Laravel Route::, ASP.NET [Http*], Actix/Axum, iOS lifecycle, Android onCreate |
| Trust boundaries | @login_required, passport.authenticate, @PreAuthorize, [Authorize], before_action :authenticate, JWT verification |
| Data stores | Django ORM, SQLAlchemy, Prisma, Sequelize, JDBC, GORM, ActiveRecord, Eloquent, EF Core, Redis |
| External calls | requests, fetch, axios, HttpClient, http.Get, RestTemplate, Net::HTTP, Guzzle |
| Sensitive data | Variable names matching password/token/api_key/credit_card/ssn patterns |
The map enriches every downstream stage:
- Triage receives a compact
Asset contextblock (entry point, auth status, nearby data stores/external calls) so the fast-tier model makes better TRUE/FALSE/NEEDS-DEEP decisions - Reachability uses asset-map entry points to classify non-Python findings (previously only Python had call-graph-based reachability; others fell back to path heuristics)
- Slice appends an
ASSET_CONTEXTblock to the slice text for the deep reviewer - Report includes an auto-generated STRIDE threat model section: attack surface summary, unguarded entry points table, STRIDE threat matrix, and high-risk paths (unguarded endpoints touching data stores or external calls)
mantis/ standalone CLI Python package
mantis/asset_patterns.py regex patterns for asset map (entry points, auth, data stores, etc.)
mantis/assetmap.py stage 0b: asset map scanner + query API
mantis/bm25.py BM25 scorer for finding-to-entry-point linking
mantis/context_graph.py optional tree-sitter call graph (typed edges, BFS reachability)
mantis/threatmodel.py STRIDE threat model generator
mantis/resources/agents/ subagent definitions (model: + tier: frontmatter)
mantis/resources/commands/ slash command for MCP mode
mantis/resources/rules/ opengrep YAML rules (Semgrep schema) + pack specs + manifest
mantis/resources/scripts/ build_manifest.py, pack_compose.py, project_findings.py, asset_map.py
mantis/resources/checklists/ OWASP Testing Guide chapters for the deep-reviewer
mantis/resources/install.sh install agents/commands into a target (MCP mode; ./install.sh wraps it)
setup.sh install opengrep + mantis CLI
doctor.sh verify install
CLAUDE.md conventions for future contributors / Claude sessions
Resources live under mantis/ so they ship inside the wheel published to
PyPI. The CLI resolves them via Path(__file__).parent/'resources' and
falls back to a top-level layout for legacy installs.
| Name | Role | Tier |
|---|---|---|
| sast-orchestrator | plan, dispatch, write report | mid |
| triage-analyst | classify findings, drop FPs | fast |
| slice-extractor | interprocedural slice with entrypoint reachability | mid |
| deep-reviewer | confirm vulnerability, map CWE/CVSS, PoC | deep |
| fix-author | minimal patch in worktree, re-verify | mid |
| deobfuscator | minified / packed / encoded files (static only) | mid |
| Pack | Selects |
|---|---|
fast |
severity ERROR, confidence HIGH |
deep |
everything |
bugbounty |
severity ERROR, confidence HIGH or MEDIUM |
cve |
rules with metadata.cve set |
sca |
dependency / lockfile rules |
mobile, mobile-ios, mobile-android |
by language and path |
web, llm, taint |
by path |
native |
decompiled C/C++ vulns + malware patterns (chimera RE bridge) |
Compose a pack:
python3 mantis/resources/scripts/pack_compose.py mantis/resources/rules/packs/fast.yaml --as-args
opengrep $(python3 mantis/resources/scripts/pack_compose.py mantis/resources/rules/packs/fast.yaml --as-args) --json <target>Findings stay local: no CI integration, no forge API calls, no telemetry or
upload of findings or source. --fix writes to a sibling git worktree, never the
working tree; reports live under <target>/.mantis/runs/.
Two kinds of outbound call exist: a once-per-day PyPI version check
(MANTIS_NO_UPDATE_CHECK=1 to disable), and — when the trivy/grype SCA engines
run (they are part of the default engine set) — a vulnerability-database fetch. To
run fully offline: --engines none (OpenGrep only), restrict --engines to the
local engines (bandit,gosec,njsscan,eslint-security,checkov), or
--engines-offline.
Optional, off unless set: MANTIS_HARUSPEX_URL enables Haruspex triage ranking
(stage 5b), which POSTs each finding's rule id, path, message and an ~80-line
code window to that endpoint. It is intended for a loopback Haruspex instance;
pointing it at a remote host sends source off the machine.
The audited repository is treated as hostile input.
-
No code from the target runs. git is invoked with hooks and fsmonitor disabled. checkov runs from a private cwd and HOME, so a planted
.checkov.yamlcannot load external checks. ESLint uses mantis's own config and formatter, never the target's. -
Prompt injection. Code sent to an LLM is wrapped in nonce-tagged delimiters that a comment cannot close. When the code contains text aimed at the reviewer ("false positive", "ignore previous instructions", hidden Unicode), a FALSE triage verdict is escalated to NEEDS-DEEP and a deep-review rejection is downgraded to partial.
-
Secrets. Key-shaped values and quoted secret assignments are masked before a window or slice leaves the machine. A
--fixpatch whose context lines contain a live secret therefore fails to apply; rotate those by hand. -
Report. Scanner messages, LLM replies and repo paths are written as inert single-line Markdown: no forged headings, table rows or raw HTML.
-
MCP install.
install.shgrants its command allowlist only when every.claude/sast-*dir is one it installed; a pre-existing dir from the target is left untouched and no permissions are granted. -
Scanner suppressions. OpenGrep runs with
--disable-nosem, sonosemgreplines still reach triage, tagged(nosemgrep in source). It also runs with--x-ignore-semgrepignore-files, because a planted.semgrepignoreof*would otherwise empty the scan; OpenGrep's default vendored/test excludes are re-applied explicitly. Your own suppressions go in.mantisignore. The extra engines' markers (# nosecfor bandit and gosec) are still honoured.
Standalone runs cache LLM replies for deterministic calls under
~/.cache/mantis/llm (keyed on model, prompts and parameters), so re-auditing an
unchanged tree costs no tokens. Sampling calls (ToAST) are never cached.
--no-cache bypasses it.
Read CLAUDE.md — it documents YAML quoting gotchas, the tier-vs-model split, the rule-pack workflow, and what not to add (CI hooks, remote upload, hardcoded model names).
After editing rules, rebuild the manifest:
python3 mantis/resources/scripts/build_manifest.pyCommit the updated mantis/resources/rules/_manifest.yaml alongside the rule change.
tests/test_rules_hygiene.py guards the rule set: every file must pass
opengrep --validate (a file that fails is skipped whole at scan time), rule ids
must be unique, and every rule needs category, cwe and confidence. Behaviour
is pinned by fixtures under tests/fixtures/rules/<case>/: a ruleid: a, b
comment names exactly the rules that must fire on that line (or on the next line,
when the comment stands alone inside a multi-line pattern); unannotated lines must
stay quiet. Add a case to _CASES when you fix or add a rule.
# 1. bump version in pyproject.toml
# 2. clean + build
rm -rf dist build mantis_sast.egg-info
pipx run build
# 3. inspect the wheel and metadata
pipx run twine check dist/*
unzip -l dist/mantis_sast-*.whl | tail -5 # confirm resources/ shipped
# 4. upload to TestPyPI first
pipx run twine upload --repository testpypi dist/*
pipx install --index-url https://test.pypi.org/simple/ \
--pip-args "--extra-index-url https://pypi.org/simple" \
mantis-sast
mantis --version
# 5. tag and upload to PyPI
git tag v0.1.0 && git push origin v0.1.0
pipx run twine upload dist/*Use an API token (__token__ as the username, the token as the password) stored in
~/.pypirc or TWINE_PASSWORD — never commit it.
Praetor drives mantis via run_mantis() for source-audit within a pentest engagement. Findings feed into praetor's save-finding pipeline and the engagement graph. The sast_to_endpoint_risk tool bridges mantis SAST findings to DAST targets — a source-level SQL injection in a controller maps to the HTTP endpoint praetor tests next.
praetor: run_mantis(path, mode='deep') → import results → sast_to_endpoint_risk → confirm_sqli on the endpoint
- Architecture: AGHAST, Slice, Vulnhuntr, Tree-of-AST (Black Hat USA 2025)
- Tooling: OpenGrep, Semgrep, litellm
- Standards: OWASP MASVS, MASTG, OWASP Top 10:2025, LLM Top 10:2025