Skip to main content

The best reachability engine for Python: tells you which dependency vulnerabilities are actually reachable from your own code.

Project description

VulnAdvisor

Stop scanning, start triaging.

VulnAdvisor is an open-source CLI that tells you which of your Python dependency vulnerabilities are actually reachable from your own code — ranked by real-world exploit likelihood (EPSS + CISA KEV) and explained in plain English.

It runs locally. Your source code never leaves your machine, and there is no telemetry. Network calls are only to public vulnerability/risk APIs (OSV, GitHub Advisory, EPSS, CISA KEV) and, optionally, your own LLM key.

In our reproducible benchmark, reachability triage cuts a naive scanner's findings by ~54% with zero missed reachable criticals — see benchmarks/REPORT.md.

Install

pip install vulnadvisor
# or run without installing:
uvx vulnadvisor --help

Requires Python 3.12+.

Quickstart (under 5 minutes)

Point it at any Python project that has a requirements.txt (or a lockfile):

uvx vulnadvisor scan examples/quickstart

You'll get one three-card finding per vulnerability, highest priority first:

  • Card A — Attack story: plain-English explanation specific to the finding.
  • Card B — Risk: a Red/Yellow/Green badge from the EPSS+KEV-driven priority.
  • Card C — Action: the verdict, the deterministic priority, the exact upgrade command, and the reachability tier with its evidence (the import site or, when known, the call path).

In the bundled examples/quickstart, PyYAML is imported and used, so its advisories are surfaced as IMPORTED with the import site — actionable. The declared-but-unused requests is deprioritized (no path from your code). That split — flag what you use, demote what you don't — is the triage.

Tip: for the most precise NOT-IMPORTED verdicts, install VulnAdvisor into the same environment as your project (pip install vulnadvisor in your project venv) so it can read installed package metadata to map distributions to import names with high confidence. Run in isolation (uvx) and a dependency it can't confidently map stays the cautious DYNAMIC-UNKNOWN.

Function-level call paths (IMPORTED-AND-CALLED)

The strongest tier — a concrete path from your code to the vulnerable symbol — needs the advisory→vulnerable-symbol dataset. Build it once (queries OSV fix commits locally):

vulnadvisor backfill --top 50      # or: vulnadvisor backfill pyyaml jinja2 ...

