Static security analysis that finds what Bandit misses.
Detects IDOR, unauthorized access, and auth bypass at the AST level — plus SQL injection, command injection, hardcoded secrets, and 20+ other categories. Zero dependencies. No GPU. Works on Python 3.9+.
python -m pip install ansede-static
# If the newest release is still propagating on PyPI:
# python -m pip install "ansede-static @ git+https://github.com/mattybellx/Ansede.git"
ansede-static src/- Quick start
- Detection coverage
- Pattern recall benchmark
- Quality and performance harnesses
- Benchmarks and public proof
- CI integration
- Contributing
| Benchmark | Result |
|---|---|
| Full test suite | 473 passed |
| CVE benchmark recall | 35/35 · 100% recall · 0% FP rate |
| Quality checks | 41/41 (100%) |
| External real-world corpus | 19/19 (100%) |
| Web-wild noise quotient | 1.64 high/critical findings per 1k LOC (N=50, target: < 2) |
Full artifact: final_product_scorecard.json · all_targets_met: true
- Install and scan one folder
pip install ansede-static
ansede-static src/ --fail-on high- Open findings in GitHub code scanning
ansede-static src/ --format sarif --output ansede.sarif- Adopt incrementally with a baseline
ansede-static src/ --format json --output .ansede-baseline.json
ansede-static src/ --baseline .ansede-baseline.json --fail-on highIf ansede-static catches a bug in your project, please ⭐ star the repo — it helps other developers find it.
Bandit finds subprocess(shell=True). It does not find this:
# CWE-639 — IDOR (Insecure Direct Object Reference)
# Bandit: silent. ansede-static: CRITICAL
@app.route("/invoice/<invoice_id>")
@login_required
def get_invoice(invoice_id):
# no WHERE user_id = current_user.id
return db.execute("SELECT * FROM invoices WHERE id = ?", (invoice_id,))Or this:
# CWE-285 — Missing Ownership Check
# Bandit: silent. ansede-static: HIGH
@app.route("/post/<post_id>/delete", methods=["POST"])
@login_required
def delete_post(post_id):
# No: if post.author_id != current_user.id: abort(403)
Post.query.filter_by(id=post_id).delete()Or this:
# CWE-862 — Missing Authentication
# Bandit: silent. ansede-static: HIGH
@app.route("/admin/users")
def list_users(): # no @login_required, no @admin_required
return User.query.all()These are the bugs that appear in CVE databases. These are the bugs that cost companies millions.
ansede-static is a zero-dependency SAST tool that detects them at the AST level.
# Zero-dependency install (plain text output)
pip install ansede-static
# With colored terminal output (recommended for local use)
pip install "ansede-static[rich]"
# Scan a directory (recursive)
ansede-static src/
# Fail CI on high/critical findings
ansede-static src/ --fail-on high
# SARIF output for GitHub Code Scanning
ansede-static src/ --format sarif --output results.sarif
# JSON for scripting
ansede-static src/ --format json | python -m json.tool
# Scan from stdin
cat app.py | ansede-static --stdin --lang python
# Only show NEW findings vs. a saved baseline
ansede-static src/ --format json --output baseline.json # first run
ansede-static src/ --baseline baseline.json # later runsSilence individual findings with a comment on the same line:
@app.route("/public/feed") # ansede: ignore[CWE-862]
def public_feed():
return get_posts()document.getElementById("out").innerHTML = safe; // ansede: ignore[CWE-79]Use # ansede: ignore (no brackets) to suppress all findings on a line.
Generate a baseline, then only see new findings on subsequent runs:
# Save current state
ansede-static src/ --format json --output .ansede-baseline.json
# CI — only fail on NEW findings
ansede-static src/ --baseline .ansede-baseline.json --fail-on highThis is ideal for adopting ansede-static on a large codebase incrementally.
| Flag | Status | Description |
|---|---|---|
--init |
Stable | Write a starter ansede.json config to the current directory |
--incremental |
Stable | Scan only files changed in git diff HEAD; ideal for pre-commit hooks on large monorepos |
--apply-fixes |
Stable | Interactively apply safe inline auto-fixes. Multi-line or ambiguous fixes stay as suggestions for manual review. |
--ai-triage |
Stable | Offline heuristic triage pass that suppresses findings in test/mock/fixture contexts and reduces false positives |
--js-backend |
Stable | Select the JS/TS engine: auto, classic, or structural |
--list-rules |
Stable | Print the detector catalog and exit |
--describe-rule |
Stable | Show the contract for a rule ID or CWE |
--list-js-backends |
Stable | Print the available JS/TS backends and exit |
Note: Auto-fixes are intentionally conservative; always review generated edits before commit.
--experimental-js-ast is retained as a legacy alias; the structural JS/TS engine is now production-default (--js-backend auto) and covers multiline sink calls, split assignments, React / JSX dangerouslySetInnerHTML flows, object-literal and decorator/file-based route/auth definitions (including Fastify, Koa-style ambient middleware, Nest decorators, and Next route files), helper-call sink resolution, helper return-value propagation across local/imported JS/TS call chains, and cached relative-import JS/TS module-graph flow for redirect/path/SSRF/route-access patterns.
Run ansede-static --init to generate a starter config, then customize it for your repo:
{
"exclude_paths": ["tests/fixtures", "build", "dist", ".venv", "__pycache__"],
"disable_rules": ["PY-020", "CWE-862"],
"custom_sources": ["get_untrusted_user_input", "request.headers.get"],
"custom_sinks": {
"my_vulnerable_db_execute": {
"cwe": "CWE-89",
"title": "Custom SQL Injection sink",
"severity": "critical"
}
}
}disable_rulesaccepts either a stable detector ID likePY-020/JS-034or a CWE likeCWE-862- malformed
custom_sinksentries are skipped with a warning instead of being silently half-applied custom_sinksuse an explicit object schema:cwe,title, and optionalseverity- legacy baselines remain readable; new JSON baselines also include a top-level
fingerprint_version
# .github/workflows/security.yml
- uses: mattybellx/Ansede@v1
with:
path: src/
fail-on: high # optional: critical/high/medium/low/never
upload-sarif: true # uploads to GitHub Code Scanning automaticallyInstall from the VS Code marketplace: Ansede Security Scanner
Squiggles appear inline on open/save and, by default, during debounced typing (ansede.scanOnType).
Clicking a CWE code opens the MITRE definition. The extension auto-detects ansede-static
inside common workspace virtualenv locations like .venv/ before falling back to PATH.
ansede.scanOnType— enable debounced scans while typingansede.scanTimeoutMs— increase for very large files or slower environmentsansede.executable— pin a custom binary path if you do not want auto-discovery
You can inspect the shipped detector catalog and JS/TS backend choices directly:
ansede-static --list-rules
ansede-static --describe-rule PY-020
ansede-static --describe-rule CWE-862
ansede-static --list-js-backends
ansede-static src/ --js-backend structuralauto currently resolves to the structural JS/TS engine while keeping the classic
engine available for comparison or regression triage.
| CWE | Category | Example pattern |
|---|---|---|
| CWE-639 | IDOR | Auth route query without ownership WHERE |
| CWE-285 | Broken Access Control / Ownership | Mutation without owner guard or admin route with auth only |
| CWE-862 | Missing Authentication | Flask/FastAPI route with no auth decorator |
| CWE-287 | Auth Bypass via Presence-Check | if token: without verifying the token |
| CWE-117 | Log Injection | Untrusted data in log.*() calls (CRLF) |
| CWE | Category | Notes |
|---|---|---|
| CWE-89 | SQL Injection | Taint: f-string, %-format, .format() |
| CWE-78 | Command Injection | subprocess + shell=True + dynamic arg |
| CWE-95 | Code Injection | eval(), exec(), compile() |
| CWE-502 | Unsafe Deserialization | pickle, marshal, yaml.load |
| CWE-22 | Path Traversal | os.path.join with unsanitized variable |
| CWE-918 | SSRF | HTTP clients with unvalidated URLs |
| CWE-798 | Hardcoded Secrets | API keys, tokens, passwords, AWS creds |
| CWE-1188 | Dangerous Defaults | debug=True, verify=False, CORS wildcard |
| CWE-327 | Weak Cryptography | MD5/SHA1 for password hashing |
| CWE-338 | Weak PRNG | random module for security tokens |
| CWE-617 | Silent Exception Swallowing | except Exception: pass |
| CWE-345 | Broken Auth | JWT verify=False |
| — | Inter-procedural Taint | Tracks taint across function calls |
| — | Cyclomatic Complexity | Flags CC > 15 (high-risk code paths) |
| CWE | Category | Example |
|---|---|---|
| CWE-79 | XSS | innerHTML, document.write, unsafe templates |
| CWE-95 | Code Injection | eval(), new Function(), setTimeout(str) |
| CWE-78 | Command Injection | exec() with template literals |
| CWE-89 | SQL Injection | Template literal in query() |
| CWE-798 | Hardcoded Secrets | API keys, JWT secrets, AWS creds |
| CWE-22 | Path Traversal | fs.readFile with req.* input |
| CWE-1321 | Prototype Pollution | Object.assign, __proto__, spread |
| CWE-1333 | ReDoS | Catastrophic backtracking regex |
| CWE-307 | No Rate Limiting | Auth routes without rate-limiter middleware |
| CWE-352 | Missing CSRF | POST/PUT without CSRF middleware |
| CWE-862 | Missing Authentication | Sensitive/admin route with no auth middleware |
| CWE-287 | Auth Bypass | if (token) gate without verification |
| CWE-639 | Route-level IDOR | findByPk(req.params.id) without owner scope |
| CWE-285 | Broken Access Control / Ownership | post.destroy() after ID lookup, no owner guard; admin route with auth only |
JS/TS route findings now carry trace evidence too, so SARIF output includes code flows that show the route, resource parameter, auth middleware (if any), the missing guard, and the lookup or mutation sink.
- Python findings are AST/dataflow heuristics over common Flask/FastAPI/Django-style patterns. They are strong on common auth, ownership, injection, and deserialization bugs, but they are not full symbolic execution.
- JavaScript / TypeScript findings are currently strongest on Express/Router-style server code. The route-aware checks reason about common middleware, role guards, credential presence checks, resource lookups, and mutations, but they are not yet parser-semantic whole-program analysis.
- Trust metadata helps triage, not certainty. Findings now include stable
rule_id, plusanalysis_kindandconfidence, so you can tell both which detector fired and whether it came from a direct pattern, route heuristic, decorator heuristic, or taint flow. That still does not guarantee exploitability. - Synthetic benchmarks are signal, not proof. The CVE corpus below measures recall on curated reproductions of real vulnerability patterns, not large real-world codebases.
| Capability | ansede-static | Bandit | Semgrep OSS |
|---|---|---|---|
| Zero runtime dependencies | ✅ | ❌ | ❌ |
| Works fully offline | ✅ | ✅ | ✅ |
| Python support | ✅ | ✅ | ✅ |
| JavaScript / TypeScript support | ✅ | ❌ | ✅ |
| SARIF output | ✅ | ❌ | ✅ |
| GitHub Action (marketplace) | ✅ | ❌ | ✅ |
| VS Code extension | ✅ | ❌ | ✅ |
| Pre-commit hook | ✅ | ✅ | ✅ |
| IDOR / CWE-639 | ✅ | ❌ | ❌* |
| Missing auth / CWE-862 | ✅ | ❌ | ❌* |
| Ownership check / CWE-285 | ✅ | ❌ | ❌* |
| Inline suppression comments | ✅ | ✅ | ✅ |
Baseline diffing (--baseline) |
✅ | ❌ | ❌ |
| Python API | ✅ | ✅ | ✅ |
| Free / open source | ✅ | ✅ | ✅ |
*Semgrep can detect these with custom rules you write yourself; not in the default ruleset.
────────────────────────────────────────────────────
ansede-static — 3 file(s) scanned
────────────────────────────────────────────────────
app.py (python)
[CRITICAL] L47 CWE-78: Command injection in run_cmd() (shell=True + dynamic arg)
[CRITICAL] L81 CWE-639: IDOR — query in get_invoice() missing ownership WHERE clause
[HIGH ] L23 CWE-89: SQL Injection in get_user() via f-string
[HIGH ] L103 CWE-862: Route /admin/users has no authentication decorator
Total: 4 findings — 2 critical, 2 high
SARIF results preserve stable analyzer-specific rule IDs (for example PY-024, JS-034), per-finding analysisKind, confidence, and trace-backed code flows so downstream tools can distinguish direct pattern matches from heuristic route findings without collapsing everything under a raw CWE.
ansede-static src/ --format sarif --output results.sarifJSON findings include stable rule_id values alongside cwe, analysis_kind, and confidence, which makes it easier to build triage dashboards, baseline filters, or CI policies around specific detectors instead of whole CWE buckets.
The top-level JSON envelope now also carries fingerprint_version, which documents the baseline fingerprint format used by that report.
ansede-static src/ --format json \
| python -c "
import sys, json
for r in json.load(sys.stdin)['results']:
for f in r['findings']:
print(f[\"severity\"], f[\"cwe\"], f[\"title\"])
"The benchmarks/ directory contains 26 hand-crafted code snippets that reproduce
vulnerability patterns from real CVE entries.
Important caveat: These are synthetic pattern reproductions written specifically to match what the tool detects. Recall on hand-crafted fixtures is a baseline sanity check — it validates that rules fire, not that they generalise to real codebases. Real-world precision and recall on projects like OWASP WebGoat or production open-source code will differ. Contributions testing against real-world CVE-affected code are welcome.
git clone https://github.com/mattybellx/Ansede
cd Ansede
pip install -e .
python -m benchmarks.nvd_benchmark ┌──────────────────────────────────────────────────────────────────┐
│ ansede-static Pattern Recall Benchmark │
│ (Synthetic CVE Pattern Reproductions — not real projects) │
└──────────────────────────────────────────────────────────────────┘
✓ CVE-2022-24439 CWE-78 python [1 critical — command injection]
✓ CVE-2022-36087 CWE-918 python [1 high — SSRF]
✓ CVE-2019-14234 CWE-89 python [1 critical — SQL injection]
✓ CVE-2021-32556 CWE-502 python [1 critical — pickle deserialization]
✓ CVE-2019-10744 CWE-1321 js [1 high — prototype pollution]
Python (13 patterns): 13/13
JS/TS (13 patterns): 13/13
All 26 synthetic patterns detected · ~43ms
Use these during development to protect trust and catch noisy regressions early:
python -m benchmarks.quality_benchmark --fail-under 100
python -m benchmarks.external_corpus --manifest benchmarks/external_manifest.json --fail-under 100
python -m benchmarks.external_corpus --manifest benchmarks/real_world_manifest.json --cache-dir .tmp/ansede-corpus --refresh
python -m benchmarks.external_corpus --manifest benchmarks/real_world_manifest.example.json --cache-dir .tmp/ansede-corpus --refresh
python -m benchmarks.perf_benchmark --iterations 10See docs/QUALITY.md for scope, caveats, and extension guidance.
The external corpus runner also supports pinned git-backed manifests for larger repo-shaped fixtures.
Use --cache-dir to control where repositories are cached, --refresh to re-fetch them, and
--offline to re-run against an existing cache without touching the network.
The repository now ships an opt-in curated manifest at benchmarks/real_world_manifest.json
with pinned NodeGoat route files selected to avoid vendor noise and keep expectations stable.
| Metric | Target |
|---|---|
| False-positive rate | < 10 % (core Python/JS rules) |
| Recall (CVE corpus) | > 85 % for injections, path traversal, RCE |
| Speed | < 10 s per 100 k LOC on commodity hardware |
| SARIF upload size | < 2 MB for typical repos |
These are aspirational goals validated by the NVD benchmark suite in benchmarks/.
What ansede-static catches:
- Injection sinks reached by tainted user input (SQLi, XSS, CMDi, SSRF)
- Insecure defaults (hard-coded credentials, weak crypto, debug mode left on)
- Access-control anti-patterns (missing decorators, IDOR, mass assignment)
- Supply-chain indicators (pickle, eval, unsafe deserialization)
Non-goals (out of scope):
- Symbolic execution / full-program formal verification
- Dynamic analysis / DAST (black-box testing)
- Business-logic flaws requiring runtime context
- Dependency-vulnerability scanning (use
pip-audit/npm auditfor that)
| Feature | ansede-static | Semgrep | CodeQL | Bandit |
|---|---|---|---|---|
| Zero-dependency install | ✅ | ❌ (Go runtime) | ❌ (query engine) | ✅ |
| Python + JS in one tool | ✅ | ✅ | ✅ | Python only |
| Cross-file taint (inter-proc) | ✅ (IFDS/IDE + bounded call-string) | ✅ | ✅ | ❌ |
| SARIF output | ✅ | ✅ | ✅ | ❌ |
| SBOM generation | ✅ | ❌ | ❌ | ❌ |
| Compliance tags (OWASP/NIST) | ✅ | ❌ | Partial | ❌ |
| HTML dashboard | ✅ | ❌ | ❌ | ❌ |
| Custom YAML rules | ✅ | ✅ (custom rules) | ✅ (QL) | ❌ |
| PR inline comments | ✅ (action) | ✅ | ✅ | ❌ |
- Detector blend remains layered. Pattern, AST, and IFDS signals are combined by confidence; rare framework/metaprogramming constructs can still require manual review.
- Minified/transpiled JS is mapped when source maps are present. If source maps are absent or stale, findings degrade to generated-file coordinates.
- Template transpilation is first-class for common Jinja2/Handlebars constructs. Highly dynamic runtime template composition can still reduce precision.
- Per-file timeouts default to 30 s; extremely large generated files may time out and should be excluded from CI scope.
1. CLI (one-shot scan) ansede-static src/
2. Pre-commit hook ansede-static --incremental
3. CI pipeline GitHub Action / GitLab CI / any shell
4. VS Code Extension Real-time inline findings
5. CISO dashboard --format ciso / --format html
Each step adds enforcement and visibility without requiring the previous step.
| Mode | Trigger | What it does |
|---|---|---|
| Core (default) | ansede-static src/ |
Pattern + AST taint, single-pass, < 2 s / file |
| Deep | --ai-triage |
Adds offline heuristic triage: suppresses test-only findings, parameterized query patterns |
| Incremental | --incremental |
Git-diff scoping — only changed files are analyzed |
| Global | Automatically enabled when scanning a directory | Two-pass: builds symbol graph in pass 1, evaluates taint in pass 2 |
from ansede_static import AnsedeConfig, scan_file, scan_code
# Scan a file
result = scan_file("app.py")
for finding in result.sorted_findings():
print(f"[{finding.severity.value}] L{finding.line} {finding.cwe}: {finding.title}")
# Scan code in memory (useful for test suites)
result = scan_code(source_code, language="python")
assert result.critical_count == 0, f"{result.critical_count} critical findings"
# Optional: apply the same ansede.json-style filters programmatically
config = AnsedeConfig(disable_rules=["CWE-862"])
filtered = scan_code(source_code, language="python", config=config)
assert all(f.cwe != "CWE-862" for f in filtered.findings)
# Select the JS engine explicitly when scanning JavaScript / TypeScript
js_result = scan_code(js_source, language="javascript", js_backend="structural")
assert js_result.language == "javascript"
# SARIF
from ansede_static.reporters import format_sarif
sarif_str = format_sarif([result])steps:
- uses: actions/checkout@v4
- uses: mattybellx/Ansede@v1
with:
path: src/
fail-on: high
upload-sarif: true- name: Security scan
run: |
python -m pip install "ansede-static @ git+https://github.com/mattybellx/Ansede.git"
ansede-static src/ --format sarif --output ansede.sarif --fail-on high
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: ansede.sarifrepos:
- repo: https://github.com/mattybellx/Ansede
rev: v1.2.0
hooks:
- id: ansede-static
args: [--fail-on, high]git clone https://github.com/mattybellx/Ansede
cd Ansede
pip install -e ".[dev]"
pytest tests/ -v
python -m benchmarks.nvd_benchmark
python -m benchmarks.quality_benchmark --fail-under 100
python -m benchmarks.perf_benchmark --iterations 10
# Self-scan: use this to catch regressions in rules, contracts, and reporters
ansede-static src/ --fail-on high- Python: Add a
_rule_NN(ctx: _Ctx)function in src/ansede_static/python_analyzer.py and register it in_detect() - JavaScript: Add either a
_Rule(...)entry or a contextual_check_*function in src/ansede_static/js_analyzer.py, then register it inanalyze_js() - Benchmark test: Add a
CVEEntry(...)to benchmarks/cve_corpus.py - See CONTRIBUTING.md for the full checklist
The full implementation is in this repository under src/ansede_static/:
| File | Purpose |
|---|---|
python_analyzer.py |
27 Python detection rules (AST/dataflow) |
js_analyzer.py |
23+ JavaScript/TypeScript detection rules |
js_ast_analyzer.py |
Production structural JS/TS engine with syntax-aware flow and framework route/auth modeling |
js_engine/ |
Shared JS engine modules for structural parsing, React/JSX analysis, Koa/Nest/Next-aware route/auth heuristics, helper-call / helper-return inter-file JS flow, cached workspace module graphs, and rule orchestration |
engine/triage.py |
Offline heuristic triage and confidence adjustments |
engine/explain.py |
Human-readable finding explanations |
reporters.py |
Text, JSON, and SARIF output formatters |
ir/global_graph.py |
Inter-procedural call graph |
cache/sqlite_store.py |
Zero-dependency result cache |
cli.py |
CLI entry point |
Benchmark corpus: benchmarks/cve_corpus.py
Public scorecards and reproducible benchmark runs: BENCHMARKS.md
Contributions are very welcome — see CONTRIBUTING.md for guidelines.
Planning notes and milestone tickets live in ROADMAP.md.
The most impactful contributions are:
- New detection rules — if you find a vulnerability class in Python or JavaScript that the tool misses, open an issue with a minimal code snippet or a PR with a new rule + test.
- False-positive reports — if the tool flags safe code, open a bug report so we can tighten the heuristic.
- Real-world corpus testing — the benchmark suite uses synthetic patterns; PRs that test against real CVE-affected open-source projects are especially valuable.
git clone https://github.com/mattybellx/Ansede
cd Ansede
pip install -e ".[dev]"
pytest tests/ -q # current validation target: full suite greenMIT — see LICENSE.
Found a real bug with ansede-static? Open a discussion or tweet about it — community signal is the best way to help other developers find this tool.
