souleyez 2.43.29__py3-none-any.whl → 3.0.0__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 +9564 -2881
  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 +564 -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 +409 -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 +417 -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 +913 -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 +219 -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 +237 -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 +23034 -10679
  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-3.0.0.dist-info}/METADATA +2 -2
  353. souleyez-3.0.0.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
@@ -18,13 +18,13 @@ def view_intelligence(engagement_id: int):
18
18
  from souleyez.intelligence.surface_analyzer import AttackSurfaceAnalyzer
19
19
  from souleyez.storage.engagements import EngagementManager
20
20
  from souleyez.ui.design_system import DesignSystem
21
-
21
+
22
22
  em = EngagementManager()
23
23
  analyzer = AttackSurfaceAnalyzer()
24
-
24
+
25
25
  engagement = em.get_by_id(engagement_id)
26
26
  if not engagement:
27
- click.echo(click.style("Error: Engagement not found", fg='red'))
27
+ click.echo(click.style("Error: Engagement not found", fg="red"))
28
28
  click.pause()
29
29
  return
30
30
 
@@ -34,11 +34,11 @@ def view_intelligence(engagement_id: int):
34
34
  show_all_recommendations = False # Track if all recommendations should be shown
35
35
 
36
36
  # Analyze once before entering loop
37
- click.echo(click.style("\n🔍 Analyzing attack surface...", fg='yellow', bold=True))
37
+ click.echo(click.style("\n🔍 Analyzing attack surface...", fg="yellow", bold=True))
38
38
  try:
39
39
  analysis = analyzer.analyze_engagement(engagement_id)
40
40
  except Exception as e:
41
- click.echo(click.style(f"Error analyzing: {e}", fg='red'))
41
+ click.echo(click.style(f"Error analyzing: {e}", fg="red"))
42
42
  click.pause()
43
43
  return
44
44
 
@@ -48,73 +48,91 @@ def view_intelligence(engagement_id: int):
48
48
  # Header
49
49
  width = DesignSystem.get_terminal_width()
50
50
  click.echo("\n┌" + "─" * (width - 2) + "┐")
51
- click.echo("│" + click.style(" ATTACK SURFACE DASHBOARD ".center(width - 2), bold=True, fg='cyan') + "│")
51
+ click.echo(
52
+ "│"
53
+ + click.style(
54
+ " ATTACK SURFACE DASHBOARD ".center(width - 2), bold=True, fg="cyan"
55
+ )
56
+ + "│"
57
+ )
52
58
  click.echo("└" + "─" * (width - 2) + "┘")
53
59
  click.echo()
54
- click.echo(f"Engagement: {click.style(engagement['name'], fg='green', bold=True)}")
60
+ click.echo(
61
+ f"Engagement: {click.style(engagement['name'], fg='green', bold=True)}"
62
+ )
55
63
  click.echo()
56
-
64
+
57
65
  # Overview section
58
- display_overview(analysis['overview'], width)
59
-
66
+ display_overview(analysis["overview"], width)
67
+
60
68
  # Top targets
61
- display_top_targets(analysis['hosts'], width, show_all_targets)
62
-
69
+ display_top_targets(analysis["hosts"], width, show_all_targets)
70
+
63
71
  # Detailed service status for #1 host
64
72
  has_more_services = False
65
- if analysis['hosts']:
66
- has_more_services = display_service_status(analysis['hosts'][0], width, show_all_services)
67
-
73
+ if analysis["hosts"]:
74
+ has_more_services = display_service_status(
75
+ analysis["hosts"][0], width, show_all_services
76
+ )
77
+
68
78
  # Exploit suggestions
69
- has_more_exploits = display_exploit_suggestions(engagement_id, analysis['hosts'], width, show_all_exploits)
70
-
79
+ has_more_exploits = display_exploit_suggestions(
80
+ engagement_id, analysis["hosts"], width, show_all_exploits
81
+ )
82
+
71
83
  # Recommendations
72
- has_more_recommendations = display_recommendations(analysis['recommendations'], width, show_all_recommendations)
73
-
84
+ has_more_recommendations = display_recommendations(
85
+ analysis["recommendations"], width, show_all_recommendations
86
+ )
87
+
74
88
  # Menu
75
89
  display_menu(width)
76
-
90
+
77
91
  try:
78
92
  choice = input("\n Select option: ").strip()
79
-
80
- if choice == 'q' or choice.lower() == 'q':
93
+
94
+ if choice == "q" or choice.lower() == "q":
81
95
  return
82
- elif choice == '1':
83
- view_host_details(engagement_id, analysis['hosts'])
84
- elif choice == '2':
96
+ elif choice == "1":
97
+ view_host_details(engagement_id, analysis["hosts"])
98
+ elif choice == "2":
85
99
  filter_services(engagement_id, analysis)
86
- elif choice == '3':
100
+ elif choice == "3":
87
101
  export_attack_surface_report(engagement_id, engagement, analysis)
88
- elif choice == '4':
89
- auto_exploit_untried(engagement_id, analysis['hosts'])
90
- elif choice == '5':
102
+ elif choice == "4":
103
+ auto_exploit_untried(engagement_id, analysis["hosts"])
104
+ elif choice == "5":
91
105
  continue # Refresh
92
- elif choice == '6':
106
+ elif choice == "6":
93
107
  show_all_targets = not show_all_targets # Toggle all targets
94
108
  continue
95
- elif choice == '7':
109
+ elif choice == "7":
96
110
  show_all_exploits = not show_all_exploits # Toggle all exploits
97
111
  continue
98
- elif choice == '8':
99
- show_all_recommendations = not show_all_recommendations # Toggle all recommendations
112
+ elif choice == "8":
113
+ show_all_recommendations = (
114
+ not show_all_recommendations
115
+ ) # Toggle all recommendations
100
116
  continue
101
- elif choice.lower() == 's':
117
+ elif choice.lower() == "s":
102
118
  show_all_services = not show_all_services # Toggle all services
103
119
  continue
104
- elif choice.lower() == 'w':
120
+ elif choice.lower() == "w":
105
121
  # Wazuh Vulnerabilities view
106
122
  from souleyez.ui.wazuh_vulns_view import show_wazuh_vulns_view
107
- show_wazuh_vulns_view(engagement_id, engagement.get('name', ''))
123
+
124
+ show_wazuh_vulns_view(engagement_id, engagement.get("name", ""))
108
125
  continue
109
- elif choice.lower() == 'g':
126
+ elif choice.lower() == "g":
110
127
  # Gap Analysis view
111
128
  from souleyez.ui.gap_analysis_view import show_gap_analysis_view
