API service for prioritize vulnerabilities. It's combination of:
- FastAPI (API gateway)
- vulnx (util to get more info for vulnerabilities)
- CVE_Prioritizer (idea of categorization)
- Critical (if exists POC or Nuclei template or vulnerability in KEV)
- High (CVSS > 6.0, EPSS > 0.2)
- Medium (CVSS > 6.0, EPSS <= 0.2)
- Low (CVSS <= 6.0, EPSS > 0.2)
- Info (everything else)
- Undefined (some trouble with CVSS and EPSS score for vulnerability)
docker run -d -p 8000:8000 --env PDCP_API_KEY=<your_key> --name cve-paas ghcr.io/denimoll/cve-paas:latest
You need to install docker itself and then do next commands:
# git clone https://github.com/denimoll/CVE-PaaS.git
# cd CVE-PaaS
# bash start.sh
For the first start you must write API key from ProjectDiscovery (you need to be registered).
Environment variables (pass via docker run --env):
Required
| Variable | Description |
|---|---|
PDCP_API_KEY |
API key from ProjectDiscovery Cloud Platform — required by vulnx to fetch CVE data |
Optional
| Variable | Default | Description |
|---|---|---|
CVSS_THRESHOLD |
6.0 |
CVSS score boundary for High/Medium/Low |
EPSS_THRESHOLD |
0.2 |
EPSS score boundary for High/Low |
ALLOW_ORIGINS |
* |
Comma-separated allowed CORS origins |
CACHE_TTL_HOURS |
0 |
Hours before cached result expires and re-fetches (0 = never expire) |
CVE_PAAS_API_KEY |
(not set) | If set, all /v1/ requests must include X-API-Key: <value> header — returns 401 otherwise |
RATE_LIMIT |
0 |
Max requests per minute per client IP across all endpoints (0 = disabled) — returns 429 when exceeded |
DB_PATH |
cve_cache.db |
Path to the SQLite cache file inside the container |
VULNX_BATCH_SIZE |
5 |
CVEs per single vulnx API call — reduce if hitting PDCP rate limits |
VULNX_BATCH_DELAY |
2.0 |
Seconds to wait between batch calls to avoid PDCP rate limits (0 = no delay) |
VULNX_RETRY_COUNT |
3 |
Total fetch attempts per batch (1 = no retry) — retries on timeout or rate-limit errors |
VULNX_RETRY_DELAY |
5.0 |
Seconds to wait between retry attempts |
ERROR_RECORD_TTL_HOURS |
24 |
Hours before a failed-fetch error record is pruned from cache (0 = keep forever) |
TRUSTED_PROXY_COUNT |
0 |
Number of trusted reverse-proxy hops — enables reading the real client IP from X-Forwarded-For for rate limiting (e.g. 1 for nginx/Traefik in front of the service) |
LOG_FORMAT |
text |
Log output format: text (human-readable) or json (one JSON object per line, for log aggregators like Loki/CloudWatch) |
Note:
RATE_LIMITuses an in-memory counter and is effective only when running a single uvicorn worker (the default). With--workers Neach process has its own counter and the effective limit isN × RATE_LIMIT.
The SQLite cache is stored at the path defined by DB_PATH (default /app/cve_cache.db). Without a volume mount the cache is lost on container restart. To persist it:
docker run -d -p 8000:8000 \
--env PDCP_API_KEY=<your_key> \
-v /path/on/host/cve_cache.db:/app/cve_cache.db \
--name cve-paas ghcr.io/denimoll/cve-paas:latest
All API Endpoints described on localhost:8000/docs:
- GET / - homepage / health check
- GET /v1/get_info/{cve_id} - Main function: return info from vulnx (old cvemap) and determine priority
- POST /v1/cve - Bulk lookup: accepts
{"cve_ids": ["CVE-YYYY-NNNNN", ...]}(1–50 IDs), returns map of CVE ID → priority result - GET /v1/reget_info/{cve_id} - Force refresh: invalidate cache and re-fetch from vulnx
- GET /v1/all_results - Return cached results with full raw info (without priorities); supports
?offset=0&limit=100(max 1000) - POST /v1/retry_errors - Re-fetch all CVEs that previously failed to load (stored as error records)
- DELETE /v1/results - Remove all cached results (use when results outdated)
The most common scenario — paste a CVE ID from a scanner report and get an instant priority verdict.
curl http://localhost:8000/v1/get_info/CVE-2021-44228{
"Priority": "Critical",
"Details": {
"CVSS": 10.0,
"EPSS": 0.97527,
"is_template": true,
"is_exploited": true,
"is_poc": true,
"Links": {
"POC": "https://github.com/...",
"Nuclei templates": "https://github.com/projectdiscovery/nuclei-templates/...",
"KEV": "https://www.cisa.gov/..."
}
}
}Priority is Critical if any of: POC exists, Nuclei template exists, vulnerability is in CISA KEV. Otherwise scored by CVSS + EPSS thresholds.
Got a list of CVEs from Trivy, Nessus, or any other scanner? Send them all at once — up to 50 per request, cache-aware.
curl -X POST http://localhost:8000/v1/cve \
-H "Content-Type: application/json" \
-d '{
"cve_ids": [
"CVE-2021-44228",
"CVE-2023-44487",
"CVE-2024-1337"
]
}'{
"CVE-2021-44228": { "Priority": "Critical", "Details": { ... } },
"CVE-2023-44487": { "Priority": "High", "Details": { ... } },
"CVE-2024-1337": { "Priority": "Medium", "Details": { ... } }
}Only CVEs not already cached will trigger a vulnx call — the rest are served instantly from SQLite.
Note on transient fetch failures: When the PDCP upstream is temporarily unavailable or rate-limits your key, individual CVEs in a batch may fail even after retries. Those CVEs are stored in the cache as error records and returned with an
"error"key instead of"Priority". They are automatically excluded from/v1/all_resultsand will be re-fetched on the next request that includes them. You can also recover them all at once withPOST /v1/retry_errors(see use case 7).
When a CVE status changes (e.g., a POC is published after initial lookup), refresh it without clearing the whole cache.
curl http://localhost:8000/v1/reget_info/CVE-2024-1337This deletes the cached entry and re-fetches from vulnx in one step.
Browse all accumulated results — useful for audits or piping into another tool.
# First page
curl "http://localhost:8000/v1/all_results?limit=50&offset=0"
# Next page
curl "http://localhost:8000/v1/all_results?limit=50&offset=50"{
"total": 142,
"offset": 0,
"limit": 50,
"results": {
"CVE-2021-44228": { ... },
"CVE-2023-44487": { ... }
}
}Lock the service with an API key so only authorized clients can use your vulnx quota.
# Start with auth enabled
docker run -d -p 8000:8000 \
--env PDCP_API_KEY=<your_pdcp_key> \
--env CVE_PAAS_API_KEY=mysecrettoken \
--env RATE_LIMIT=120 \
--name cve-paas ghcr.io/denimoll/cve-paas:latest
# Query with the key
curl -H "X-API-Key: mysecrettoken" \
http://localhost:8000/v1/get_info/CVE-2021-44228Requests without a valid X-API-Key header return 401. Rate limiting (here: 120 req/min per IP) returns 429 when exceeded.
Force all clients to re-fetch fresh data on next request — useful after a long gap or known stale period.
curl -X DELETE http://localhost:8000/v1/results{ "Status": "OK", "Endpoint": "remove_results" }After a PDCP outage or rate-limit window, some CVEs in a bulk request may be stored as error records. Recover them all in one call:
curl -X POST http://localhost:8000/v1/retry_errors{
"retried": 3,
"results": {
"CVE-2024-1111": { "Priority": "High", "Details": { ... } },
"CVE-2024-2222": { "Priority": "Medium", "Details": { ... } },
"CVE-2024-3333": { "error": "vulnx error: Rate limit exceeded" }
}
}retried is the number of error records found. CVEs that still fail are stored as error records again and can be retried later. You can tune retry behaviour with VULNX_RETRY_COUNT (default 3 total attempts) and VULNX_RETRY_DELAY (default 5 s between attempts).
- New flow: Publish to ghcr.io, update start instructions
- Endpoints: Rework endpoint list (DELETE /results, proper HTTP codes)
- Annotation: Add annotation to params for all endpoints
- Settings: Configurable CVSS/EPSS thresholds via env
- Optimization: SQLite cache (aiosqlite), FastAPI lifespan init
- Security: Configurable ALLOW_ORIGINS, non-root Docker user, pinned vulnx version
- Tests: Pytest suite with CI workflow
- Cache TTL: Auto-expire cached results after configurable period (CACHE_TTL_HOURS)
- Bulk endpoint: POST /v1/cve with array of CVE IDs for batch processing (1–50 IDs, cache-aware)
- API auth: Optional X-API-Key header (CVE_PAAS_API_KEY env) to protect quota
- Rate limiting: Per-client request throttling (RATE_LIMIT requests/minute, sliding window)
- API versioning: /v1/ prefix for stable contract with consumers
- Pydantic response models: Typed response schemas for OpenAPI codegen
- Retry mechanism: Automatic retries with error record persistence and POST /v1/retry_errors
- Reverse proxy support: TRUSTED_PROXY_COUNT for correct client IP behind nginx/Traefik
- Structured logging: LOG_FORMAT=json for Loki/CloudWatch log aggregators
- Prometheus metrics: GET /metrics endpoint (request count, latency, cache hit rate) for Prometheus + Grafana stack