souleyez 2.43.29__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 +22827 -10678
  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.29.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.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -17,14 +17,14 @@ console = Console()
17
17
 
18
18
  # Severity colors for click
19
19
  SEVERITY_COLORS = {
20
- 'Critical': 'red',
21
- 'High': 'yellow',
22
- 'Medium': 'white',
23
- 'Low': 'bright_black',
24
- 'critical': 'red',
25
- 'high': 'yellow',
26
- 'medium': 'white',
27
- 'low': 'bright_black'
20
+ "Critical": "red",
21
+ "High": "yellow",
22
+ "Medium": "white",
23
+ "Low": "bright_black",
24
+ "critical": "red",
25
+ "high": "yellow",
26
+ "medium": "white",
27
+ "low": "bright_black",
28
28
  }
29
29
 
30
30
 
@@ -42,16 +42,30 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
42
42
 
43
43
  # Header
44
44
  click.echo("\n┌" + "─" * (width - 2) + "┐")
45
- click.echo("│" + click.style(" WAZUH GAP ANALYSIS ".center(width - 2), bold=True, fg='cyan') + "│")
45
+ click.echo(
46
+ "│"
47
+ + click.style(
48
+ " WAZUH GAP ANALYSIS ".center(width - 2), bold=True, fg="cyan"
49
+ )
50
+ + "│"
51
+ )
46
52
  click.echo("└" + "─" * (width - 2) + "┘")
47
53
  click.echo()
48
- click.echo(click.style(" Compare Wazuh (passive) vs Scan (active) findings", fg='bright_black'))
54
+ click.echo(
55
+ click.style(
56
+ " Compare Wazuh (passive) vs Scan (active) findings", fg="bright_black"
57
+ )
58
+ )
49
59
  click.echo()
50
60
 
51
61
  # Check if Wazuh is configured
52
62
  config = WazuhConfig.get_config(engagement_id)
53
- if not config or not config.get('enabled'):
54
- click.echo(click.style(" ⚠️ Wazuh is not configured for this engagement.", fg='yellow'))
63
+ if not config or not config.get("enabled"):
64
+ click.echo(
65
+ click.style(
66
+ " ⚠️ Wazuh is not configured for this engagement.", fg="yellow"
67
+ )
68
+ )
55
69
  click.echo()
56
70
  click.echo(" Configure Wazuh in Settings → Integrations → Wazuh SIEM")
57
71
  click.echo(" Then sync vulnerabilities before running gap analysis.")
@@ -60,7 +74,7 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
60
74
  click.echo()
61
75
  click.echo(" [q] Back")
62
76
  click.echo()
63
- if click.getchar().lower() == 'q':
77
+ if click.getchar().lower() == "q":
64
78
  return
65
79
  continue
66
80
 
@@ -68,8 +82,8 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
68
82
  sync = WazuhVulnSync(engagement_id)
69
83
  sync_status = sync.get_sync_status()
70
84
 
71
- if not sync_status.get('synced'):
72
- click.echo(click.style(" ⚠️ No Wazuh data synced yet.", fg='yellow'))
85
+ if not sync_status.get("synced"):
86
+ click.echo(click.style(" ⚠️ No Wazuh data synced yet.", fg="yellow"))
73
87
  click.echo()
74
88
  click.echo(" Press [s] to sync vulnerabilities from Wazuh first.")
75
89
  click.echo()
@@ -80,9 +94,9 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
80
94
  click.echo()
81
95
 
82
96
  key = click.getchar().lower()
83
- if key == 's':
97
+ if key == "s":
84
98
  _do_sync(engagement_id)
85
- elif key == 'q':
99
+ elif key == "q":
86
100
  return
87
101
  continue
88
102
 
@@ -108,60 +122,79 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
108
122
  try:
109
123
  choice = input(" Select option: ").strip().lower()
110
124
 
111
- if choice == 'q':
125
+ if choice == "q":
112
126
  return
113
- elif choice == '1':
127
+ elif choice == "1":
114
128
  _show_wazuh_only(result, width)
115
- elif choice == '2':
129
+ elif choice == "2":
116
130
  _show_scan_only(result, width)
117
- elif choice == '3':
131
+ elif choice == "3":
118
132
  _show_confirmed(result, width)
119
- elif choice == 'a':
133
+ elif choice == "a":
120
134
  _show_actionable_gaps(engagement_id, width)
121
- elif choice == 's':
135
+ elif choice == "s":
122
136
  _do_sync(engagement_id)
123
137
  except (KeyboardInterrupt, EOFError):
124
138
  return
125
139
 
126
140
 
127
- def _render_summary_dashboard(result: GapAnalysisResult, stats: Dict, width: int) -> None:
141
+ def _render_summary_dashboard(
142
+ result: GapAnalysisResult, stats: Dict, width: int
143
+ ) -> None:
128
144
  """Render the summary dashboard."""
129
145
  wazuh_total = result.wazuh_total
130
146
  scan_total = result.scan_total
131
147
  confirmed = len(result.confirmed)
132
148
  wazuh_only = len(result.wazuh_only)
