Skip to content

Architecture

Hugo edited this page Sep 25, 2026 · 1 revision

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.

Source layout

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

Why a representation of its own

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.py

Interprocedural analysis

Taint 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.

Judging, as a separate step

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.

Testing

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 change

Snapshots 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.

Clone this wiki locally