Skip to content

Plugins and Models

Hugo edited this page Sep 25, 2026 · 1 revision

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.

Symbols

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

The manifest

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.

Kinds of plugin

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

Models

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

Merging and conflicts

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.

Models read from the project

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.

Advisories from a plugin

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.

Refining findings

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

Traceability

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.

Clone this wiki locally