Repository navigation
Plugins and Models
Everything the analyzer knows about a framework — which calls are sources, which are sinks, what sanitizes what — is declared in plugins. Supporting another framework, or teaching the analyzer your project's own validation helpers, does not mean forking it.
coretrace-python-analyzer --check src/ --plugins ./my-plugins
coretrace-python-analyzer --check src/ --plugins ./my-plugins --no-bundled-plugins--plugins is repeatable and loads on top of the bundled plugins.
Full guide: docs/plugins.md in the repository.
Every API is named by a canonical symbol: python. followed by the dotted path, resolved through
imports and aliases. python.os.system therefore covers os.system, from os import system and
from os import system as run.
Calling a symbol denotes that symbol, so sqlite3.connect(p).cursor().execute is
python.sqlite3.connect.cursor.execute, and app = Flask(__name__) gives app.route the symbol
python.flask.Flask.route. Builtins are python.builtins.<name>.
name = "sensitive-logging"
version = "1.0.0"
plugin_api = ">=1,<2"
requires = ["ir.ssa"]
provides = ["vulnerability.sensitive-logging"]
[entrypoint]
module = "sensitive_logging"
class = "SensitiveLoggingPlugin"| Field | Meaning |
|---|---|
name |
Unique plugin name, also used in cache keys |
version |
The plugin's own version; changing it invalidates cached results |
plugin_api |
The plugin API versions supported. The current API is version 1. |
requires |
Capabilities the plugin needs; a mismatch with the class's requires is a manifest error |
provides |
Capabilities offered, for documentation and conflict detection |
entrypoint |
The module (without .py, next to the manifest) and the class in it |
The entrypoint module may be a package (<module>/__init__.py) when a plugin grows past one
file. Two plugins may use the same module name; each is imported under a name derived from its
path.
| Base class | Purpose |
|---|---|
SymbolCallDetector |
Report a call to given symbols |
TaintDetector |
Report a taint flow |
ModelPlugin |
Declare sources, sinks, sanitizers — reports nothing itself |
SecretDetector |
Recognise a secret pattern |
ProjectPlugin |
Project-wide checks, and refine
|
Plugin |
Anything else |
A ModelPlugin declares what the detectors consume:
class BottleModels(ModelPlugin):
name: ClassVar[str] = "bottle-models"
models: ClassVar[tuple[Model, ...]] = (
EntryPoint(SymbolId("python.bottle.route"), "http"),
Source(SymbolId("python.bottle.request.query"), "http"),
Sink(SymbolId("python.bottle.redirect"), TaintKind.REDIRECT),
Sanitizer(SymbolId("python.bottle.html_escape"), TaintKind.HTML),
)| Model | Declares |
|---|---|
Source(symbol, label, kinds=ALL) |
A call or attribute whose value is attacker-controlled; label names it in messages (http, stdin, argv, http-response) |
Sink(symbol, kinds, positions=(), keywords=()) |
A call dangerous for kinds. positions/keywords limit a kind to those arguments; a starred argument or **kwargs reaches no restricted kind. |
Sanitizer(symbol, kinds) |
A call whose result is safe for kinds, including inside a project function the data goes through |
EntryPoint(symbol, label, kinds=ALL) |
A decorator or class base whose functions receive their parameters as label input |
TypedParameter(symbol, label, kinds=ALL) |
A class whose annotated parameters carry label input, such as Django's HttpRequest
|
NamedParameter(pattern, label, kinds) |
A regex on parameter names, as password carries CREDENTIAL
|
RouteRegistrar(symbol, argument, label, …) |
A call registering a view, such as Django's path
|
SuffixSink(suffix, kinds, positions=()) |
A sink matched by the end of the symbol, for methods of any class such as objects.raw
|
SafeArgument(symbol, argument, values, …) |
A call to symbol whose argument denotes one of values is not a sink, such as yaml.load with Loader=yaml.SafeLoader. Only an explicitly given value makes the call safe. |
TemplateRender(symbol, position=0, keyword=…) |
A call rendering with autoescaping, such as render_to_string
|
Validator(symbol, kinds=ALL, argument=0) |
A callable whose truth proves its argument safe for the kinds it declares |
AuthorizationGuard(symbol, label) |
A decorator or condition restricting who reaches the code; a flow behind it is a hotspot |
Two plugins may describe the same symbol. An identical model is registered once, and two Sink
models merge their kinds, positions and keywords — so a plugin can add SQL to a sink
another declared for COMMAND. Any other difference is a conflict that stops the analysis with
a ModelError naming both plugins.
Plugin.project_models(root) lets a plugin read models from the project under analysis — for
instance validators the project declares in a file:
class DeclaredValidators(ModelPlugin):
name: ClassVar[str] = "declared-validators"
def project_models(self, root: Path) -> Sequence[Model]:
path = root / "validators.txt"
lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() else []
return tuple(Validator(SymbolId(line)) for line in lines if line)It is called once per directory check, in every process, before any module, and its models
are part of the cache key. A single-file check reads no project. A model declared wrongly raises
ModelError, reported as an error (exit status 2) rather than a traceback.
A Validator may name a function of the project by its project symbol
(python.hc.accounts.views._allow_redirect), and the refutation recognises it in its own module
too, where a call to it has no imported symbol.
A model plugin may carry advisories, a tuple of Advisory values
(coretrace_python.dependency), as the bundled sample-advisories plugin does. Requirements
matching them are reported, calls to the affected symbols become reachable vulnerabilities, and
tainted calls become exploitable ones. See
Dependencies and Advisories.
ProjectPlugin.refine(ctx, findings) is called once with every finding of the run, after all
plugins have reported. It returns the same findings, in the same order, and may only change
severity and confidence or add metadata.
The rule, the location, the message and the evidence a finding carries are not the refiner's to touch: the engine rejects a refinement that changes or drops any of them. A plugin can express "this matters less here", never "this did not happen".
Each plugin a run loaded appears in the report by manifest name and version, with the SHA-256 of its content — and the cache key uses the same digest. See Reports and CI.