112
- show_gap_analysis_view(engagement_id, engagement.get('name', ''))
129
+
130
+ show_gap_analysis_view(engagement_id, engagement.get("name", ""))
113
131
  continue
114
132
  else:
115
- click.echo(click.style("Invalid option", fg='red'))
133
+ click.echo(click.style("Invalid option", fg="red"))
116
134
  click.pause()
117
-
135
+
118
136
  except (KeyboardInterrupt, EOFError):
119
137
  return
120
138
 
@@ -122,18 +140,21 @@ def view_intelligence(engagement_id: int):
122
140
  def display_overview(overview: Dict, width: int):
123
141
  """Display overview statistics."""
124
142
  click.echo("═" * width)
125
- click.echo(click.style("📊 OVERVIEW", bold=True, fg='cyan'))
143
+ click.echo(click.style("📊 OVERVIEW", bold=True, fg="cyan"))
126
144
  click.echo("─" * width)
127
145
  click.echo()
128
-
146
+
129
147
  click.echo(f"Total Hosts: {overview['total_hosts']}")
130
148
  click.echo(f"Total Services: {overview['total_services']}")
131
-
132
- exploited_pct = overview['exploitation_percentage']
133
- color = 'green' if exploited_pct > 50 else 'yellow' if exploited_pct > 20 else 'red'
134
- click.echo(f"Exploited: {overview['exploited_services']} / {overview['total_services']} ", nl=False)
149
+
150
+ exploited_pct = overview["exploitation_percentage"]
151
+ color = "green" if exploited_pct > 50 else "yellow" if exploited_pct > 20 else "red"
152
+ click.echo(
153
+ f"Exploited: {overview['exploited_services']} / {overview['total_services']} ",
154
+ nl=False,
155
+ )
135
156
  click.echo(click.style(f"({exploited_pct}%)", fg=color))
136
-
157
+
137
158
  click.echo(f"Credentials Found: {overview['credentials_found']}")
138
159
  click.echo(f"Critical Findings: {overview['critical_findings']}")
139
160
  click.echo()
@@ -142,69 +163,92 @@ def display_overview(overview: Dict, width: int):
142
163
  def display_top_targets(hosts: List[Dict], width: int, show_all: bool = False):
143
164
  """Display top targets by attack surface."""
144
165
  click.echo("═" * width)
145
- click.echo(click.style("🎯 TOP TARGETS (by attack surface)", bold=True, fg='cyan'))
166
+ click.echo(click.style("🎯 TOP TARGETS (by attack surface)", bold=True, fg="cyan"))
146
167
  click.echo("─" * width)
147
168
  click.echo()
148
-
169
+
149
170
  if not hosts:
150
- click.echo(" " + click.style("No hosts found", fg='yellow'))
171
+ click.echo(" " + click.style("No hosts found", fg="yellow"))
151
172
  return False
152
-
173
+
153
174
  # Show top 3 or all
154
175
  display_count = len(hosts) if show_all else min(3, len(hosts))
155
176
  has_more = len(hosts) > 3
156
-
177
+
157
178
  for idx, host in enumerate(hosts[:display_count], 1):
158
179
  # Host header - Color thresholds match help system documentation
159
180
  # 80-100: CRITICAL (red), 60-79: HIGH (yellow), 40-59: MEDIUM (white), 0-39: LOW (dim)
160
- score_color = 'red' if host['score'] >= 80 else 'yellow' if host['score'] >= 60 else 'white' if host['score'] >= 40 else 'white'
181
+ score_color = (
182
+ "red"
183
+ if host["score"] >= 80
184
+ else (
185
+ "yellow"
186
+ if host["score"] >= 60
187
+ else "white" if host["score"] >= 40 else "white"
188
+ )
189
+ )
161
190
  click.echo(f"#{idx} {click.style(host['host'], bold=True)} ", nl=False)
162
- if host.get('hostname'):
191
+ if host.get("hostname"):
163
192
  click.echo(f"({host['hostname']}) ", nl=False)
164
193
  click.echo(f"[Score: {click.style(str(host['score']), fg=score_color)}]")
165
-
194
+
166
195
  # Stats
167
- num_services = len(host.get('services', []))
168
- click.echo(f" ├─ {host['open_ports']} open ports | {num_services} services | ", nl=False)
196
+ num_services = len(host.get("services", []))
197
+ click.echo(
198
+ f" ├─ {host['open_ports']} open ports | {num_services} services | ",
199
+ nl=False,
200
+ )
169
201
  click.echo(f"{host['findings']} findings ", nl=False)
170
- if host['critical_findings'] > 0:
171
- click.echo(click.style(f"({host['critical_findings']} critical)", fg='red'))
202
+ if host["critical_findings"] > 0:
203
+ click.echo(click.style(f"({host['critical_findings']} critical)", fg="red"))
172
204
  else:
173
205
  click.echo()
174
-
206
+
175
207
  # Exploitation progress
176
- prog = host['exploitation_progress']
177
- pct = round((prog['exploited'] / prog['total'] * 100) if prog['total'] > 0 else 0, 0)
178
- prog_color = 'green' if pct > 50 else 'yellow' if pct > 20 else 'red'
179
- click.echo(f" ├─ Exploitation: {prog['exploited']}/{prog['total']} services ", nl=False)
208
+ prog = host["exploitation_progress"]
209
+ pct = round(
210
+ (prog["exploited"] / prog["total"] * 100) if prog["total"] > 0 else 0, 0
211
+ )
212
+ prog_color = "green" if pct > 50 else "yellow" if pct > 20 else "red"
213
+ click.echo(
214
+ f" ├─ Exploitation: {prog['exploited']}/{prog['total']} services ",
215
+ nl=False,
216
+ )
180
217
  click.echo(click.style(f"({int(pct)}%)", fg=prog_color))
181
-
218
+
182
219
  # Actions
183
220
  click.echo(f" └─ ", nl=False)
184
- click.echo(click.style("[View Details]", fg='cyan'), nl=False)
221
+ click.echo(click.style("[View Details]", fg="cyan"), nl=False)
185
222
  click.echo(" ", nl=False)
186
- click.echo(click.style("[Scan More]", fg='cyan'), nl=False)
187
- if prog['not_tried'] > 0:
223
+ click.echo(click.style("[Scan More]", fg="cyan"), nl=False)
224
+ if prog["not_tried"] > 0:
188
225
  click.echo(" ", nl=False)
189
- click.echo(click.style("[Auto-Exploit]", fg='green'))
226
+ click.echo(click.style("[Auto-Exploit]", fg="green"))
190
227
  else:
