Safeguard
Application Security

10 dimensions of Python static analysis

Python static analysis spans ten distinct techniques, from AST linting to reachability analysis — most teams run only two or three, missing real exploitable risk.

Safeguard Research Team
Research
Updated 8 min read

Python static analysis is not one technique — it is at least ten, and most teams only run two or three of them. Choosing among the many python static analysis tools on the market means understanding which of these ten dimensions each one actually covers, since a tool built for one layer rarely covers the rest. A typical Bandit scan checks for roughly 70 built-in security patterns, covering categories like hardcoded passwords (B105) and SQL injection (B608), but it never touches type errors, dead code, or whether a flagged function is actually reachable from untrusted input. Meanwhile PyPI now hosts more than 590,000 packages, and a single pip install can pull in dozens of transitive dependencies that a lint pass never even looks at. CVE-2007-4559, a path-traversal flaw in Python's tarfile.extractall, sat exploitable in an estimated 350,000 open-source repositories for 15 years before Python 3.12 finally shipped a fix in October 2023 — proof that lexical scanning alone misses vulnerabilities that only reachability and data-flow analysis can catch. This post breaks down the ten dimensions that actually matter.

What are the core dimensions of Python static analysis?

The foundational dimensions are lexical/AST analysis, type analysis, and control-flow analysis — three layers that catch different bug classes before a single line of Python executes. Lexical and AST-based tools like Pyflakes and Flake8 parse the abstract syntax tree to flag undefined names, unused imports, and shadowed variables; Flake8's default configuration runs around 80 combined pycodestyle and Pyflakes checks on every commit. Type analysis, formalized by PEP 484 in 2015, lets tools like mypy and Pyright walk function signatures and catch mismatches — passing a str where an Optional[int] is expected — without running the code. Control-flow analysis goes a step further, building a graph of every branch and loop so a scanner can tell whether a dangerous call sits behind an if that's never satisfied in practice, or on a path every request actually takes. None of these three dimensions alone tells you whether a bug is exploitable — they tell you where to look.

How does taint analysis differ from simple pattern matching?

Taint analysis differs from pattern matching because it tracks how untrusted data moves through a program, not just whether a dangerous function appears in the source. A pattern matcher flags every call to os.system() regardless of its argument; a taint engine only flags it when the argument traces back to a source like request.args.get() or sys.argv without passing through a sanitizer. This is the difference between a scanner that fires 200 times on a 50,000-line Flask app and one that fires twice, on the two calls an attacker could actually reach. The tarfile case is the clearest real-world illustration: Trellix researchers disclosed in 2022 that CVE-2007-4559 remained live in hundreds of thousands of codebases because extractall() let archive entries write anywhere on disk via ../ path segments, and static tools that merely flagged "tarfile usage" produced too much noise for anyone to triage. Python 3.12 fixed it by adding a filter argument (defaulting to data as of Python 3.14) that rejects unsafe paths — but teams still running 3.8 through 3.11 needed taint analysis to know which of their own extractall() calls handled attacker-supplied archives at all.

What does reachability analysis add on top of a dependency scan?

Reachability analysis adds proof that a vulnerable function in a dependency is actually invoked by your code, which is what separates an exploitable finding from a theoretical one. A standard software composition analysis (SCA) scan matches your requirements.txt or pyproject.toml against a CVE database and reports every vulnerable package version present — full stop. If your Django project pins a version affected by CVE-2024-27351 (a ReDoS in django.utils.text.Truncator), an SCA tool reports it whether or not your code ever calls that function. Reachability analysis builds a call graph from your entry points down through every import, including transitive dependencies, and checks whether execution can actually reach the vulnerable line. Industry data backs up why this matters at scale: multiple vendor studies from 2023–2024 found that 70–85% of CVEs flagged by SCA tools in typical applications are in code paths that are never called at runtime. For a team triaging 400 open CVE tickets, reachability is what cuts that number to the 60 or so that deserve a sprint.

Which rule frameworks turn raw findings into risk you can prioritize?