133
149
  scan_only = len(result.scan_only)
134
- coverage = stats.get('coverage_pct', 0)
150
+ coverage = stats.get("coverage_pct", 0)
135
151
 
136
152
  # Coverage color
137
153
  if coverage >= 80:
138
- coverage_color = 'green'
154
+ coverage_color = "green"
139
155
  elif coverage >= 50:
140
- coverage_color = 'yellow'
156
+ coverage_color = "yellow"
141
157
  else:
142
- coverage_color = 'red'
158
+ coverage_color = "red"
143
159
 
144
160
  # Detection Sources
145
161
  click.echo(click.style(" DETECTION SOURCES", bold=True))
146
- click.echo(f" Wazuh (passive): {click.style(str(wazuh_total), fg='cyan', bold=True)} CVEs")
147
- click.echo(f" Scans (active): {click.style(str(scan_total), fg='cyan', bold=True)} CVEs")
162
+ click.echo(
163
+ f" Wazuh (passive): {click.style(str(wazuh_total), fg='cyan', bold=True)} CVEs"
164
+ )
165
+ click.echo(
166
+ f" Scans (active): {click.style(str(scan_total), fg='cyan', bold=True)} CVEs"
167
+ )
148
168
  click.echo()
149
169
 
150
170
  # Analysis Results
151
171
  click.echo(click.style(" ANALYSIS RESULTS", bold=True))
152
- click.echo(f" {click.style('✓', fg='green')} Confirmed (both): {click.style(str(confirmed), bold=True)}")
153
- click.echo(f" {click.style('', fg='yellow')} Wazuh Only: {click.style(str(wazuh_only), bold=True)} ← Scans missed these")
154
- click.echo(f" {click.style('○', fg='blue')} Scan Only: {click.style(str(scan_only), bold=True)} ← Wazuh missed these")
172
+ click.echo(
173
+ f" {click.style('', fg='green')} Confirmed (both): {click.style(str(confirmed), bold=True)}"
174
+ )
175
+ click.echo(
176
+ f" {click.style('⚠', fg='yellow')} Wazuh Only: {click.style(str(wazuh_only), bold=True)} ← Scans missed these"
177
+ )
178
+ click.echo(
179
+ f" {click.style('○', fg='blue')} Scan Only: {click.style(str(scan_only), bold=True)} ← Wazuh missed these"
180
+ )
155
181
  click.echo()
156
182
 
157
183
  # Coverage
158
- click.echo(f" Coverage: " + click.style(f"{coverage:.1f}%", fg=coverage_color, bold=True) +
159
- " of Wazuh vulns confirmed by scans")
184
+ click.echo(
185
+ f" Coverage: "
186
+ + click.style(f"{coverage:.1f}%", fg=coverage_color, bold=True)
187
+ + " of Wazuh vulns confirmed by scans"
188
+ )
160
189
  click.echo()
161
190
 
162
191
  # Severity breakdown
163
- sev_breakdown = stats.get('by_severity', {})
164
- if sev_breakdown.get('wazuh_only') or sev_breakdown.get('scan_only') or sev_breakdown.get('confirmed'):
192
+ sev_breakdown = stats.get("by_severity", {})
193
+ if (
194
+ sev_breakdown.get("wazuh_only")
195
+ or sev_breakdown.get("scan_only")
196
+ or sev_breakdown.get("confirmed")
197
+ ):
165
198
  _render_severity_breakdown(sev_breakdown, width)
166
199
 
167
200
 
@@ -170,19 +203,14 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
170
203
  from rich.table import Table
171
204
  from rich import box
172
205
 
173
- SEVERITY_ICONS = {
174
- 'Critical': '🔴',
175
- 'High': '🟠',
176
- 'Medium': '🟡',
177
- 'Low': '⚪'
178
- }
206
+ SEVERITY_ICONS = {"Critical": "🔴", "High": "🟠", "Medium": "🟡", "Low": "⚪"}
179
207
 
180
208
  table = Table(
181
209
  show_header=True,
182
210
  header_style="bold",
183
211
  box=box.SIMPLE,
184
212
  padding=(0, 2),
185
- expand=False
213
+ expand=False,
186
214
  )
187
215
 
188
216
  table.add_column("Severity", width=14)
@@ -190,18 +218,18 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
190
218
  table.add_column("Scan Only", width=12, justify="right")
191
219
  table.add_column("Confirmed", width=12, justify="right")
192
220
 
193
- for sev in ['Critical', 'High', 'Medium', 'Low']:
194
- icon = SEVERITY_ICONS.get(sev, '')
195
- color = SEVERITY_COLORS.get(sev, 'white')
196
- wazuh_only = breakdown.get('wazuh_only', {}).get(sev, 0)
197
- scan_only = breakdown.get('scan_only', {}).get(sev, 0)
198
- confirmed = breakdown.get('confirmed', {}).get(sev, 0)
221
+ for sev in ["Critical", "High", "Medium", "Low"]:
222
+ icon = SEVERITY_ICONS.get(sev, "")
223
+ color = SEVERITY_COLORS.get(sev, "white")
224
+ wazuh_only = breakdown.get("wazuh_only", {}).get(sev, 0)
225
+ scan_only = breakdown.get("scan_only", {}).get(sev, 0)
226
+ confirmed = breakdown.get("confirmed", {}).get(sev, 0)
199
227
 