191
228
  click.echo()
192
-
229
+
193
230
  click.echo()
194
-
231
+
195
232
  # Show "show more" indicator
196
233
  if has_more and not show_all:
197
234
  remaining = len(hosts) - display_count
198
- click.echo(click.style(f" ... and {remaining} more hosts (option [6] to show all)", fg='bright_black'))
235
+ click.echo(
236
+ click.style(
237
+ f" ... and {remaining} more hosts (option [6] to show all)",
238
+ fg="bright_black",
239
+ )
240
+ )
199
241
  click.echo()
200
-
242
+
201
243
  return has_more
202
244
 
203
245
 
204
246
  def display_service_status(host: Dict, width: int, show_all: bool = False):
205
247
  """Display detailed service status for a host."""
206
248
  click.echo("═" * width)
207
- click.echo(click.style(f"🔓 EXPLOITATION STATUS - {host['host']}", bold=True, fg='cyan'))
249
+ click.echo(
250
+ click.style(f"🔓 EXPLOITATION STATUS - {host['host']}", bold=True, fg="cyan")
251
+ )
208
252
  click.echo("─" * width)
209
253
  click.echo()
210
254
 
@@ -214,35 +258,36 @@ def display_service_status(host: Dict, width: int, show_all: bool = False):
214
258
  click.echo("─" * 170)
215
259
 
216
260
  # Show services (10 or all)
217
- all_services = host.get('services', [])
261
+ all_services = host.get("services", [])
218
262
  services_to_show = all_services if show_all else all_services[:10]
219
263
 
220
264
  if not services_to_show:
221
- click.echo(" " + click.style("No services found", fg='yellow'))
265
+ click.echo(" " + click.style("No services found", fg="yellow"))
222
266
  click.echo()
223
267
  return False
224
268
 
225
269
  for service in services_to_show:
226
- port = str(service['port']).ljust(7)
227
- svc = (service.get('service') or 'unknown')[:17].ljust(18)
270
+ port = str(service["port"]).ljust(7)
271
+ svc = (service.get("service") or "unknown")[:17].ljust(18)
228
272
 
229
273
  # Clean version: remove "syn-ack ttl XX" prefix
230
- version = service['version'] or ''
274
+ version = service["version"] or ""
231
275
  import re
232
- version = re.sub(r'^syn-ack ttl \d+\s*', '', version)
276
+
277
+ version = re.sub(r"^syn-ack ttl \d+\s*", "", version)
233
278
  ver = version[:34].ljust(35)
234
279
 
235
280
  # Status with emoji
236
- status = service.get('status', 'unknown')
237
- if status == 'exploited':
281
+ status = service.get("status", "unknown")
282
+ if status == "exploited":
238
283
  status_display = "✅ EXPLOITED".ljust(17)
239
- elif status == 'attempted':
284
+ elif status == "attempted":
240
285
  status_display = "🔄 ATTEMPTED".ljust(17)
241
286
  else:
242
287
  status_display = "⚠️ NOT TRIED".ljust(17)
243
288
 
244
289
  # Actions
245
- actions = (service.get('suggested_actions') or [])[:2]
290
+ actions = (service.get("suggested_actions") or [])[:2]
246
291
  actions_str = " | ".join([f"[{a}]" for a in actions]) if actions else ""
247
292
 
248
293
  click.echo(f"{port} {svc} {ver} {status_display} {actions_str}")
@@ -250,21 +295,38 @@ def display_service_status(host: Dict, width: int, show_all: bool = False):
250
295
  total_services = len(all_services)
251
296
  has_more = total_services > 10 and not show_all
252
297
  if has_more:
253
- click.echo(f"\n... and {total_services - 10} more services " + click.style("[Press 's' to show all]", fg='cyan'))
298
+ click.echo(
299
+ f"\n... and {total_services - 10} more services "
300
+ + click.style("[Press 's' to show all]", fg="cyan")
301
+ )
254
302
 
255
303
  click.echo()
256
304
 
257
305
  # Legend
258
306
  click.echo("Legend:")
259
- click.echo(" " + click.style("✅ EXPLOITED", fg='green') + " - Successfully exploited, session/creds obtained")
260
- click.echo(" " + click.style("🔄 ATTEMPTED", fg='yellow') + " - Exploit tried but failed or no results yet")
261
- click.echo(" " + click.style("⚠️ NOT TRIED", fg='red') + " - No exploitation attempts logged")
307
+ click.echo(
308
+ " "
309
+ + click.style(" EXPLOITED", fg="green")
310
+ + " - Successfully exploited, session/creds obtained"
311
+ )
312
+ click.echo(
313
+ " "
314
+ + click.style("🔄 ATTEMPTED", fg="yellow")
315
+ + " - Exploit tried but failed or no results yet"
316
+ )
317
+ click.echo(
318
+ " "
319
+ + click.style("⚠️ NOT TRIED", fg="red")
320
+ + " - No exploitation attempts logged"
321
+ )
262
322
  click.echo()
263
323
 
264
324
  return has_more
265
325
 
266
326
 
267
- def display_exploit_suggestions(engagement_id: int, top_hosts: List[Dict], width: int, show_all: bool = False):
327
+ def display_exploit_suggestions(
328
+ engagement_id: int, top_hosts: List[Dict], width: int, show_all: bool = False
329
+ ):
268
330
  """Display exploit suggestions for top hosts."""
269
331
  from souleyez.intelligence.exploit_suggestions import ExploitSuggestionEngine
270
332
  from rich.console import Console
