Python — Deep-Dive Guide
Python is a primary language for Gadriel Code: it has the deepest GVL rule catalogue, a parameter-bound taint engine with cross-file (workspace) tracking on by default, and full SCA across the whole packaging ecosystem. This guide goes below the summary in languages.md — read that first for the capability matrix, and scan-coverage.md for the six scan types this page draws on.
Which scans apply, and how deep
| Scan type | Applies to Python? | Depth |
|---|---|---|
| SAST | ✅ | Full tree-sitter front-end + parameter-bound taint with cross-file workspace tracking. Deepest rule catalogue of any language. |
| SCA | ✅ | Six manifest/lockfile parsers (see below); OSV vulnerabilities, license, health, reachability. |
| Secrets | ✅ | Language-agnostic — regex + entropy + git history over .py and config text. |
| Config | ✅ | Language-agnostic — CI/CD, .env, MCP, system prompts. |
| API | ✅ | Python is the best-supported framework family for source-defined REST route extraction, plus OpenAPI/GraphQL. |
SAST is where Python is unmatched. Everything below is about the SAST
front-end (python.rs) and its taint engine (taint/, workspace_taint/).
The taint engine
Sources it recognizes
The engine seeds taint from two mechanisms.
1. Entry-point parameters. When a function is classified as a request
handler, every parameter is seeded as request-bound taint (RequestParameter
origin). The self/cls receiver of a method is explicitly never tainted.
Classification is heuristic (heuristic_classify_entry_point):
- Name prefixes —
handle_,view_,get_,post_,put_,delete_,patch_. These are gated on a web-framework import soget_config()andput_cache()in a non-web module are not treated as HTTP handlers; without a framework import or decorator, a prefix match is downgraded to theLocalCliprovenance tier rather thanRemote. - Exact names —
handle,view,endpoint,index. - Request-like parameter — a parameter named
request/req, or annotatedHttpRequest/...Request, setsis_inferred_request_bound. - Decorators — see framework awareness below.
2. Known input-sink calls (KNOWN_INPUT_CALLS) — substring-matched on an
assignment right-hand side:
pythonx = request.args["q"] # Remotedata = request.get_json() # Remotename = input() # LocalClimode = sys.argv[1] # LocalClikey = os.environ["SECRET"] # Env
The recognized set is tight and web-centric (request.json, request.form,
request.args, request.values, request.data, request.body,
request.params, request.query, flask.request.*, input, sys.argv,
os.environ). Values like request.headers, request.cookies, os.getenv,
or sys.stdin are not named sources — they reach taint only via the
entry-point parameter path or a rule's own source: list.
Framework awareness (Flask / Django / FastAPI)
Detection is decorator + name + import based; there is no per-framework routing table.
- Framework import gate —
flask,fastapi,django,starlette,aiohttp,tornado,bottle,falcon,sanic,quart. Presence flips name-prefix handlers to theRemoteprovenance tier. - Web-handler decorator suffixes —
route,get,post,put,delete,patch,head,options,endpoint,api_view,websocket,api_route. Covers Flask@app.route, FastAPI@app.get/@router.post, DRF@api_view. A web decorator forcesRemote. - CLI decorator suffixes —
command,group,callback(Click/Typer) yield theLocalClitier; a web decorator wins if both are present. - Django views — function-based views are caught by the
request-parameter heuristic; rules distinguish real views fromModel.save(self, ...)overrides.
There is no FastAPI dependency-injection (Depends(...)) modeling, no
Pydantic-model awareness, and no Django URLconf parsing.
Sink families and finding classes
Sinks are not hard-coded in the engine — they live in the GVL rule corpus
as ast_call (single call-site shape) and ast_flow (source→sink) rules. The
Python classes with the deepest coverage:
| Class | Sinks (as they appear in code) | CWE |
|---|---|---|
| Deserialization | pickle.loads(data), marshal.loads(data), yaml.load(stream) (no SafeLoader), yaml.unsafe_load(stream) | CWE-502 |
| Code execution | eval(expr), exec(code), __import__(name) | CWE-94 |
| OS command injection | os.system, os.popen, subprocess.run/call/check_output/check_call | CWE-78 |
| SQL injection | cursor.execute(f"... {x}"), db.execute(...) | CWE-89 |
| Path traversal / SSRF | open(), os.path.join(BASE, request_data) with a request-bound path | CWE-22 |
pythonimport pickle, yamlfrom flask import request@app.post("/import") # web decorator → Remote taintdef import_state():blob = request.get_json()["b"] # KnownInputSink, Remotereturn pickle.loads(blob) # CWE-502 deserialization sink
pythondef view_run(request): # name prefix + request param → entry pointcmd = request.args["cmd"]os.system(cmd) # CWE-78 command injection
Cross-file (workspace) taint — on by default
Python has full cross-file taint, and it is on by default for directory scans. A source seeded in one module is followed through an import into a sink in another.
bashgadriel code scan . # workspace-aware (default)gadriel code scan . --no-workspace-taint # per-file fallback
- The switch is
--no-workspace-taint(also[scan] workspace_taint = falsein.gadriel.toml, orGADRIEL_SCAN_WORKSPACE_TAINT=false). Default istrue. - Workspace taint runs only for directory targets; a single-file scan is always per-file.
- Active banner:
SAST: workspace-aware (cross-file Python + JS/TS + Rust + Go taint enabled). Disabled on a directory:SAST: per-file (--no-workspace-taint set). - The coordinator resolves cross-file calls through a workspace function index
populated from
import .../from ... import ...bindings, following bare-identifier and import-alias-qualified callees (helpers.run(x)). It runs a bounded fixed-point loop (production cap of 12 rounds).
Intra-file, the engine is already N-hop interprocedural. Not resolved
cross-file: method calls (self.m(), obj.m()), lambdas/nested-def callees,
closure-captured variables, and wildcard imports (from x import *).
Sanitizer / guard recognition
Two layers.
- Structural dominance guards — a finding is
suppressed only on positive structural proof that a validator guard
dominates the sink: either an enclosing
if html.escape(...)block, or an early-exitif not ok: raisebefore the sink. Early-exit kinds arereturn,raise,break,continue. The vetted sanitizer seed ishtml.escape,bleach.clean,escape,conditional_escape;mark_safeis deliberately excluded — it asserts safety without transforming and is itself the sink. Every uncertainty (module-scope sink, guard after the sink, substring-only match) fails open → the finding fires. - Rule-side sanitizers — the analyzer is deliberately flow-insensitive
(a re-clean
x = sanitize(x)afterx = request.bodydoes not untaint the function environment), so sanitized reassignments are caught at the matcher sink gate via each rule'ssanitizers:list andsanitize_*/escape_*name heuristics. Rules also usebody_excludes_any_ofto suppress when an auth guard (login_required,IsAuthenticated,LoginRequiredMixin, …) is present in the handler.
SCA — Python packaging ecosystem
Python has the broadest lockfile/manifest coverage of any ecosystem. All
parsers emit Ecosystem::Pypi and feed OSV, license, health, and reachability
analysis (see scan-coverage.md §2).
| Parser | Input | Direct vs transitive |
|---|---|---|
requirements.txt | pip plaintext (PEP 440/508: == === ~= != >= <= > <) | all Direct |
requirements lock | pip-compile lock | derived from # via provenance comments |
Pipfile.lock | pipenv JSON (default + develop) | both flattened to Direct |
poetry.lock | Poetry TOML [[package]] | category = "main" → Direct; dev/extras → Transitive |
uv.lock | uv TOML | topological — zero in-degree in the dependency graph → Direct |
pyproject.toml | PEP 621 + Poetry + Hatch dependency tables | all Direct |
VCS, path, URL, and wildcard (*) dependencies are skipped; entries are
deduped on (name, version).
False positives, provenance, and test scope
Python findings flow through the same FP model as every other scanner (false-positives.md). The Python-specific behaviours:
- Source provenance — every origin carries a tier (
Remote<LocalCli<Env<Filesystem, withRemotethe most dangerous).sys.argv/input→LocalCli;os.environ→Env; request objects →Remote. A rule withrequires_source_provenance: { fire: [remote] }gates only on attacker-remote flows; env/CLI sources demote out of the gating stream. APropagated-only set has no tier and fails open (kept). - Test scope — Python test detection is path-based (
test_scope.rsis Rust-only). Findings on test paths (_test.py,/tests/,/testdata/,/spec/,/fuzz/, …) are demoted one severity band; on throwaway test paths, post-demote Info/Low/Medium findings are dropped. Non-production paths (/examples/,/scripts/,conftest.py,/migrations/,Dockerfile, …) are demoted but never pruned. A rule withexcludes_in_scope: [test]is skipped entirely on a Python test path. - Low-confidence SAST is advisory — a bare
ast_callmatch with no import gate, taint trace, or entropy check lands at Low confidence and is routed to the non-gating advisory stream. Taint-backedast_flowfindings are Medium and gate.
Precise call-shape predicates cut Python FPs further: os.getenv('X', '') with
a literal default does not trip a "no-default" rule; verify=False fires only
on the literal False; f-string interpolations are walked for taint but
dict-literal keys, string content, comments, and type annotations are excluded.
Honest scope limits
- Sink coverage is rule-defined, not engine-defined — depth tracks the GVL corpus, not the binary.
- Flow-insensitive — a single fixed-point taint set per function; a re-clean is not tracked (deliberate over-tag bias, mitigated by matcher sanitizers).
- Cross-file resolves only bare-identifier and import-alias-qualified callees — method calls, lambdas, closures, and wildcard imports are not followed across files.
- Multi-target unpacking (
a, b = foo()) collapses to the first identifier. - No base-class walk — a subclass does not inherit a parent's
self.*taint. - The
GraphReachableentry-point policy needs an external oracle; the default falls back to the decorator+name heuristic.
Related documentation
- languages.md — the cross-language capability matrix.
- scan-coverage.md — the six scan types and eight pillars.
- false-positives.md — confidence, effective risk, provenance, waivers.
- tiers.md — Tier-0/1/2, and
banditTier-1 corroboration for Python via--corroborate.