When the symbol an advisory patched is one your code calls directly, the next scan upgrades the finding to IMPORTED-AND-CALLED and prints the path (e.g. main -> parse -> yaml.load). Advisories whose fix touches only library-internal symbols reached via a public API stay at IMPORTED (sound: we never claim a call we can't prove) — improving that linkage is on the roadmap.

Plain-English explanations (optional)

Card A uses a deterministic template by default. Export an Anthropic key to get an LLM-written "attack story" (priority stays deterministic — the model only explains, it never changes the number):

export ANTHROPIC_API_KEY=sk-...
vulnadvisor scan .            # Card A now reads like a senior engineer wrote it
vulnadvisor scan . --no-explain   # turn it off

Reachability tiers (the noise-killer)

Every finding carries a confidence tier — VulnAdvisor never gives a binary "reachable / not":

  • IMPORTED-AND-CALLED — a concrete call path to the vulnerable symbol exists (function-level).
  • IMPORTED — the package is imported by your code (evidence: the import site, file:line).
  • DYNAMIC-UNKNOWN — dynamic import/exec, reflection, unreadable files, or an uncertain import-name mapping mean usage cannot be ruled out. Never treated as safe.
  • NOT-IMPORTED — the package is never imported. The only confidently-safe tier; these are deprioritized and labeled "No path from your code".

Soundness is the rule: a false "you're safe" can cause a breach, so anything uncertain escalates to DYNAMIC-UNKNOWN rather than being silently downgraded. Reflective getattr dispatch is resolved with optional Pyright type info, and framework-routed handlers (FastAPI routes, Django URLconf views and @receiver signals) are recognized as entry points so the call path is rooted at the real handler.

Priority scoring (deterministic)

Priority is computed by code and is fully reproducible — no randomness, no clock, no I/O. The optional LLM layer only explains a finding; it never changes the number.

Given a CVSS base severity (0–10), an EPSS exploit probability (0–1), and CISA KEV membership:

sev  = severity / 10
risk = 0.6 * epss + 0.4 * sev      # when EPSS is known
risk = sev                         # when EPSS is unknown (severity is not zeroed out)
value = round(100 * risk, 1)       # 0–100 priority

EPSS is weighted above severity because triage is about real-world exploit likelihood. Soundness guards: KEV dominates (a known-exploited vuln is floored to 90/CRITICAL), and an unknown CVSS falls back to a moderate default (5.0, flagged) rather than being scored as 0.

Bands → verdicts:

Score Band Verdict
≥ 90 CRITICAL Fix now
70–89.9 HIGH Fix this sprint
40–69.9 MEDIUM Plan a fix
15–39.9 LOW Monitor
< 15 INFO Deprioritize

Output formats & CI gating

vulnadvisor scan PATH --format {terminal,json,sarif}:

  • terminal (default) — the three-card view.
  • json — a stable machine report (schema_version 1.0): tool, degraded_sources, summary, and findings[] ordered by descending priority (each with dependency, advisory, EPSS, KEV, score, reachability + call paths, and the minimal safe fix command).
  • sarif — valid SARIF 2.1.0, so results show up in the GitHub Security tab, ordered by our triage priority.

--fail-on <band|score> sets the exit code: the scan exits 1 when any finding meets or exceeds the threshold (a band name like high, or a number 0100), else 0. Invalid values are a usage error (exit 2).

GitHub Actions

- name: VulnAdvisor reachability triage
  run: |
    pipx install vulnadvisor
    vulnadvisor scan . --format sarif --fail-on high > results.sarif
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: results.sarif

Privacy

VulnAdvisor is built for environments where source code must not leave the machine:

  • No telemetry, no analytics, no phone-home. Ever.
  • Source code is analyzed locally; only dependency names/versions are sent to public advisory APIs (OSV, GitHub Advisory, EPSS, CISA KEV), cached in a local SQLite database.
  • The optional LLM layer is the only call to a non-public service, uses your own ANTHROPIC_API_KEY, and sends only the finding metadata it needs to write the explanation. It is off unless you set the key, and --no-explain disables it entirely.

Develop

uv sync
uv run ruff check && uv run ruff format --check
uv run mypy --strict src
uv run pytest

See CONTRIBUTING.md. Run the noise-reduction benchmark with uv run python -m benchmarks.

License

Apache-2.0. © VulnAdvisor contributors.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vulnadvisor-1.0.3.tar.gz (231.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vulnadvisor-1.0.3-py3-none-any.whl (99.3 kB view details)

Uploaded Python 3

File details

Details for the file vulnadvisor-1.0.3.tar.gz.

File metadata

  • Download URL: vulnadvisor-1.0.3.tar.gz
  • Upload date:
  • Size: 231.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for vulnadvisor-1.0.3.tar.gz
Algorithm Hash digest
SHA256 3a17dc4971c7e64cbe7a5fd43ceba138a48d2f4fb9189eeb501b4fd67a610c1b
MD5 4e60e03ea551e7e53f6baac0e5f824e3
BLAKE2b-256 5f86ae020fe98f68a657b6d03221b8c5f0608b3855d6a38cf274bfac1affe1f4

See more details on using hashes here.

Provenance

The following attestation bundles were made for vulnadvisor-1.0.3.tar.gz:

Publisher: release.yml on Parthav99/vulnadvisor_v2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vulnadvisor-1.0.3-py3-none-any.whl.

File metadata

  • Download URL: vulnadvisor-1.0.3-py3-none-any.whl
  • Upload date:
  • Size: 99.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for vulnadvisor-1.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 ec04204fbdf84311517f8c24c0c2d479a7abdba2c2d5a52d8a3de10188fa8eb3
MD5 8a4efd1c30f085130da74d377fd300a0
BLAKE2b-256 e9c606629be1b4e2d167b8dfbaf84e9afaa6fc0503b64b84c80a939957855cd3

See more details on using hashes here.

Provenance

The following attestation bundles were made for vulnadvisor-1.0.3-py3-none-any.whl:

Publisher: release.yml on Parthav99/vulnadvisor_v2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page