200
228
  table.add_row(
201
229
  f"{icon} [{color}]{sev}[/{color}]",
202
230
  str(wazuh_only) if wazuh_only else "-",
203
231
  str(scan_only) if scan_only else "-",
204
- str(confirmed) if confirmed else "-"
232
+ str(confirmed) if confirmed else "-",
205
233
  )
206
234
 
207
235
  console.print(" ", table)
@@ -220,16 +248,31 @@ def _show_wazuh_only(result: GapAnalysisResult, width: int) -> None:
220
248
  width = DesignSystem.get_terminal_width()
221
249
 
222
250
  click.echo("\n┌" + "─" * (width - 2) + "┐")
223
- click.echo("│" + click.style(" WAZUH ONLY - SCANS MISSED ".center(width - 2), bold=True, fg='yellow') + "│")
251
+ click.echo(
252
+ "│"
253
+ + click.style(
254
+ " WAZUH ONLY - SCANS MISSED ".center(width - 2), bold=True, fg="yellow"
255
+ )
256
+ + "│"
257
+ )
224
258
  click.echo("└" + "─" * (width - 2) + "┘")
225
259
  click.echo()
226
260
 
227
261
  click.echo(f" {len(gaps)} CVEs detected by Wazuh but NOT by active scans.")
228
- click.echo(click.style(" These may be local/package vulnerabilities not exposed to network scanning.", fg='bright_black'))
262
+ click.echo(
263
+ click.style(
264
+ " These may be local/package vulnerabilities not exposed to network scanning.",
265
+ fg="bright_black",
266
+ )
267
+ )
229
268
  click.echo()
230
269
 
231
270
  if not gaps:
232
- click.echo(click.style(" ✓ No gaps - all Wazuh vulns confirmed by scans!", fg='green'))
271
+ click.echo(
272
+ click.style(
273
+ " ✓ No gaps - all Wazuh vulns confirmed by scans!", fg="green"
274
+ )
275
+ )
233
276
  click.echo()
234
277
  click.pause(" Press any key to return...")
235
278
  return
@@ -245,7 +288,14 @@ def _show_wazuh_only(result: GapAnalysisResult, width: int) -> None:
245
288
  end_idx = min(start_idx + page_size, len(gaps))
246
289
  page_gaps = gaps[start_idx:end_idx]
247
290
 
248
- _render_gaps_table(page_gaps, width, show_package=True, page=page, page_size=page_size, view_all=view_all)
291
+ _render_gaps_table(
292
+ page_gaps,
293
+ width,
294
+ show_package=True,
295
+ page=page,
296
+ page_size=page_size,
297
+ view_all=view_all,
298
+ )
249
299
 
250
300
  # Pagination info
251
301
  if view_all:
@@ -270,17 +320,17 @@ def _show_wazuh_only(result: GapAnalysisResult, width: int) -> None:
270
320
  try:
271
321
  choice = input(" Select option: ").strip().lower()
272
322
 
273
- if choice == 'q':
323
+ if choice == "q":
274
324
  return
275
- elif choice == 'i':
325
+ elif choice == "i":
276
326
  _interactive_gaps_mode(gaps, "WAZUH ONLY GAPS", show_package=True)
277
- elif choice == 't':
327
+ elif choice == "t":
278
328
  view_all = not view_all
279
329
  if not view_all:
280
330
  page = 0
281
- elif choice == 'n' and not view_all and page < total_pages - 1:
331
+ elif choice == "n" and not view_all and page < total_pages - 1:
282
332
  page += 1
283
- elif choice == 'p' and not view_all and page > 0:
333
+ elif choice == "p" and not view_all and page > 0:
284
334
  page -= 1
285
335
  elif choice.isdigit():
286
336
  idx = int(choice) - 1
@@ -302,16 +352,31 @@ def _show_scan_only(result: GapAnalysisResult, width: int) -> None:
302
352
  width = DesignSystem.get_terminal_width()
303
353
 
304
354
  click.echo("\n┌" + "─" * (width - 2) + "┐")
305
- click.echo("│" + click.style(" SCAN ONLY - WAZUH MISSED ".center(width - 2), bold=True, fg='blue') + "│")
355
+ click.echo(
356
+ "│"
357
+ + click.style(
358
+ " SCAN ONLY - WAZUH MISSED ".center(width - 2), bold=True, fg="blue"
359
+ )
360
+ + "│"
361
+ )
306
362
  click.echo("└" + "─" * (width - 2) + "┘")
307
363
  click.echo()
308
364
 
309
365
  click.echo(f" {len(gaps)} CVEs detected by active scans but NOT by Wazuh.")