@@ -272,214 +334,254 @@ def display_exploit_suggestions(engagement_id: int, top_hosts: List[Dict], width
272
334
  console = Console()
273
335
  # Disable SearchSploit in dashboard to prevent UI hangs - use manual Exploit Suggestions menu instead
274
336
  engine = ExploitSuggestionEngine(use_searchsploit=False)
275
-
337
+
276
338
  click.echo("═" * width)
277
- click.echo(click.style("💣 EXPLOIT SUGGESTIONS", bold=True, fg='red'))
339
+ click.echo(click.style("💣 EXPLOIT SUGGESTIONS", bold=True, fg="red"))
278
340
  click.echo("─" * width)
279
341
  click.echo()
280
-
342
+
281
343
  # Get suggestions for top 1 or all hosts
282
344
  display_count = len(top_hosts) if show_all else 1
283
345
  hosts_with_exploits = 0
284
-
346
+
285
347
  for host_data in top_hosts[:display_count]:
286
- host_ip = host_data.get('host') or host_data.get('ip_address')
348
+ host_ip = host_data.get("host") or host_data.get("ip_address")
287
349
  if not host_ip:
288
350
  continue
289
-
351
+
290
352
  # Get host_id from IP
291
353
  from souleyez.storage.hosts import HostManager
354
+
292
355
  hm = HostManager()
293
356
  host_obj = hm.get_host_by_ip(engagement_id, host_ip)
294
357
  if not host_obj:
295
358
  continue
296
-
297
- host_id = host_obj['id']
359
+
360
+ host_id = host_obj["id"]
298
361
  suggestions = engine.generate_suggestions(engagement_id, host_id)
299
-
300
- if not suggestions['hosts']:
362
+
363
+ if not suggestions["hosts"]:
301
364
  continue
302
-
303
- host_suggestions = suggestions['hosts'][0]
304
- ip = host_suggestions['ip']
305
- hostname = host_suggestions.get('hostname', '')
306
-
365
+
366
+ host_suggestions = suggestions["hosts"][0]
367
+ ip = host_suggestions["ip"]
368
+ hostname = host_suggestions.get("hostname", "")
369
+
307
370
  # Count services with exploits
308
- services_with_exploits = [s for s in host_suggestions['services'] if s.get('exploits')]
371
+ services_with_exploits = [
372
+ s for s in host_suggestions["services"] if s.get("exploits")
373
+ ]
309
374
  if not services_with_exploits:
310
375
  continue
311
-
376
+
312
377
  hosts_with_exploits += 1
313
-
378
+
314
379
  # Host header
315
380
  display_name = f"{ip}" + (f" ({hostname})" if hostname else "")
316
381
  console.print(f"[cyan]┌─ {display_name}[/cyan]")
317
-
382
+
318
383
  # Collect all exploits from all services with their service info
319
384
  all_exploits = []
320
385
  for svc in services_with_exploits:
321
- for exploit in svc.get('exploits', []):
322
- all_exploits.append({
323
- 'exploit': exploit,
324
- 'port': svc['port'],
325
- 'service': svc['service'],
326
- 'version': svc.get('version', 'unknown')
327
- })
328
-
386
+ for exploit in svc.get("exploits", []):
387
+ all_exploits.append(
388
+ {
389
+ "exploit": exploit,
390
+ "port": svc["port"],
391
+ "service": svc["service"],
392
+ "version": svc.get("version", "unknown"),
393
+ }
394
+ )
395
+
329
396
  # Sort by severity (critical > high > medium > low > info)
330
- severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3, 'info': 4}
331
- all_exploits.sort(key=lambda x: severity_order.get(x['exploit'].get('severity', 'info'), 5))
332
-
397
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
398
+ all_exploits.sort(
399
+ key=lambda x: severity_order.get(x["exploit"].get("severity", "info"), 5)
400
+ )
401
+
333
402
  # Show top 2 exploits when collapsed, all when expanded
334
- display_exploit_count = len(all_exploits) if show_all else min(2, len(all_exploits))
403
+ display_exploit_count = (
404
+ len(all_exploits) if show_all else min(2, len(all_exploits))
405
+ )
335
406
  has_more_exploits = len(all_exploits) > 2
336
-
407
+
337
408
  for idx, item in enumerate(all_exploits[:display_exploit_count]):
338
- exploit = item['exploit']
339
- port = item['port']
340
- service = item['service']
341
- version = item['version']
342
-
343
- severity = exploit.get('severity', 'info')
409
+ exploit = item["exploit"]
410
+ port = item["port"]
411
+ service = item["service"]
412
+ version = item["version"]
413
+
414
+ severity = exploit.get("severity", "info")
344
415
  severity_colors = {
345
- 'critical': 'red',
346
- 'high': 'yellow',
347
- 'medium': 'blue',
348
- 'low': 'white',
349
- 'info': 'dim'
416
+ "critical": "red",
417
+ "high": "yellow",
418
+ "medium": "blue",
419
+ "low": "white",
420
+ "info": "dim",
350
421
  }
351
- color = severity_colors.get(severity, 'white')
352
-
422
+ color = severity_colors.get(severity, "white")
423
+
353
424
  # Service info
354
425
  console.print(f" ├─ Port {port}/{service} [dim]({version})[/dim]")
355
-
426
+
356
427
  # Title and severity
357
- title = exploit['title'][:60]
428
+ title = exploit["title"][:60]
358
429
  severity_display = severity.upper()
359
- console.print(f" ├─ [{color}]{title}[/{color}] [{color}][{severity_display}][/{color}]")
360
-
430
+ console.print(
431
+ f" ├─ [{color}]{title}[/{color}] [{color}][{severity_display}][/{color}]"
432
+ )
433
+
361
434
  # MSF module
362
- msf_module = exploit.get('msf_module', 'N/A')
435
+ msf_module = exploit.get("msf_module", "N/A")
363
436
  console.print(f" │ MSF: [cyan]{msf_module}[/cyan]")
364
-
437
+
365
438
  # CVE if available
366
- if exploit.get('cve'):
439
+ if exploit.get("cve"):
367
440
  console.print(f" │ CVE: [yellow]{exploit['cve']}[/yellow]")
368
-
441
+
369
442
  # Description (truncated)
370
- desc = exploit.get('description', '')[:80]
443
+ desc = exploit.get("description", "")[:80]
371
444
  console.print(f" │ [dim]{desc}[/dim]")
372
445
  console.print()
373
-
446
+
374
447
  click.echo()
375
448
 
376
449
  # Count total hosts with exploits (do this BEFORE showing "no exploits" message)
377
450
  total_with_exploits = 0
378
451
  for host_data in top_hosts:
379
- host_ip = host_data.get('host') or host_data.get('ip_address')
452
+ host_ip = host_data.get("host") or host_data.get("ip_address")
380
453
  if not host_ip:
381
454
  continue
382
455
  from souleyez.storage.hosts import HostManager
456
+
383
457
  hm = HostManager()
384
458
  host_obj = hm.get_host_by_ip(engagement_id, host_ip)
385
459
  if not host_obj:
386
460
  continue
387
- suggestions = engine.generate_suggestions(engagement_id, host_obj['id'])
388
- if suggestions['hosts'] and suggestions['hosts'][0]['services']:
389
- services_with_exploits = [s for s in suggestions['hosts'][0]['services'] if s.get('exploits')]
461
+ suggestions = engine.generate_suggestions(engagement_id, host_obj["id"])
462
+ if suggestions["hosts"] and suggestions["hosts"][0]["services"]:
463
+ services_with_exploits = [
464
+ s for s in suggestions["hosts"][0]["services"] if s.get("exploits")
465
+ ]
390
466
  if services_with_exploits:
