Coverage for src/alprina_cli/services/cve_service.py: 18%
137 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2CVE/CWE/CVSS Service - Enriches findings with professional vulnerability references.
3Integrates with NVD API 2.0 for CVE data and maps findings to CWE/OWASP.
4"""
6import os
7from typing import Optional, Dict
8from loguru import logger
9import requests
10from functools import lru_cache
13class CVEService:
14 """
15 Service for enriching security findings with CVE/CWE/CVSS data.
16 Uses NVD API 2.0 for CVE lookups and local mappings for CWE/OWASP.
17 """
19 NVD_API_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0"
21 # CWE mappings for common vulnerability types
22 CWE_MAPPINGS = {
23 "SQL Injection": "CWE-89",
24 "SQLi": "CWE-89",
25 "sql_injection": "CWE-89",
27 "XSS": "CWE-79",
28 "Cross-Site Scripting": "CWE-79",
29 "cross_site_scripting": "CWE-79",
31 "Hardcoded Secret": "CWE-798",
32 "Hardcoded Password": "CWE-798",
33 "Hardcoded Credential": "CWE-798",
34 "hardcoded_credential": "CWE-798",
36 "Path Traversal": "CWE-22",
37 "Directory Traversal": "CWE-22",
38 "path_traversal": "CWE-22",
40 "Command Injection": "CWE-78",
41 "OS Command Injection": "CWE-78",
42 "command_injection": "CWE-78",
44 "XXE": "CWE-611",
45 "XML External Entity": "CWE-611",
47 "CSRF": "CWE-352",
48 "Cross-Site Request Forgery": "CWE-352",
50 "SSRF": "CWE-918",
51 "Server-Side Request Forgery": "CWE-918",
53 "Insecure Deserialization": "CWE-502",
54 "deserialization": "CWE-502",
56 "Authentication Bypass": "CWE-287",
57 "Broken Authentication": "CWE-287",
59 "Broken Access Control": "CWE-284",
60 "Authorization Bypass": "CWE-284",
62 "Sensitive Data Exposure": "CWE-200",
63 "Information Disclosure": "CWE-200",
65 "Security Misconfiguration": "CWE-16",
66 "Insecure Configuration": "CWE-16",
68 "Weak Cryptography": "CWE-327",
69 "Insufficient Encryption": "CWE-327",
71 "Debug Mode": "CWE-489",
72 "Active Debug Code": "CWE-489",
74 "Insecure Randomness": "CWE-330",
75 "Weak Random": "CWE-330",
77 "Race Condition": "CWE-362",
78 "TOCTOU": "CWE-367",
80 "Buffer Overflow": "CWE-120",
81 "buffer_overflow": "CWE-120",
83 "Integer Overflow": "CWE-190",
84 "Numeric Error": "CWE-190",
85 }
87 # OWASP Top 10 2021 mappings
88 OWASP_MAPPINGS = {
89 "CWE-89": "A03:2021 – Injection",
90 "CWE-79": "A03:2021 – Injection",
91 "CWE-78": "A03:2021 – Injection",
92 "CWE-611": "A05:2021 – Security Misconfiguration",
94 "CWE-798": "A07:2021 – Identification and Authentication Failures",
95 "CWE-287": "A07:2021 – Identification and Authentication Failures",
96 "CWE-327": "A02:2021 – Cryptographic Failures",
97 "CWE-330": "A02:2021 – Cryptographic Failures",
99 "CWE-22": "A01:2021 – Broken Access Control",
100 "CWE-352": "A01:2021 – Broken Access Control",
101 "CWE-284": "A01:2021 – Broken Access Control",
103 "CWE-918": "A10:2021 – Server-Side Request Forgery (SSRF)",
105 "CWE-502": "A08:2021 – Software and Data Integrity Failures",
107 "CWE-200": "A01:2021 – Broken Access Control",
108 "CWE-16": "A05:2021 – Security Misconfiguration",
109 "CWE-489": "A05:2021 – Security Misconfiguration",
111 "CWE-120": "A03:2021 – Injection",
112 "CWE-190": "A04:2021 – Insecure Design",
113 "CWE-362": "A04:2021 – Insecure Design",
114 "CWE-367": "A04:2021 – Insecure Design",
115 }
117 # CVSS score estimates for vulnerability types (when CVE not available)
118 CVSS_ESTIMATES = {
119 "SQL Injection": 9.8,
120 "Command Injection": 9.8,
121 "Authentication Bypass": 9.1,
122 "Hardcoded Secret": 7.5,
123 "Path Traversal": 7.5,
124 "SSRF": 8.6,
125 "XXE": 8.2,
126 "XSS": 6.1,
127 "CSRF": 6.5,
128 "Insecure Deserialization": 8.1,
129 "Weak Cryptography": 7.4,
130 "Debug Mode": 5.3,
131 "Information Disclosure": 5.3,
132 }
134 def __init__(self):
135 """Initialize CVE service with optional API key."""
136 self.api_key = os.getenv("NVD_API_KEY")
137 if self.api_key:
138 logger.info("NVD API key found - enhanced rate limits available")
139 else:
140 logger.debug("No NVD API key - using limited rate (5 req/30sec)")
142 @lru_cache(maxsize=100)
143 def get_cve_details(self, cve_id: str) -> Optional[Dict]:
144 """
145 Fetch CVE details from NVD API 2.0.
147 Args:
148 cve_id: CVE identifier (e.g., "CVE-2025-1234")
150 Returns:
151 Dict with CVE data or None if not found/error
152 """
153 try:
154 headers = {}
155 if self.api_key:
156 headers["apiKey"] = self.api_key
158 response = requests.get(
159 self.NVD_API_BASE,
160 params={"cveId": cve_id},
161 headers=headers,
162 timeout=5.0
163 )
165 if response.status_code == 200:
166 data = response.json()
167 return self._parse_cve_data(data)
168 else:
169 logger.warning(f"NVD API returned {response.status_code} for {cve_id}")
170 return None
172 except Exception as e:
173 logger.debug(f"Could not fetch CVE {cve_id}: {e}")
174 return None
176 def _parse_cve_data(self, nvd_response: Dict) -> Optional[Dict]:
177 """Parse NVD API response into simplified format."""
178 try:
179 vulnerabilities = nvd_response.get("vulnerabilities", [])
180 if not vulnerabilities:
181 return None
183 vuln = vulnerabilities[0]
184 cve = vuln.get("cve", {})
186 # Extract CVSS score (try v3.1, v3.0, then v2.0)
187 cvss_data = {}
188 metrics = cve.get("metrics", {})
190 if "cvssMetricV31" in metrics and metrics["cvssMetricV31"]:
191 cvss_data = metrics["cvssMetricV31"][0]["cvssData"]
192 elif "cvssMetricV30" in metrics and metrics["cvssMetricV30"]:
193 cvss_data = metrics["cvssMetricV30"][0]["cvssData"]
194 elif "cvssMetricV2" in metrics and metrics["cvssMetricV2"]:
195 cvss_data = metrics["cvssMetricV2"][0]["cvssData"]
197 # Extract CWE
198 weaknesses = cve.get("weaknesses", [])
199 cwe_id = None
200 if weaknesses:
201 descriptions = weaknesses[0].get("description", [])
202 if descriptions:
203 cwe_id = descriptions[0].get("value")
205 # Extract description
206 descriptions = cve.get("descriptions", [])
207 description = descriptions[0].get("value", "") if descriptions else ""
209 return {
210 "cve_id": cve.get("id"),
211 "cvss_score": cvss_data.get("baseScore"),
212 "cvss_severity": cvss_data.get("baseSeverity"),
213 "cvss_vector": cvss_data.get("vectorString"),
214 "cwe_id": cwe_id,
215 "description": description,
216 "url": f"https://nvd.nist.gov/vuln/detail/{cve.get('id')}"
217 }
219 except Exception as e:
220 logger.error(f"Error parsing CVE data: {e}")
221 return None
223 def enrich_finding(self, finding: Dict) -> Dict:
224 """
225 Enrich a security finding with CVE/CWE/CVSS data.
227 Args:
228 finding: Original finding dict from scanner
230 Returns:
231 Enhanced finding with CVE/CWE/CVSS references
232 """
233 try:
234 # Get vulnerability type
235 vuln_type = finding.get("type", "")
237 # Add CWE ID and URL
238 cwe_id = self._get_cwe_for_type(vuln_type)
239 if cwe_id:
240 finding["cwe"] = cwe_id
241 finding["cwe_name"] = self._get_cwe_name(cwe_id)
242 finding["cwe_url"] = f"https://cwe.mitre.org/data/definitions/{cwe_id.split('-')[1]}.html"
244 # Add OWASP Top 10 mapping
245 owasp = self._get_owasp_mapping(cwe_id)
246 if owasp:
247 finding["owasp"] = owasp
248 finding["owasp_url"] = "https://owasp.org/Top10/"
250 # Add CVSS score estimate if not present
251 if "cvss_score" not in finding:
252 estimated_cvss = self._estimate_cvss(vuln_type, finding.get("severity", "MEDIUM"))
253 if estimated_cvss:
254 finding["cvss_score"] = estimated_cvss
255 finding["cvss_severity"] = self._cvss_to_severity(estimated_cvss)
257 # If finding has a specific CVE ID, fetch detailed data
258 if "cve_id" in finding:
259 cve_data = self.get_cve_details(finding["cve_id"])
260 if cve_data:
261 finding.update(cve_data)
263 # Add reference links
264 finding["references"] = self._build_references(finding)
266 return finding
268 except Exception as e:
269 logger.error(f"Error enriching finding: {e}")
270 return finding
272 def _get_cwe_for_type(self, vuln_type: str) -> Optional[str]:
273 """Map vulnerability type to CWE ID."""
274 # Try exact match first
275 if vuln_type in self.CWE_MAPPINGS:
276 return self.CWE_MAPPINGS[vuln_type]
278 # Try case-insensitive partial match
279 vuln_type_lower = vuln_type.lower()
280 for key, value in self.CWE_MAPPINGS.items():
281 if key.lower() in vuln_type_lower or vuln_type_lower in key.lower():
282 return value
284 return None
286 def _get_cwe_name(self, cwe_id: str) -> str:
287 """Get human-readable name for CWE ID."""
288 cwe_names = {
289 "CWE-89": "SQL Injection",
290 "CWE-79": "Cross-site Scripting (XSS)",
291 "CWE-798": "Use of Hard-coded Credentials",
292 "CWE-22": "Path Traversal",
293 "CWE-78": "OS Command Injection",
294 "CWE-611": "XML External Entity (XXE)",
295 "CWE-352": "Cross-Site Request Forgery (CSRF)",
296 "CWE-918": "Server-Side Request Forgery (SSRF)",
297 "CWE-502": "Insecure Deserialization",
298 "CWE-287": "Improper Authentication",
299 "CWE-284": "Improper Access Control",
300 "CWE-200": "Information Exposure",
301 "CWE-16": "Configuration Issues",
302 "CWE-327": "Weak Cryptography",
303 "CWE-489": "Active Debug Code",
304 "CWE-330": "Insufficient Entropy",
305 "CWE-362": "Race Condition",
306 "CWE-120": "Buffer Overflow",
307 "CWE-190": "Integer Overflow",
308 }
309 return cwe_names.get(cwe_id, cwe_id)
311 def _get_owasp_mapping(self, cwe_id: str) -> Optional[str]:
312 """Map CWE ID to OWASP Top 10 2021 category."""
313 return self.OWASP_MAPPINGS.get(cwe_id)
315 def _estimate_cvss(self, vuln_type: str, severity: str) -> Optional[float]:
316 """
317 Estimate CVSS score based on vulnerability type and severity.
319 Args:
320 vuln_type: Type of vulnerability
321 severity: CRITICAL/HIGH/MEDIUM/LOW
323 Returns:
324 Estimated CVSS score (0.0-10.0)
325 """
326 # Try to get base estimate from vulnerability type
327 base_score = None
328 for key, score in self.CVSS_ESTIMATES.items():
329 if key.lower() in vuln_type.lower():
330 base_score = score
331 break
333 # If no type match, use severity
334 if base_score is None:
335 severity_scores = {
336 "CRITICAL": 9.0,
337 "HIGH": 7.5,
338 "MEDIUM": 5.5,
339 "LOW": 3.5,
340 "INFO": 0.0
341 }
342 base_score = severity_scores.get(severity.upper(), 5.0)
344 return base_score
346 def _cvss_to_severity(self, cvss_score: float) -> str:
347 """Convert CVSS score to severity rating."""
348 if cvss_score >= 9.0:
349 return "CRITICAL"
350 elif cvss_score >= 7.0:
351 return "HIGH"
352 elif cvss_score >= 4.0:
353 return "MEDIUM"
354 elif cvss_score > 0.0:
355 return "LOW"
356 else:
357 return "NONE"
359 def _build_references(self, finding: Dict) -> list:
360 """Build list of reference URLs for finding."""
361 references = []
363 # CWE reference
364 if "cwe_url" in finding:
365 references.append({
366 "type": "CWE",
367 "name": f"{finding.get('cwe', 'CWE')}: {finding.get('cwe_name', 'Weakness')}",
368 "url": finding["cwe_url"]
369 })
371 # OWASP reference
372 if "owasp_url" in finding:
373 references.append({
374 "type": "OWASP",
375 "name": finding.get("owasp", "OWASP Top 10"),
376 "url": finding["owasp_url"]
377 })
379 # CVE reference (if specific CVE)
380 if "cve_id" in finding:
381 references.append({
382 "type": "CVE",
383 "name": finding["cve_id"],
384 "url": f"https://nvd.nist.gov/vuln/detail/{finding['cve_id']}"
385 })
386 else:
387 # Generic NVD search
388 references.append({
389 "type": "NVD",
390 "name": "CVE Database Search",
391 "url": "https://nvd.nist.gov/vuln/search"
392 })
394 return references
397# Global CVE service instance
398_cve_service = None
401def get_cve_service() -> CVEService:
402 """Get or create global CVE service instance."""
403 global _cve_service
404 if _cve_service is None:
405 _cve_service = CVEService()
406 return _cve_service
409# Convenience function for quick enrichment
410def enrich_finding(finding: Dict) -> Dict:
411 """
412 Convenience function to enrich a finding with CVE/CWE/CVSS data.
414 Args:
415 finding: Finding dict from scanner
417 Returns:
418 Enriched finding with professional references
419 """
420 service = get_cve_service()
421 return service.enrich_finding(finding)
424# Batch enrichment
425def enrich_findings(findings: list) -> list:
426 """
427 Enrich multiple findings with CVE/CWE/CVSS data.
429 Args:
430 findings: List of finding dicts
432 Returns:
433 List of enriched findings
434 """
435 service = get_cve_service()
436 return [service.enrich_finding(f) for f in findings]