310
- click.echo(click.style(" This may indicate: missing Wazuh agent, detection rule gap, or network-only vuln.", fg='bright_black'))
366
+ click.echo(
367
+ click.style(
368
+ " This may indicate: missing Wazuh agent, detection rule gap, or network-only vuln.",
369
+ fg="bright_black",
370
+ )
371
+ )
311
372
  click.echo()
312
373
 
313
374
  if not gaps:
314
- click.echo(click.style(" ✓ No gaps - Wazuh detected all scan findings!", fg='green'))
375
+ click.echo(
376
+ click.style(
377
+ " ✓ No gaps - Wazuh detected all scan findings!", fg="green"
378
+ )
379
+ )
315
380
  click.echo()
316
381
  click.pause(" Press any key to return...")
317
382
  return
@@ -327,7 +392,14 @@ def _show_scan_only(result: GapAnalysisResult, width: int) -> None:
327
392
  end_idx = min(start_idx + page_size, len(gaps))
328
393
  page_gaps = gaps[start_idx:end_idx]
329
394
 
330
- _render_gaps_table(page_gaps, width, show_tool=True, page=page, page_size=page_size, view_all=view_all)
395
+ _render_gaps_table(
396
+ page_gaps,
397
+ width,
398
+ show_tool=True,
399
+ page=page,
400
+ page_size=page_size,
401
+ view_all=view_all,
402
+ )
331
403
 
332
404
  # Pagination info
333
405
  if view_all:
@@ -352,17 +424,17 @@ def _show_scan_only(result: GapAnalysisResult, width: int) -> None:
352
424
  try:
353
425
  choice = input(" Select option: ").strip().lower()
354
426
 
355
- if choice == 'q':
427
+ if choice == "q":
356
428
  return
357
- elif choice == 'i':
429
+ elif choice == "i":
358
430
  _interactive_gaps_mode(gaps, "SCAN ONLY GAPS", show_tool=True)
359
- elif choice == 't':
431
+ elif choice == "t":
360
432
  view_all = not view_all
361
433
  if not view_all:
362
434
  page = 0
363
- elif choice == 'n' and not view_all and page < total_pages - 1:
435
+ elif choice == "n" and not view_all and page < total_pages - 1:
364
436
  page += 1
365
- elif choice == 'p' and not view_all and page > 0:
437
+ elif choice == "p" and not view_all and page > 0:
366
438
  page -= 1
367
439
  elif choice.isdigit():
368
440
  idx = int(choice) - 1
@@ -384,16 +456,31 @@ def _show_confirmed(result: GapAnalysisResult, width: int) -> None:
384
456
  width = DesignSystem.get_terminal_width()
385
457
 
386
458
  click.echo("\n┌" + "─" * (width - 2) + "┐")
387
- click.echo("│" + click.style(" CONFIRMED - BOTH SOURCES ".center(width - 2), bold=True, fg='green') + "│")
459
+ click.echo(
460
+ "│"
461
+ + click.style(
462
+ " CONFIRMED - BOTH SOURCES ".center(width - 2), bold=True, fg="green"
463
+ )
464
+ + "│"
465
+ )
388
466
  click.echo("└" + "─" * (width - 2) + "┘")
389
467
  click.echo()
390
468
 
391
469
  click.echo(f" {len(gaps)} CVEs detected by BOTH Wazuh and active scans.")
392
- click.echo(click.style(" High confidence - prioritize these for exploitation.", fg='bright_black'))
470
+ click.echo(
471
+ click.style(
472
+ " High confidence - prioritize these for exploitation.",
473
+ fg="bright_black",
474
+ )
475
+ )
393
476
  click.echo()
394
477
 
395
478
  if not gaps:
396
- click.echo(click.style(" No confirmed matches between Wazuh and scans.", fg='yellow'))
479
+ click.echo(
480
+ click.style(
481
+ " No confirmed matches between Wazuh and scans.", fg="yellow"
482
+ )
483
+ )
397
484
  click.echo()
398
485
  click.pause(" Press any key to return...")
399
486
  return
@@ -409,7 +496,14 @@ def _show_confirmed(result: GapAnalysisResult, width: int) -> None:
409
496
  end_idx = min(start_idx + page_size, len(gaps))
410
497
  page_gaps = gaps[start_idx:end_idx]
411
498
 
412
- _render_gaps_table(page_gaps, width, show_both=True, page=page, page_size=page_size, view_all=view_all)
499
+ _render_gaps_table(
500
+ page_gaps,
501
+ width,
502
+ show_both=True,
503
+ page=page,
504
+ page_size=page_size,
505
+ view_all=view_all,
506
+ )
413
507
 
414
508
  # Pagination info
415
509
  if view_all:
@@ -434,17 +528,17 @@ def _show_confirmed(result: GapAnalysisResult, width: int) -> None:
434
528
  try:
435
529
  choice = input(" Select option: ").strip().lower()
436
530
 
437
- if choice == 'q':
531
+ if choice == "q":
438
532
  return
439
- elif choice == 'i':
533
+ elif choice == "i":
440
534
  _interactive_gaps_mode(gaps, "CONFIRMED GAPS", show_both=True)
441
- elif choice == 't':
535
+ elif choice == "t":
442
536
  view_all = not view_all