391
467
  total_with_exploits += 1
392
468
 
393
469
  # Show message based on whether ANY hosts have exploits (not just displayed ones)
394
470
  if total_with_exploits == 0:
395
- click.echo(click.style(" No exploit suggestions available yet", fg='yellow'))
471
+ click.echo(click.style(" No exploit suggestions available yet", fg="yellow"))
396
472
  click.echo()
397
- click.echo(click.style(" 💡 Possible reasons:", fg='cyan'))
473
+ click.echo(click.style(" 💡 Possible reasons:", fg="cyan"))
398
474
  click.echo(" • Service versions not detected - Run: nmap -sV <target>")
399
475
  click.echo(" • Services are up-to-date with no known exploits")
400
476
  click.echo(" • SearchSploit database needs updating")
401
477
  click.echo()
402
-
478
+
403
479
  # Show diagnostic info
404
- total_services = sum(len(h.get('services', [])) for h in top_hosts)
405
- services_with_version = sum(1 for h in top_hosts for s in h.get('services', [])
406
- if s.get('version') and s.get('version') != 'Unknown')
407
-
408
- click.echo(click.style(" Diagnostics:", fg='cyan'))
480
+ total_services = sum(len(h.get("services", [])) for h in top_hosts)
481
+ services_with_version = sum(
482
+ 1
483
+ for h in top_hosts
484
+ for s in h.get("services", [])
485
+ if s.get("version") and s.get("version") != "Unknown"
486
+ )
487
+
488
+ click.echo(click.style(" Diagnostics:", fg="cyan"))
409
489
  click.echo(f" • Total services scanned: {total_services}")
410
490
  click.echo(f" • Services with version info: {services_with_version}")
411
491
  if services_with_version < total_services:
412
- click.echo(f" • Missing version info: {total_services - services_with_version}")
492
+ click.echo(
493
+ f" • Missing version info: {total_services - services_with_version}"
494
+ )
413
495
  click.echo()
414
496
  elif hosts_with_exploits == 0 and total_with_exploits > 0:
415
497
  # Exploits exist but not in the displayed hosts
416
- click.echo(click.style(f" 💡 {total_with_exploits} host(s) with exploits available - use option [7] to show all", fg='cyan'))
417
-
498
+ click.echo(
499
+ click.style(
500
+ f" 💡 {total_with_exploits} host(s) with exploits available - use option [7] to show all",
501
+ fg="cyan",
502
+ )
503
+ )
504
+
418
505
  has_more = total_with_exploits > 1 and not show_all and hosts_with_exploits > 0
419
506
  if has_more and not show_all:
420
507
  remaining_hosts = total_with_exploits - display_count
421
508
  # Also count total remaining exploits
422
509
  total_exploits = 0
423
510
  for host_data in top_hosts:
424
- host_ip = host_data.get('host') or host_data.get('ip_address')
511
+ host_ip = host_data.get("host") or host_data.get("ip_address")
425
512
  if not host_ip:
426
513
  continue
427
514
  from souleyez.storage.hosts import HostManager
515
+
428
516
  hm = HostManager()
429
517
  host_obj = hm.get_host_by_ip(engagement_id, host_ip)
430
518
  if not host_obj:
431
519
  continue
432
- suggestions = engine.generate_suggestions(engagement_id, host_obj['id'])
433
- if suggestions['hosts'] and suggestions['hosts'][0]['services']:
434
- for svc in suggestions['hosts'][0]['services']:
435
- total_exploits += len(svc.get('exploits', []))
436
-
520
+ suggestions = engine.generate_suggestions(engagement_id, host_obj["id"])
521
+ if suggestions["hosts"] and suggestions["hosts"][0]["services"]:
522
+ for svc in suggestions["hosts"][0]["services"]:
523
+ total_exploits += len(svc.get("exploits", []))
524
+
437
525
  # Calculate shown exploits (top 2 from top host)
438
526
  shown_exploits = display_exploit_count if hosts_with_exploits > 0 else 0
439
527
  remaining_exploits = max(0, total_exploits - shown_exploits)
440
-
441
- click.echo(click.style(f" ... and {remaining_exploits} more exploits from {remaining_hosts} more hosts (option [7] to show all)", fg='bright_black'))
442
-
528
+
529
+ click.echo(
530
+ click.style(
531
+ f" ... and {remaining_exploits} more exploits from {remaining_hosts} more hosts (option [7] to show all)",
532
+ fg="bright_black",
533
+ )
534
+ )
535
+
443
536
  click.echo()
444
537
  return has_more
445
538
 
446
539
 
447
- def display_recommendations(recommendations: List[Dict], width: int, show_all: bool = False):
540
+ def display_recommendations(
541
+ recommendations: List[Dict], width: int, show_all: bool = False
542
+ ):
448
543
  """Display recommended next steps."""
449
544
  if not recommendations:
450
545
  return False
451
-
546
+
452
547
  click.echo("═" * width)
453
- click.echo(click.style("💡 RECOMMENDED NEXT STEPS", bold=True, fg='cyan'))
548
+ click.echo(click.style("💡 RECOMMENDED NEXT STEPS", bold=True, fg="cyan"))
454
549
  click.echo("─" * width)
455
550
  click.echo()
456
-
457
- priority_emoji = {'high': '🎯', 'medium': '🔍', 'low': '📝'}
458
-
551
+
552
+ priority_emoji = {"high": "🎯", "medium": "🔍", "low": "📝"}
553
+
459
554
  # Show top 3 or all
460
555
  display_count = len(recommendations) if show_all else min(3, len(recommendations))
461
556
  has_more = len(recommendations) > 3
462
-
557
+
463
558
  for idx, rec in enumerate(recommendations[:display_count], 1):
464
- emoji = priority_emoji.get(rec['priority'], '')
465
- click.echo(f"{idx}. {emoji} {rec['action']} - {rec['service']} on {rec['host']}:{rec['port']}")
559
+ emoji = priority_emoji.get(rec["priority"], "")
560
+ click.echo(
561
+ f"{idx}. {emoji} {rec['action']} - {rec['service']} on {rec['host']}:{rec['port']}"
562
+ )
466
563
  click.echo(f" {rec['reason']}")
467
564
  click.echo(f" {click.style('[Enqueue Action]', fg='green')}")
468
565
  click.echo()
469
-
566
+
470
567
  # Show "show more" indicator
471
568
  if has_more and not show_all:
472
569
  remaining = len(recommendations) - display_count