Rule frameworks turn raw findings into prioritized risk by mapping each specific check to a CWE (Common Weakness Enumeration) category and a severity score, rather than leaving every result as an undifferentiated "issue." Bandit's roughly 70 checks are grouped into B1xx (misc), B2xx (application), B3xx (blacklists), B4xx (imports), B5xx (crypto), B6xx (injection), and B7xx (XSS/templating) series, and each maps to a CWE — B608 (SQL injection via string formatting) maps to CWE-89, B301 (unsafe pickle deserialization) maps to CWE-502, and B105 (hardcoded password strings) maps to CWE-798. Semgrep extends this with over 30 Python rules aligned to the OWASP Top 10 for 2021, letting a scan output "this is a CWE-89 SQL injection, OWASP A03:2021" instead of a bare line number. Semgrep's rule engine isn't Python-specific either — the same CWE-mapped approach powers javascript static analysis alongside Go, Java, and Ruby rulesets, so teams standardizing on one tool across a polyglot codebase get consistent severity scoring everywhere. That mapping matters operationally: a security team can set a policy of "block the PR on any CWE-89 or CWE-502 finding above CVSS 7.0" instead of manually re-triaging severity every time a new rule fires.

How do dependency, secrets, and typosquatting scans extend analysis past your own code?

These scans extend static analysis past your own code because most Python risk today originates in the supply chain, not in code your team wrote. Dependency-confusion attacks demonstrated this directly: in 2021, researcher Alex Birsan uploaded packages with names matching internal libraries used by more than 35 companies — including Apple, PayPal, and Microsoft — to public PyPI and npm, and internal build systems pulled the public (attacker-controlled) version automatically, earning him over $130,000 in bug bounties. Secrets scanning catches a parallel problem: GitGuardian's 2024 State of Secrets Sprawl report found 23.77 million secrets exposed on public GitHub commits in 2023, a 28% increase year over year, with hardcoded API keys and database credentials the most common category. And typosquatting scans catch malicious packages riding on naming confusion — Phylum's research team documented over 550 malicious packages uploaded to PyPI in a single week in March 2023 alone, many typosquats of popular libraries like requests and urllib3 designed to execute on pip install via malicious setup.py scripts.

Why does SBOM generation matter for a static analysis program?

SBOM generation matters because it turns a one-time scan result into a living inventory you can query the next time a new CVE drops, instead of re-scanning from scratch. A Software Bill of Materials, in CycloneDX or SPDX format, lists every direct and transitive Python dependency, its resolved version, and its license — and since Executive Order 14028 in May 2021 pushed SBOMs into federal procurement requirements, CycloneDX 1.5 (released 2023) added native support for reading dependency metadata straight out of pyproject.toml and poetry.lock. When a new CVE is disclosed — as happened with the pyyaml yaml.load() deserialization issue tracked as CVE-2020-1747 — a team with an up-to-date SBOM can answer "are we affected, and where" in minutes by querying the SBOM, instead of spending days grepping repositories.

How Safeguard Helps

Safeguard runs these ten dimensions as a single connected pipeline instead of ten disconnected python static analysis tools you have to stitch together yourself, and if your stack pairs Python services with a JavaScript frontend, the same reachability-first approach applies to javascript static analysis findings there too. Reachability analysis traces every SCA and SAST finding through your actual call graph, so a CWE-502 pickle-deserialization flaw or a vulnerable transitive dependency only ranks as urgent when your code can actually execute it. Griffin AI reads that same context — the taint path, the CWE mapping, and the reachability verdict — to explain each finding in plain language and generate an auto-fix pull request with the corrected code, so engineers fix real issues in minutes rather than opening a ticket to research a false positive. Safeguard also generates CycloneDX-format SBOMs automatically on every build and ingests SBOMs from vendors and open-source dependencies alike, giving security teams one queryable inventory across their entire Python supply chain — from PyPI typosquats to hardcoded secrets to unpatched CVEs like CVE-2007-4559.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.