souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -18,27 +18,28 @@ console = Console()
18
18
 
19
19
  # Severity colors
20
20
  SEVERITY_COLORS = {
21
- 'Critical': 'red',
22
- 'High': 'yellow',
23
- 'Medium': 'white',
24
- 'Low': 'bright_black',
25
- 'critical': 'red',
26
- 'high': 'yellow',
27
- 'medium': 'white',
28
- 'low': 'bright_black'
21
+ "Critical": "red",
22
+ "High": "yellow",
23
+ "Medium": "white",
24
+ "Low": "bright_black",
25
+ "critical": "red",
26
+ "high": "yellow",
27
+ "medium": "white",
28
+ "low": "bright_black",
29
29
  }
30
30
 
31
31
  SEVERITY_ICONS = {
32
- 'Critical': '[red bold]C[/red bold]',
33
- 'High': '[yellow bold]H[/yellow bold]',
34
- 'Medium': '[white]M[/white]',
35
- 'Low': '[bright_black]L[/bright_black]'
32
+ "Critical": "[red bold]C[/red bold]",
33
+ "High": "[yellow bold]H[/yellow bold]",
34
+ "Medium": "[white]M[/white]",
35
+ "Low": "[bright_black]L[/bright_black]",
36
36
  }
37
37
 
38
38
 
39
39
  @dataclass
40
40
  class SplunkVulnGap:
41
41
  """Represents a vulnerability gap between detection sources."""
42
+
42
43
  cve_id: str
43
44
  severity: str
44
45
  host_ip: str
@@ -52,6 +53,7 @@ class SplunkVulnGap:
52
53
  @dataclass
53
54
  class SplunkGapResult:
54
55
  """Result of Splunk gap analysis."""
56
+
55
57
  splunk_total: int = 0
56
58
  scan_total: int = 0
57
59
  splunk_only: List[SplunkVulnGap] = field(default_factory=list)
@@ -60,7 +62,9 @@ class SplunkGapResult:
60
62
  coverage_pct: float = 0.0
61
63
 
62
64
 