473
- click.echo(click.style(f" ... and {remaining} more recommendations (option [8] to show all)", fg='bright_black'))
570
+ click.echo(
571
+ click.style(
572
+ f" ... and {remaining} more recommendations (option [8] to show all)",
573
+ fg="bright_black",
574
+ )
575
+ )
474
576
  click.echo()
475
-
577
+
476
578
  return has_more
477
579
 
478
580
 
479
581
  def display_menu(width: int):
480
582
  """Display menu options."""
481
583
  click.echo("═" * width)
482
- click.echo(click.style("OPTIONS", bold=True, fg='yellow'))
584
+ click.echo(click.style("OPTIONS", bold=True, fg="yellow"))
483
585
  click.echo("─" * width)
484
586
  click.echo()
485
587
  click.echo(" [1] View Host Details - Select and view specific host")
@@ -492,7 +594,7 @@ def display_menu(width: int):
492
594
  click.echo(" [7] Show All Exploits - Display all exploit suggestions")
493
595
  click.echo(" [8] Show All Recommendations - Display all next steps")
494
596
  click.echo()
495
- click.echo(click.style(" WAZUH INTEGRATION", bold=True, fg='blue'))
597
+ click.echo(click.style(" WAZUH INTEGRATION", bold=True, fg="blue"))
496
598
  click.echo(" [w] Wazuh Vulnerabilities - View agent-detected CVEs")
497
599
  click.echo(" [g] Gap Analysis - Compare Wazuh vs scan findings")
498
600
  click.echo()
@@ -504,54 +606,74 @@ def display_menu(width: int):
504
606
  def filter_services(engagement_id: int, analysis: Dict):
505
607
  """Filter services by criteria."""
506
608
  from souleyez.ui.design_system import DesignSystem
507
-
609
+
508
610
  DesignSystem.clear_screen()
509
611
  click.echo()
510
- click.echo(click.style("🔍 FILTER SERVICES", bold=True, fg='cyan'))
612
+ click.echo(click.style("🔍 FILTER SERVICES", bold=True, fg="cyan"))
511
613
  click.echo("=" * 80)
512
614
  click.echo()
513
-
615
+
514
616
  # Prompt for filter criteria
515
617
  click.echo("Filter by:")
516
- service_filter = click.prompt(" Service name (or press Enter to skip)", default="", show_default=False)
517
- port_filter = click.prompt(" Port number (or press Enter to skip)", default="", show_default=False)
518
- protocol_filter = click.prompt(" Protocol (tcp/udp, or press Enter to skip)", default="", show_default=False)
519
-
618
+ service_filter = click.prompt(
619
+ " Service name (or press Enter to skip)", default="", show_default=False
620
+ )
621
+ port_filter = click.prompt(
622
+ " Port number (or press Enter to skip)", default="", show_default=False
623
+ )
624
+ protocol_filter = click.prompt(
625
+ " Protocol (tcp/udp, or press Enter to skip)", default="", show_default=False
626
+ )
627
+
520
628
  # Apply filters
521
629
  filtered_hosts = []
522
- for host in analysis['hosts']:
630
+ for host in analysis["hosts"]:
523
631
  filtered_services = []
524
- for svc in host.get('services', []):
632
+ for svc in host.get("services", []):
525
633
  # Apply filters
526
- if service_filter and service_filter.lower() not in (svc.get('service') or '').lower():
634
+ if (
635
+ service_filter
636
+ and service_filter.lower() not in (svc.get("service") or "").lower()
637
+ ):
527
638
  continue
528
- if port_filter and str(svc.get('port', '')) != port_filter:
639
+ if port_filter and str(svc.get("port", "")) != port_filter:
529
640
  continue
530
- if protocol_filter and svc.get('protocol', '').lower() != protocol_filter.lower():
641
+ if (
642
+ protocol_filter
643
+ and svc.get("protocol", "").lower() != protocol_filter.lower()
644
+ ):
531
645
  continue
532
646
  filtered_services.append(svc)
533
-
647
+
534
648
  if filtered_services:
535
649
  host_copy = host.copy()
536
- host_copy['services'] = filtered_services
650
+ host_copy["services"] = filtered_services
537
651
  filtered_hosts.append(host_copy)
538
-
652
+
539
653
  # Display results
540
654
  click.echo()
541
- click.echo(click.style(f"📊 Found {len(filtered_hosts)} host(s) with matching services", fg='green', bold=True))
655
+ click.echo(
656
+ click.style(
657
+ f"📊 Found {len(filtered_hosts)} host(s) with matching services",
658
+ fg="green",
659
+ bold=True,
660
+ )
661
+ )
542
662
  click.echo()
543
-
663
+
544
664
  if not filtered_hosts:
545
- click.echo(click.style(" No services match your filters", fg='yellow'))
665
+ click.echo(click.style(" No services match your filters", fg="yellow"))
546
666
  else:
547
667
  for host in filtered_hosts:
548
668
  click.echo(f" {host['host']} - {len(host['services'])} service(s)")
549
- for svc in host['services'][:5]: # Show first 5
550
- service_name = (svc.get('service') or 'unknown')[:30]
551
- click.echo(f" • {svc['port']}/{svc.get('protocol', 'tcp')} - {service_name}")
552
- if len(host['services']) > 5:
669
+ for svc in host["services"][:5]: # Show first 5
670
+ service_name = (svc.get("service") or "unknown")[:30]
671
+ click.echo(
672
+ f" • {svc['port']}/{svc.get('protocol', 'tcp')} - {service_name}"
673
+ )
674
+ if len(host["services"]) > 5:
553
675
  click.echo(f" ... and {len(host['services']) - 5} more")
554
-
676
+
555
677
  click.echo()
556
678
  click.pause()
557
679
 
@@ -559,17 +681,17 @@ def filter_services(engagement_id: int, analysis: Dict):
559
681
  def view_host_details(engagement_id: int, hosts: List[Dict]):
560
682
  """View detailed information for a specific host."""
561
683
  from souleyez.ui.design_system import DesignSystem
562
-
684
+
563
685
  if not hosts:
564
- click.echo(click.style("\nNo hosts available", fg='yellow'))
686
+ click.echo(click.style("\nNo hosts available", fg="yellow"))
565
687
  click.pause()
566
688
  return
567
-
689
+
568
690
  click.echo("\n" + click.style("Select host:", bold=True))
569
691
  for idx, host in enumerate(hosts, 1):
570
692
  click.echo(f" [{idx}] {host['host']} (Score: {host['score']})")
571
693
  click.echo(" [q] Cancel")
572
-
694
+
573
695
  try:
574
696
  choice = int(input("\n Select option: ").strip())
575
697
  if choice > 0 and choice <= len(hosts):
