Repository navigation
Architecture
The engine is a pipeline: each stage produces a representation the next one consumes, and a plugin attaches to the stage it needs rather than re-parsing Python.
SourceManager loads files, one project of modules
↓
Frontend parse
↓
PyHIR parser-independent high-level representation
↓
Semantic resolution of imports and scopes
↓
PyIR lowering to a small intermediate representation
↓
CFG control-flow graphs
↓
SSA static single assignment
↓
Data flow abstract interpretation
↓
Interprocedural function summaries, iterated to a fixpoint over the module graph
↓
Taint + refutation flows found, then judged against the guards on their path
↓
Plugins detectors, models, secret scanners, project checks
↓
Reporters text, JSON, SARIF, SBOM, VEX
Full design and migration plan: docs/architecture.md in the repository.
src/coretrace_python/:
| Package | Role |
|---|---|
source |
Source manager: loading and identifying files |
frontend |
Parsing |
hir |
PyHIR, the parser-independent high-level representation |
semantic |
Scope, import and symbol analysis; SymbolId lives here |
ir |
PyIR, the small intermediate representation |
cfg |
Control-flow graphs |
dataflow |
Data-flow analyses |
abstract |
Abstract interpretation |
interprocedural |
Function summaries across the module graph |
taint |
Sources, sinks, sanitizers, flows and the model types |
analysis |
Analysis providers the detectors consume |
dependency |
Dependency graph, advisories, policy |
findings |
The finding model, severity and confidence |
plugins |
Plugin API and loading |
bundled |
The plugins shipped with the package |
reporters |
Output formats |
PyHIR is parser-independent, and PyIR is deliberately small. Everything downstream —
control flow, SSA, data flow, taint — works on a handful of constructs rather than on the whole
Python grammar. That is what makes an unsupported construct a local loss: a function using
syntax outside the subset is reported as unsupported-syntax and the rest of the file is still
analysed.
You can look at both:
coretrace-python-analyzer --emit-ir src/app.py
coretrace-python-analyzer --emit-ir --ssa src/app.pyTaint follows values between functions and across files through function summaries, into objects (containers, attributes, instances of the project's own classes) and through closures.
A summary keeps, for each parameter, what reaches the return value or an external call — and the taint kinds cleared on every path from that parameter, which is how a sanitizer inside a project function protects every caller.
Summaries are iterated to a fixpoint over the module graph. That is why analysis time grows a little faster than linearly with project size — see Performance and Caching.
Finding a flow and deciding what it means are separate. The refutation stage takes a flow and the guards dominating it and returns a verdict with its evidence, which travels in the finding's metadata. See How a Flow is Judged.
python -m venv .venv
python -m pip install -e ".[dev]"
python -m mypy
python -m pytest
python -m ruff check .The non-regression suite analyses the public repositories pinned in
tests/regression/repositories.toml and compares findings and coverage against the snapshots
in tests/regression/expected/. It clones on first use, needs the network, and runs in its own
CI job:
python -m pytest -m regression
CORETRACE_REGRESSION_UPDATE=1 python -m pytest -m regression # record an intended changeSnapshots record the verdict of each taint finding, so a change that turns a vulnerability into a hotspot shows up as a diff rather than passing silently.