443
537
  if not view_all:
444
538
  page = 0
445
- elif choice == 'n' and not view_all and page < total_pages - 1:
539
+ elif choice == "n" and not view_all and page < total_pages - 1:
446
540
  page += 1
447
- elif choice == 'p' and not view_all and page > 0:
541
+ elif choice == "p" and not view_all and page > 0:
448
542
  page -= 1
449
543
  elif choice.isdigit():
450
544
  idx = int(choice) - 1
@@ -468,15 +562,23 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
468
562
  width = DesignSystem.get_terminal_width()
469
563
 
470
564
  click.echo("\n┌" + "─" * (width - 2) + "┐")
471
- click.echo("│" + click.style(" ACTIONABLE GAPS ".center(width - 2), bold=True, fg='yellow') + "│")
565
+ click.echo(
566
+ "│"
567
+ + click.style(" ACTIONABLE GAPS ".center(width - 2), bold=True, fg="yellow")
568
+ + "│"
569
+ )
472
570
  click.echo("└" + "─" * (width - 2) + "┘")
473
571
  click.echo()
474
572
 
475
- click.echo(f" {len(gaps)} prioritized vulnerabilities from Wazuh that need targeted scanning.")
573
+ click.echo(
574
+ f" {len(gaps)} prioritized vulnerabilities from Wazuh that need targeted scanning."
575
+ )
476
576
  click.echo()
477
577
 
478
578
  if not gaps:
479
- click.echo(click.style(" ✓ No actionable gaps - great scan coverage!", fg='green'))
579
+ click.echo(
580
+ click.style(" ✓ No actionable gaps - great scan coverage!", fg="green")
581
+ )
480
582
  click.echo()
481
583
  click.pause(" Press any key to return...")
482
584
  return
@@ -511,21 +613,23 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
511
613
  else:
512
614
  display_idx = (page * page_size) + idx + 1
513
615
 
514
- priority = gap.get('priority', 'medium')
515
- priority_display = "[red]HIGH[/red]" if priority == 'high' else "[yellow]MEDIUM[/yellow]"
616
+ priority = gap.get("priority", "medium")
617
+ priority_display = (
618
+ "[red]HIGH[/red]" if priority == "high" else "[yellow]MEDIUM[/yellow]"
619
+ )
516
620
 
517
- severity = gap.get('severity', 'Medium')
518
- sev_color = SEVERITY_COLORS.get(severity, 'white')
621
+ severity = gap.get("severity", "Medium")
622
+ sev_color = SEVERITY_COLORS.get(severity, "white")
519
623
 
520
624
  table.add_row(
521
625
  "○",
522
626
  str(display_idx),
523
627
  priority_display,
524
- gap.get('cve_id', '-'),
628
+ gap.get("cve_id", "-"),
525
629
  f"[{sev_color}]{severity}[/{sev_color}]",
526
- gap.get('host_ip', '-'),
527
- gap.get('package', '-')[:19],
528
- gap.get('recommendation', '-')[:34]
630
+ gap.get("host_ip", "-"),
631
+ gap.get("package", "-")[:19],
632
+ gap.get("recommendation", "-")[:34],
529
633
  )
530
634
 
531
635
  console.print(table)
@@ -553,17 +657,17 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
553
657
  try:
554
658
  choice = input(" Select option: ").strip().lower()
555
659
 
556
- if choice == 'q':
660
+ if choice == "q":
557
661
  return
558
- elif choice == 'i':
662
+ elif choice == "i":
559
663
  _interactive_actionable_gaps_mode(gaps, "ACTIONABLE GAPS")
560
- elif choice == 't':
664
+ elif choice == "t":
561
665
  view_all = not view_all
562
666
  if not view_all:
563
667
  page = 0
564
- elif choice == 'n' and not view_all and page < total_pages - 1:
668
+ elif choice == "n" and not view_all and page < total_pages - 1:
565
669
  page += 1
566
- elif choice == 'p' and not view_all and page > 0:
670
+ elif choice == "p" and not view_all and page > 0:
567
671
  page -= 1
568
672
  elif choice.isdigit():
569
673
  idx = int(choice) - 1
@@ -574,8 +678,14 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
574
678
 
575
679
 
576
680
  def _render_gaps_table(
577
- gaps: List, width: int, show_package: bool = False, show_tool: bool = False, show_both: bool = False,
578
- page: int = 0, page_size: int = 20, view_all: bool = False
681
+ gaps: List,
682
+ width: int,
683
+ show_package: bool = False,
684
+ show_tool: bool = False,
685
+ show_both: bool = False,
686
+ page: int = 0,
687
+ page_size: int = 20,
688
+ view_all: bool = False,
579
689
  ) -> None:
580
690
  """Render gaps table with pagination support."""
581
691
  table = DesignSystem.create_table()