@@ -577,58 +699,68 @@ def view_host_details(engagement_id: int, hosts: List[Dict]):
577
699
  # Display full service table
578
700
  DesignSystem.clear_screen()
579
701
  width = get_terminal_width()
580
-
702
+
581
703
  click.echo("\n" + "=" * width)
582
- click.echo(click.style(f"Host: {host['host']}", bold=True, fg='cyan').center(width))
583
- if host.get('hostname'):
584
- click.echo(click.style(f"({host['hostname']})", fg='bright_black').center(width))
704
+ click.echo(
705
+ click.style(f"Host: {host['host']}", bold=True, fg="cyan").center(width)
706
+ )
707
+ if host.get("hostname"):
708
+ click.echo(
709
+ click.style(f"({host['hostname']})", fg="bright_black").center(
710
+ width
711
+ )
712
+ )
585
713
  click.echo("=" * width + "\n")
586
-
587
- click.echo(f"Attack Surface Score: {click.style(str(host['score']), bold=True)}")
714
+
715
+ click.echo(
716
+ f"Attack Surface Score: {click.style(str(host['score']), bold=True)}"
717
+ )
588
718
  click.echo(f"Open Ports: {host['open_ports']}")
589
719
  click.echo(f"Services: {len(host.get('services', []))}")
590
- click.echo(f"Findings: {host['findings']} ({host['critical_findings']} critical)")
720
+ click.echo(
721
+ f"Findings: {host['findings']} ({host['critical_findings']} critical)"
722
+ )
591
723
  click.echo()
592
-
724
+
593
725
  # Show all services in a table
594
726
  from rich.console import Console
595
727
  from rich.table import Table
596
728
  from souleyez.ui.design_system import DesignSystem
597
-
729
+
598
730
  console = Console(width=width - 4)
599
731
  table = Table(
600
732
  show_header=True,
601
733
  header_style="bold cyan",
602
734
  box=DesignSystem.TABLE_BOX,
603
735
  padding=(0, 1),
604
- expand=True
736
+ expand=True,
605
737
  )
606
-
738
+
607
739
  table.add_column("Port", width=8)
608
740
  table.add_column("Service", width=20)
609
741
  table.add_column("Version", width=30)
610
742
  table.add_column("Status", width=15)
611
743
  table.add_column("Creds", width=8, justify="center")
612
744
  table.add_column("Findings", width=10, justify="center")
613
-
614
- for svc in host.get('services', []):
615
- status_emoji = {'exploited': '', 'attempted': '🔄', 'not_tried': '⚠️'}
616
- emoji = status_emoji.get(svc['status'], '')
745
+
746
+ for svc in host.get("services", []):
747
+ status_emoji = {"exploited": "", "attempted": "🔄", "not_tried": "⚠️"}
748
+ emoji = status_emoji.get(svc["status"], "")
617
749
  status_text = f"{emoji} {svc['status'].replace('_', ' ').upper()}"
618
-
619
- version = svc.get('version') or '-'
620
-
750
+
751
+ version = svc.get("version") or "-"
752
+
621
753
  table.add_row(
622
- str(svc['port']),
623
- svc.get('service') or 'unknown',
754
+ str(svc["port"]),
755
+ svc.get("service") or "unknown",
624
756
  version,
625
757
  status_text,
626
- str(svc.get('credentials', 0)),
627
- str(svc.get('findings', 0))
758
+ str(svc.get("credentials", 0)),
759
+ str(svc.get("findings", 0)),
628
760
  )
629
-
761
+
630
762
  console.print(table)
631
-
763
+
632
764
  click.pause("\nPress any key to return...")
633
765
  except (ValueError, KeyboardInterrupt, EOFError):
634
766
  pass
@@ -636,23 +768,26 @@ def view_host_details(engagement_id: int, hosts: List[Dict]):
636
768
 
637
769
  def auto_exploit_untried(engagement_id: int, hosts: List[Dict]):
638
770
  """Auto-enqueue exploits for untried services."""
