Skip to main content

Reduce false-positive CVE alerts by checking whether vulnerable dependency code is actually reachable

Project description

ca9 — CVE reachability analysis for Python

ca9

Stop fixing CVEs that don't affect you.

Python 3.10+ License: MPL-2.0 PyPI Zero Dependencies Skylos A+ (99)


The problem

Your SCA tool (Snyk, Dependabot, Trivy, Grype) flags every CVE in your dependency tree. You get 60 alerts. Your team scrambles. But most of those CVEs are in code your application never imports, never calls, and never executes.

You're patching vulnerabilities in functions you don't use, in packages you didn't know you had, in code paths your app will never reach.

That's wasted engineering time. That's alert fatigue. That's how real vulnerabilities get ignored.

What ca9 does

ca9 takes your CVE list and answers one question per vulnerability: is this code actually reachable from your application?

pip install ca9[cli]
ca9 scan --repo . --coverage coverage.json
CVE ID               Package   Severity  Verdict
--------------------------------------------------------------
GHSA-cpwx-vrp4-4pq7  Jinja2    high      REACHABLE
GHSA-frmv-pr5f-9mcr  Django    critical  UNREACHABLE (static)
GHSA-mrwq-x4v8-fh7p  Pygments  medium    UNREACHABLE (dynamic)
--------------------------------------------------------------
Total: 61  |  Reachable: 25  |  Unreachable: 36  |  Inconclusive: 0

59% of flagged CVEs are unreachable — only 25 of 61 require action

36 CVEs eliminated. No manual triage. No guessing.

How it works

ca9 combines three techniques to prove whether vulnerable code is reachable:

1. Static analysis (AST import tracing) — Parses every Python file in your repo and traces import statements. If a vulnerable package is never imported, it's unreachable.

2. Transitive dependency resolution — Uses importlib.metadata to walk the dependency tree. If urllib3 is only installed because requests pulled it in, and requests is never imported, both are unreachable.

3. Dynamic analysis (coverage.py) — Checks whether vulnerable code was actually executed during your test suite. A package might be imported but the specific vulnerable function might never be called.

For each CVE:
  Is the package imported?
  ├── NO  → UNREACHABLE (static)
  └── YES → Was vulnerable code executed in tests?
      ├── NO  → UNREACHABLE (dynamic)
      ├── YES → REACHABLE
      └── No coverage data → INCONCLUSIVE

ca9 is conservative — it only marks something unreachable when it can prove it.

Why ca9 over other tools

ca9 Traditional SCA (Snyk, Dependabot, Trivy) GitHub code scanning
Reachability analysis Static + dynamic + transitive No — flags everything in the dependency tree Limited — no dynamic analysis
Submodule precision Identifies the exact vulnerable function/module Package-level only Varies
Works without SCA tool Yes — ca9 scan queries OSV.dev directly Requires its own scanner Requires GitHub
Dynamic analysis Yes — uses your existing coverage.py data No No
Runtime dependencies Zero (stdlib only) Heavy Hosted service
Setup time pip install ca9[cli] — one command Account, config, integration Repository setup
Output Actionable verdicts with reasoning traces Alert list with no reachability context Alert list
CI integration Pipe JSON output to any tool Vendor-specific dashboards GitHub-only

ca9 doesn't replace your SCA tool. It makes it useful. Snyk finds the CVEs. ca9 tells you which ones matter.

Real-world results

Django REST Framework — 37 CVEs, 19% noise

A focused library that genuinely uses most of its deps. Even here, ca9 found 7 CVEs in packages that are installed but never imported (redis, sentry-sdk, pip):

$ ca9 scan --repo /path/to/drf -v

GHSA-g92j-qhmh-64v2  sentry-sdk  low       UNREACHABLE (static)
                      -> 'sentry-sdk' is not imported and not a dependency of any imported package
GHSA-8fww-64cx-x8p5  redis       high      UNREACHABLE (static)
                      -> 'redis' is not imported and not a dependency of any imported package
...
Total: 37  |  Reachable: 0  |  Unreachable: 7  |  Inconclusive: 30

Flask app with bloated deps — 61 CVEs, 59% noise

A Flask app that imports 4 packages but has 19 pinned in requirements.txt (Django, tornado, Pygments added "just in case"):

$ ca9 scan --repo demo/ --coverage demo/coverage.json

Total: 61  |  Reachable: 25  |  Unreachable: 36  |  Inconclusive: 0

59% of flagged CVEs are unreachable — only 25 of 61 require action

Django alone brought 21 CVEs that were pure noise.

The pattern: ca9's value scales with how bloated your dependency list is — which in enterprise codebases is typically very.

