
Runtime-aware SCA — proves which CVEs are actually reachable, not just installed.
Runtime-aware SCA — proves which CVEs are actually reachable, not just installed.
VulnReach is now an official OWASP Project. 🎉




Language support: Python is fully production-ready (taint, AST, route, runtime). Java and JavaScript have functional call graph analysis and are experimental. Go, C#, and PHP are on the roadmap. See ROADMAP.md for details.
VulnReach builds on standard SCA output by adding reachability context — proving through static analysis, taint tracking, and live runtime coverage which of the detected CVEs can actually be reached in your application.
POST /scan/{id}/cancel stops in-progress scanssummary + classified buckets on GET /scan/{id}VULNREACH_ALLOW_DOCKER_DAEMON=truedocker-socket-proxyPOST /findings/{id}/next-steps produces analyst-facing remediation guidance (immediate actions, validation probes, upgrade paths, monitoring) for a deterministic finding. Lazy / on-demand: scans never call the LLM, and LLM failures degrade gracefully. The deterministic verdict is read-only. See docs/api.md.scan.runtime.ebpf tracing mode (Linux-focused, explicit opt-in)See:
Each CVE is classified through a five-layer evidence chain:
1. SCA (Trivy) → is the package installed and vulnerable?
2. Taint analysis (tainter) → does user input flow to the vulnerable sink?
3. AST analysis → is the vulnerable function in your call graph?
4. Route exposure → is the call path reachable from an HTTP endpoint?
5. Runtime coverage → was the vulnerable code actually executed?
The result is a prioritised finding list with four tiers:
| Tier | Meaning |
|---|---|
DYNAMICALLY_REACHABLE | Runtime coverage confirmed execution — fix immediately |
Security notice — before starting, copy
.env.exampleto.env.localand replace everyCHANGE_MEvalue with a strong random secret.
Do not expose VulnReach on a public network without setting real credentials and configuringCORS_ORIGINS.
git clone https://github.com/ihrishikesh0896/vulnreach.git
cd vulnreach
# 1. Create your local config
cp .env.example .env.local
# 2. Fill in every CHANGE_ME — generate secrets with: openssl rand -hex 32
$EDITOR .env.local
# 3. Start the stack
docker compose up --build
# Optional: enable dynamic runtime scans (Docker daemon access via restricted socket proxy)
# docker compose -f docker-compose.yml -f docker-compose.runtime.yml up --build
Auth options:
POST /loginSettings -> API Keys, then use it as Authorization: Bearer <API_KEY># Get a token (replace with the credentials you set in .env.local)
TOKEN=$(curl -s -X POST http://localhost:8000/login \
-H "Content-Type: application/json" \
-d '{"username":"<your-admin-user>","password":"<your-admin-password>"}' | jq -r .access_token)
# Start scan from a GitHub repo
curl -X POST http://localhost:8000/scan \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"repo_url":"https://github.com/yourorg/yourapp"}'
# Poll for results
curl http://localhost:8000/scan/<scan_id> \
-H "Authorization: Bearer $TOKEN" | jq .summary
coverage.pypolicy.block_if fails builds on confirmed critical findingsAuthorization: Bearer <API_KEY>)GET /scan/{id}/export/pdfprovider: none; Ollama supported for offline useScan target: multi-tier-dvpa — an intentionally vulnerable Python/Django application with 72 raw CVEs across 11 packages.
46% of findings were moved out of the undifferentiated "fix everything" queue into a prioritised action list. In a typical production service (where many transitive dependencies are never called), this figure rises to 70–90%.
Full methodology, evidence chain detail, and package-level breakdown: docs/benchmark.md
scan.yml config referencetrivy on PATH (install)jq (used in quick start examples — install)Optional (all skip gracefully if absent):
semgrep — pip install semgreptainter — pip install tainter (taint-flow analysis; see development guide)Apache 2.0 — see LICENSE.
STATICALLY_REACHABLE| Code path proven via AST/taint — high priority |
UNCERTAIN | Weak signal only — investigate |
NOT_REACHABLE | No evidence — suppress from alert queue |
| Layer | Result |
|---|
| Raw CVEs (Trivy) | 72 |
| Classified findings (VulnReach) | 90 |
| DYNAMICALLY_REACHABLE — fix now | 49 |
| STATICALLY_REACHABLE — fix this sprint | 23 |
| UNCERTAIN — investigate | 18 |
| NOT_REACHABLE — suppress | 0 |
| CI pipeline gate | BLOCKED |