639
- click.echo(click.style("\n⚠️ WARNING: This will enqueue multiple exploit jobs", fg='yellow', bold=True))
640
-
641
- # Count untried services
642
- untried_count = sum(
643
- h['exploitation_progress']['not_tried']
644
- for h in hosts
771
+ click.echo(
772
+ click.style(
773
+ "\n⚠️ WARNING: This will enqueue multiple exploit jobs",
774
+ fg="yellow",
775
+ bold=True,
776
+ )
645
777
  )
646
-
778
+
779
+ # Count untried services
780
+ untried_count = sum(h["exploitation_progress"]["not_tried"] for h in hosts)
781
+
647
782
  if untried_count == 0:
648
- click.echo(click.style("\nNo untried services found!", fg='green'))
783
+ click.echo(click.style("\nNo untried services found!", fg="green"))
649
784
  click.echo()
650
-
785
+
651
786
  # Show diagnostic info
652
- total_services = sum(len(h.get('services', [])) for h in hosts)
653
- exploited = sum(h['exploitation_progress']['exploited'] for h in hosts)
654
- attempted = sum(h['exploitation_progress']['attempted'] for h in hosts)
655
-
787
+ total_services = sum(len(h.get("services", [])) for h in hosts)
788
+ exploited = sum(h["exploitation_progress"]["exploited"] for h in hosts)
789
+ attempted = sum(h["exploitation_progress"]["attempted"] for h in hosts)
790
+
656
791
  click.echo("Service Status Summary:")
657
792
  click.echo(f" Total services: {total_services}")
658
793
  click.echo(f" ✅ Exploited: {exploited}")
@@ -660,40 +795,40 @@ def auto_exploit_untried(engagement_id: int, hosts: List[Dict]):
660
795
  click.echo(f" ⚠️ Not tried: {untried_count}")
661
796
  click.echo()
662
797
  click.echo("All services have already been tested!")
663
-
798
+
664
799
  click.pause()
665
800
  return
666
-
801
+
667
802
  click.echo(f"Found {untried_count} untried services")
668
-
803
+
669
804
  try:
670
805
  if not click.confirm("\nContinue?", default=False):
671
806
  return
672
807
  except (KeyboardInterrupt, EOFError):
673
808
  return
674
-
809
+
675
810
  from souleyez.engine.background import enqueue_job
676
-
811
+
677
812
  queued = 0
678
813
  for host in hosts:
679
- for service in host.get('services', []):
680
- if service['status'] == 'not_tried':
814
+ for service in host.get("services", []):
815
+ if service["status"] == "not_tried":
681
816
  # Suggest tool for service
682
- tool = suggest_tool_for_service(service.get('service') or 'unknown')
817
+ tool = suggest_tool_for_service(service.get("service") or "unknown")
683
818
  if tool:
684
819
  try:
685
820
  enqueue_job(
686
821
  tool=tool,
687
- target=host['host'],
688
- args=['-p', str(service['port'])],
822
+ target=host["host"],
823
+ args=["-p", str(service["port"])],
689
824
  label=f"Auto-exploit: {service.get('service') or 'unknown'}",
690
- engagement_id=engagement_id
825
+ engagement_id=engagement_id,
691
826
  )
692
827
  queued += 1
693
828
  except:
694
829
  pass # Skip if enqueue fails
695
-
696
- click.echo(click.style(f"\n✓ Queued {queued} jobs", fg='green'))
830
+
831
+ click.echo(click.style(f"\n✓ Queued {queued} jobs", fg="green"))
697
832
  click.pause()
698
833
 
699
834
 
@@ -701,22 +836,22 @@ def suggest_tool_for_service(service: str) -> str:
701
836
  """Suggest appropriate tool for a service."""
702
837
  service_lower = service.lower()
703
838
  mapping = {
704
- 'http': 'gobuster',
705
- 'https': 'gobuster',
706
- 'ssh': 'hydra',
707
- 'ftp': 'hydra',
708
- 'telnet': 'hydra',
709
- 'mysql': 'hydra',
710
- 'postgresql': 'hydra',
711
- 'smb': 'enum4linux',
712
- 'netbios-ssn': 'enum4linux',
713
- 'microsoft-ds': 'enum4linux'
839
+ "http": "gobuster",
840
+ "https": "gobuster",
841
+ "ssh": "hydra",
842
+ "ftp": "hydra",
843
+ "telnet": "hydra",
844
+ "mysql": "hydra",
845
+ "postgresql": "hydra",
846
+ "smb": "enum4linux",
847
+ "netbios-ssn": "enum4linux",
848
+ "microsoft-ds": "enum4linux",
714
849
  }
715
-
850
+
716
851
  for key, tool in mapping.items():
717
852
  if key in service_lower:
718
853
  return tool
719
-
854
+
720
855
  return None
721
856
 
722
857
 
@@ -724,17 +859,17 @@ def export_attack_surface_report(engagement_id: int, engagement: Dict, analysis:
724
859
  """Export attack surface analysis to text report."""
725
860
  import os
726
861
  from datetime import datetime
727
-
862
+
728
863
  # Create output directory
729
864
  output_dir = os.path.expanduser("~/.souleyez/exports")
730
865
  os.makedirs(output_dir, exist_ok=True)
731
-
866
+
732
867
  # Generate filename
733
868
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
734
- safe_name = engagement['name'].replace(' ', '_').replace('/', '_')
869
+ safe_name = engagement["name"].replace(" ", "_").replace("/", "_")
735
870
  filename = f"{safe_name}_attack_surface_{timestamp}.txt"
736
871
  filepath = os.path.join(output_dir, filename)
737
-
872
+
738
873
  # Generate report
739
874
  lines = []
740
875
  lines.append("=" * 70)
@@ -742,43 +877,51 @@ def export_attack_surface_report(engagement_id: int, engagement: Dict, analysis:
742
877
  lines.append("=" * 70)
743
878
  lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
744
879
  lines.append("")
745
-
880
+
746
881
  # Overview
747
- overview = analysis['overview']
882
+ overview = analysis["overview"]
748
883
  lines.append("OVERVIEW")
749
884
  lines.append("-" * 70)
750
885
  lines.append(f"Total Hosts: {overview['total_hosts']}")
751
886
  lines.append(f"Total Services: {overview['total_services']}")
752
- lines.append(f"Exploited: {overview['exploited_services']} / {overview['total_services']} ({overview['exploitation_percentage']}%)")
887
+ lines.append(
888
+ f"Exploited: {overview['exploited_services']} / {overview['total_services']} ({overview['exploitation_percentage']}%)"
889
+ )
753
890
  lines.append(f"Credentials Found: {overview['credentials_found']}")
754
891
  lines.append(f"Critical Findings: {overview['critical_findings']}")
755
892
  lines.append("")
756
-
893
+
757
894
  # Top targets
758
895
  lines.append("TOP TARGETS (by attack surface)")
759
896
  lines.append("-" * 70)
760
- for idx, host in enumerate(analysis['hosts'][:5], 1):
897
+ for idx, host in enumerate(analysis["hosts"][:5], 1):
761
898
  lines.append(f"\n#{idx} {host['host']} (Score: {host['score']})")
762
- lines.append(f" Ports: {host['open_ports']} | Services: {host['services']} | Findings: {host['findings']}")
763
- prog = host['exploitation_progress']
764
- pct = round((prog['exploited'] / prog['total'] * 100) if prog['total'] > 0 else 0, 1)
899
+ lines.append(
900
+ f" Ports: {host['open_ports']} | Services: {host['services']} | Findings: {host['findings']}"
901
+ )
902
+ prog = host["exploitation_progress"]
903
+ pct = round(
904
+ (prog["exploited"] / prog["total"] * 100) if prog["total"] > 0 else 0, 1
905
+ )
765
906
  lines.append(f" Exploitation: {prog['exploited']}/{prog['total']} ({pct}%)")
766
-
907
+
767
908
  lines.append("")
768
-
909
+
769
910
  # Recommendations
770
- if analysis['recommendations']:
911
+ if analysis["recommendations"]:
771
912
  lines.append("RECOMMENDED NEXT STEPS")
772
913
  lines.append("-" * 70)
773
- for idx, rec in enumerate(analysis['recommendations'], 1):
774
- lines.append(f"\n{idx}. {rec['action']} - {rec['service']} on {rec['host']}:{rec['port']}")
914
+ for idx, rec in enumerate(analysis["recommendations"], 1):
915
+ lines.append(
916
+ f"\n{idx}. {rec['action']} - {rec['service']} on {rec['host']}:{rec['port']}"
917
+ )
775
918
  lines.append(f" Priority: {rec['priority'].upper()}")
776
919
  lines.append(f" Reason: {rec['reason']}")
777
-
920
+
778
921
  # Write file
779
- with open(filepath, 'w') as f:
780
- f.write('\n'.join(lines))
781
-
782
- click.echo(click.style(f"\n✓ Report exported to:", fg='green'))
922
+ with open(filepath, "w") as f:
923
+ f.write("\n".join(lines))
924
+
925
+ click.echo(click.style(f"\n✓ Report exported to:", fg="green"))
783
926
  click.echo(f" {filepath}")
784
927
  click.pause()