| name | static |
| description | Use when the user wants static analysis / SAST / type-checking / lint-with-teeth on a Python codebase — finding security smells, logic bugs, type errors, obvious defects without running the code. Wraps ruff (full ruleset including the bandit-port S rules), semgrep (registry-backed pattern + dataflow rules), and mypy (type checking). Emits garlicpress-shape findings so the output merges with fuzz/static pipelines without translation. Three categories, not duplicates: ruff covers AST patterns + bandit; semgrep covers cross-file dataflow; mypy covers types. Skip bandit (ruff -S already covers it). |
static
scripts/static.sh (or python3 -m beigebox.skills.static) — run ruff + semgrep + mypy against a Python repo and emit a single ranked finding list. Companion to the fuzz skill: same garlicpress-shape output, same async pipeline, callable from a Trinity orchestrator alongside fuzz.
When to invoke
- User asks for "static analysis", "SAST", "find bugs without running", "lint with teeth"
- An audit / review needs the static side of a static-plus-fuzz sweep
- Catching things
fuzz can't reach: dead code, unused imports, deprecated APIs, type-confused branches that never run with random inputs
Why these three tools
ruff is the bandit successor for Python — its S ruleset is a port of bandit (exec, eval, pickle, weak crypto, subprocess(shell=True), etc.) and runs in milliseconds. Adding bandit separately would mostly re-find the same things.
semgrep covers ground ruff can't: cross-file dataflow, framework-specific patterns (Django, Flask, requests), and a curated registry of security/correctness rule packs. Slower, but reaches deeper.
mypy covers a category neither ruff nor semgrep can touch — type checking. Catches arg-type mismatches, missing attributes, None flowing into non-Optional parameters. Highest-bug-per-line of any static tool when applied to typed code.
So the trio is ruff (fast, AST-pattern, bandit-equivalent) + semgrep (slower, dataflow, registry) + mypy (types). Skip bandit; ruff covers it.
Usage
scripts/static.sh /path/to/repo
scripts/static.sh /path/to/repo \
--ruff-select 'F,E9,B,S' \
--semgrep-config 'p/security-audit'
scripts/static.sh /path/to/repo --out /tmp/static.json
scripts/static.sh /path/to/repo --format summary
scripts/static.sh /path/to/repo --no-semgrep
From Python:
from beigebox.skills.static import run_static
result = await run_static(
"/path/to/repo",
ruff_select="F,E9,B,S",
semgrep_config="p/security-audit",
)
CLI options
| Flag | Default | Meaning |
|---|
--ruff-select | F,E9,B,S,ASYNC,ARG,RUF,PL,TRY | Ruff rule selection. Bug-and-security-biased; skips style noise by default. |
--ruff-ignore | None | Ruff rules to ignore. Comma-separated. |
--semgrep-config | p/python | Semgrep config: registry shortcut (p/python, p/security-audit, p/owasp-top-ten), file path, or comma-list. |
--mypy-strict | false | Run mypy in --strict mode. Much louder; lots of low-severity findings. |
--mypy-follow-imports | silent | Mypy --follow-imports value. silent = don't recurse into untyped deps. |
--no-ruff | false | Skip the ruff runner. |
--no-semgrep | false | Skip the semgrep runner. |
--no-mypy | false | Skip the mypy runner. |
--ruff-timeout | 120 | Seconds. |
--semgrep-timeout | 600 | Seconds. Semgrep can be slow on the first run while it caches rules. |
--mypy-timeout | 300 | Seconds. Mypy can be slow on first run; --no-incremental disables caching for clean subprocess invocation. |
--format | json | json or summary. |
--out | None | Write JSON output to this file. |
Output shape
{
"findings": [
{
"finding_id": "static_<hash>",
"severity": "high",
"type": "security",
"location": "src/api/routes.py:42",
"description": "ruff/S301: Use of pickle, possible RCE on untrusted input",
"evidence": "ruff rule S301\ndocs: https://...",
"traceability": {"file": "src/api/routes.py", "line": 42, "git_sha": null},
"static_meta": {"tool": "ruff", "rule_id": "S301", "column": 8, "url": "https://..."}
}
],
"stats": {
"total_findings": 14,
"ruff_count": 9,
"semgrep_count": 5,
"ruff_duration_seconds": 0.47,
"semgrep_duration_seconds": 38.2,
"ruff_error": null,
"semgrep_error": null
},
"raw_results": {"ruff": {...}, "semgrep": {...}}
}
Severity mapping
Ruff — by rule prefix, with high-severity bumps for the actually dangerous S rules:
| Prefix | Severity | Type | Notes |
|---|
E9 | high | logic_error | Syntax errors |
F | medium | logic_error | Pyflakes (undefined names, unused imports) |
B | medium | logic_error | Bugbear |
S (default) | medium | security | Bandit-port |
S102/301/307/311/324/501/502/506/602/605/608/701 | high | security | exec, pickle, eval, weak crypto/random/hash, SSL bypass, shell=True, SQL injection, jinja2 autoescape off |
ASYNC | medium | logic_error | Async/await footguns |
ARG, RUF, PL, TRY | low | logic_error | Style-adjacent bugs |
UP, SIM, I, E, W, N, D | low | style | Pure style — disabled in default --ruff-select |
Semgrep — by severity field plus metadata.category:
| Field | Mapping |
|---|
severity: ERROR | high |
severity: WARNING | medium |
severity: INFO | low |
metadata.category: security/vuln | type=security |
metadata.category: correctness/best-practice | type=logic_error |
metadata.category: performance | type=resource_leak |
Mypy — by error level + error code:
| Field | Mapping |
|---|
level: error, code in {attr-defined, union-attr, call-arg, arg-type, return-value, assignment, operator, index, no-redef, valid-type} | high / logic_error (would crash at runtime) |
level: error (other codes) | medium / logic_error |
level: note | low / logic_error (informational; "Revealed type is..." etc.) |
Behavior notes
- Per-runner failure isolation. If ruff is missing or semgrep hits a network error fetching its rule pack, that runner's
error field is set and the other runner's findings still come back. Pipeline never raises.
- Parallel execution. Ruff and semgrep run as concurrent subprocesses via
asyncio.create_subprocess_exec; ruff usually finishes in <1s while semgrep is still pulling rules.
- Dedupe. Findings are deduped on
(location, tool, rule_id). Cross-tool overlap on the same line is kept — same bug found by two tools is signal, not noise.
- Severity sort. Findings come back sorted critical → high → medium → low, then by location.
- Exit code. CLI exits 1 if any high+ severity finding surfaced, 3 if both runners errored, 0 otherwise. (Ruff alone exits 0/1 by finding presence; we override that for the union case.)
- Semgrep first run. Pulls registry rules over the network and caches them in
~/.semgrep/. Budget extra time on cold cache, ~zero overhead after.
Requirements
python3 ≥ 3.11
ruff on PATH (pip install ruff or already-bundled in many dev envs)
semgrep on PATH (pip install semgrep)
mypy on PATH (pip install mypy)
- Any tool missing → that runner emits
error; the skill still runs the others
Anti-patterns
- Don't use this for type-checking —
mypy / pyright are a different category (they need full env resolution; static-skill is rule-based). If the user asks for type analysis, point them there.
- Don't crank
--semgrep-config p/r2c-ci or similar mega-pack on a 100k-LOC monorepo without an --out file and a long --semgrep-timeout — semgrep can take 10+ min on big codebases with broad rule packs.
- Don't pair this with
bandit — ruff -S already covers it. Picking both gives you duplicate findings with different IDs.
- Don't treat the output as ground truth without triage. SAST has a higher false-positive rate than fuzzing; expect to drop ~30% of findings as "intended pattern" or "context-aware safe" once a human reviews.