GCA · LANGUAGE GUIDES

    Python

    Deep-dive: Python is a primary language with the deepest GVL rule coverage.

    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 typeApplies to Python?Depth
    SASTFull tree-sitter front-end + parameter-bound taint with cross-file workspace tracking. Deepest rule catalogue of any language.
    SCASix manifest/lockfile parsers (see below); OSV vulnerabilities, license, health, reachability.
    SecretsLanguage-agnostic — regex + entropy + git history over .py and config text.
    ConfigLanguage-agnostic — CI/CD, .env, MCP, system prompts.
    APIPython 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 prefixeshandle_, view_, get_, post_, put_, delete_, patch_. These are gated on a web-framework import so get_config() and put_cache() in a non-web module are not treated as HTTP handlers; without a framework import or decorator, a prefix match is downgraded to the LocalCli provenance tier rather than Remote.
    • Exact nameshandle, view, endpoint, index.
    • Request-like parameter — a parameter named request/req, or annotated HttpRequest/...Request, sets is_inferred_request_bound.
    • Decorators — see framework awareness below.

    2. Known input-sink calls (KNOWN_INPUT_CALLS) — substring-matched on an assignment right-hand side:

    x = request.args["q"] # Remote
    data = request.get_json() # Remote
    name = input() # LocalCli
    mode = sys.argv[1] # LocalCli
    key = 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 gateflask, fastapi, django, starlette, aiohttp, tornado, bottle, falcon, sanic, quart. Presence flips name-prefix handlers to the Remote provenance tier.
    • Web-handler decorator suffixesroute, 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 forces Remote.
    • CLI decorator suffixescommand, group, callback (Click/Typer) yield the LocalCli tier; a web decorator wins if both are present.
    • Django views — function-based views are caught by the request-parameter heuristic; rules distinguish real views from Model.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:

    ClassSinks (as they appear in code)CWE
    Deserializationpickle.loads(data), marshal.loads(data), yaml.load(stream) (no SafeLoader), yaml.unsafe_load(stream)CWE-502
    Code executioneval(expr), exec(code), __import__(name)CWE-94
    OS command injectionos.system, os.popen, subprocess.run/call/check_output/check_callCWE-78
    SQL injectioncursor.execute(f"... {x}"), db.execute(...)CWE-89
    Path traversal / SSRFopen(), os.path.join(BASE, request_data) with a request-bound pathCWE-22
    import pickle, yaml
    from flask import request
    @app.post("/import") # web decorator → Remote taint
    def import_state():
    blob = request.get_json()["b"] # KnownInputSink, Remote
    return pickle.loads(blob) # CWE-502 deserialization sink
    def view_run(request): # name prefix + request param → entry point
    cmd = 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.

    gadriel code scan . # workspace-aware (default)
    gadriel code scan . --no-workspace-taint # per-file fallback
    • The switch is --no-workspace-taint (also [scan] workspace_taint = false in .gadriel.toml, or GADRIEL_SCAN_WORKSPACE_TAINT=false). Default is true.
    • 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.

    1. 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-exit if not ok: raise before the sink. Early-exit kinds are return, raise, break, continue. The vetted sanitizer seed is html.escape, bleach.clean, escape, conditional_escape; mark_safe is 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.
    2. Rule-side sanitizers — the analyzer is deliberately flow-insensitive (a re-clean x = sanitize(x) after x = request.body does not untaint the function environment), so sanitized reassignments are caught at the matcher sink gate via each rule's sanitizers: list and sanitize_*/escape_* name heuristics. Rules also use body_excludes_any_of to 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).

    ParserInputDirect vs transitive
    requirements.txtpip plaintext (PEP 440/508: == === ~= != >= <= > <)all Direct
    requirements lockpip-compile lockderived from # via provenance comments
    Pipfile.lockpipenv JSON (default + develop)both flattened to Direct
    poetry.lockPoetry TOML [[package]]category = "main" → Direct; dev/extras → Transitive
    uv.lockuv TOMLtopological — zero in-degree in the dependency graph → Direct
    pyproject.tomlPEP 621 + Poetry + Hatch dependency tablesall 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, with Remote the most dangerous). sys.argv/inputLocalCli; os.environEnv; request objects → Remote. A rule with requires_source_provenance: { fire: [remote] } gates only on attacker-remote flows; env/CLI sources demote out of the gating stream. A Propagated-only set has no tier and fails open (kept).
    • Test scope — Python test detection is path-based (test_scope.rs is 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 with excludes_in_scope: [test] is skipped entirely on a Python test path.
    • Low-confidence SAST is advisory — a bare ast_call match with no import gate, taint trace, or entropy check lands at Low confidence and is routed to the non-gating advisory stream. Taint-backed ast_flow findings 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 GraphReachable entry-point policy needs an external oracle; the default falls back to the decorator+name heuristic.

    • 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 bandit Tier-1 corroboration for Python via --corroborate.