Threat intelligence & enrichment
Once a CVE has been matched to installed software, the engine enriches it with threat intelligence, scores it into a 0–100 risk number and priority, and produces remediation guidance (recommendation, urgency, SLA). Three packages do this work:
intelligence/— the multi-source enrichment aggregator.research/— direct CVE lookup across NVD, CVE.org and CISA KEV.scoring/+analysis/— risk scoring and portfolio/vendor analysis.remediation/— the finding lifecycle, SLA tracking, and metrics.
Two enrichment paths
The engine has two complementary ways to pull CVE data.
research.CVEResearcher — direct lookup
research/cve_lookup.py is the lightweight researcher used for interactive
lookups (vulnmgmt vuln lookup, comprehensive_search). It talks to three free
sources directly:
| Source | Method | What it returns |
|---|---|---|
| NIST NVD | search_nvd() | CVSS v3.1→v3.0→v2 (first available), description, references, affected CPEs + version ranges |
| CVE.org / MITRE | lookup_cve_org() | Authoritative record from cveawg.mitre.org — description, affected products, CVSS v3.1 |
| CISA KEV | search_cisa_kev() | Whether the CVE is in the Known Exploited Vulnerabilities catalog → sets is_exploited=True |
Highlights of the implementation:
- NVD search strategy picks the most precise query available, in order:
exact
cveId→virtualMatchString(a CPEvendor:product:version, letting NVD do server-side version-range matching) →keywordSearchfallback. - Rate limiting is automatic. Without an
NVD_API_KEYthe delay is 6.0s (5 req/30s); with a key it’s 0.6s (50 req/30s). A sliding-window limiter plus a single 30s back-off on403keep it under NVD’s cap. get_research_urls()builds human links (NVD, MITRE, CVE.org, GitHub CVE JSON, Exploit-DB, X-Force) that end up in the report’s Research Sources sheet.
intelligence.ThreatIntelligenceAggregator — full enrichment
intelligence/aggregator.py is the heavy pipeline used by scan/pipeline. It
fans out across the integration clients and
merges everything into one EnrichedCVE dataclass.
agg = ThreatIntelligenceAggregator(api_keys, enabled_sources=["otx", "cve_awg", "nvd"])enriched = agg.enrich_batch(cve_matches, max_workers=5) # parallel, ThreadPoolExecutorSource selection. enabled_sources defaults to the CVE-specific feeds
(otx, cve_awg, nvd). The aggregator splits feeds into two groups:
- CVE sources —
otx,mitre,cve_awg,nvd— driven by CVE id. - IOC sources —
virustotal,shodan,greynoise,abuseipdb,urlhaus— driven by IPs/URLs/hashes. During CVE enrichment these are initialized but recorded asnot_applicable, so a CVE run doesn’t waste time (or rate limit) on them.
From the CLI you pick sources explicitly:
# Default (CVE feeds only): otx + mitrevulnmgmt vuln lookup --app "Foxit Reader" --version "2024" --enrich
# Fastest, most complete CVE datavulnmgmt vuln scan apps.json --enrich --sources otx -o results.json
# CVE data + adversary TTPsvulnmgmt vuln pipeline apps.json --sources otx --sources mitre -o results.jsonWhat each source contributes to EnrichedCVE:
| Source | Fields populated |
|---|---|
| CISA KEV | is_exploited, kev_due_date |
| OTX | otx_cvss_v2/v3, otx_cwe, otx_references, otx_targeted_products, otx_pulse_count, otx_threat_actors, otx_malware_families, otx_tags |
| CVE AWG | cve_awg_references, cve_awg_affected_products, cve_awg_problem_types, cve_awg_assigner, cve_awg_date_published |
| NVD | nvd_cvss_v2/v30/v31_detailed, nvd_references, nvd_weaknesses, nvd_cpe_matches, plus nvd_cpe_matched (CPE matched to the app with a confidence score) |
| MITRE ATT&CK | mitre_techniques, mitre_tactics |
| EPSS | epss_score, epss_percentile, epss_date |
| VirusTotal / GreyNoise / Shodan / AbuseIPDB / URLhaus | IOC fields (vt_malicious_samples, grey_scanner_activity, shodan_exposed_hosts, abuse_reports, urlhaus_malware_urls) — used for scoring inputs when present |
Every EnrichedCVE also records provenance: sources_checked,
sources_succeeded, sources_failed, and a per-source enrichment_details
block. This is what powers the Enrichment Details report sheet and makes a
failed feed visible rather than silent.
Performance note (Jan 2026). OTX enrichment used to make a separate pulse search that timed out (30–127s per CVE). Pulse data is already inside the CVE-details response, so that call was removed — a ~100× speedup to ~1–2s per CVE. Prefer
--sources otxfor scans; avoid IOC feeds for CVE enrichment.
Free vs. paid sources
The engine is designed to run on free intelligence. The reference notes
(FREE_THREAT_INTEL_APIS.md, ENRICHMENT_SOURCES.md) split them as:
- Tier 1 – completely free: NVD (5 req/30s free, 50 with a free key), MITRE ATT&CK, CISA KEV, EPSS, URLhaus, PhishTank.
- Tier 2 – free tier with limits: AlienVault OTX (generous), VirusTotal (4/min, 500/day), Shodan (~100/mo), GreyNoise (~5,000/mo), AbuseIPDB (1,000/day).
- Paid: IBM X-Force (Standard tier / $99+ mo — Freemium blocks the API), and other commercial feeds (Recorded Future, ReversingLabs, …) not integrated.
The single required key for good CVE enrichment is OTX_API_KEY; NVD_API_KEY
is optional but strongly recommended for throughput. Keys are read from .env.
Scoring: from enriched CVE to risk
scoring/risk_engine.py (RiskScoringEngine) converts each EnrichedCVE into a
ScoredCVE with a 0–100 risk_score and a Priority. The score is a weighted
sum of six components:
| Component | Weight | Input |
|---|---|---|
| CVSS base score | 20% | cvss_score / 10 × 100 |
| EPSS | 15% | epss_score × 100 (probability of exploitation) |
| Active exploitation | 25% | 100 if KEV-exploited, 75 if GreyNoise scanner activity, 50 if OTX pulses > 10 |
| Threat-actor activity | 15% | min(#OTX threat actors × 25, 100) |
| Malware association | 15% | min(#malware families × 20, 100), +30 VirusTotal, +20 URLhaus |
| Asset exposure | 10% | tiered by asset_count (1/10/50/100+), +25 if Shodan-exposed |
Priority is derived from the score (≥80 CRITICAL, ≥60 HIGH, ≥40 MEDIUM,
≥20 LOW, else INFO) — with one override: an actively exploited CVE with
CVSS ≥ 9.0 is always CRITICAL. The engine also emits a recommendation string
and a remediation_urgency timeline directly off the priority and exploitation
signals.
There are two other, simpler scorers used elsewhere:
analysis/risk_scoring.py(RiskAnalyzer) scores applications (not single CVEs) — counting critical/high/medium/low + exploited vulns, weighting by a device-count exposure multiplier (core.models.RiskScore.calculate_risk_score), and producing per-app recommendations and vendor-risk rollups (DeepAnalyzer.analyze_vendor_risk,generate_executive_summary).output.core.models.RiskMetrics.from_findings()computes the report-level severity roll-up and overall risk level shown on report cover pages.
Remediation guidance
Remediation output has two layers: the guidance text produced during scoring,
and the lifecycle tracking in remediation/.
Guidance text
RiskScoringEngine._generate_recommendation() / _calculate_urgency() produce
the human guidance that lands in reports and tickets. Examples:
| Priority | Recommendation | Urgency |
|---|---|---|
| CRITICAL (exploited) | “PATCH IMMEDIATELY - Actively exploited in the wild” | IMMEDIATE (0–24 hours) |
| CRITICAL (high EPSS) | “URGENT: Patch within 24 hours - N% exploitation probability” | 24–48 hours |
| HIGH | ”Patch within 7 days - …” (cites EPSS % or named threat actors) | 7 days |
| MEDIUM | ”Patch within 30 days - Medium risk” | 30 days |
| LOW | ”Patch within 90 days - Low risk” | 90 days |
Lifecycle & SLA — remediation/workflow.py
RemediationWorkflow is a state machine over a VulnerabilityFinding (a CVE on
a specific asset). States: DISCOVERED → ASSESSED → ASSIGNED → IN_PROGRESS → VERIFIED → CLOSED, plus ACCEPTED_RISK, DEFERRED, EXCEPTION, REOPENED.
VALID_TRANSITIONS enforces legal moves and every change appends a
StateTransition (who/when/why) to the finding’s history; timestamps
(assessed_at, remediated_at, …) are stamped automatically.
SLA targets are priority-driven:
| Priority | SLA (days) |
|---|---|
| CRITICAL | 3 |
| HIGH | 7 |
| MEDIUM | 30 |
| LOW | 90 |
calculate_sla_due_date() sets the due date; check_sla_breach() flags an open
finding past due. Findings also carry cw_ticket_id / jira_ticket_id so a
finding links back to the ticket created via
ConnectWise Manage.
Metrics — remediation/metrics.py
SLATracker rolls a set of findings into a RemediationMetrics report:
- Time metrics — MTTR (mean/median time to remediate), time-to-assign, time-to-close.
- SLA compliance — compliance rate, breach rate, within-SLA count.
- Distributions — open counts by priority and by state.
- Operational views — aging report (
get_aging_report), SLAs at risk (get_sla_at_risk), overdue findings, and per-assignee workload.
Related
- Integrations library — the feed clients this pipeline drives.
- Reporting & output — where scored/enriched CVEs become reports.
- CLI commands — the
vuln lookup / scan / pipelinecommands.