Documentation
CA9 is a CVE reachability analysis tool for Python. It takes your CVE list and answers one question per vulnerability: is this code actually reachable from your application?
Installation
pip install ca9[cli]
The core library depends only on packaging and the Python standard library. The click package is included with the [cli] extra.
Quick Start
Scan your project against OSV.dev — no config files needed:
pip install ca9[cli] ca9 scan --repo .
Or analyze an existing SCA report:
ca9 check snyk.json --repo .
ca9 scan
Scans installed packages via OSV.dev. Works with any Python project — no Snyk, Dependabot, or config files required.
ca9 scan --repo . ca9 scan --repo . --format json --output results.json ca9 scan --repo . --coverage coverage.json --verbose
ca9 check
Analyzes an existing SCA report. 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 .
CLI Options
Common options
| Option | Description | Default |
|---|---|---|
| -r, --repo PATH | Path to the project repository | . |
| -c, --coverage PATH | Path to coverage.json for dynamic analysis | — |
| -f, --format | Output format: table, json, or sarif | table |
| -o, --output PATH | Write output to file instead of stdout | — |
| -v, --verbose | Show reasoning trace for each verdict | — |
| --no-auto-coverage | Disable automatic coverage discovery | — |
| --show-confidence | Show confidence score in table output | — |
| --show-evidence-source | Show evidence extraction source | — |
Scan-only options
| Option | Description |
|---|---|
| --offline | Use only cached OSV data, no network requests |
| --refresh-cache | Clear OSV cache before fetching |
| --max-osv-workers N | Max concurrent OSV detail fetches (default: 8) |
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Clean — no reachable CVEs |
| 1 | Reachable CVEs found — action needed |
| 2 | Inconclusive only — need more coverage data |
Verdicts
CA9 classifies each CVE into one of four verdicts:
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.
Confidence Scoring
Every verdict is backed by structured evidence. Use --show-confidence to see scores.
Signals checked
version_in_range— Is the installed version within the affected range?package_imported— Is the package imported anywhere in the repo?submodule_imported— Is the specific vulnerable submodule imported?coverage_seen— Was the vulnerable code executed during tests?affected_component_source— How was the vulnerable component identified?
Confidence buckets
| Bucket | Score | Meaning |
|---|---|---|
| High | 80–100 | Strong evidence supports the verdict |
| Medium | 60–79 | Moderate evidence, reasonable certainty |
| Low | 40–59 | Weak evidence, treat with caution |
| Weak | 0–39 | Very little evidence, manual review recommended |
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 is 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.
Decision logic:
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 → INCONCLUSIVESCA Tool Integrations
CA9 supports reports from four SCA tools, auto-detected by format:
Snyk
ca9 check snyk.json --repo .Dependabot
ca9 check dependabot.json --repo .Trivy
ca9 check trivy.json --repo .pip-audit
ca9 check pip-audit.json --repo .Output Formats
Three output formats via -f / --format:
table— Human-readable table (default)json— Machine-readable JSON with full evidence objectssarif— SARIF format with stable fingerprints for CI integration
CI/CD Integration
Use exit codes and SARIF output to integrate CA9 into your pipeline:
# GitHub Actions example
- name: Run CA9 reachability check
run: |
pip install ca9[cli]
ca9 check snyk.json --repo . --format sarif --output ca9.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ca9.sarifConfig File
Create a .ca9.toml in your project root to set defaults. CLI flags override config values.
# .ca9.toml repo = "src" coverage = "coverage.json" format = "json" verbose = true
Caching & Offline Mode
CA9 caches OSV vulnerability details (24h TTL) and GitHub commit file lists (7-day TTL) at ~/.cache/ca9/.
ca9 scan --repo . --offline # use cached data only ca9 scan --repo . --refresh-cache # clear cache and re-fetch
Set GITHUB_TOKEN to avoid GitHub API rate limits when CA9 fetches commit data:
export GITHUB_TOKEN=ghp_... ca9 check snyk.json --repo .
Python API
Use CA9 as a library in your own Python code:
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}")
print(f" confidence: {result.confidence_score}")
print(f" reason: {result.reason}")Limitations
- Static analysis traces
importstatements and 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 cannot 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).
CA9 is open source under the MPL-2.0 license. Built by Skylos.
