Idea
KB `known_vulns[]` is currently human-curated per-service YAML. It drifts — a CVE published last month won't show up until someone files a PR. NVD publishes a daily JSON feed (`nvdcve-1.1-modified.json.gz`) that's roughly 5-15 MB and can be downloaded once, cached locally, and matched against `(port.product, port.version)` at render time.
Stays consistent with recon-deck's offline-first ethos: the operator triggers a refresh manually (or via a cron / launchd job they configure); recon-deck never reaches out on its own.
Proposal
Persistence
```
cve_intel {
cve_id PRIMARY KEY, -- CVE-YYYY-NNNNN
description,
cvss_v3, -- numeric, nullable
severity, -- critical|high|medium|low|info, derived from cvss_v3
published_at,
modified_at,
cpe_match, -- JSON array of CPE configurations from NVD
url -- canonical NVD link
}
```
CPE match is the load-bearing piece — NVD encodes affected versions as CPE 2.3 strings (`cpe:2.3:a:vsftpd:vsftpd:2.3.4:::::::*`). nmap already extracts `port.cpe[]`, so the match is a CPE-vs-CPE compare.
Refresh flow
- `/api/cve-intel/refresh` route (POST, opt-in toggle in `/settings`, off by default)
- Downloads `nvdcve-1.1-modified.json.gz` from `https://nvd.nist.gov/feeds/json/cve/1.1/\` (NVD allows unauthenticated, rate-limited public access)
- Streams + parses → upserts into `cve_intel`
- Stores last-refresh timestamp in `app_state`
Render integration
Per-port detail pane: alongside KB `known_vulns`, a new "NVD matches" section listing CVEs whose CPE matches the port's `cpe[]`, sorted by CVSS desc, capped at 20. Each row: `severity badge · CVE-id · short description · NVD link`.
Add-as-finding hooks the same prefill flow as KB known_vulns.
Conflict with searchsploit
Different surface: searchsploit = "is there an exploit?", NVD = "is this version vulnerable?". Rendered side-by-side; operators consult both.
Out of scope
- Full NVD historical archive (ships only the modified-feed; full feed = 100s of MB, deferred to a "deep refresh" command)
- EPSS / KEV enrichment (CISA KEV would be a follow-up — small JSON, very useful)
- Auto-refresh — strictly manual / external-cron triggered to keep offline contract
Acceptance
Notes
Pairs naturally with #14 conditionals — a future predicate `cve_present(severity_at_least)` could light up checklists when a high-severity CVE is matched, but that's deferred.
Idea
KB `known_vulns[]` is currently human-curated per-service YAML. It drifts — a CVE published last month won't show up until someone files a PR. NVD publishes a daily JSON feed (`nvdcve-1.1-modified.json.gz`) that's roughly 5-15 MB and can be downloaded once, cached locally, and matched against `(port.product, port.version)` at render time.
Stays consistent with recon-deck's offline-first ethos: the operator triggers a refresh manually (or via a cron / launchd job they configure); recon-deck never reaches out on its own.
Proposal
Persistence
```
cve_intel {
cve_id PRIMARY KEY, -- CVE-YYYY-NNNNN
description,
cvss_v3, -- numeric, nullable
severity, -- critical|high|medium|low|info, derived from cvss_v3
published_at,
modified_at,
cpe_match, -- JSON array of CPE configurations from NVD
url -- canonical NVD link
}
```
CPE match is the load-bearing piece — NVD encodes affected versions as CPE 2.3 strings (`cpe:2.3:a:vsftpd:vsftpd:2.3.4:::::::*`). nmap already extracts `port.cpe[]`, so the match is a CPE-vs-CPE compare.
Refresh flow
Render integration
Per-port detail pane: alongside KB `known_vulns`, a new "NVD matches" section listing CVEs whose CPE matches the port's `cpe[]`, sorted by CVSS desc, capped at 20. Each row: `severity badge · CVE-id · short description · NVD link`.
Add-as-finding hooks the same prefill flow as KB known_vulns.
Conflict with searchsploit
Different surface: searchsploit = "is there an exploit?", NVD = "is this version vulnerable?". Rendered side-by-side; operators consult both.
Out of scope
Acceptance
Notes
Pairs naturally with #14 conditionals — a future predicate `cve_present(severity_at_least)` could light up checklists when a high-severity CVE is matched, but that's deferred.