63
- def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> None:
65
+ def show_splunk_gap_analysis_view(
66
+ engagement_id: int, engagement_name: str = ""
67
+ ) -> None:
64
68
  """
65
69
  Display Splunk gap analysis view.
66
70
 
@@ -76,17 +80,36 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
76
80
 
77
81
  # Header
78
82
  click.echo("\n┌" + "─" * (width - 2) + "┐")
79
- click.echo("│" + click.style(" SPLUNK GAP ANALYSIS ".center(width - 2), bold=True, fg='cyan') + "│")
83
+ click.echo(
84
+ "│"
85
+ + click.style(
86
+ " SPLUNK GAP ANALYSIS ".center(width - 2), bold=True, fg="cyan"
87
+ )
88
+ + "│"
89
+ )
80
90
  click.echo("└" + "─" * (width - 2) + "┘")
81
91
  click.echo()
82
- click.echo(click.style(" Compare Splunk (passive/synced) vs Scan (active) findings", fg='bright_black'))
92
+ click.echo(
93
+ click.style(
94
+ " Compare Splunk (passive/synced) vs Scan (active) findings",
95
+ fg="bright_black",
96
+ )
97
+ )
83
98
  click.echo()
84
99
 
85
100
  # Check if Splunk is configured
86
101
  config = WazuhConfig.get_config(engagement_id)
87
102
 
88
- if not config or config.get('siem_type') != 'splunk' or not config.get('enabled'):
89
- click.echo(click.style(" Splunk is not configured for this engagement.", fg='yellow'))
103
+ if (
104
+ not config
105
+ or config.get("siem_type") != "splunk"
106
+ or not config.get("enabled")
107
+ ):
108
+ click.echo(
109
+ click.style(
110
+ " Splunk is not configured for this engagement.", fg="yellow"
111
+ )
112
+ )
90
113
  click.echo()
91
114
  click.echo(" Configure Splunk in Settings -> SIEM Integration")
92
115
  click.echo()
@@ -95,7 +118,7 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
95
118
  click.echo(" [q] Back")
96
119
  click.echo()
97
120
  try:
98
- if click.getchar().lower() == 'q':
121
+ if click.getchar().lower() == "q":
99
122
  return
100
123
  except (KeyboardInterrupt, EOFError):
101
124
  return
@@ -104,20 +127,23 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
104
127
  # Get Splunk client and run analysis
105
128
  try:
106
129
  from souleyez.integrations.siem.splunk import SplunkSIEMClient
107
- client = SplunkSIEMClient.from_config({
108
- 'api_url': config.get('api_url', ''),
109
- 'username': config.get('username', ''),
110
- 'password': config.get('password', ''),
111
- 'verify_ssl': config.get('verify_ssl', False),
112
- 'default_index': config.get('default_index', 'main'),
113
- })
130
+
131
+ client = SplunkSIEMClient.from_config(
132
+ {
133
+ "api_url": config.get("api_url", ""),
134
+ "username": config.get("username", ""),
135
+ "password": config.get("password", ""),
136
+ "verify_ssl": config.get("verify_ssl", False),
137
+ "default_index": config.get("default_index", "main"),
138
+ }
139
+ )
114
140
 
115
141
  # Run gap analysis
116
142
  result = _analyze_gaps(engagement_id, client)
117
143
  stats = _get_coverage_stats(result)
118
144
 
119
145
  except Exception as e:
120
- click.echo(click.style(f" Error connecting to Splunk: {e}", fg='red'))
146
+ click.echo(click.style(f" Error connecting to Splunk: {e}", fg="red"))
121
147
  click.echo()
122
148
  click.echo(" Make sure the wazuh_vulns index exists and has data.")
123
149
  click.echo(" Run: python3 scripts/wazuh_vuln_to_splunk.py --all")
@@ -142,17 +168,17 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
142
168
  try:
143
169
  choice = input(" Select option: ").strip().lower()
144
170
 
145
- if choice == 'q':
171
+ if choice == "q":
146
172
  return
147
- elif choice == '1':
173
+ elif choice == "1":
148
174
  _show_splunk_only(result, width)
149
- elif choice == '2':
175
+ elif choice == "2":
150
176
  _show_scan_only(result, width)
151
- elif choice == '3':
177
+ elif choice == "3":
152
178
  _show_confirmed(result, width)
153
- elif choice == 'a':
179
+ elif choice == "a":
154
180
  _show_actionable_gaps(result, width)
155
- elif choice == 'r':
181
+ elif choice == "r":
156
182
  continue # Refresh
157
183
  except (KeyboardInterrupt, EOFError):
158
184
  return
@@ -179,13 +205,13 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
179
205
  # Group by CVE ID to find matches
180
206
  splunk_by_cve: Dict[str, List[Dict]] = {}
181
207
  for v in splunk_vulns:
182
- cve = v.get('cve_id')
208
+ cve = v.get("cve_id")
183
209
  if cve:
184
210
  splunk_by_cve.setdefault(cve, []).append(v)
185
211
 
186
212
  scan_by_cve: Dict[str, List[Dict]] = {}
187
213
  for f in scan_findings:
188
- cve = f.get('cve_id')
214
+ cve = f.get("cve_id")
189
215
  if cve:
190
216
  scan_by_cve.setdefault(cve, []).append(f)
191
217
 
@@ -202,16 +228,18 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
202
228
  splunk_v = splunk_entries[0]
203
229
  scan_f = scan_entries[0]
204
230
 
205
- result.confirmed.append(SplunkVulnGap(
206
- cve_id=cve_id,
207
- severity=splunk_v.get('severity', 'Medium'),
208
- host_ip=f"{splunk_v.get('host_ip', '-')} / {scan_f.get('host_ip', '-')}",
209
- source='both',
210
- splunk_details=splunk_v,
211
- scan_details=scan_f,
212
- recommendation=f"Confirmed by both sources ({len(splunk_entries)} Splunk, {len(scan_entries)} scan)",
213
- confidence='high'
214
- ))
231
+ result.confirmed.append(
232
+ SplunkVulnGap(
233
+ cve_id=cve_id,
234
+ severity=splunk_v.get("severity", "Medium"),
235
+ host_ip=f"{splunk_v.get('host_ip', '-')} / {scan_f.get('host_ip', '-')}",
236
+ source="both",
237
+ splunk_details=splunk_v,
238
+ scan_details=scan_f,
239
+ recommendation=f"Confirmed by both sources ({len(splunk_entries)} Splunk, {len(scan_entries)} scan)",
240
+ confidence="high",
241
+ )
242
+ )
215
243
 
216
244
  # Find Splunk-only CVEs (scan didn't find this CVE anywhere)
217
245
  splunk_only_cves = all_splunk_cves - all_scan_cves
@@ -219,15 +247,17 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
219
247
  splunk_entries = splunk_by_cve[cve_id]
220
248
  splunk_v = splunk_entries[0]
221
249
 
222
- result.splunk_only.append(SplunkVulnGap(
223
- cve_id=cve_id,
224
- severity=splunk_v.get('severity', 'Medium'),
225
- host_ip=splunk_v.get('host_ip', '-'),
226
- source='splunk',
227
- splunk_details=splunk_v,
228
- recommendation="Package-level vulnerability - network scans typically can't detect",
229
- confidence='medium'
230
- ))
250
+ result.splunk_only.append(
251
+ SplunkVulnGap(
252
+ cve_id=cve_id,
253
+ severity=splunk_v.get("severity", "Medium"),
254
+ host_ip=splunk_v.get("host_ip", "-"),
255
+ source="splunk",
256
+ splunk_details=splunk_v,
257
+ recommendation="Package-level vulnerability - network scans typically can't detect",
258
+ confidence="medium",
259
+ )
260
+ )
231
261
 
232
262
  # Find Scan-only CVEs (Splunk/Wazuh didn't detect this CVE)
233
263
  scan_only_cves = all_scan_cves - all_splunk_cves
@@ -235,15 +265,17 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
235
265
  scan_entries = scan_by_cve[cve_id]
236
266
  scan_f = scan_entries[0]
237
267
 
238
- result.scan_only.append(SplunkVulnGap(
239
- cve_id=cve_id,
240
- severity=scan_f.get('severity', 'Medium'),
241
- host_ip=scan_f.get('host_ip', '-'),
242
- source='scan',
243
- scan_details=scan_f,
244
- recommendation="Network-exposed CVE - Wazuh agent may not be installed on this host",
245
- confidence='medium'
246
- ))
268
+ result.scan_only.append(
269
+ SplunkVulnGap(
270
+ cve_id=cve_id,
271
+ severity=scan_f.get("severity", "Medium"),
272
+ host_ip=scan_f.get("host_ip", "-"),
273
+ source="scan",
274
+ scan_details=scan_f,
275
+ recommendation="Network-exposed CVE - Wazuh agent may not be installed on this host",
276
+ confidence="medium",
277
+ )
278
+ )
247
279
 
248
280
  # Calculate coverage based on unique CVEs
249
281
  total_unique_cves = len(all_splunk_cves | all_scan_cves)
@@ -262,41 +294,45 @@ def _get_splunk_cves(client) -> List[Dict[str, Any]]:
262
294
  result = []
263
295
  for v in vulns:
264
296
  # Map agent_name to host_ip if available
265
- host_ip = v.get('agent_ip') or v.get('agent_name', '-')
297
+ host_ip = v.get("agent_ip") or v.get("agent_name", "-")
266
298
  # Normalize CVE ID to uppercase for matching
267
- cve_id = v.get('cve_id', '')
299
+ cve_id = v.get("cve_id", "")
268
300
  if cve_id:
269
301
  cve_id = cve_id.upper()
270
- result.append({
271
- 'cve_id': cve_id,
272
- 'severity': v.get('severity', 'Medium'),
273
- 'host_ip': host_ip,
274
- 'package_name': v.get('package_name'),
275
- 'package_version': v.get('package_version'),
276
- 'cvss_score': v.get('cvss_score'),
277
- 'agent_name': v.get('agent_name'),
278
- 'os_name': v.get('os_name')
279
- })
302
+ result.append(
303
+ {
304
+ "cve_id": cve_id,
305
+ "severity": v.get("severity", "Medium"),
306
+ "host_ip": host_ip,
307
+ "package_name": v.get("package_name"),
308
+ "package_version": v.get("package_version"),
309
+ "cvss_score": v.get("cvss_score"),
310
+ "agent_name": v.get("agent_name"),
311
+ "os_name": v.get("os_name"),
312
+ }
313
+ )
280
314
  return result
281
315
  except Exception as e:
282
- click.echo(click.style(f" Warning: Error fetching Splunk vulns: {e}", fg='yellow'))
316
+ click.echo(
317
+ click.style(f" Warning: Error fetching Splunk vulns: {e}", fg="yellow")
318
+ )
283
319
  return []
284
320
 
285
321
 
286
322
  def _get_scan_cves(engagement_id: int) -> List[Dict[str, Any]]:
287
323
  """Get CVE findings from active scans."""
288
324
  import re
325
+
289
326
  db = get_db()
290
- cve_pattern = re.compile(r'CVE-\d{4}-\d+', re.IGNORECASE)
327
+ cve_pattern = re.compile(r"CVE-\d{4}-\d+", re.IGNORECASE)
291
328
  result = []
292
329
 
293
330
  # Get hosts for this engagement
294
331
  hosts = db.execute(
295
- "SELECT id, ip_address FROM hosts WHERE engagement_id = ?",
296
- (engagement_id,)
332
+ "SELECT id, ip_address FROM hosts WHERE engagement_id = ?", (engagement_id,)
297
333
  )
298
334
 
299
- host_map = {h['id']: h['ip_address'] for h in hosts}
335
+ host_map = {h["id"]: h["ip_address"] for h in hosts}
300
336
 
301
337
  if not host_map:
302
338
  return []
@@ -304,58 +340,65 @@ def _get_scan_cves(engagement_id: int) -> List[Dict[str, Any]]:
304
340
  # 1. Get nuclei_findings with CVE IDs (they have cve_id column)
305
341
  nuclei_findings = db.execute(
306
342
  "SELECT * FROM nuclei_findings WHERE engagement_id = ? AND cve_id IS NOT NULL",
307
- (engagement_id,)
343
+ (engagement_id,),
308
344
  )
309
345
 
310
346
  for f in nuclei_findings:
311
- cve_id = f.get('cve_id')
347
+ cve_id = f.get("cve_id")
312
348
  if cve_id:
313
349
  # Extract host IP from matched_at if available
314
- matched_at = f.get('matched_at', '')
350
+ matched_at = f.get("matched_at", "")
315
351
  host_ip = None
316
352
  # Try to extract IP from matched_at URL
317
- ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', matched_at)
353
+ ip_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", matched_at)
318
354
  if ip_match:
319
355
  host_ip = ip_match.group(1)
320
356
 
321
- result.append({
322
- 'cve_id': cve_id.upper(),
323
- 'host_ip': host_ip or '-',
324
- 'severity': (f.get('severity') or 'Medium').title(),
325
- 'title': f.get('name'),
326
- 'tool': 'nuclei',
327
- 'port': None,
328
- 'service': None
329
- })
357
+ result.append(
358
+ {
359
+ "cve_id": cve_id.upper(),
360
+ "host_ip": host_ip or "-",
361
+ "severity": (f.get("severity") or "Medium").title(),
362
+ "title": f.get("name"),
363
+ "tool": "nuclei",
364
+ "port": None,
365
+ "service": None,
366
+ }
367
+ )
330
368
 
331
369
  # 2. Get regular findings and extract CVEs from title/refs
332
370
  host_ids = list(host_map.keys())
333
- placeholders = ','.join('?' * len(host_ids))
334
- findings = db.execute(f"""
371
+ placeholders = ",".join("?" * len(host_ids))
372
+ findings = db.execute(
373
+ f"""
335
374
  SELECT f.*, h.ip_address
336
375
  FROM findings f
337
376
  JOIN hosts h ON f.host_id = h.id
338
377
  WHERE f.host_id IN ({placeholders})
339
378
  AND (f.title LIKE '%CVE-%' OR f.refs LIKE '%CVE-%')
340
- """, tuple(host_ids))
379
+ """,
380
+ tuple(host_ids),
381
+ )
341
382
 
342
383
  for f in findings:
343
384
  # Try to extract CVE from title
344
- title = f.get('title', '') or ''
345
- refs = f.get('refs', '') or ''
346
- combined = title + ' ' + refs
385
+ title = f.get("title", "") or ""
386
+ refs = f.get("refs", "") or ""
387
+ combined = title + " " + refs
347
388
 
348
389
  matches = cve_pattern.findall(combined)
349
390
  for cve_id in matches:
350
- result.append({
351
- 'cve_id': cve_id.upper(),
352
- 'host_ip': f.get('ip_address'),
353
- 'severity': (f.get('severity') or 'Medium').title(),
354
- 'title': f.get('title'),
355
- 'tool': f.get('tool'),
356
- 'port': f.get('port'),
357
- 'service': None
358
- })
391
+ result.append(
392
+ {
393
+ "cve_id": cve_id.upper(),
394
+ "host_ip": f.get("ip_address"),
395
+ "severity": (f.get("severity") or "Medium").title(),
396
+ "title": f.get("title"),
397
+ "tool": f.get("tool"),
398
+ "port": f.get("port"),
399
+ "service": None,
400
+ }
401
+ )
359
402
 
360
403
  return result
361
404
 
@@ -363,26 +406,28 @@ def _get_scan_cves(engagement_id: int) -> List[Dict[str, Any]]:
363
406
  def _get_coverage_stats(result: SplunkGapResult) -> Dict[str, Any]:
364
407
  """Calculate coverage statistics."""
365
408
  stats = {
366
- 'coverage_pct': result.coverage_pct,
367
- 'by_severity': {
368
- 'splunk_only': {},
369
- 'scan_only': {},
370
- 'confirmed': {}
371
- }
409
+ "coverage_pct": result.coverage_pct,
410
+ "by_severity": {"splunk_only": {}, "scan_only": {}, "confirmed": {}},
372
411
  }
373
412
 
374
413
  # Count by severity
375
414
  for gap in result.splunk_only:
376
415
  sev = gap.severity
377
- stats['by_severity']['splunk_only'][sev] = stats['by_severity']['splunk_only'].get(sev, 0) + 1
416
+ stats["by_severity"]["splunk_only"][sev] = (
417
+ stats["by_severity"]["splunk_only"].get(sev, 0) + 1
418
+ )
378
419
 
379
420
  for gap in result.scan_only:
380
421
  sev = gap.severity
381
- stats['by_severity']['scan_only'][sev] = stats['by_severity']['scan_only'].get(sev, 0) + 1
422
+ stats["by_severity"]["scan_only"][sev] = (
423
+ stats["by_severity"]["scan_only"].get(sev, 0) + 1
424
+ )
382
425
 
383
426
  for gap in result.confirmed:
384
427
  sev = gap.severity
385
- stats['by_severity']['confirmed'][sev] = stats['by_severity']['confirmed'].get(sev, 0) + 1
428
+ stats["by_severity"]["confirmed"][sev] = (
429
+ stats["by_severity"]["confirmed"].get(sev, 0) + 1
430
+ )
386
431
 
387
432
  return stats
388
433
 
@@ -394,48 +439,73 @@ def _render_summary_dashboard(result: SplunkGapResult, stats: Dict, width: int)
394
439
  confirmed = len(result.confirmed)
395
440
  splunk_only = len(result.splunk_only)
396
441
  scan_only = len(result.scan_only)
397
- coverage = stats.get('coverage_pct', 0)
442
+ coverage = stats.get("coverage_pct", 0)
398
443
 
399
444
  # Coverage color
400
445
  if coverage >= 80:
401
- coverage_color = 'green'
446
+ coverage_color = "green"
402
447
  elif coverage >= 50:
403
- coverage_color = 'yellow'
448
+ coverage_color = "yellow"
404
449
  else:
405
- coverage_color = 'red'
450
+ coverage_color = "red"
406
451
 
407
452
  # Detection Sources
408
453
  click.echo(click.style(" DETECTION SOURCES", bold=True))
409
- click.echo(f" Splunk (passive): {click.style(str(splunk_total), fg='cyan', bold=True)} CVEs")
410
- click.echo(f" Scans (active): {click.style(str(scan_total), fg='cyan', bold=True)} CVEs")
454
+ click.echo(
455
+ f" Splunk (passive): {click.style(str(splunk_total), fg='cyan', bold=True)} CVEs"
456
+ )
457
+ click.echo(
458
+ f" Scans (active): {click.style(str(scan_total), fg='cyan', bold=True)} CVEs"
459
+ )
411
460
  click.echo()
412
461
 
413
462
  # Analysis Results
414
463
  click.echo(click.style(" ANALYSIS RESULTS", bold=True))
415
- click.echo(f" ✓ Confirmed (both): {click.style(str(confirmed), fg='green', bold=True)}")
416
- click.echo(f" Splunk Only: {click.style(str(splunk_only), fg='yellow', bold=True)} <- Scans missed these")
417
- click.echo(f" ◐ Scan Only: {click.style(str(scan_only), fg='blue', bold=True)} <- Splunk missed these")
464
+ click.echo(
465
+ f" Confirmed (both): {click.style(str(confirmed), fg='green', bold=True)}"
466
+ )
467
+ click.echo(
468
+ f" ⚠ Splunk Only: {click.style(str(splunk_only), fg='yellow', bold=True)} <- Scans missed these"
469
+ )
470
+ click.echo(
471
+ f" ◐ Scan Only: {click.style(str(scan_only), fg='blue', bold=True)} <- Splunk missed these"
472
+ )
418
473
  click.echo()
419
474
 
420
475
  # Coverage
421
- click.echo(f" Coverage: " + click.style(f"{coverage:.1f}%", fg=coverage_color, bold=True) +
422
- " of Splunk vulns confirmed by scans")
476
+ click.echo(
477
+ f" Coverage: "
478
+ + click.style(f"{coverage:.1f}%", fg=coverage_color, bold=True)
479
+ + " of Splunk vulns confirmed by scans"
480
+ )
423
481
  click.echo()
424
482
 
425
483
  # Add explanation note for low coverage
426
484
  if coverage < 20:
427
- click.echo(click.style(" NOTE: ", fg='cyan', bold=True) +
428
- click.style("Low overlap is expected. Splunk/Wazuh detects ", fg='bright_black') +
429
- click.style("package-level", fg='cyan') +
430
- click.style(" vulns", fg='bright_black'))
431
- click.echo(click.style(" (installed software), while scans find ", fg='bright_black') +
432
- click.style("network-exposed", fg='cyan') +
433
- click.style(" vulns (services).", fg='bright_black'))
485
+ click.echo(
486
+ click.style(" NOTE: ", fg="cyan", bold=True)
487
+ + click.style(
488
+ "Low overlap is expected. Splunk/Wazuh detects ", fg="bright_black"
489
+ )
490
+ + click.style("package-level", fg="cyan")
491
+ + click.style(" vulns", fg="bright_black")
492
+ )
493
+ click.echo(
494
+ click.style(
495
+ " (installed software), while scans find ", fg="bright_black"
496
+ )
497
+ + click.style("network-exposed", fg="cyan")
498
+ + click.style(" vulns (services).", fg="bright_black")
499
+ )
434
500
  click.echo()
435
501
 
436
502
  # Severity breakdown
437
- sev_breakdown = stats.get('by_severity', {})
438
- if sev_breakdown.get('splunk_only') or sev_breakdown.get('scan_only') or sev_breakdown.get('confirmed'):
503
+ sev_breakdown = stats.get("by_severity", {})
504
+ if (
505
+ sev_breakdown.get("splunk_only")
506
+ or sev_breakdown.get("scan_only")
507
+ or sev_breakdown.get("confirmed")
508
+ ):
439
509
  _render_severity_breakdown(sev_breakdown, width)
440
510
 
441
511
 
@@ -446,7 +516,7 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
446
516
  header_style="bold",
447
517
  box=box.SIMPLE,
448
518
  padding=(0, 2),
449
- expand=False
519
+ expand=False,
450
520
  )
451
521
 
452
522
  table.add_column("Severity", width=14)
@@ -454,17 +524,17 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
454
524
  table.add_column("Scan Only", width=12, justify="right")
455
525
  table.add_column("Confirmed", width=12, justify="right")
456
526
 
457
- for sev in ['Critical', 'High', 'Medium', 'Low']:
458
- color = SEVERITY_COLORS.get(sev, 'white')
459
- splunk_only = breakdown.get('splunk_only', {}).get(sev, 0)
460
- scan_only = breakdown.get('scan_only', {}).get(sev, 0)
461
- confirmed = breakdown.get('confirmed', {}).get(sev, 0)
527
+ for sev in ["Critical", "High", "Medium", "Low"]:
528
+ color = SEVERITY_COLORS.get(sev, "white")
529
+ splunk_only = breakdown.get("splunk_only", {}).get(sev, 0)
530
+ scan_only = breakdown.get("scan_only", {}).get(sev, 0)
531
+ confirmed = breakdown.get("confirmed", {}).get(sev, 0)
462
532
 
463
533
  table.add_row(
464
534
  f"[{color}]{sev}[/{color}]",
465
535
  str(splunk_only) if splunk_only else "-",
466
536
  str(scan_only) if scan_only else "-",
467
- str(confirmed) if confirmed else "-"
537
+ str(confirmed) if confirmed else "-",
468
538
  )
469
539
 
470
540
  console.print(" ", table)
@@ -483,16 +553,31 @@ def _show_splunk_only(result: SplunkGapResult, width: int) -> None:
483
553
  width = DesignSystem.get_terminal_width()
484
554
 
485
555
  click.echo("\n┌" + "─" * (width - 2) + "┐")
486
- click.echo("│" + click.style(" SPLUNK ONLY - SCANS MISSED ".center(width - 2), bold=True, fg='yellow') + "│")
556
+ click.echo(
557
+ "│"
558
+ + click.style(
559
+ " SPLUNK ONLY - SCANS MISSED ".center(width - 2), bold=True, fg="yellow"
560
+ )
561
+ + "│"
562
+ )
487
563
  click.echo("└" + "─" * (width - 2) + "┘")
488
564
  click.echo()
489
565
 
490
566
  click.echo(f" {len(gaps)} CVEs detected by Splunk but NOT by active scans.")
491
- click.echo(click.style(" These may be local/package vulnerabilities not exposed to network scanning.", fg='bright_black'))
567
+ click.echo(
568
+ click.style(
569
+ " These may be local/package vulnerabilities not exposed to network scanning.",
570
+ fg="bright_black",
571
+ )
572
+ )
492
573
  click.echo()
493
574
 
494
575
  if not gaps:
495
- click.echo(click.style(" V No gaps - all Splunk vulns confirmed by scans!", fg='green'))
576
+ click.echo(
577
+ click.style(
578
+ " V No gaps - all Splunk vulns confirmed by scans!", fg="green"
579
+ )
580
+ )
496
581
  click.echo()
497
582
  click.pause(" Press any key to return...")
498
583
  return
@@ -508,7 +593,13 @@ def _show_splunk_only(result: SplunkGapResult, width: int) -> None:
508
593
  end_idx = min(start_idx + page_size, len(gaps))
509
594
  page_gaps = gaps[start_idx:end_idx]
510
595
 
511
- _render_gaps_table(page_gaps, show_package=True, page=page, page_size=page_size, view_all=view_all)
596
+ _render_gaps_table(
597
+ page_gaps,
598
+ show_package=True,
599
+ page=page,
600
+ page_size=page_size,
601
+ view_all=view_all,
602
+ )
512
603
 
513
604
  # Pagination info
514
605
  if view_all:
@@ -534,17 +625,17 @@ def _show_splunk_only(result: SplunkGapResult, width: int) -> None:
534
625
  try:
535
626
  choice = input(" Select option: ").strip().lower()
536
627
 
537
- if choice == 'q':
628
+ if choice == "q":
538
629
  return
539
- elif choice == 'i':
630
+ elif choice == "i":
540
631
  _interactive_gaps_mode(gaps, "SPLUNK ONLY GAPS", show_package=True)
541
- elif choice == 't':
632
+ elif choice == "t":
542
633
  view_all = not view_all
543
634
  if not view_all:
544
635
  page = 0
545
- elif choice == 'n' and not view_all and page < total_pages - 1:
636
+ elif choice == "n" and not view_all and page < total_pages - 1:
546
637
  page += 1
547
- elif choice == 'p' and not view_all and page > 0:
638
+ elif choice == "p" and not view_all and page > 0:
548
639
  page -= 1
549
640
  elif choice.isdigit():
550
641
  idx = int(choice) - 1
@@ -566,16 +657,31 @@ def _show_scan_only(result: SplunkGapResult, width: int) -> None:
566
657
  width = DesignSystem.get_terminal_width()
567
658
 
568
659
  click.echo("\n┌" + "─" * (width - 2) + "┐")
569
- click.echo("│" + click.style(" SCAN ONLY - SPLUNK MISSED ".center(width - 2), bold=True, fg='blue') + "│")
660
+ click.echo(
661
+ "│"
662
+ + click.style(
663
+ " SCAN ONLY - SPLUNK MISSED ".center(width - 2), bold=True, fg="blue"
664
+ )
665
+ + "│"
666
+ )
570
667
  click.echo("└" + "─" * (width - 2) + "┘")
571
668
  click.echo()
572
669
 
573
670
  click.echo(f" {len(gaps)} CVEs detected by active scans but NOT by Splunk.")
574
- click.echo(click.style(" This may indicate: missing Wazuh agent, detection rule gap, or network-only vuln.", fg='bright_black'))
671
+ click.echo(
672
+ click.style(
673
+ " This may indicate: missing Wazuh agent, detection rule gap, or network-only vuln.",
674
+ fg="bright_black",
675
+ )
676
+ )
575
677
  click.echo()
576
678
 
577
679
  if not gaps:
578
- click.echo(click.style(" V No gaps - Splunk detected all scan findings!", fg='green'))
680
+ click.echo(
681
+ click.style(
682
+ " V No gaps - Splunk detected all scan findings!", fg="green"
683
+ )
684
+ )
579
685
  click.echo()
580
686
  click.pause(" Press any key to return...")
581
687
  return
@@ -591,7 +697,9 @@ def _show_scan_only(result: SplunkGapResult, width: int) -> None:
591
697
  end_idx = min(start_idx + page_size, len(gaps))
592
698
  page_gaps = gaps[start_idx:end_idx]
593
699
 
594
- _render_gaps_table(page_gaps, show_tool=True, page=page, page_size=page_size, view_all=view_all)
700
+ _render_gaps_table(
701
+ page_gaps, show_tool=True, page=page, page_size=page_size, view_all=view_all
702
+ )
595
703
 
596
704
  # Pagination info
597
705
  if view_all:
@@ -617,17 +725,17 @@ def _show_scan_only(result: SplunkGapResult, width: int) -> None:
617
725
  try:
618
726
  choice = input(" Select option: ").strip().lower()
619
727
 
620
- if choice == 'q':
728
+ if choice == "q":
621
729
  return
622
- elif choice == 'i':
730
+ elif choice == "i":
623
731
  _interactive_gaps_mode(gaps, "SCAN ONLY GAPS", show_tool=True)
624
- elif choice == 't':
732
+ elif choice == "t":
625
733
  view_all = not view_all
626
734
  if not view_all:
627
735
  page = 0
628
- elif choice == 'n' and not view_all and page < total_pages - 1:
736
+ elif choice == "n" and not view_all and page < total_pages - 1:
629
737
  page += 1
630
- elif choice == 'p' and not view_all and page > 0:
738
+ elif choice == "p" and not view_all and page > 0:
631
739
  page -= 1
632
740
  elif choice.isdigit():
633
741
  idx = int(choice) - 1
@@ -649,16 +757,31 @@ def _show_confirmed(result: SplunkGapResult, width: int) -> None:
649
757
  width = DesignSystem.get_terminal_width()
650
758
 
651
759
  click.echo("\n┌" + "─" * (width - 2) + "┐")
652
- click.echo("│" + click.style(" CONFIRMED - BOTH SOURCES ".center(width - 2), bold=True, fg='green') + "│")
760
+ click.echo(
761
+ "│"
762
+ + click.style(
763
+ " CONFIRMED - BOTH SOURCES ".center(width - 2), bold=True, fg="green"
764
+ )
765
+ + "│"
766
+ )
653
767
  click.echo("└" + "─" * (width - 2) + "┘")
654
768
  click.echo()
655
769
 
656
770
  click.echo(f" {len(gaps)} CVEs detected by BOTH Splunk and active scans.")
657
- click.echo(click.style(" High confidence - prioritize these for exploitation.", fg='bright_black'))
771
+ click.echo(
772
+ click.style(
773
+ " High confidence - prioritize these for exploitation.",
774
+ fg="bright_black",
775
+ )
776
+ )
658
777
  click.echo()
659
778
 
660
779
  if not gaps:
661
- click.echo(click.style(" No confirmed matches between Splunk and scans.", fg='yellow'))
780
+ click.echo(
781
+ click.style(
782
+ " No confirmed matches between Splunk and scans.", fg="yellow"
783
+ )
784
+ )
662
785
  click.echo()
663
786
  click.pause(" Press any key to return...")
664
787
  return
@@ -674,7 +797,9 @@ def _show_confirmed(result: SplunkGapResult, width: int) -> None:
674
797
  end_idx = min(start_idx + page_size, len(gaps))
675
798
  page_gaps = gaps[start_idx:end_idx]
676
799
 
677
- _render_gaps_table(page_gaps, show_both=True, page=page, page_size=page_size, view_all=view_all)
800
+ _render_gaps_table(
801
+ page_gaps, show_both=True, page=page, page_size=page_size, view_all=view_all
802
+ )
678
803
 
679
804
  # Pagination info
680
805
  if view_all:
@@ -700,17 +825,17 @@ def _show_confirmed(result: SplunkGapResult, width: int) -> None:
700
825
  try:
701
826
  choice = input(" Select option: ").strip().lower()
702
827
 
703
- if choice == 'q':
828
+ if choice == "q":
704
829
  return
705
- elif choice == 'i':
830
+ elif choice == "i":
706
831
  _interactive_gaps_mode(gaps, "CONFIRMED GAPS", show_both=True)
707
- elif choice == 't':
832
+ elif choice == "t":
708
833
  view_all = not view_all
709
834
  if not view_all:
710
835
  page = 0
711
- elif choice == 'n' and not view_all and page < total_pages - 1:
836
+ elif choice == "n" and not view_all and page < total_pages - 1:
712
837
  page += 1
713
- elif choice == 'p' and not view_all and page > 0:
838
+ elif choice == "p" and not view_all and page > 0:
714
839
  page -= 1
715
840
  elif choice.isdigit():
716
841
  idx = int(choice) - 1
@@ -729,35 +854,53 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
729
854
  # Get high-priority gaps (Critical/High severity from Splunk that scans missed)
730
855
  gaps = []
731
856
  for g in result.splunk_only:
732
- if g.severity in ('Critical', 'High'):
733
- gaps.append({
734
- 'cve_id': g.cve_id,
735
- 'severity': g.severity,
736
- 'host_ip': g.host_ip,
737
- 'package': g.splunk_details.get('package_name', '-') if g.splunk_details else '-',
738
- 'package_version': g.splunk_details.get('package_version', '-') if g.splunk_details else '-',
739
- 'recommendation': g.recommendation,
740
- 'priority': 'high' if g.severity == 'Critical' else 'medium'
741
- })
857
+ if g.severity in ("Critical", "High"):
858
+ gaps.append(
859
+ {
860
+ "cve_id": g.cve_id,
861
+ "severity": g.severity,
862
+ "host_ip": g.host_ip,
863
+ "package": (
864
+ g.splunk_details.get("package_name", "-")
865
+ if g.splunk_details
866
+ else "-"
867
+ ),
868
+ "package_version": (
869
+ g.splunk_details.get("package_version", "-")
870
+ if g.splunk_details
871
+ else "-"
872
+ ),
873
+ "recommendation": g.recommendation,
874
+ "priority": "high" if g.severity == "Critical" else "medium",
875
+ }
876
+ )
742
877
 
743
878
  # Sort by severity
744
- severity_order = {'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3}
745
- gaps.sort(key=lambda x: severity_order.get(x['severity'], 4))
879
+ severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
880
+ gaps.sort(key=lambda x: severity_order.get(x["severity"], 4))
746
881
 
747
882
  while True:
748
883
  DesignSystem.clear_screen()
749
884
  width = DesignSystem.get_terminal_width()
750
885
 
751
886
  click.echo("\n┌" + "─" * (width - 2) + "┐")
752
- click.echo("│" + click.style(" ACTIONABLE GAPS ".center(width - 2), bold=True, fg='yellow') + "│")
887
+ click.echo(
888
+ "│"
889
+ + click.style(" ACTIONABLE GAPS ".center(width - 2), bold=True, fg="yellow")
890
+ + "│"
891
+ )
753
892
  click.echo("└" + "─" * (width - 2) + "┘")
754
893
  click.echo()
755
894
 
756
- click.echo(f" {len(gaps)} prioritized vulnerabilities from Splunk that need targeted scanning.")
895
+ click.echo(
896
+ f" {len(gaps)} prioritized vulnerabilities from Splunk that need targeted scanning."
897
+ )
757
898
  click.echo()
758
899
 
759
900
  if not gaps:
760
- click.echo(click.style(" V No actionable gaps - great scan coverage!", fg='green'))
901
+ click.echo(
902
+ click.style(" V No actionable gaps - great scan coverage!", fg="green")
903
+ )
761
904
  click.echo()
762
905
  click.pause(" Press any key to return...")
763
906
  return
@@ -791,21 +934,23 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
791
934
  else:
792
935
  display_idx = (page * page_size) + idx + 1
793
936
 
794
- priority = gap.get('priority', 'medium')
795
- priority_display = "[red]HIGH[/red]" if priority == 'high' else "[yellow]MEDIUM[/yellow]"
937
+ priority = gap.get("priority", "medium")
938
+ priority_display = (
939
+ "[red]HIGH[/red]" if priority == "high" else "[yellow]MEDIUM[/yellow]"
940
+ )
796
941
 
797
- severity = gap.get('severity', 'Medium')
798
- sev_color = SEVERITY_COLORS.get(severity, 'white')
942
+ severity = gap.get("severity", "Medium")
943
+ sev_color = SEVERITY_COLORS.get(severity, "white")
799
944
 
800
945
  table.add_row(
801
946
  "○",
802
947
  str(display_idx),
803
948
  priority_display,
804
- gap.get('cve_id', '-'),
949
+ gap.get("cve_id", "-"),
805
950
  f"[{sev_color}]{severity}[/{sev_color}]",
806
- gap.get('host_ip', '-'),
807
- gap.get('package', '-')[:19],
808
- gap.get('recommendation', '-')[:34]
951
+ gap.get("host_ip", "-"),
952
+ gap.get("package", "-")[:19],
953
+ gap.get("recommendation", "-")[:34],
809
954
  )
810
955
 
811
956
  console.print(table)
@@ -834,17 +979,17 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
834
979
  try:
835
980
  choice = input(" Select option: ").strip().lower()
836
981
 
837
- if choice == 'q':
982
+ if choice == "q":
838
983
  return
839
- elif choice == 'i':
984
+ elif choice == "i":
840
985
  _interactive_actionable_gaps_mode(gaps)
841
- elif choice == 't':
986
+ elif choice == "t":
842
987
  view_all = not view_all
843
988
  if not view_all:
844
989
  page = 0
845
- elif choice == 'n' and not view_all and page < total_pages - 1:
990
+ elif choice == "n" and not view_all and page < total_pages - 1:
846
991
  page += 1
847
- elif choice == 'p' and not view_all and page > 0:
992
+ elif choice == "p" and not view_all and page > 0:
848
993
  page -= 1
849
994
  elif choice.isdigit():
850
995
  idx = int(choice) - 1
@@ -855,8 +1000,13 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
855
1000
 
856
1001
 
857
1002
  def _render_gaps_table(
858
- gaps: List, show_package: bool = False, show_tool: bool = False, show_both: bool = False,
859
- page: int = 0, page_size: int = 20, view_all: bool = False
1003
+ gaps: List,
1004
+ show_package: bool = False,
1005
+ show_tool: bool = False,
1006
+ show_both: bool = False,
1007
+ page: int = 0,
1008
+ page_size: int = 20,
1009
+ view_all: bool = False,
860
1010
  ) -> None:
861
1011
  """Render gaps table with pagination support."""
862
1012
  table = DesignSystem.create_table()
@@ -885,27 +1035,35 @@ def _render_gaps_table(
885
1035
  display_idx = (page * page_size) + idx + 1
886
1036
 
887
1037
  severity = gap.severity
888
- sev_color = SEVERITY_COLORS.get(severity, 'white')
1038
+ sev_color = SEVERITY_COLORS.get(severity, "white")
889
1039
 
890
1040
  row = [
891
1041
  "○",
892
1042
  str(display_idx),
893
1043
  gap.cve_id,
894
1044
  f"[{sev_color}]{severity}[/{sev_color}]",
895
- gap.host_ip or "-"
1045
+ gap.host_ip or "-",
896
1046
  ]
897
1047
 
898
1048
  if show_package:
899
- package = gap.splunk_details.get('package_name', '-') if gap.splunk_details else '-'
1049
+ package = (
1050
+ gap.splunk_details.get("package_name", "-")
1051
+ if gap.splunk_details
1052
+ else "-"
1053
+ )
900
1054
  row.append(package[:24])
901
1055
  row.append(gap.recommendation[:34])
902
1056
  elif show_tool:
903
- tool = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
1057
+ tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
904
1058
  row.append(tool)
905
1059
  row.append(gap.recommendation[:39])
906
1060
  elif show_both:
907
- package = gap.splunk_details.get('package_name', '-') if gap.splunk_details else '-'
908
- tool = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
1061
+ package = (
1062
+ gap.splunk_details.get("package_name", "-")
1063
+ if gap.splunk_details
1064
+ else "-"
1065
+ )
1066
+ tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
909
1067
  row.append(package[:19])
910
1068
  row.append(tool)
911
1069
  row.append(f"[green]{gap.confidence}[/green]")
@@ -920,12 +1078,14 @@ def _show_gap_detail(gap: SplunkVulnGap) -> None:
920
1078
  DesignSystem.clear_screen()
921
1079
  width = DesignSystem.get_terminal_width()
922
1080
 
923
- cve = gap.cve_id or 'Unknown'
924
- severity = gap.severity or 'Medium'
925
- sev_color = SEVERITY_COLORS.get(severity, 'white')
1081
+ cve = gap.cve_id or "Unknown"
1082
+ severity = gap.severity or "Medium"
1083
+ sev_color = SEVERITY_COLORS.get(severity, "white")
926
1084
 
927
1085
  click.echo("\n┌" + "─" * (width - 2) + "┐")
928
- click.echo("│" + click.style(f" {cve} ".center(width - 2), bold=True, fg='cyan') + "│")
1086
+ click.echo(
1087
+ "│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
1088
+ )
929
1089
  click.echo("└" + "─" * (width - 2) + "┘")
930
1090
  click.echo()
931
1091
 
@@ -964,20 +1124,24 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
964
1124
  DesignSystem.clear_screen()
965
1125
  width = DesignSystem.get_terminal_width()
966
1126
 
967
- cve = gap.get('cve_id', 'Unknown')
968
- severity = gap.get('severity', 'Medium')
969
- sev_color = SEVERITY_COLORS.get(severity, 'white')
1127
+ cve = gap.get("cve_id", "Unknown")
1128
+ severity = gap.get("severity", "Medium")
1129
+ sev_color = SEVERITY_COLORS.get(severity, "white")
970
1130
 
971
1131
  click.echo("\n┌" + "─" * (width - 2) + "┐")
972
- click.echo("│" + click.style(f" {cve} ".center(width - 2), bold=True, fg='cyan') + "│")
1132
+ click.echo(
1133
+ "│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
1134
+ )
973
1135
  click.echo("└" + "─" * (width - 2) + "┘")
974
1136
  click.echo()
975
1137
 
976
- priority = gap.get('priority', 'medium')
977
- priority_color = 'red' if priority == 'high' else 'yellow'
1138
+ priority = gap.get("priority", "medium")
1139
+ priority_color = "red" if priority == "high" else "yellow"
978
1140
 
979
1141
  click.echo(f" Severity: " + click.style(severity, fg=sev_color, bold=True))
980
- click.echo(f" Priority: " + click.style(priority.upper(), fg=priority_color, bold=True))
1142
+ click.echo(
1143
+ f" Priority: " + click.style(priority.upper(), fg=priority_color, bold=True)
1144
+ )
981
1145
  click.echo(f" Host: {gap.get('host_ip', '-')}")
982
1146
  click.echo()
983
1147
 
@@ -986,7 +1150,7 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
986
1150
  click.echo(f" Version: {gap.get('package_version', '-')}")
987
1151
  click.echo()
988
1152
 
989
- if gap.get('recommendation'):
1153
+ if gap.get("recommendation"):
990
1154
  click.echo(click.style(" Recommendation:", bold=True))
991
1155
  click.echo(f" {gap.get('recommendation')}")
992
1156
  click.echo()
@@ -995,13 +1159,17 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
995
1159
 
996
1160
 
997
1161
  def _interactive_gaps_mode(
998
- gaps: List, title: str, show_package: bool = False, show_tool: bool = False, show_both: bool = False
1162
+ gaps: List,
1163
+ title: str,
1164
+ show_package: bool = False,
1165
+ show_tool: bool = False,
1166
+ show_both: bool = False,
999
1167
  ) -> None:
1000
1168
  """Interactive selection mode for gaps."""
1001
1169
  from souleyez.ui.interactive_selector import interactive_select
1002
1170
 
1003
1171
  if not gaps:
1004
- click.echo(click.style(" No gaps to select.", fg='yellow'))
1172
+ click.echo(click.style(" No gaps to select.", fg="yellow"))
1005
1173
  click.pause()
1006
1174
  return
1007
1175
 
@@ -1009,57 +1177,69 @@ def _interactive_gaps_mode(
1009
1177
  gap_items = []
1010
1178
  for gap in gaps:
1011
1179
  item = {
1012
- 'id': id(gap),
1013
- 'cve_id': gap.cve_id or '-',
1014
- 'severity': gap.severity or 'Medium',
1015
- 'host': gap.host_ip or '-',
1016
- 'raw': gap
1180
+ "id": id(gap),
1181
+ "cve_id": gap.cve_id or "-",
1182
+ "severity": gap.severity or "Medium",
1183
+ "host": gap.host_ip or "-",
1184
+ "raw": gap,
1017
1185
  }
1018
1186
  if show_package:
1019
- item['package'] = gap.splunk_details.get('package_name', '-')[:20] if gap.splunk_details else '-'
1187
+ item["package"] = (
1188
+ gap.splunk_details.get("package_name", "-")[:20]
1189
+ if gap.splunk_details
1190
+ else "-"
1191
+ )
1020
1192
  elif show_tool:
1021
- item['tool'] = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
1193
+ item["tool"] = (
1194
+ gap.scan_details.get("tool", "-") if gap.scan_details else "-"
1195
+ )
1022
1196
  elif show_both:
1023
- item['package'] = gap.splunk_details.get('package_name', '-')[:15] if gap.splunk_details else '-'
1024
- item['tool'] = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
1197
+ item["package"] = (
1198
+ gap.splunk_details.get("package_name", "-")[:15]
1199
+ if gap.splunk_details
1200
+ else "-"
1201
+ )
1202
+ item["tool"] = (
1203
+ gap.scan_details.get("tool", "-") if gap.scan_details else "-"
1204
+ )
1025
1205
  gap_items.append(item)
1026
1206
 
1027
1207
  columns = [
1028
- {'name': 'CVE', 'key': 'cve_id', 'width': 18},
1029
- {'name': 'Severity', 'key': 'severity', 'width': 10},
1030
- {'name': 'Host', 'key': 'host', 'width': 15}
1208
+ {"name": "CVE", "key": "cve_id", "width": 18},
1209
+ {"name": "Severity", "key": "severity", "width": 10},
1210
+ {"name": "Host", "key": "host", "width": 15},
1031
1211
  ]
1032
1212
 
1033
1213
  if show_package:
1034
- columns.append({'name': 'Package', 'key': 'package', 'width': 20})
1214
+ columns.append({"name": "Package", "key": "package", "width": 20})
1035
1215
  elif show_tool:
1036
- columns.append({'name': 'Tool', 'key': 'tool', 'width': 15})
1216
+ columns.append({"name": "Tool", "key": "tool", "width": 15})
1037
1217
  elif show_both:
1038
- columns.append({'name': 'Package', 'key': 'package', 'width': 15})
1039
- columns.append({'name': 'Tool', 'key': 'tool', 'width': 15})
1218
+ columns.append({"name": "Package", "key": "package", "width": 15})
1219
+ columns.append({"name": "Tool", "key": "tool", "width": 15})
1040
1220
 
1041
1221
  def format_cell(item: Dict, key: str) -> str:
1042
- if key == 'severity':
1043
- sev = item.get('severity', 'Medium')
1044
- color = SEVERITY_COLORS.get(sev, 'white')
1222
+ if key == "severity":
1223
+ sev = item.get("severity", "Medium")
1224
+ color = SEVERITY_COLORS.get(sev, "white")
1045
1225
  return f"[{color}]{sev}[/{color}]"
1046
- return str(item.get(key, '-'))
1226
+ return str(item.get(key, "-"))
1047
1227
 
1048
1228
  selected_ids: set = set()
1049
1229
  interactive_select(
1050
1230
  items=gap_items,
1051
1231
  columns=columns,
1052
1232
  selected_ids=selected_ids,
1053
- get_id=lambda g: g['id'],
1233
+ get_id=lambda g: g["id"],
1054
1234
  title=title,
1055
- format_cell=format_cell
1235
+ format_cell=format_cell,
1056
1236
  )
1057
1237
 
1058
1238
  # Show details of first selected
1059
1239
  if selected_ids:
1060
1240
  for item in gap_items:
1061
- if item['id'] in selected_ids:
1062
- _show_gap_detail(item['raw'])
1241
+ if item["id"] in selected_ids:
1242
+ _show_gap_detail(item["raw"])
1063
1243
  break
1064
1244
 
1065
1245
 
@@ -1068,55 +1248,57 @@ def _interactive_actionable_gaps_mode(gaps: List[Dict]) -> None:
1068
1248
  from souleyez.ui.interactive_selector import interactive_select
1069
1249
 
1070
1250
  if not gaps:
1071
- click.echo(click.style(" No gaps to select.", fg='yellow'))
1251
+ click.echo(click.style(" No gaps to select.", fg="yellow"))
1072
1252
  click.pause()
1073
1253
  return
1074
1254
 
1075
1255
  # Prepare items for interactive selector
1076
1256
  gap_items = []
1077
1257
  for gap in gaps:
1078
- gap_items.append({
1079
- 'id': id(gap),
1080
- 'cve_id': gap.get('cve_id', '-'),
1081
- 'severity': gap.get('severity', 'Medium'),
1082
- 'priority': gap.get('priority', 'medium').upper(),
1083
- 'host': gap.get('host_ip', '-'),
1084
- 'package': gap.get('package', '-')[:20],
1085
- 'raw': gap
1086
- })
1258
+ gap_items.append(
1259
+ {
1260
+ "id": id(gap),
1261
+ "cve_id": gap.get("cve_id", "-"),
1262
+ "severity": gap.get("severity", "Medium"),
1263
+ "priority": gap.get("priority", "medium").upper(),
1264
+ "host": gap.get("host_ip", "-"),
1265
+ "package": gap.get("package", "-")[:20],
1266
+ "raw": gap,
1267
+ }
1268
+ )
1087
1269
 
1088
1270
  columns = [
1089
- {'name': 'Priority', 'key': 'priority', 'width': 8},
1090
- {'name': 'CVE', 'key': 'cve_id', 'width': 18},
1091
- {'name': 'Severity', 'key': 'severity', 'width': 10},
1092
- {'name': 'Host', 'key': 'host', 'width': 15},
1093
- {'name': 'Package', 'key': 'package', 'width': 20}
1271
+ {"name": "Priority", "key": "priority", "width": 8},
1272
+ {"name": "CVE", "key": "cve_id", "width": 18},
1273
+ {"name": "Severity", "key": "severity", "width": 10},
1274
+ {"name": "Host", "key": "host", "width": 15},
1275
+ {"name": "Package", "key": "package", "width": 20},
1094
1276
  ]
1095
1277
 
1096
1278
  def format_cell(item: Dict, key: str) -> str:
1097
- if key == 'severity':
1098
- sev = item.get('severity', 'Medium')
1099
- color = SEVERITY_COLORS.get(sev, 'white')
1279
+ if key == "severity":
1280
+ sev = item.get("severity", "Medium")
1281
+ color = SEVERITY_COLORS.get(sev, "white")
1100
1282
  return f"[{color}]{sev}[/{color}]"
1101
- elif key == 'priority':
1102
- pri = item.get('priority', 'MEDIUM')
1103
- color = 'red' if pri == 'HIGH' else 'yellow'
1283
+ elif key == "priority":
1284
+ pri = item.get("priority", "MEDIUM")
1285
+ color = "red" if pri == "HIGH" else "yellow"
1104
1286
  return f"[{color}]{pri}[/{color}]"
1105
- return str(item.get(key, '-'))
1287
+ return str(item.get(key, "-"))
1106
1288
 
1107
1289
  selected_ids: set = set()
1108
1290
  interactive_select(
1109
1291
  items=gap_items,
1110
1292
  columns=columns,
1111
1293
  selected_ids=selected_ids,
1112
- get_id=lambda g: g['id'],
1294
+ get_id=lambda g: g["id"],
1113
1295
  title="ACTIONABLE GAPS",
1114
- format_cell=format_cell
1296
+ format_cell=format_cell,
1115
1297
  )
1116
1298
 
1117
1299
  # Show details of first selected
1118
1300
  if selected_ids:
1119
1301
  for item in gap_items:
1120
- if item['id'] in selected_ids:
1121
- _show_actionable_gap_detail(item['raw'])
1302
+ if item["id"] in selected_ids:
1303
+ _show_actionable_gap_detail(item["raw"])
1122
1304
  break