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

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""" 

5 

6import os 

7from typing import Optional, Dict 

8from loguru import logger 

9import requests 

10from functools import lru_cache 

11 

12 

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 """ 

18 

19 NVD_API_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0" 

20 

21 # CWE mappings for common vulnerability types 

22 CWE_MAPPINGS = { 

23 "SQL Injection": "CWE-89", 

24 "SQLi": "CWE-89", 

25 "sql_injection": "CWE-89", 

26 

27 "XSS": "CWE-79", 

28 "Cross-Site Scripting": "CWE-79", 

29 "cross_site_scripting": "CWE-79", 

30 

31 "Hardcoded Secret": "CWE-798", 

32 "Hardcoded Password": "CWE-798", 

33 "Hardcoded Credential": "CWE-798", 

34 "hardcoded_credential": "CWE-798", 

35 

36 "Path Traversal": "CWE-22", 

37 "Directory Traversal": "CWE-22", 

38 "path_traversal": "CWE-22", 

39 

40 "Command Injection": "CWE-78", 

41 "OS Command Injection": "CWE-78", 

42 "command_injection": "CWE-78", 

43 

44 "XXE": "CWE-611", 

45 "XML External Entity": "CWE-611", 

46 

47 "CSRF": "CWE-352", 

48 "Cross-Site Request Forgery": "CWE-352", 

49 

50 "SSRF": "CWE-918", 

51 "Server-Side Request Forgery": "CWE-918", 

52 

53 "Insecure Deserialization": "CWE-502", 

54 "deserialization": "CWE-502", 

55 

56 "Authentication Bypass": "CWE-287", 

57 "Broken Authentication": "CWE-287", 

58 

59 "Broken Access Control": "CWE-284", 

60 "Authorization Bypass": "CWE-284", 

61 

62 "Sensitive Data Exposure": "CWE-200", 

63 "Information Disclosure": "CWE-200", 

64 

65 "Security Misconfiguration": "CWE-16", 

66 "Insecure Configuration": "CWE-16", 

67 

68 "Weak Cryptography": "CWE-327", 

69 "Insufficient Encryption": "CWE-327", 

70 

71 "Debug Mode": "CWE-489", 

72 "Active Debug Code": "CWE-489", 

73 

74 "Insecure Randomness": "CWE-330", 

75 "Weak Random": "CWE-330", 

76 

77 "Race Condition": "CWE-362", 

78 "TOCTOU": "CWE-367", 

79 

80 "Buffer Overflow": "CWE-120", 

81 "buffer_overflow": "CWE-120", 

82 

83 "Integer Overflow": "CWE-190", 

84 "Numeric Error": "CWE-190", 

85 } 

86 

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", 

93 

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", 

98 

99 "CWE-22": "A01:2021 – Broken Access Control", 

100 "CWE-352": "A01:2021 – Broken Access Control", 

101 "CWE-284": "A01:2021 – Broken Access Control", 

102 

103 "CWE-918": "A10:2021 – Server-Side Request Forgery (SSRF)", 

104 

105 "CWE-502": "A08:2021 – Software and Data Integrity Failures", 

106 

107 "CWE-200": "A01:2021 – Broken Access Control", 

108 "CWE-16": "A05:2021 – Security Misconfiguration", 

109 "CWE-489": "A05:2021 – Security Misconfiguration", 

110 

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 } 

116 

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 } 

133 

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)") 

141 

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. 

146 

147 Args: 

148 cve_id: CVE identifier (e.g., "CVE-2025-1234") 

149 

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 

157 

158 response = requests.get( 

159 self.NVD_API_BASE, 

160 params={"cveId": cve_id}, 

161 headers=headers, 

162 timeout=5.0 

163 ) 

164 

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 

171 

172 except Exception as e: 

173 logger.debug(f"Could not fetch CVE {cve_id}: {e}") 

174 return None 

175 

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 

182 

183 vuln = vulnerabilities[0] 

184 cve = vuln.get("cve", {}) 

185 

186 # Extract CVSS score (try v3.1, v3.0, then v2.0) 

187 cvss_data = {} 

188 metrics = cve.get("metrics", {}) 

189 

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"] 

196 

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") 

204 

205 # Extract description 

206 descriptions = cve.get("descriptions", []) 

207 description = descriptions[0].get("value", "") if descriptions else "" 

208 

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 } 

218 

219 except Exception as e: 

220 logger.error(f"Error parsing CVE data: {e}") 

221 return None 

222 

223 def enrich_finding(self, finding: Dict) -> Dict: 

224 """ 

225 Enrich a security finding with CVE/CWE/CVSS data. 

226 

227 Args: 

228 finding: Original finding dict from scanner 

229 

230 Returns: 

231 Enhanced finding with CVE/CWE/CVSS references 

232 """ 

233 try: 

234 # Get vulnerability type 

235 vuln_type = finding.get("type", "") 

236 

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" 

243 

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/" 

249 

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) 

256 

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) 

262 

263 # Add reference links 

264 finding["references"] = self._build_references(finding) 

265 

266 return finding 

267 

268 except Exception as e: 

269 logger.error(f"Error enriching finding: {e}") 

270 return finding 

271 

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] 

277 

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 

283 

284 return None 

285 

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) 

310 

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) 

314 

315 def _estimate_cvss(self, vuln_type: str, severity: str) -> Optional[float]: 

316 """ 

317 Estimate CVSS score based on vulnerability type and severity. 

318 

319 Args: 

320 vuln_type: Type of vulnerability 

321 severity: CRITICAL/HIGH/MEDIUM/LOW 

322 

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 

332 

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) 

343 

344 return base_score 

345 

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" 

358 

359 def _build_references(self, finding: Dict) -> list: 

360 """Build list of reference URLs for finding.""" 

361 references = [] 

362 

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 }) 

370 

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 }) 

378 

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 }) 

393 

394 return references 

395 

396 

397# Global CVE service instance 

398_cve_service = None 

399 

400 

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 

407 

408 

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. 

413 

414 Args: 

415 finding: Finding dict from scanner 

416 

417 Returns: 

418 Enriched finding with professional references 

419 """ 

420 service = get_cve_service() 

421 return service.enrich_finding(finding) 

422 

423 

424# Batch enrichment 

425def enrich_findings(findings: list) -> list: 

426 """ 

427 Enrich multiple findings with CVE/CWE/CVSS data. 

428 

429 Args: 

430 findings: List of finding dicts 

431 

432 Returns: 

433 List of enriched findings 

434 """ 

435 service = get_cve_service() 

436 return [service.enrich_finding(f) for f in findings]