Skip to content

Dependencies and Advisories

Hugo edited this page Sep 25, 2026 · 1 revision

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.

Getting an advisory database

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

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

Levels of evidence

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.

The advisory file

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

Entry points that are read, not called

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

attacker_arguments

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.

Conditions

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

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

Suppressing a requirement finding

pyyaml==5.3.1  # coretrace: ignore[vulnerable-dependency]

Artifacts

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

Clone this wiki locally