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

OptionDescriptionDefault
-r, --repo PATHPath to the project repository.
-c, --coverage PATHPath to coverage.json for dynamic analysis
-f, --formatOutput format: table, json, or sariftable
-o, --output PATHWrite output to file instead of stdout
-v, --verboseShow reasoning trace for each verdict
--no-auto-coverageDisable automatic coverage discovery
--show-confidenceShow confidence score in table output
--show-evidence-sourceShow evidence extraction source

Scan-only options

OptionDescription
--offlineUse only cached OSV data, no network requests
--refresh-cacheClear OSV cache before fetching
--max-osv-workers NMax concurrent OSV detail fetches (default: 8)

Exit Codes

CodeMeaning
0Clean — no reachable CVEs
1Reachable CVEs found — action needed
2Inconclusive 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

BucketScoreMeaning
High80–100Strong evidence supports the verdict
Medium60–79Moderate evidence, reasonable certainty
Low40–59Weak evidence, treat with caution
Weak0–39Very 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 → INCONCLUSIVE

SCA 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 objects
  • sarif — 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.sarif

Config 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 import statements 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.