Repository navigation
Dependencies and Advisories
A vulnerable requirement is a fact about a file. Whether it is a vulnerability in your project depends on whether your code reaches it, and whether attacker data gets there. The analyzer reports those as distinct levels of evidence rather than one alarm.
Everything here works offline.
The bundled sample-advisories plugin ships a small offline database. A real feed stays offline
too: convert a public OSV dump once, then read it at every check.
coretrace-python-analyzer --import-advisories osv-dump.zip advisories.json
coretrace-python-analyzer --check src/ --advisories advisories.jsonA directory check reads advisories.json at the project root and every file passed with
--advisories. A local entry wins over a plugin's for the same advisory.
Every dependency finding records the highest level established, in its level metadata:
| Level | Meaning |
|---|---|
declared |
The requirement allows a vulnerable version |
imported |
A module of the package is imported somewhere |
reachable |
An entry point or affected symbol is called, or an entry point marked read is read |
exploitable |
Attacker input reaches it — reported as critical |
reachable and exploitable also carry entry_point, justification, conditions, and which
conditions the call meets (conditions_met) or leaves open (conditions_pending_review).
A requirement whose entry points are only called with arguments that contradict a condition
stays imported, and its finding lists those calls in ruled_out — for example
app.views:12 python.yaml.load(Loader=python.yaml.SafeLoader). The evidence for not being
affected is shown rather than silently dropped.
{
"schema": 1,
"advisories": [
{
"id": "CVE-2020-1747",
"package": "pyyaml",
"vulnerable": "<5.4",
"summary": "yaml.load can execute arbitrary code from untrusted documents",
"severity": "critical",
"affected_symbols": ["python.yaml.constructor.FullConstructor.construct_python_object_apply"],
"entry_points": [
{
"symbol": "python.yaml.load",
"justification": "same path when Loader is FullLoader",
"conditions": [
{"kind": "argument", "text": "Loader is FullLoader, the default",
"argument": "Loader", "position": 1,
"values": ["python.yaml.FullLoader", "python.yaml.loader.FullLoader"],
"default": true},
{"kind": "semantic", "text": "the document carries a python/object/apply tag"}
]
}
],
"modules": ["yaml"],
"aliases": ["GHSA-8q59-q68h-6hv4"]
}
]
}| Field | Meaning |
|---|---|
vulnerable |
A version specifier, or several separated by || when several release series are affected |
affected_symbols |
The functions the fix changed |
entry_points |
The public APIs through which a project reaches them |
modules |
Top-level modules the package installs, so an imported package is told from a merely required one. Defaults to the package name. |
A call to either kind of symbol is reachable. Each entry point carries the justification that ties it to the fix — the commit, or the call path.
"read": true marks an attribute whose getter runs the affected code, such as a request body
parsed on first access:
{"symbol": "python.flask.request.form", "justification": "…", "read": true}Reading it is reachable, called or not: request.form['name'], for key in request.form and
request.form.get('name') all read it. A function reading it several times is reported once,
where it first reads it.
Most vulnerabilities need attacker input in one argument: the path send_from_directory
serves, not its download_name; the URL requests.get fetches, not its body.
"attacker_arguments": [{"argument": "url", "position": 0}]Named by keyword argument and, when it may be passed positionally, by position.
"variadic": true extends a position to every later one — as safe_join(directory, *pathnames)
takes: {"position": 1, "variadic": true}.
A call is exploitable only when attacker input reaches one of them; attacker input in another
argument leaves it reachable. An argument unpacked with *args/**kwargs may fill any of
them, so it counts and a wrapper forwarding them still reaches the entry point. An entry point
without attacker_arguments is exploitable through any argument.
An argument condition is decided at each call. It names the argument by keyword and, when
it may be passed positionally, by position (counted from 0, a method's receiver excluded).
values are what makes the call affected, written as the analyzer sees arguments: a symbol by
its canonical name (python.yaml.FullLoader, and every spelling a project may use, such as
python.yaml.loader.FullLoader), a constant as Python writes it (True, None, '/static').
default: true says an absent argument means an affected value.
- A call passing another known value does not reach the vulnerability.
- A call passing a variable, or unpacking
*args/**kwargs, leaves the condition pending review rather than deciding either way.
A semantic condition — the document's tags, the platform, the template's origin — cannot be
decided and is always pending review, never dropped.
--policy FILE, or coretrace-policy.toml at the root, adds two rules:
| Rule | Trigger |
|---|---|
denied-dependency |
A package the policy denies |
unpinned-dependency |
A requirement without an exact pin when the policy requires pins |
pyyaml==5.3.1 # coretrace: ignore[vulnerable-dependency]
--sbom PATH writes a CycloneDX bill of materials; --vex PATH writes an OpenVEX document
saying, for each advisory affecting a requirement, whether it affects the project. Both name the
advisory data they were produced with — see Reports and CI.