@@ -605,27 +715,31 @@ def _render_gaps_table(
605
715
  display_idx = (page * page_size) + idx + 1
606
716
 
607
717
  severity = gap.severity
608
- sev_color = SEVERITY_COLORS.get(severity, 'white')
718
+ sev_color = SEVERITY_COLORS.get(severity, "white")
609
719
 
610
720
  row = [
611
721
  "○",
612
722
  str(display_idx),
613
723
  gap.cve_id,
614
724
  f"[{sev_color}]{severity}[/{sev_color}]",
615
- gap.host_ip or "-"
725
+ gap.host_ip or "-",
616
726
  ]
617
727
 
618
728
  if show_package:
619
- package = gap.wazuh_details.get('package_name', '-') if gap.wazuh_details else '-'
729
+ package = (
730
+ gap.wazuh_details.get("package_name", "-") if gap.wazuh_details else "-"
731
+ )
620
732
  row.append(package[:24])
621
733
  row.append(gap.recommendation[:34])
622
734
  elif show_tool:
623
- tool = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
735
+ tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
624
736
  row.append(tool)
625
737
  row.append(gap.recommendation[:39])
626
738
  elif show_both:
627
- package = gap.wazuh_details.get('package_name', '-') if gap.wazuh_details else '-'
628
- tool = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
739
+ package = (
740
+ gap.wazuh_details.get("package_name", "-") if gap.wazuh_details else "-"
741
+ )
742
+ tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
629
743
  row.append(package[:19])
630
744
  row.append(tool)
631
745
  row.append(f"[green]{gap.confidence}[/green]")
@@ -640,12 +754,14 @@ def _show_gap_detail(gap) -> None:
640
754
  DesignSystem.clear_screen()
641
755
  width = DesignSystem.get_terminal_width()
642
756
 
643
- cve = gap.cve_id or 'Unknown'
644
- severity = gap.severity or 'Medium'
645
- sev_color = SEVERITY_COLORS.get(severity, 'white')
757
+ cve = gap.cve_id or "Unknown"
758
+ severity = gap.severity or "Medium"
759
+ sev_color = SEVERITY_COLORS.get(severity, "white")
646
760
 
647
761
  click.echo("\n┌" + "─" * (width - 2) + "┐")
648
- click.echo("│" + click.style(f" {cve} ".center(width - 2), bold=True, fg='cyan') + "│")
762
+ click.echo(
763
+ "│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
764
+ )
649
765
  click.echo("└" + "─" * (width - 2) + "┘")
650
766
  click.echo()
651
767
 
@@ -682,20 +798,24 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
682
798
  DesignSystem.clear_screen()
683
799
  width = DesignSystem.get_terminal_width()
684
800
 
685
- cve = gap.get('cve_id', 'Unknown')
686
- severity = gap.get('severity', 'Medium')
687
- sev_color = SEVERITY_COLORS.get(severity, 'white')
801
+ cve = gap.get("cve_id", "Unknown")
802
+ severity = gap.get("severity", "Medium")
803
+ sev_color = SEVERITY_COLORS.get(severity, "white")
688
804
 
689
805
  click.echo("\n┌" + "─" * (width - 2) + "┐")
690
- click.echo("│" + click.style(f" {cve} ".center(width - 2), bold=True, fg='cyan') + "│")
806
+ click.echo(
807
+ "│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
808
+ )
691
809
  click.echo("└" + "─" * (width - 2) + "┘")
692
810
  click.echo()
693
811
 
694
- priority = gap.get('priority', 'medium')
695
- priority_color = 'red' if priority == 'high' else 'yellow'
812
+ priority = gap.get("priority", "medium")
813
+ priority_color = "red" if priority == "high" else "yellow"
696
814
 
697
815
  click.echo(f" Severity: " + click.style(severity, fg=sev_color, bold=True))
698
- click.echo(f" Priority: " + click.style(priority.upper(), fg=priority_color, bold=True))
816
+ click.echo(
817
+ f" Priority: " + click.style(priority.upper(), fg=priority_color, bold=True)
818
+ )
699
819
  click.echo(f" Host: {gap.get('host_ip', '-')}")
700
820
  click.echo()
701
821
 
@@ -704,12 +824,12 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
704
824
  click.echo(f" Version: {gap.get('package_version', '-')}")
705
825
  click.echo()
706
826
 
707
- if gap.get('recommendation'):
827
+ if gap.get("recommendation"):
708
828
  click.echo(click.style(" Recommendation:", bold=True))
709
829
  click.echo(f" {gap.get('recommendation')}")
710
830
  click.echo()
711
831
 
712
- if gap.get('scan_command'):
832
+ if gap.get("scan_command"):
713
833
  click.echo(click.style(" Suggested Scan Command:", bold=True))
714
834
  click.echo(f" {gap.get('scan_command')}")
715
835
  click.echo()
@@ -717,12 +837,18 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
717
837
  click.pause(" Press any key to return...")
718
838
 
719
839
 
720
- def _interactive_gaps_mode(gaps: List, title: str, show_package: bool = False, show_tool: bool = False, show_both: bool = False) -> None:
840
+ def _interactive_gaps_mode(
841
+ gaps: List,
842
+ title: str,
843
+ show_package: bool = False,
844
+ show_tool: bool = False,
845
+ show_both: bool = False,
846
+ ) -> None:
721
847
  """Interactive selection mode for gaps."""
722
848
  from souleyez.ui.interactive_selector import interactive_select
723
849
 
724
850
  if not gaps:
725
- click.echo(click.style(" No gaps to select.", fg='yellow'))
851
+ click.echo(click.style(" No gaps to select.", fg="yellow"))
726
852
  click.pause()
727
853
  return
728
854
 
@@ -730,57 +856,69 @@ def _interactive_gaps_mode(gaps: List, title: str, show_package: bool = False, s
730
856
  gap_items = []
731
857
  for gap in gaps:
732
858
  item = {
733
- 'id': id(gap),
734
- 'cve_id': gap.cve_id or '-',
735
- 'severity': gap.severity or 'Medium',
736
- 'host': gap.host_ip or '-',
737
- 'raw': gap
859
+ "id": id(gap),
860
+ "cve_id": gap.cve_id or "-",
861
+ "severity": gap.severity or "Medium",
862
+ "host": gap.host_ip or "-",
863
+ "raw": gap,
738
864
  }
739
865
  if show_package:
740
- item['package'] = gap.wazuh_details.get('package_name', '-')[:20] if gap.wazuh_details else '-'
866
+ item["package"] = (
867
+ gap.wazuh_details.get("package_name", "-")[:20]
868
+ if gap.wazuh_details
869
+ else "-"
870
+ )
741
871
  elif show_tool:
742
- item['tool'] = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
872
+ item["tool"] = (
873
+ gap.scan_details.get("tool", "-") if gap.scan_details else "-"
874
+ )
743
875
  elif show_both:
744
- item['package'] = gap.wazuh_details.get('package_name', '-')[:15] if gap.wazuh_details else '-'
745
- item['tool'] = gap.scan_details.get('tool', '-') if gap.scan_details else '-'
876
+ item["package"] = (
877
+ gap.wazuh_details.get("package_name", "-")[:15]
878
+ if gap.wazuh_details
879
+ else "-"
880
+ )
881
+ item["tool"] = (
882
+ gap.scan_details.get("tool", "-") if gap.scan_details else "-"
883
+ )
746
884
  gap_items.append(item)
747
885
 
748
886
  columns = [
749
- {'name': 'CVE', 'key': 'cve_id', 'width': 18},
750
- {'name': 'Severity', 'key': 'severity', 'width': 10},
751
- {'name': 'Host', 'key': 'host', 'width': 15}
887
+ {"name": "CVE", "key": "cve_id", "width": 18},
888
+ {"name": "Severity", "key": "severity", "width": 10},
889
+ {"name": "Host", "key": "host", "width": 15},
752
890
  ]
753
891
 
754
892
  if show_package:
755
- columns.append({'name': 'Package', 'key': 'package', 'width': 20})
893
+ columns.append({"name": "Package", "key": "package", "width": 20})
756
894
  elif show_tool:
757
- columns.append({'name': 'Tool', 'key': 'tool', 'width': 15})
895
+ columns.append({"name": "Tool", "key": "tool", "width": 15})
758
896
  elif show_both:
759
- columns.append({'name': 'Package', 'key': 'package', 'width': 15})
760
- columns.append({'name': 'Tool', 'key': 'tool', 'width': 15})
897
+ columns.append({"name": "Package", "key": "package", "width": 15})
898
+ columns.append({"name": "Tool", "key": "tool", "width": 15})
761
899
 
762
900
  def format_cell(item: Dict, key: str) -> str:
763
- if key == 'severity':
764
- sev = item.get('severity', 'Medium')
765
- color = SEVERITY_COLORS.get(sev, 'white')
901
+ if key == "severity":
902
+ sev = item.get("severity", "Medium")
903
+ color = SEVERITY_COLORS.get(sev, "white")
766
904
  return f"[{color}]{sev}[/{color}]"
767
- return str(item.get(key, '-'))
905
+ return str(item.get(key, "-"))
768
906
 
769
907
  selected_ids: set = set()
770
908
  interactive_select(
771
909
  items=gap_items,
772
910
  columns=columns,
773
911
  selected_ids=selected_ids,
774
- get_id=lambda g: g['id'],
912
+ get_id=lambda g: g["id"],
775
913
  title=title,
776
- format_cell=format_cell
914
+ format_cell=format_cell,
777
915
  )
778
916
 
779
917
  # Show details of first selected
780
918
  if selected_ids:
781
919
  for item in gap_items:
782
- if item['id'] in selected_ids:
783
- _show_gap_detail(item['raw'])
920
+ if item["id"] in selected_ids:
921
+ _show_gap_detail(item["raw"])
784
922
  break
785
923
 
786
924
 
@@ -789,57 +927,59 @@ def _interactive_actionable_gaps_mode(gaps: List[Dict], title: str) -> None:
789
927
  from souleyez.ui.interactive_selector import interactive_select
790
928
 
791
929
  if not gaps:
792
- click.echo(click.style(" No gaps to select.", fg='yellow'))
930
+ click.echo(click.style(" No gaps to select.", fg="yellow"))
793
931
  click.pause()
794
932
  return
795
933
 
796
934
  # Prepare items for interactive selector
797
935
  gap_items = []
798
936
  for gap in gaps:
799
- gap_items.append({
800
- 'id': id(gap),
801
- 'cve_id': gap.get('cve_id', '-'),
802
- 'severity': gap.get('severity', 'Medium'),
803
- 'priority': gap.get('priority', 'medium').upper(),
804
- 'host': gap.get('host_ip', '-'),
805
- 'package': gap.get('package', '-')[:20],
806
- 'raw': gap
807
- })
937
+ gap_items.append(
938
+ {
939
+ "id": id(gap),
940
+ "cve_id": gap.get("cve_id", "-"),
941
+ "severity": gap.get("severity", "Medium"),
942
+ "priority": gap.get("priority", "medium").upper(),
943
+ "host": gap.get("host_ip", "-"),
944
+ "package": gap.get("package", "-")[:20],
945
+ "raw": gap,
946
+ }
947
+ )
808
948
 
809
949
  columns = [
810
- {'name': 'Priority', 'key': 'priority', 'width': 8},
811
- {'name': 'CVE', 'key': 'cve_id', 'width': 18},
812
- {'name': 'Severity', 'key': 'severity', 'width': 10},
813
- {'name': 'Host', 'key': 'host', 'width': 15},
814
- {'name': 'Package', 'key': 'package', 'width': 20}
950
+ {"name": "Priority", "key": "priority", "width": 8},
951
+ {"name": "CVE", "key": "cve_id", "width": 18},
952
+ {"name": "Severity", "key": "severity", "width": 10},
953
+ {"name": "Host", "key": "host", "width": 15},
954
+ {"name": "Package", "key": "package", "width": 20},
815
955
  ]
816
956
 
817
957
  def format_cell(item: Dict, key: str) -> str:
818
- if key == 'severity':
819
- sev = item.get('severity', 'Medium')
820
- color = SEVERITY_COLORS.get(sev, 'white')
958
+ if key == "severity":
959
+ sev = item.get("severity", "Medium")
960
+ color = SEVERITY_COLORS.get(sev, "white")
821
961
  return f"[{color}]{sev}[/{color}]"
822
- elif key == 'priority':
823
- pri = item.get('priority', 'MEDIUM')
824
- color = 'red' if pri == 'HIGH' else 'yellow'
962
+ elif key == "priority":
963
+ pri = item.get("priority", "MEDIUM")
964
+ color = "red" if pri == "HIGH" else "yellow"
825
965
  return f"[{color}]{pri}[/{color}]"
826
- return str(item.get(key, '-'))
966
+ return str(item.get(key, "-"))
827
967
 
828
968
  selected_ids: set = set()
829
969
  interactive_select(
830
970
  items=gap_items,
831
971
  columns=columns,
832
972
  selected_ids=selected_ids,
833
- get_id=lambda g: g['id'],
973
+ get_id=lambda g: g["id"],
834
974
  title=title,
835
- format_cell=format_cell
975
+ format_cell=format_cell,
836
976
  )
837
977
 
838
978
  # Show details of first selected
839
979
  if selected_ids:
840
980
  for item in gap_items:
841
- if item['id'] in selected_ids:
842
- _show_actionable_gap_detail(item['raw'])
981
+ if item["id"] in selected_ids:
982
+ _show_actionable_gap_detail(item["raw"])
843
983
  break
844
984
 
845
985
 
@@ -849,7 +989,11 @@ def _do_sync(engagement_id: int) -> None:
849
989
  width = DesignSystem.get_terminal_width()
850
990
 
851
991
  click.echo("\n┌" + "─" * (width - 2) + "┐")
852
- click.echo("│" + click.style(" SYNCING FROM WAZUH ".center(width - 2), bold=True, fg='blue') + "│")
992
+ click.echo(
993
+ "│"
994
+ + click.style(" SYNCING FROM WAZUH ".center(width - 2), bold=True, fg="blue")
995
+ + "│"
996
+ )
853
997
  click.echo("└" + "─" * (width - 2) + "┘")
854
998
  click.echo()
855
999
 
@@ -861,13 +1005,13 @@ def _do_sync(engagement_id: int) -> None:
861
1005
  click.echo()
862
1006
 
863
1007
  if result.success:
864
- click.echo(click.style(" ✓ Sync complete!", fg='green', bold=True))
1008
+ click.echo(click.style(" ✓ Sync complete!", fg="green", bold=True))
865
1009
  click.echo(f" Fetched: {result.total_fetched}")
866
1010
  click.echo(f" Mapped hosts: {result.mapped_hosts}")
867
1011
  else:
868
- click.echo(click.style(" ✗ Sync failed", fg='red', bold=True))
1012
+ click.echo(click.style(" ✗ Sync failed", fg="red", bold=True))
869
1013
  for err in result.errors:
870
- click.echo(click.style(f" {err}", fg='red'))
1014
+ click.echo(click.style(f" {err}", fg="red"))
871
1015
 
872
1016
  click.echo()
873
1017
  click.pause(" Press any key to continue...")