Quick start

Scan installed packages (no SCA tool needed)

pip install ca9[cli]
ca9 scan --repo .

This queries OSV.dev for vulnerabilities in your installed packages. Works with any Python project. No Snyk, no Dependabot, no config files.

Add dynamic analysis for better results

coverage run --source=.,$(python -c "import site; print(site.getsitepackages()[0])") -m pytest
coverage json -o coverage.json
ca9 scan --repo . --coverage coverage.json

Analyze an existing SCA report

ca9 check snyk.json --repo . --coverage coverage.json
ca9 check dependabot.json --repo .

Format is auto-detected. Supports Snyk, Dependabot, Trivy, and pip-audit:

ca9 check snyk.json --repo .
ca9 check dependabot.json --repo .
ca9 check trivy.json --repo .
ca9 check pip-audit.json --repo .

Verdicts

Verdict What it means What to do
REACHABLE Vulnerable code is imported and was executed in tests Fix this
UNREACHABLE (static) Package is never imported — not even transitively Suppress with confidence
UNREACHABLE (dynamic) Package is imported but vulnerable code was never executed Likely safe — monitor
INCONCLUSIVE Imported but no coverage data to prove execution Add coverage or review manually

CLI reference

ca9 scan [OPTIONS]              Scan installed packages via OSV.dev
ca9 check SCA_REPORT [OPTIONS]  Analyze a Snyk/Dependabot report

Options:
  -r, --repo PATH                  Path to the project repository  [default: .]
  -c, --coverage PATH              Path to coverage.json for dynamic analysis
  -f, --format [table|json|sarif]  Output format  [default: table]
  -o, --output PATH                Write output to file instead of stdout
  -v, --verbose                    Show reasoning trace for each verdict

Exit codes:
  0  Clean — no reachable CVEs
  1  Reachable CVEs found — action needed
  2  Inconclusive only — need more coverage data

Config file

Create a .ca9.toml in your project root to set defaults:

repo = "src"
coverage = "coverage.json"
format = "json"
verbose = true

Config is auto-discovered from the current directory upward. CLI flags override config values.

Library usage

import json
from pathlib import Path
from ca9.parsers.snyk import SnykParser
from ca9.engine import analyze

data = json.loads(Path("snyk.json").read_text())
vulns = SnykParser().parse(data)

report = analyze(
    vulnerabilities=vulns,
    repo_path=Path("./my-project"),
    coverage_path=Path("coverage.json"),
)

for result in report.results:
    print(f"{result.vulnerability.id}: {result.verdict.value}{result.reason}")

Zero dependencies

ca9's core library uses only the Python standard library. The click package is optional — only needed if you use the CLI. This means you can embed ca9 in CI pipelines, security toolchains, or other Python tools without adding to your dependency tree.

Limitations

  • Static analysis traces import statements and importlib.metadata dependency trees. Dynamic imports (importlib.import_module, __import__) are not detected.
  • Coverage quality directly impacts dynamic analysis. If your tests don't exercise a code path, ca9 can't detect it dynamically.
  • Transitive dependency resolution requires packages to be installed. Without installed deps, ca9 falls back to direct-import-only checking.
  • Python only (for now).

Development

git clone https://github.com/your-org/ca9.git
cd ca9
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest tests/ -v

License

MPL-2.0

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

ca9-0.1.1.tar.gz (1.7 MB view details)

Uploaded Source

Built Distribution

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

ca9-0.1.1-py3-none-any.whl (30.5 kB view details)

Uploaded Python 3

File details

Details for the file ca9-0.1.1.tar.gz.

File metadata

  • Download URL: ca9-0.1.1.tar.gz
  • Upload date:
  • Size: 1.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.9

File hashes

Hashes for ca9-0.1.1.tar.gz
Algorithm Hash digest
SHA256 70e8b89081a286474c71a0a5533a7c720ccad8c841e79212ff2e8fde19638cd9
MD5 4ad8fc58df3c3a5db2d68fbbff9f218d
BLAKE2b-256 de1b3864ee16ea3e3cfc1f30564f7d3ce752bbe6bdacd63a6468bf9d19e15b33

See more details on using hashes here.

File details

Details for the file ca9-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: ca9-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 30.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.6.9

File hashes

Hashes for ca9-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a939b940120d99f3c1fb3a8be55095ca9170c0df7a8e2e3b8496e2184bc568e9
MD5 a1060ecdea0d78fcf3f3def244d3f733
BLAKE2b-256 a8a5ab5d9aa1b9af696ccfc2533dd571ee8f8ad045819eae3b9e2f441be4d455

See more details on using hashes here.

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