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
@@ -10,6 +10,7 @@ Features:
10
10
  - Direct MSF execution integration
11
11
  - Detailed info views with full context
12
12
  """
13
+
13
14
  import click
14
15
  import os
15
16
  import tempfile
@@ -25,57 +26,54 @@ from souleyez.core.msf_integration import MSFResourceGenerator, MSFConsoleManage
25
26
  console = Console()
26
27
 
27
28
  # Status indicators
28
- STATUS_ICONS = {
29
- 'not_tried': '⚪',
30
- 'attempted': '🔄',
31
- 'failed': '❌',
32
- 'success': '✅'
33
- }
29
+ STATUS_ICONS = {"not_tried": "⚪", "attempted": "🔄", "failed": "❌", "success": "✅"}
34
30
 
35
31
  # Actionability indicators
36
32
  ACTION_ICONS = {
37
- 'one_click': '🎯', # Has MSF module, ready to execute
38
- 'setup_needed': '🔧', # Requires configuration/parameters
39
- 'manual': '📄', # Manual exploitation required
40
- 'research': '🔍' # Needs more research
33
+ "one_click": "🎯", # Has MSF module, ready to execute
34
+ "setup_needed": "🔧", # Requires configuration/parameters
35
+ "manual": "📄", # Manual exploitation required
36
+ "research": "🔍", # Needs more research
41
37
  }
42
38
 
43
39
 
44
40
  def _determine_actionability(exploit: Dict) -> str:
45
41
  """Determine how actionable an exploit is."""
46
42
  # One-click if has MSF module and no obvious parameters needed
47
- if exploit.get('msf_module'):
43
+ if exploit.get("msf_module"):
48
44
  # Check if it needs creds or configuration (heuristic)
49
- title_lower = exploit.get('title', '').lower()
50
- if any(word in title_lower for word in ['auth', 'credential', 'login', 'password']):
51
- return 'setup_needed'
45
+ title_lower = exploit.get("title", "").lower()
46
+ if any(
47
+ word in title_lower for word in ["auth", "credential", "login", "password"]
48
+ ):
49
+ return "setup_needed"
52
50
  # Check description for complexity indicators
53
- desc = exploit.get('description', '').lower()
54
- if any(word in desc for word in ['requires', 'must configure', 'needs']):
55
- return 'setup_needed'
56
- return 'one_click'
51
+ desc = exploit.get("description", "").lower()
52
+ if any(word in desc for word in ["requires", "must configure", "needs"]):
53
+ return "setup_needed"
54
+ return "one_click"
57
55
 
58
56
  # Has EDB but no MSF module - likely manual
59
- if exploit.get('edb_id'):
60
- return 'manual'
57
+ if exploit.get("edb_id"):
58
+ return "manual"
61
59
 
62
60
  # Has CVE but no module - research needed
63
- if exploit.get('cve'):
64
- return 'research'
61
+ if exploit.get("cve"):
62
+ return "research"
65
63
 
66
- return 'research'
64
+ return "research"
67
65
 
68
66
 
69
67
  def _render_status_badge(status: str) -> str:
70
68
  """Render a status badge with icon."""
71
- icon = STATUS_ICONS.get(status, '')
72
- if status == 'not_tried':
69
+ icon = STATUS_ICONS.get(status, "")
70
+ if status == "not_tried":
73
71
  return f"{icon}"
74
- elif status == 'attempted':
72
+ elif status == "attempted":
75
73
  return f"[yellow]{icon}[/yellow]"
76
- elif status == 'failed':
74
+ elif status == "failed":
77
75
  return f"[red]{icon}[/red]"
78
- elif status == 'success':
76
+ elif status == "success":
79
77
  return f"[green]{icon}[/green]"
80
78
  return icon
81
79
 
@@ -83,24 +81,24 @@ def _render_status_badge(status: str) -> str:
83
81
  def _render_actionability_badge(actionability: str) -> str:
84
82
  """Render actionability badge with text label."""
85
83
  labels = {
86
- 'one_click': '[green]READY[/green]',
87
- 'setup_needed': '[yellow]SETUP[/yellow]',
88
- 'manual': '[cyan]MANUAL[/cyan]',
89
- 'research': '[dim]RESEARCH[/dim]'
84
+ "one_click": "[green]READY[/green]",
85
+ "setup_needed": "[yellow]SETUP[/yellow]",
86
+ "manual": "[cyan]MANUAL[/cyan]",
87
+ "research": "[dim]RESEARCH[/dim]",
90
88
  }
91
- return labels.get(actionability, '[dim]UNKNOWN[/dim]')
89
+ return labels.get(actionability, "[dim]UNKNOWN[/dim]")
92
90
 
93
91
 
94
92
  def _format_severity_badge(severity: str) -> str:
95
93
  """Format severity with color - full names, no abbreviations."""
96
- severity = severity or 'info'
97
- if severity == 'critical':
94
+ severity = severity or "info"
95
+ if severity == "critical":
98
96
  return "[red bold]CRITICAL[/red bold]"
99
- elif severity == 'high':
97
+ elif severity == "high":
100
98
  return "[yellow bold]HIGH[/yellow bold]"
101
- elif severity == 'medium':
99
+ elif severity == "medium":
102
100
  return "[white]MEDIUM[/white]"
103
- elif severity == 'low':
101
+ elif severity == "low":
104
102
  return "[dim]LOW[/dim]"
105
103
  else:
106
104
  return "[dim]INFO[/dim]"
@@ -112,7 +110,7 @@ def _render_exploits_table(
112
110
  show_all: bool = False,
113
111
  filter_untried: bool = False,
114
112
  start_index: int = 1,
115
- selected_exploits: set = None
113
+ selected_exploits: set = None,
116
114
  ) -> None:
117
115
  """Render exploits in an enhanced table format."""
118
116
  if not exploits:
@@ -120,7 +118,7 @@ def _render_exploits_table(
120
118
 
121
119
  # Filter if needed
122
120
  if filter_untried:
123
- exploits = [e for e in exploits if e.get('attempt_status') == 'not_tried']
121
+ exploits = [e for e in exploits if e.get("attempt_status") == "not_tried"]
124
122
 
125
123
  if not exploits:
126
124
  console.print(f" [dim]No untried exploits in this category[/dim]")
@@ -134,11 +132,13 @@ def _render_exploits_table(
134
132
  header_style="bold cyan",
135
133
  box=None,
136
134
  padding=(0, 1),
137
- width=min(width - 6, 140)
135
+ width=min(width - 6, 140),
138
136
  )
139
137
 
140
138
  table.add_column("○", width=3, justify="center") # Selection checkbox
141
- table.add_column("ID", width=4, style="cyan") # Row ID for selection - leftmost for easy reference
139
+ table.add_column(
140
+ "ID", width=4, style="cyan"
141
+ ) # Row ID for selection - leftmost for easy reference
142
142
  table.add_column("STATUS", width=7) # Attempt status
143
143
  table.add_column("ACTION", width=7) # Actionability indicator
144
144
  table.add_column("SEVERITY", width=9)
@@ -148,46 +148,52 @@ def _render_exploits_table(
148
148
  table.add_column("MODULE/EDB", width=24)
149
149
 
150
150
  for idx, exploit in enumerate(display_exploits, start_index):
151
- status = exploit.get('attempt_status', 'not_tried')
151
+ status = exploit.get("attempt_status", "not_tried")
152
152
  actionability = _determine_actionability(exploit)
153
- severity = _format_severity_badge(exploit.get('severity', 'info'))
153
+ severity = _format_severity_badge(exploit.get("severity", "info"))
154
154
 
155
- title = exploit.get('title', 'Unknown')[:58]
156
- cve = (exploit.get('cve') or '-')[:14]
155
+ title = exploit.get("title", "Unknown")[:58]
156
+ cve = (exploit.get("cve") or "-")[:14]
157
157
 
158
- source = exploit.get('source', 'msf_kb')
159
- source_display = 'MSF' if source == 'msf_kb' else 'EDB'
158
+ source = exploit.get("source", "msf_kb")
159
+ source_display = "MSF" if source == "msf_kb" else "EDB"
160
160
 
161
161
  # Module/EDB display with smart truncation
162
- if exploit.get('msf_module'):
163
- module_parts = exploit['msf_module'].split('/')
164
- module_display = '/'.join(module_parts[-2:]) if len(module_parts) >= 2 else module_parts[-1]
162
+ if exploit.get("msf_module"):
163
+ module_parts = exploit["msf_module"].split("/")
164
+ module_display = (
165
+ "/".join(module_parts[-2:])
166
+ if len(module_parts) >= 2
167
+ else module_parts[-1]
168
+ )
165
169
 
166
170
  # Smart truncation at word boundaries
167
171
  if len(module_display) > 24:
168
172
  # Try to truncate at last underscore or slash before limit
169
173
  truncate_at = 21 # Leave room for "..."
170
174
  last_sep = max(
171
- module_display.rfind('_', 0, truncate_at),
172
- module_display.rfind('/', 0, truncate_at)
175
+ module_display.rfind("_", 0, truncate_at),
176
+ module_display.rfind("/", 0, truncate_at),
173
177
  )
174
178
  if last_sep > 10: # Don't truncate too early
175
179
  module_display = module_display[:last_sep] + "..."
176
180
  else:
177
181
  module_display = module_display[:21] + "..."
178
- elif exploit.get('edb_id'):
182
+ elif exploit.get("edb_id"):
179
183
  module_display = f"EDB-{exploit['edb_id']}"[:24]
180
184
  else:
181
- module_display = '-'
185
+ module_display = "-"
182
186
 
183
187
  # Add description as subtle hint if available
184
- description = exploit.get('description', '')
188
+ description = exploit.get("description", "")
185
189
  if description and len(title) < 50:
186
190
  title = f"{title}\n[dim]{description[:50]}[/dim]"
187
191
 
188
192
  # Determine checkbox state
189
- exploit_id = exploit.get('identifier', '')
190
- checkbox = '●' if (selected_exploits and exploit_id in selected_exploits) else '○'
193
+ exploit_id = exploit.get("identifier", "")
194
+ checkbox = (
195
+ "●" if (selected_exploits and exploit_id in selected_exploits) else "○"
196
+ )
191
197
 
192
198
  table.add_row(
193
199
  checkbox,
@@ -198,29 +204,32 @@ def _render_exploits_table(
198
204
  title,
199
205
  cve,
200
206
  source_display,
201
- module_display
207
+ module_display,
202
208
  )
203
209
 
204
210
  console.print(table)
205
211
 
206
212
  if not show_all and len(exploits) > 5:
207
- console.print(f" [dim]... and {len(exploits) - 5} more. Press [x] to expand.[/dim]")
213
+ console.print(
214
+ f" [dim]... and {len(exploits) - 5} more. Press [x] to expand.[/dim]"
215
+ )
208
216
 
209
217
 
210
218
  def _render_quick_wins(host: Dict, width: int, selected_exploits: set = None) -> None:
211
219
  """Render Quick Wins section - critical + one-click + untried."""
212
220
  all_exploits = []
213
- for service in host.get('services', []):
214
- for exploit in service.get('exploits', []):
215
- exploit['_service'] = service # Track which service
221
+ for service in host.get("services", []):
222
+ for exploit in service.get("exploits", []):
223
+ exploit["_service"] = service # Track which service
216
224
  all_exploits.append(exploit)
217
225
 
218
226
  # Filter for quick wins
219
227
  quick_wins = [
220
- e for e in all_exploits
221
- if e.get('severity') == 'critical'
222
- and e.get('attempt_status') == 'not_tried'
223
- and _determine_actionability(e) in ['one_click', 'setup_needed']
228
+ e
229
+ for e in all_exploits
230
+ if e.get("severity") == "critical"
231
+ and e.get("attempt_status") == "not_tried"
232
+ and _determine_actionability(e) in ["one_click", "setup_needed"]
224
233
  ]
225
234
 
226
235
  if not quick_wins:
@@ -229,7 +238,9 @@ def _render_quick_wins(host: Dict, width: int, selected_exploits: set = None) ->
229
238
  # Header
230
239
  separator = "━" * (width - 4)
231
240
  console.print(f"\n {separator}")
232
- console.print(f" 🎯 [red bold]QUICK WINS[/red bold] - [white]Critical untried exploits ready to execute[/white]")
241
+ console.print(
242
+ f" 🎯 [red bold]QUICK WINS[/red bold] - [white]Critical untried exploits ready to execute[/white]"
243
+ )
233
244
  console.print(f" {separator}\n")
234
245
 
235
246
  # Table
@@ -238,7 +249,7 @@ def _render_quick_wins(host: Dict, width: int, selected_exploits: set = None) ->
238
249
  header_style="bold red",
239
250
  box=None,
240
251
  padding=(0, 1),
241
- width=min(width - 6, 140)
252
+ width=min(width - 6, 140),
242
253
  )
243
254
 
244
255
  table.add_column("○", width=3, justify="center") # Selection checkbox
@@ -251,22 +262,24 @@ def _render_quick_wins(host: Dict, width: int, selected_exploits: set = None) ->
251
262
  table.add_column("MODULE", width=24)
252
263
 
253
264
  for idx, exploit in enumerate(quick_wins[:10], 1):
254
- status = exploit.get('attempt_status', 'not_tried')
255
- severity = _format_severity_badge(exploit.get('severity', 'info'))
265
+ status = exploit.get("attempt_status", "not_tried")
266
+ severity = _format_severity_badge(exploit.get("severity", "info"))
256
267
  actionability = _render_actionability_badge(_determine_actionability(exploit))
257
- service = exploit['_service']
268
+ service = exploit["_service"]
258
269
  service_name = f"{service.get('service')}:{service.get('port')}"
259
- title = exploit.get('title', 'Unknown')[:43]
270
+ title = exploit.get("title", "Unknown")[:43]
260
271
 
261
- if exploit.get('msf_module'):
262
- module_parts = exploit['msf_module'].split('/')
263
- module_display = '/'.join(module_parts[-2:])[:22]
272
+ if exploit.get("msf_module"):
273
+ module_parts = exploit["msf_module"].split("/")
274
+ module_display = "/".join(module_parts[-2:])[:22]
264
275
  else:
265
- module_display = '-'
276
+ module_display = "-"
266
277
 
267
278
  # Determine checkbox state
268
- exploit_id = exploit.get('identifier', '')
269
- checkbox = '●' if (selected_exploits and exploit_id in selected_exploits) else '○'
279
+ exploit_id = exploit.get("identifier", "")
280
+ checkbox = (
281
+ "●" if (selected_exploits and exploit_id in selected_exploits) else "○"
282
+ )
270
283
 
271
284
  table.add_row(
272
285
  checkbox,
@@ -276,11 +289,13 @@ def _render_quick_wins(host: Dict, width: int, selected_exploits: set = None) ->
276
289
  actionability,
277
290
  service_name,
278
291
  title,
279
- module_display
292
+ module_display,
280
293
  )
281
294
 
282
295
  console.print(table)
283
- console.print(f"\n [dim]💡 Press \\[e] to execute | Full SearchSploit via menu \\[o][/dim]\n")
296
+ console.print(
297
+ f"\n [dim]💡 Press \\[e] to execute | Full SearchSploit via menu \\[o][/dim]\n"
298
+ )
284
299
 
285
300
 
286
301
  def _render_severity_group(
@@ -288,7 +303,7 @@ def _render_severity_group(
288
303
  exploits: List[Dict],
289
304
  width: int,
290
305
  collapsed: bool,
291
- filter_untried: bool = False
306
+ filter_untried: bool = False,
292
307
  ) -> None:
293
308
  """Render a collapsible severity group."""
294
309
  if not exploits:
@@ -297,7 +312,9 @@ def _render_severity_group(
297
312
  # Filter if needed
298
313
  display_exploits = exploits
299
314
  if filter_untried:
300
- display_exploits = [e for e in exploits if e.get('attempt_status') == 'not_tried']
315
+ display_exploits = [
316
+ e for e in exploits if e.get("attempt_status") == "not_tried"
317
+ ]
301
318
 
302
319
  if not display_exploits:
303
320
  return
@@ -307,31 +324,49 @@ def _render_severity_group(
307
324
  collapse_icon = "▼" if not collapsed else "▶"
308
325
 
309
326
  severity_display = severity.upper()
310
- color = "red" if severity == "critical" else "yellow" if severity == "high" else "white" if severity == "medium" else "dim"
327
+ color = (
328
+ "red"
329
+ if severity == "critical"
330
+ else (
331
+ "yellow"
332
+ if severity == "high"
333
+ else "white" if severity == "medium" else "dim"
334
+ )
335
+ )
311
336
 
312
337
  console.print(f"\n {separator}")
313
- console.print(f" {collapse_icon} [{color} bold]{severity_display}[/{color} bold] [{color}]({len(display_exploits)} exploits)[/{color}]")
338
+ console.print(
339
+ f" {collapse_icon} [{color} bold]{severity_display}[/{color} bold] [{color}]({len(display_exploits)} exploits)[/{color}]"
340
+ )
314
341
  console.print(f" {separator}\n")
315
342
 
316
343
  if not collapsed:
317
- _render_exploits_table(display_exploits, width, show_all=False, filter_untried=False)
344
+ _render_exploits_table(
345
+ display_exploits, width, show_all=False, filter_untried=False
346
+ )
318
347
  else:
319
348
  console.print(f" [dim]Collapsed. Press [{severity[0]}] to expand.[/dim]\n")
320
349
 
321
350
 
322
- def _render_service_groups(host: Dict, width: int, collapsed_services: set, filter_untried: bool, selected_exploits: set = None) -> None:
351
+ def _render_service_groups(
352
+ host: Dict,
353
+ width: int,
354
+ collapsed_services: set,
355
+ filter_untried: bool,
356
+ selected_exploits: set = None,
357
+ ) -> None:
323
358
  """Render exploits grouped by service with collapse capability."""
324
359
  global_index = 1 # Track global exploit ID across all services
325
- services = host.get('services', [])
326
- services_with_exploits = [s for s in services if s.get('exploits')]
360
+ services = host.get("services", [])
361
+ services_with_exploits = [s for s in services if s.get("exploits")]
327
362
 
328
363
  for service in services_with_exploits:
329
364
  service_key = f"{service.get('service')}:{service.get('port')}"
330
365
  collapsed = service_key in collapsed_services
331
366
 
332
- exploits = service.get('exploits', [])
367
+ exploits = service.get("exploits", [])
333
368
  if filter_untried:
334
- exploits = [e for e in exploits if e.get('attempt_status') == 'not_tried']
369
+ exploits = [e for e in exploits if e.get("attempt_status") == "not_tried"]
335
370
 
336
371
  if not exploits:
337
372
  continue
@@ -340,20 +375,22 @@ def _render_service_groups(host: Dict, width: int, collapsed_services: set, filt
340
375
  separator = "━" * (width - 4)
341
376
  collapse_icon = "▼" if not collapsed else "▶"
342
377
 
343
- service_name = service.get('service', 'unknown').upper()
344
- port = service.get('port', '?')
345
- version = service.get('version', 'version unknown')
378
+ service_name = service.get("service", "unknown").upper()
379
+ port = service.get("port", "?")
380
+ version = service.get("version", "version unknown")
346
381
 
347
382
  console.print(f"\n {separator}")
348
- console.print(f" {collapse_icon} [bright_white bold]{service_name}:{port}[/bright_white bold] - [cyan]{version}[/cyan]")
383
+ console.print(
384
+ f" {collapse_icon} [bright_white bold]{service_name}:{port}[/bright_white bold] - [cyan]{version}[/cyan]"
385
+ )
349
386
  console.print(f" {separator}\n")
350
387
 
351
388
  if not collapsed:
352
389
  # Group by severity
353
- critical = [e for e in exploits if e.get('severity') == 'critical']
354
- high = [e for e in exploits if e.get('severity') == 'high']
355
- medium = [e for e in exploits if e.get('severity') == 'medium']
356
- low = [e for e in exploits if e.get('severity') == 'low']
390
+ critical = [e for e in exploits if e.get("severity") == "critical"]
391
+ high = [e for e in exploits if e.get("severity") == "high"]
392
+ medium = [e for e in exploits if e.get("severity") == "medium"]
393
+ low = [e for e in exploits if e.get("severity") == "low"]
357
394
 
358
395
  all_sev_exploits = critical + high + medium + low
359
396
 
@@ -364,7 +401,7 @@ def _render_service_groups(host: Dict, width: int, collapsed_services: set, filt
364
401
  show_all=True,
365
402
  filter_untried=False,
366
403
  start_index=global_index,
367
- selected_exploits=selected_exploits
404
+ selected_exploits=selected_exploits,
368
405
  )
369
406
 
370
407
  # Increment global index for next service
@@ -378,17 +415,17 @@ def _render_additional_attack_vectors(host: Dict, width: int, show: bool) -> Non
378
415
  if not show:
379
416
  return
380
417
 
381
- services = host.get('services', [])
418
+ services = host.get("services", [])
382
419
  technique_map = {}
383
420
 
384
421
  for service in services:
385
- service_type = service.get('service', 'unknown')
386
- techniques = service.get('techniques', [])
422
+ service_type = service.get("service", "unknown")
423
+ techniques = service.get("techniques", [])
387
424
 
388
425
  if techniques and service_type not in technique_map:
389
426
  technique_map[service_type] = {
390
- 'port': service.get('port'),
391
- 'techniques': techniques
427
+ "port": service.get("port"),
428
+ "techniques": techniques,
392
429
  }
393
430
 
394
431
  if not technique_map:
@@ -396,12 +433,14 @@ def _render_additional_attack_vectors(host: Dict, width: int, show: bool) -> Non
396
433
 
397
434
  separator = "━" * (width - 4)
398
435
  console.print(f"\n {separator}")
399
- console.print(f" 💡 [cyan bold]ADDITIONAL ATTACK VECTORS[/cyan bold] [dim](Generic Techniques)[/dim]")
436
+ console.print(
437
+ f" 💡 [cyan bold]ADDITIONAL ATTACK VECTORS[/cyan bold] [dim](Generic Techniques)[/dim]"
438
+ )
400
439
  console.print(f" {separator}\n")
401
440
 
402
441
  for service_type, data in sorted(technique_map.items()):
403
- port = data['port']
404
- techniques = data['techniques']
442
+ port = data["port"]
443
+ techniques = data["techniques"]
405
444
 
406
445
  console.print(f" [cyan bold]{service_type.upper()}:{port}[/cyan bold]\n")
407
446
 
@@ -411,7 +450,7 @@ def _render_additional_attack_vectors(host: Dict, width: int, show: bool) -> Non
411
450
  header_style="bold cyan",
412
451
  box=None,
413
452
  padding=(0, 1),
414
- width=min(width - 6, 110)
453
+ width=min(width - 6, 110),
415
454
  )
416
455
 
417
456
  table.add_column("TECHNIQUE", width=40)
@@ -419,10 +458,10 @@ def _render_additional_attack_vectors(host: Dict, width: int, show: bool) -> Non
419
458
  table.add_column("MODULE", width=45)
420
459
 
421
460
  for tech in techniques:
422
- name = tech.get('name', 'Unknown')[:38]
423
- severity = _format_severity_badge(tech.get('severity', 'info'))
424
- modules = tech.get('msf_modules', [])
425
- module = modules[0].split('/')[-1][:43] if modules else '-'
461
+ name = tech.get("name", "Unknown")[:38]
462
+ severity = _format_severity_badge(tech.get("severity", "info"))
463
+ modules = tech.get("msf_modules", [])
464
+ module = modules[0].split("/")[-1][:43] if modules else "-"
426
465
 
427
466
  table.add_row(name, severity, module)
428
467
 
@@ -449,39 +488,47 @@ def _show_detailed_info(exploit: Dict) -> None:
449
488
  console.print(f" [bold]{exploit.get('title', 'Unknown Exploit')}[/bold]\n")
450
489
 
451
490
  # Metadata
452
- console.print(f" [cyan]Severity:[/cyan] {_format_severity_badge(exploit.get('severity', 'info'))}")
453
- console.print(f" [cyan]Actionability:[/cyan] {_render_actionability_badge(_determine_actionability(exploit))}")
454
- console.print(f" [cyan]Attempt Status:[/cyan] {_render_status_badge(exploit.get('attempt_status', 'not_tried'))}")
491
+ console.print(
492
+ f" [cyan]Severity:[/cyan] {_format_severity_badge(exploit.get('severity', 'info'))}"
493
+ )
494
+ console.print(
495
+ f" [cyan]Actionability:[/cyan] {_render_actionability_badge(_determine_actionability(exploit))}"
496
+ )
497
+ console.print(
498
+ f" [cyan]Attempt Status:[/cyan] {_render_status_badge(exploit.get('attempt_status', 'not_tried'))}"
499
+ )
455
500
 
456
- if exploit.get('cve'):
501
+ if exploit.get("cve"):
457
502
  console.print(f" [cyan]CVE:[/cyan] {exploit['cve']}")
458
503
 
459
- if exploit.get('msf_module'):
504
+ if exploit.get("msf_module"):
460
505
  console.print(f" [cyan]Metasploit Module:[/cyan] {exploit['msf_module']}")
461
506
 
462
- if exploit.get('edb_id'):
507
+ if exploit.get("edb_id"):
463
508
  console.print(f" [cyan]Exploit-DB ID:[/cyan] {exploit['edb_id']}")
464
509
 
465
510
  console.print()
466
511
 
467
512
  # Description
468
- if exploit.get('description'):
513
+ if exploit.get("description"):
469
514
  console.print(f" [cyan bold]Description:[/cyan bold]")
470
515
  console.print(f" {exploit['description']}\n")
471
516
 
472
517
  # Match type
473
- match_type = exploit.get('match_type', 'unknown')
518
+ match_type = exploit.get("match_type", "unknown")
474
519
  console.print(f" [cyan]Match Type:[/cyan] {match_type.upper()}\n")
475
520
 
476
521
  # Actionability assessment
477
522
  actionability = _determine_actionability(exploit)
478
523
  console.print(f" [cyan bold]Actionability Assessment:[/cyan bold]")
479
- if actionability == 'one_click':
524
+ if actionability == "one_click":
480
525
  console.print(f" 🎯 One-Click Ready - Has MSF module, can execute immediately")
481
- elif actionability == 'setup_needed':
526
+ elif actionability == "setup_needed":
482
527
  console.print(f" 🔧 Setup Needed - May require credentials or configuration")
483
- elif actionability == 'manual':
484
- console.print(f" 📄 Manual Exploitation - Requires manual steps, check Exploit-DB")
528
+ elif actionability == "manual":
529
+ console.print(
530
+ f" 📄 Manual Exploitation - Requires manual steps, check Exploit-DB"
531
+ )
485
532
  else:
486
533
  console.print(f" 🔍 Research Required - No ready-to-use module available")
487
534
 
@@ -494,7 +541,7 @@ def _show_detailed_info(exploit: Dict) -> None:
494
541
  console.print(f" • Network connectivity to target")
495
542
 
496
543
  # Check if MSF is available
497
- if exploit.get('msf_module'):
544
+ if exploit.get("msf_module"):
498
545
  msf = MSFConsoleManager()
499
546
  if msf.is_available():
500
547
  console.print(f" • [green]✓[/green] Metasploit Framework installed")
@@ -502,8 +549,11 @@ def _show_detailed_info(exploit: Dict) -> None:
502
549
  console.print(f" • [red]✗[/red] Metasploit Framework required (not found)")
503
550
 
504
551
  # Check for credential requirements
505
- if any(word in exploit.get('title', '').lower() or word in exploit.get('description', '').lower()
506
- for word in ['auth', 'credential', 'login', 'password', 'authenticated']):
552
+ if any(
553
+ word in exploit.get("title", "").lower()
554
+ or word in exploit.get("description", "").lower()
555
+ for word in ["auth", "credential", "login", "password", "authenticated"]
556
+ ):
507
557
  console.print(f" • Valid credentials may be required")
508
558
  console.print(f" [dim](Check credentials database for this host)[/dim]")
509
559
 
@@ -511,10 +561,10 @@ def _show_detailed_info(exploit: Dict) -> None:
511
561
 
512
562
  # Expected impact
513
563
  console.print(f" [cyan bold]Expected Impact:[/cyan bold]")
514
- if exploit.get('severity') == 'critical':
564
+ if exploit.get("severity") == "critical":
515
565
  console.print(f" • Full system compromise possible")
516
566
  console.print(f" • Remote code execution likely")
517
- elif exploit.get('severity') == 'high':
567
+ elif exploit.get("severity") == "high":
518
568
  console.print(f" • Significant access or information disclosure")
519
569
  else:
520
570
  console.print(f" • Limited impact, may require chaining with other exploits")
@@ -522,10 +572,14 @@ def _show_detailed_info(exploit: Dict) -> None:
522
572
  console.print()
523
573
 
524
574
  # Related URLs
525
- if exploit.get('cve'):
526
- console.print(f" [cyan]NVD URL:[/cyan] https://nvd.nist.gov/vuln/detail/{exploit['cve']}")
527
- if exploit.get('edb_id'):
528
- console.print(f" [cyan]Exploit-DB URL:[/cyan] https://www.exploit-db.com/exploits/{exploit['edb_id']}")
575
+ if exploit.get("cve"):
576
+ console.print(
577
+ f" [cyan]NVD URL:[/cyan] https://nvd.nist.gov/vuln/detail/{exploit['cve']}"
578
+ )
579
+ if exploit.get("edb_id"):
580
+ console.print(
581
+ f" [cyan]Exploit-DB URL:[/cyan] https://www.exploit-db.com/exploits/{exploit['edb_id']}"
582
+ )
529
583
 
530
584
  console.print()
531
585
  console.print(f" [dim]Press any key to return...[/dim]")
@@ -586,19 +640,33 @@ def _show_help_overlay() -> None:
586
640
 
587
641
  console.print(f" [dim]STATUS Indicators:[/dim]")
588
642
  console.print(f" [green]✅[/green] SUCCESS - Exploit succeeded")
589
- console.print(f" [yellow]🔄[/yellow] TRIED - Exploit attempted (result unknown)")
643
+ console.print(
644
+ f" [yellow]🔄[/yellow] TRIED - Exploit attempted (result unknown)"
645
+ )
590
646
  console.print(f" [red]❌[/red] FAILED - Exploit failed")
591
647
  console.print(f" ⚪ NOT TRIED - Not yet attempted\n")
592
648
 
593
649
  console.print(f" [dim]ACTION Types:[/dim]")
594
- console.print(f" [green]READY[/green] - Execute immediately (one-click, zero prep)")
595
- console.print(f" [yellow]SETUP[/yellow] - Needs configuration first (install tools, set options, generate payload)")
596
- console.print(f" [cyan]MANUAL[/cyan] - Requires hands-on work (can't be fully automated)")
597
- console.print(f" [dim]RESEARCH[/dim] - Needs investigation to determine if applicable\n")
650
+ console.print(
651
+ f" [green]READY[/green] - Execute immediately (one-click, zero prep)"
652
+ )
653
+ console.print(
654
+ f" [yellow]SETUP[/yellow] - Needs configuration first (install tools, set options, generate payload)"
655
+ )
656
+ console.print(
657
+ f" [cyan]MANUAL[/cyan] - Requires hands-on work (can't be fully automated)"
658
+ )
659
+ console.print(
660
+ f" [dim]RESEARCH[/dim] - Needs investigation to determine if applicable\n"
661
+ )
598
662
 
599
663
  console.print(f" [dim]SEVERITY Levels:[/dim]")
600
- console.print(f" [red bold]CRITICAL[/red bold] - Immediate exploitation, full system compromise")
601
- console.print(f" [yellow bold]HIGH[/yellow bold] - Significant security impact, privilege escalation")
664
+ console.print(
665
+ f" [red bold]CRITICAL[/red bold] - Immediate exploitation, full system compromise"
666
+ )
667
+ console.print(
668
+ f" [yellow bold]HIGH[/yellow bold] - Significant security impact, privilege escalation"
669
+ )
602
670
  console.print(f" [white]MEDIUM[/white] - Moderate risk, limited access")
603
671
  console.print(f" [dim]LOW[/dim] - Minor security issue")
604
672
  console.print(f" [dim]INFO[/dim] - Informational only\n")
@@ -607,7 +675,9 @@ def _show_help_overlay() -> None:
607
675
  click.getchar()
608
676
 
609
677
 
610
- def _check_prerequisites(exploit: Dict, engagement_id: int, host: Dict) -> Tuple[bool, List[str]]:
678
+ def _check_prerequisites(
679
+ exploit: Dict, engagement_id: int, host: Dict
680
+ ) -> Tuple[bool, List[str]]:
611
681
  """
612
682
  Check if exploit prerequisites are met.
613
683
 
@@ -617,20 +687,25 @@ def _check_prerequisites(exploit: Dict, engagement_id: int, host: Dict) -> Tuple
617
687
  missing = []
618
688
 
619
689
  # Check if exploit needs credentials
620
- title_lower = exploit.get('title', '').lower()
621
- desc_lower = exploit.get('description', '').lower()
690
+ title_lower = exploit.get("title", "").lower()
691
+ desc_lower = exploit.get("description", "").lower()
622
692
 
623
- if any(word in title_lower or word in desc_lower for word in ['auth', 'credential', 'login', 'password', 'authenticated']):
693
+ if any(
694
+ word in title_lower or word in desc_lower
695
+ for word in ["auth", "credential", "login", "password", "authenticated"]
696
+ ):
624
697
  # Check if we have credentials for this host/service
625
698
  try:
626
699
  from souleyez.storage.credentials import CredentialsManager
700
+
627
701
  cm = CredentialsManager()
628
702
  creds = cm.list_credentials(engagement_id)
629
703
 
630
704
  # Look for credentials matching this host or service
631
- host_ip = host.get('ip')
705
+ host_ip = host.get("ip")
632
706
  has_creds = any(
633
- cred.get('host') == host_ip or cred.get('source', '').startswith(host_ip)
707
+ cred.get("host") == host_ip
708
+ or cred.get("source", "").startswith(host_ip)
634
709
  for cred in creds
635
710
  )
636
711
 
@@ -640,7 +715,7 @@ def _check_prerequisites(exploit: Dict, engagement_id: int, host: Dict) -> Tuple
640
715
  missing.append("Credentials (unable to verify)")
641
716
 
642
717
  # Check for MSF module availability
643
- if exploit.get('msf_module'):
718
+ if exploit.get("msf_module"):
644
719
  msf = MSFConsoleManager()
645
720
  if not msf.is_available():
646
721
  missing.append("Metasploit Framework installation")
@@ -652,10 +727,7 @@ def _check_prerequisites(exploit: Dict, engagement_id: int, host: Dict) -> Tuple
652
727
 
653
728
 
654
729
  def _execute_msf_module(
655
- exploit: Dict,
656
- host: Dict,
657
- service: Dict,
658
- engagement_id: int
730
+ exploit: Dict, host: Dict, service: Dict, engagement_id: int
659
731
  ) -> bool:
660
732
  """
661
733
  Execute MSF module for an exploit.
@@ -663,7 +735,7 @@ def _execute_msf_module(
663
735
  Returns:
664
736
  True if execution started successfully, False otherwise
665
737
  """
666
- msf_module = exploit.get('msf_module')
738
+ msf_module = exploit.get("msf_module")
667
739
  if not msf_module:
668
740
  console.print(" [red]No MSF module available for this exploit[/red]")
669
741
  click.pause()
@@ -697,21 +769,32 @@ def _execute_msf_module(
697
769
  # Build resource script
698
770
  script = generator.generate_header()
699
771
  script += f"# Exploit: {exploit.get('title', 'Unknown')}\n"
700
- script += f"# Target: {host.get('ip')} - {service.get('service')}:{service.get('port')}\n"
772
+ script += (
773
+ f"# Target: {host.get('ip')} - {service.get('service')}:{service.get('port')}\n"
774
+ )
701
775
  script += f"# Severity: {exploit.get('severity', 'unknown').upper()}\n\n"
702
776
 
703
777
  script += f"use {msf_module}\n"
704
778
  script += f"set RHOSTS {host.get('ip')}\n"
705
779
 
706
- if service.get('port'):
780
+ if service.get("port"):
707
781
  script += f"set RPORT {service.get('port')}\n"
708
782
 
709
783
  # Check if module needs credentials (login/bruteforce scanners)
710
- login_modules = ['ssh_login', 'telnet_login', 'ftp_login', 'smb_login', 'mysql_login',
711
- 'postgres_login', 'vnc_login', 'rdp_scanner', 'http_login']
712
- enumuser_modules = ['ssh_enumusers', 'smb_enumusers']
784
+ login_modules = [
785
+ "ssh_login",
786
+ "telnet_login",
787
+ "ftp_login",
788
+ "smb_login",
789
+ "mysql_login",
790
+ "postgres_login",
791
+ "vnc_login",
792
+ "rdp_scanner",
793
+ "http_login",
794
+ ]
795
+ enumuser_modules = ["ssh_enumusers", "smb_enumusers"]
713
796
 
714
- module_name = msf_module.split('/')[-1] if '/' in msf_module else msf_module
797
+ module_name = msf_module.split("/")[-1] if "/" in msf_module else msf_module
715
798
 
716
799
  if any(login_mod in module_name for login_mod in login_modules):
717
800
  # Login modules need username file and USER_AS_PASS
@@ -725,7 +808,9 @@ def _execute_msf_module(
725
808
  # User enumeration modules need username file
726
809
  script += "# Username wordlist for enumeration\n"
727
810
  script += "set USER_FILE data/wordlists/soul_users.txt\n"
728
- console.print(" [yellow]⚠️ Using default username wordlist (soul_users.txt)[/yellow]")
811
+ console.print(
812
+ " [yellow]⚠️ Using default username wordlist (soul_users.txt)[/yellow]"
813
+ )
729
814
 
730
815
  # Check if module needs payload
731
816
  if generator._module_needs_payload(msf_module):
@@ -743,16 +828,16 @@ def _execute_msf_module(
743
828
  # Show script preview
744
829
  console.print("\n [cyan bold]Resource Script Preview:[/cyan bold]")
745
830
  console.print(" " + "─" * 60)
746
- for line in script.split('\n')[:15]:
831
+ for line in script.split("\n")[:15]:
747
832
  console.print(f" {line}")
748
- script_lines = script.split('\n')
833
+ script_lines = script.split("\n")
749
834
  if len(script_lines) > 15:
750
835
  console.print(f" ... ({len(script_lines) - 15} more lines)")
751
836
  console.print(" " + "─" * 60)
752
837
  console.print()
753
838
 
754
839
  # Save script
755
- with tempfile.NamedTemporaryFile(mode='w', suffix='.rc', delete=False) as f:
840
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".rc", delete=False) as f:
756
841
  f.write(script)
757
842
  rc_file = f.name
758
843
 
@@ -766,9 +851,11 @@ def _execute_msf_module(
766
851
  console.print(" [q] Cancel")
767
852
  console.print()
768
853
 
769
- choice = click.prompt(" Select option", type=str, default='1', show_default=False).strip()
854
+ choice = click.prompt(
855
+ " Select option", type=str, default="1", show_default=False
856
+ ).strip()
770
857
 
771
- if choice == '1':
858
+ if choice == "1":
772
859
  console.print("\n [cyan]Launching msfconsole...[/cyan]")
773
860
  console.print(" [dim]The script will load but won't auto-execute.[/dim]")
774
861
  console.print(" [dim]Review settings and type 'exploit' when ready.[/dim]\n")
@@ -782,12 +869,12 @@ def _execute_msf_module(
782
869
  # Mark as attempted
783
870
  exploit_attempts.record_attempt(
784
871
  engagement_id=engagement_id,
785
- host_id=host.get('host_id'),
786
- exploit_identifier=exploit.get('identifier'),
787
- exploit_title=exploit.get('title', 'Unknown'),
788
- status='attempted',
789
- service_id=service.get('service_id'),
790
- notes=f"Executed via MSF module: {msf_module}"
872
+ host_id=host.get("host_id"),
873
+ exploit_identifier=exploit.get("identifier"),
874
+ exploit_title=exploit.get("title", "Unknown"),
875
+ status="attempted",
876
+ service_id=service.get("service_id"),
877
+ notes=f"Executed via MSF module: {msf_module}",
791
878
  )
792
879
 
793
880
  console.print("\n [green]✓ Exploit execution completed[/green]")
@@ -806,12 +893,15 @@ def _execute_msf_module(
806
893
  except:
807
894
  pass
808
895
 
809
- elif choice == '2':
896
+ elif choice == "2":
810
897
  # Just save the script
811
- save_path = click.prompt(" Save as", default=f"exploit_{exploit.get('identifier', 'unknown').replace(':', '_')}.rc")
898
+ save_path = click.prompt(
899
+ " Save as",
900
+ default=f"exploit_{exploit.get('identifier', 'unknown').replace(':', '_')}.rc",
901
+ )
812
902
 
813
903
  try:
814
- with open(save_path, 'w') as f:
904
+ with open(save_path, "w") as f:
815
905
  f.write(script)
816
906
  console.print(f"\n [green]✓ Script saved to: {save_path}[/green]")
817
907
  console.print(f" Run with: msfconsole -r {save_path}")
@@ -840,11 +930,13 @@ def _batch_execute_critical(engagement_id: int, host: Dict) -> int:
840
930
  """
841
931
  # Get all critical one-click exploits that are untried
842
932
  all_exploits = []
843
- for service in host.get('services', []):
844
- for exploit in service.get('exploits', []):
845
- if (exploit.get('severity') == 'critical' and
846
- exploit.get('attempt_status') == 'not_tried' and
847
- _determine_actionability(exploit) == 'one_click'):
933
+ for service in host.get("services", []):
934
+ for exploit in service.get("exploits", []):
935
+ if (
936
+ exploit.get("severity") == "critical"
937
+ and exploit.get("attempt_status") == "not_tried"
938
+ and _determine_actionability(exploit) == "one_click"
939
+ ):
848
940
  all_exploits.append((exploit, service))
849
941
 
850
942
  if not all_exploits:
@@ -853,7 +945,9 @@ def _batch_execute_critical(engagement_id: int, host: Dict) -> int:
853
945
  return 0
854
946
 
855
947
  # Show what will be executed
856
- console.print(f"\n [red bold]⚠️ BATCH EXECUTION - {len(all_exploits)} Exploits[/red bold]\n")
948
+ console.print(
949
+ f"\n [red bold]⚠️ BATCH EXECUTION - {len(all_exploits)} Exploits[/red bold]\n"
950
+ )
857
951
  console.print(" The following critical exploits will be executed:\n")
858
952
 
859
953
  for idx, (exploit, service) in enumerate(all_exploits, 1):
@@ -861,7 +955,9 @@ def _batch_execute_critical(engagement_id: int, host: Dict) -> int:
861
955
  console.print(f" Target: {service.get('service')}:{service.get('port')}")
862
956
  console.print()
863
957
 
864
- console.print(" [yellow]This will launch MSF console for each exploit sequentially.[/yellow]")
958
+ console.print(
959
+ " [yellow]This will launch MSF console for each exploit sequentially.[/yellow]"
960
+ )
865
961
  console.print(" [yellow]Each exploit will require manual confirmation.[/yellow]\n")
866
962
 
867
963
  if not click.confirm(" Proceed with batch execution?", default=False):
@@ -880,7 +976,9 @@ def _batch_execute_critical(engagement_id: int, host: Dict) -> int:
880
976
  if not click.confirm("\n Continue to next exploit?", default=True):
881
977
  break
882
978
 
883
- console.print(f"\n [green]✓ Batch execution complete: {executed}/{len(all_exploits)} executed[/green]")
979
+ console.print(
980
+ f"\n [green]✓ Batch execution complete: {executed}/{len(all_exploits)} executed[/green]"
981
+ )
884
982
  click.pause()
885
983
  return executed
886
984
 
@@ -888,8 +986,8 @@ def _batch_execute_critical(engagement_id: int, host: Dict) -> int:
888
986
  def _build_exploit_index(host: Dict) -> List[Tuple[Dict, Dict]]:
889
987
  """Build a flat index of exploits with their service context for easy selection."""
890
988
  exploit_index = []
891
- for service in host.get('services', []):
892
- for exploit in service.get('exploits', []):
989
+ for service in host.get("services", []):
990
+ for exploit in service.get("exploits", []):
893
991
  exploit_index.append((exploit, service))
894
992
  return exploit_index
895
993
 
@@ -916,14 +1014,20 @@ def _mark_exploit_interactive(engagement_id: int, host: Dict) -> bool:
916
1014
  exploit_num = click.prompt(" Enter exploit ID", type=int)
917
1015
 
918
1016
  if exploit_num < 1 or exploit_num > len(exploit_index):
919
- console.print(f" [red]Invalid ID. Must be between 1 and {len(exploit_index)}[/red]")
1017
+ console.print(
1018
+ f" [red]Invalid ID. Must be between 1 and {len(exploit_index)}[/red]"
1019
+ )
920
1020
  click.pause()
921
1021
  return False
922
1022
 
923
1023
  exploit, service = exploit_index[exploit_num - 1]
924
1024
 
925
- console.print(f"\n Selected: [cyan]{exploit.get('title', 'Unknown')[:60]}[/cyan]")
926
- console.print(f" Current status: {_render_status_badge(exploit.get('attempt_status', 'not_tried'))}\n")
1025
+ console.print(
1026
+ f"\n Selected: [cyan]{exploit.get('title', 'Unknown')[:60]}[/cyan]"
1027
+ )
1028
+ console.print(
1029
+ f" Current status: {_render_status_badge(exploit.get('attempt_status', 'not_tried'))}\n"
1030
+ )
927
1031
 
928
1032
  console.print(" New status:")
929
1033
  console.print(" [n] Not tried (⚪)")
@@ -934,12 +1038,7 @@ def _mark_exploit_interactive(engagement_id: int, host: Dict) -> bool:
934
1038
 
935
1039
  status_choice = click.prompt(" Select status", type=str).strip().lower()
936
1040
 
937
- status_map = {
938
- 'n': 'not_tried',
939
- 't': 'attempted',
940
- 'f': 'failed',
941
- 's': 'success'
942
- }
1041
+ status_map = {"n": "not_tried", "t": "attempted", "f": "failed", "s": "success"}
943
1042
 
944
1043
  if status_choice not in status_map:
945
1044
  console.print(" [red]Invalid status selection[/red]")
@@ -949,19 +1048,23 @@ def _mark_exploit_interactive(engagement_id: int, host: Dict) -> bool:
949
1048
  new_status = status_map[status_choice]
950
1049
 
951
1050
  # Optional notes
952
- notes = click.prompt(" Add notes (optional, press Enter to skip)",
953
- type=str, default="", show_default=False)
1051
+ notes = click.prompt(
1052
+ " Add notes (optional, press Enter to skip)",
1053
+ type=str,
1054
+ default="",
1055
+ show_default=False,
1056
+ )
954
1057
 
955
1058
  # Record the attempt
956
- host_id = host.get('host_id')
1059
+ host_id = host.get("host_id")
957
1060
  exploit_attempts.record_attempt(
958
1061
  engagement_id=engagement_id,
959
1062
  host_id=host_id,
960
- exploit_identifier=exploit.get('identifier'),
961
- exploit_title=exploit.get('title', 'Unknown'),
1063
+ exploit_identifier=exploit.get("identifier"),
1064
+ exploit_title=exploit.get("title", "Unknown"),
962
1065
  status=new_status,
963
- service_id=service.get('service_id'),
964
- notes=notes if notes else None
1066
+ service_id=service.get("service_id"),
1067
+ notes=notes if notes else None,
965
1068
  )
966
1069
 
967
1070
  console.print(f"\n [green]✓ Exploit marked as {new_status}[/green]")
@@ -996,63 +1099,66 @@ def _interactive_exploit_select(host: Dict, selected_exploits: set) -> set:
996
1099
  # Build items for selector
997
1100
  items = []
998
1101
  for idx, (exploit, service) in enumerate(exploit_index, 1):
999
- items.append({
1000
- 'idx': idx,
1001
- 'identifier': exploit.get('identifier', ''),
1002
- 'title': exploit.get('title', 'Unknown')[:45],
1003
- 'severity': exploit.get('severity', 'info'),
1004
- 'status': exploit.get('attempt_status', 'not_tried'),
1005
- 'service': f"{service.get('service', '?')}:{service.get('port', '?')}",
1006
- 'msf_module': exploit.get('msf_module', ''),
1007
- 'actionability': _determine_actionability(exploit)
1008
- })
1102
+ items.append(
1103
+ {
1104
+ "idx": idx,
1105
+ "identifier": exploit.get("identifier", ""),
1106
+ "title": exploit.get("title", "Unknown")[:45],
1107
+ "severity": exploit.get("severity", "info"),
1108
+ "status": exploit.get("attempt_status", "not_tried"),
1109
+ "service": f"{service.get('service', '?')}:{service.get('port', '?')}",
1110
+ "msf_module": exploit.get("msf_module", ""),
1111
+ "actionability": _determine_actionability(exploit),
1112
+ }
1113
+ )
1009
1114
 
1010
1115
  # Column definitions
1011
1116
  columns = [
1012
- {'name': 'ID', 'width': 4, 'key': 'idx', 'justify': 'right'},
1013
- {'name': 'Severity', 'width': 10, 'key': 'severity'},
1014
- {'name': 'Status', 'width': 8, 'key': 'status'},
1015
- {'name': 'Service', 'width': 15, 'key': 'service'},
1016
- {'name': 'Exploit', 'width': 45, 'key': 'title'},
1117
+ {"name": "ID", "width": 4, "key": "idx", "justify": "right"},
1118
+ {"name": "Severity", "width": 10, "key": "severity"},
1119
+ {"name": "Status", "width": 8, "key": "status"},
1120
+ {"name": "Service", "width": 15, "key": "service"},
1121
+ {"name": "Exploit", "width": 45, "key": "title"},
1017
1122
  ]
1018
1123
 
1019
1124
  def format_cell(item: dict, key: str) -> str:
1020
1125
  value = item.get(key)
1021
- if key == 'severity':
1126
+ if key == "severity":
1022
1127
  return _format_severity_badge(value)
1023
- elif key == 'status':
1128
+ elif key == "status":
1024
1129
  return _render_status_badge(value)
1025
- return str(value) if value else '-'
1130
+ return str(value) if value else "-"
1026
1131
 
1027
1132
  while True:
1028
1133
  interactive_select(
1029
1134
  items=items,
1030
1135
  columns=columns,
1031
1136
  selected_ids=selected_exploits,
1032
- get_id=lambda e: e['identifier'],
1033
- title='SELECT EXPLOITS',
1034
- format_cell=format_cell
1137
+ get_id=lambda e: e["identifier"],
1138
+ title="SELECT EXPLOITS",
1139
+ format_cell=format_cell,
1035
1140
  )
1036
1141
 
1037
1142
  if not selected_exploits:
1038
1143
  return selected_exploits
1039
1144
 
1040
1145
  from souleyez.core.models import get_current_engagement
1146
+
1041
1147
  current = get_current_engagement()
1042
1148
  if not current:
1043
1149
  return selected_exploits
1044
1150
 
1045
- result = _exploit_bulk_action_menu(current['engagement_id'], host, selected_exploits)
1046
- if result == 'back':
1151
+ result = _exploit_bulk_action_menu(
1152
+ current["engagement_id"], host, selected_exploits
1153
+ )
1154
+ if result == "back":
1047
1155
  return selected_exploits
1048
- elif result == 'clear':
1156
+ elif result == "clear":
1049
1157
  selected_exploits.clear()
1050
1158
 
1051
1159
 
1052
1160
  def _exploit_bulk_action_menu(
1053
- engagement_id: int,
1054
- host: Dict,
1055
- selected_exploits: set
1161
+ engagement_id: int, host: Dict, selected_exploits: set
1056
1162
  ) -> str:
1057
1163
  """
1058
1164
  Show bulk action menu for selected exploits.
@@ -1066,20 +1172,25 @@ def _exploit_bulk_action_menu(
1066
1172
  'continue' to stay in view, 'clear' to clear selection
1067
1173
  """
1068
1174
  if not selected_exploits:
1069
- return 'continue'
1175
+ return "continue"
1070
1176
 
1071
1177
  # Get exploit details for selected items
1072
1178
  exploit_index = _build_exploit_index(host)
1073
1179
  selected_items = [
1074
- (exploit, service) for exploit, service in exploit_index
1075
- if exploit.get('identifier') in selected_exploits
1180
+ (exploit, service)
1181
+ for exploit, service in exploit_index
1182
+ if exploit.get("identifier") in selected_exploits
1076
1183
  ]
1077
1184
 
1078
1185
  # Count by category
1079
- executable_count = len([
1080
- e for e, s in selected_items
1081
- if e.get('msf_module') and _determine_actionability(e) in ['one_click', 'setup_needed']
1082
- ])
1186
+ executable_count = len(
1187
+ [
1188
+ e
1189
+ for e, s in selected_items
1190
+ if e.get("msf_module")
1191
+ and _determine_actionability(e) in ["one_click", "setup_needed"]
1192
+ ]
1193
+ )
1083
1194
 
1084
1195
  console.print()
1085
1196
  console.print(f" [bold]Selected: {len(selected_exploits)} exploit(s)[/bold]")
@@ -1093,58 +1204,68 @@ def _exploit_bulk_action_menu(
1093
1204
  console.print(" \\[q] Back")
1094
1205
 
1095
1206
  try:
1096
- action = click.prompt(" Select option", default='q', show_default=False).strip().lower()
1207
+ action = (
1208
+ click.prompt(" Select option", default="q", show_default=False)
1209
+ .strip()
1210
+ .lower()
1211
+ )
1097
1212
 
1098
- if action in ['s', 't', 'f', 'n']:
1213
+ if action in ["s", "t", "f", "n"]:
1099
1214
  # Mark exploits with status
1100
- status_map = {'s': 'success', 't': 'attempted', 'f': 'failed', 'n': 'not_tried'}
1215
+ status_map = {
1216
+ "s": "success",
1217
+ "t": "attempted",
1218
+ "f": "failed",
1219
+ "n": "not_tried",
1220
+ }
1101
1221
  new_status = status_map[action]
1102
1222
 
1103
1223
  marked = 0
1104
1224
  for exploit, service in selected_items:
1105
1225
  exploit_attempts.record_attempt(
1106
1226
  engagement_id=engagement_id,
1107
- host_id=host.get('host_id'),
1108
- exploit_identifier=exploit.get('identifier'),
1109
- exploit_title=exploit.get('title', 'Unknown'),
1227
+ host_id=host.get("host_id"),
1228
+ exploit_identifier=exploit.get("identifier"),
1229
+ exploit_title=exploit.get("title", "Unknown"),
1110
1230
  status=new_status,
1111
- service_id=service.get('service_id'),
1112
- notes=f"Bulk marked via multi-select"
1231
+ service_id=service.get("service_id"),
1232
+ notes=f"Bulk marked via multi-select",
1113
1233
  )
1114
1234
  marked += 1
1115
1235
 
1116
- console.print(f"\n [green]Marked {marked} exploit(s) as {new_status}[/green]")
1236
+ console.print(
1237
+ f"\n [green]Marked {marked} exploit(s) as {new_status}[/green]"
1238
+ )
1117
1239
  click.pause()
1118
1240
 
1119
1241
  # Clear cache to force refresh
1120
1242
  from souleyez.intelligence.exploit_suggestions import _SUGGESTION_CACHE
1243
+
1121
1244
  _SUGGESTION_CACHE.clear()
1122
1245
 
1123
- return 'clear' # Clear selection after marking
1246
+ return "clear" # Clear selection after marking
1124
1247
 
1125
- elif action == 'q':
1248
+ elif action == "q":
1126
1249
  if executable_count > 0:
1127
1250
  # Queue exploits for execution
1128
1251
  return _queue_selected_exploits(engagement_id, host, selected_items)
1129
1252
  else:
1130
1253
  # Back
1131
- return 'back'
1254
+ return "back"
1132
1255
 
1133
- elif action == 'c':
1256
+ elif action == "c":
1134
1257
  selected_exploits.clear()
1135
1258
  console.print("\n [dim]Selection cleared[/dim]")
1136
- return 'continue'
1259
+ return "continue"
1137
1260
 
1138
1261
  except (KeyboardInterrupt, click.Abort):
1139
1262
  pass
1140
1263
 
1141
- return 'continue'
1264
+ return "continue"
1142
1265
 
1143
1266
 
1144
1267
  def _queue_selected_exploits(
1145
- engagement_id: int,
1146
- host: Dict,
1147
- selected_items: List[Tuple[Dict, Dict]]
1268
+ engagement_id: int, host: Dict, selected_items: List[Tuple[Dict, Dict]]
1148
1269
  ) -> str:
1149
1270
  """
1150
1271
  Queue selected exploits as background jobs for sequential execution.
@@ -1161,17 +1282,23 @@ def _queue_selected_exploits(
1161
1282
 
1162
1283
  # Filter to executable exploits
1163
1284
  executable = [
1164
- (e, s) for e, s in selected_items
1165
- if e.get('msf_module') and _determine_actionability(e) in ['one_click', 'setup_needed']
1285
+ (e, s)
1286
+ for e, s in selected_items
1287
+ if e.get("msf_module")
1288
+ and _determine_actionability(e) in ["one_click", "setup_needed"]
1166
1289
  ]
1167
1290
 
1168
1291
  if not executable:
1169
- console.print(" [yellow]No executable exploits selected (need MSF module)[/yellow]")
1292
+ console.print(
1293
+ " [yellow]No executable exploits selected (need MSF module)[/yellow]"
1294
+ )
1170
1295
  click.pause()
1171
- return 'continue'
1296
+ return "continue"
1172
1297
 
1173
1298
  # Show confirmation
1174
- console.print(f"\n [cyan]Queue {len(executable)} exploit(s) for execution?[/cyan]\n")
1299
+ console.print(
1300
+ f"\n [cyan]Queue {len(executable)} exploit(s) for execution?[/cyan]\n"
1301
+ )
1175
1302
 
1176
1303
  for idx, (exploit, service) in enumerate(executable[:5], 1):
1177
1304
  console.print(f" {idx}. {exploit.get('title', 'Unknown')[:50]}")
@@ -1181,12 +1308,14 @@ def _queue_selected_exploits(
1181
1308
  console.print(f" ... and {len(executable) - 5} more")
1182
1309
 
1183
1310
  console.print()
1184
- console.print(" [yellow]Note: Exploits will run sequentially as background jobs[/yellow]")
1311
+ console.print(
1312
+ " [yellow]Note: Exploits will run sequentially as background jobs[/yellow]"
1313
+ )
1185
1314
  console.print(" [dim]Check Job Queue to monitor progress[/dim]")
1186
1315
  console.print()
1187
1316
 
1188
1317
  if not click.confirm(" Proceed?", default=False):
1189
- return 'continue'
1318
+ return "continue"
1190
1319
 
1191
1320
  # Queue each exploit as a job
1192
1321
  queued = 0
@@ -1195,9 +1324,9 @@ def _queue_selected_exploits(
1195
1324
 
1196
1325
  for exploit, service in executable:
1197
1326
  # Create MSF resource script content
1198
- msf_module = exploit.get('msf_module')
1199
- target_ip = host.get('ip')
1200
- target_port = service.get('port')
1327
+ msf_module = exploit.get("msf_module")
1328
+ target_ip = host.get("ip")
1329
+ target_port = service.get("port")
1201
1330
 
1202
1331
  # Build args for MSF execution
1203
1332
  args = f"--module {msf_module} --rhost {target_ip}"
@@ -1206,12 +1335,12 @@ def _queue_selected_exploits(
1206
1335
 
1207
1336
  # Queue the job with metadata
1208
1337
  job_id = enqueue_job(
1209
- tool='msf_exploit',
1338
+ tool="msf_exploit",
1210
1339
  target=target_ip,
1211
1340
  args=args,
1212
1341
  label=f"Exploit: {exploit.get('title', 'Unknown')[:40]}",
1213
1342
  engagement_id=engagement_id,
1214
- parent_id=prev_job_id if prev_job_id else None
1343
+ parent_id=prev_job_id if prev_job_id else None,
1215
1344
  )
1216
1345
 
1217
1346
  if first_job_id is None:
@@ -1222,12 +1351,12 @@ def _queue_selected_exploits(
1222
1351
  # Mark as attempted
1223
1352
  exploit_attempts.record_attempt(
1224
1353
  engagement_id=engagement_id,
1225
- host_id=host.get('host_id'),
1226
- exploit_identifier=exploit.get('identifier'),
1227
- exploit_title=exploit.get('title', 'Unknown'),
1228
- status='attempted',
1229
- service_id=service.get('service_id'),
1230
- notes=f"Queued as job #{job_id}"
1354
+ host_id=host.get("host_id"),
1355
+ exploit_identifier=exploit.get("identifier"),
1356
+ exploit_title=exploit.get("title", "Unknown"),
1357
+ status="attempted",
1358
+ service_id=service.get("service_id"),
1359
+ notes=f"Queued as job #{job_id}",
1231
1360
  )
1232
1361
 
1233
1362
  console.print(f"\n [green]Queued {queued} exploit(s) as background jobs[/green]")
@@ -1237,9 +1366,10 @@ def _queue_selected_exploits(
1237
1366
 
1238
1367
  # Clear cache
1239
1368
  from souleyez.intelligence.exploit_suggestions import _SUGGESTION_CACHE
1369
+
1240
1370
  _SUGGESTION_CACHE.clear()
1241
1371
 
1242
- return 'clear'
1372
+ return "clear"
1243
1373
 
1244
1374
 
1245
1375
  def view_exploit_suggestions(engagement_id):
@@ -1248,11 +1378,13 @@ def view_exploit_suggestions(engagement_id):
1248
1378
  from souleyez.storage.engagements import EngagementManager
1249
1379
 
1250
1380
  em = EngagementManager()
1251
- engine = ExploitSuggestionEngine(use_searchsploit=False) # Disable SearchSploit for instant loading
1381
+ engine = ExploitSuggestionEngine(
1382
+ use_searchsploit=False
1383
+ ) # Disable SearchSploit for instant loading
1252
1384
 
1253
1385
  current = em.get_by_id(engagement_id)
1254
1386
  if not current:
1255
- click.echo(click.style("Engagement not found", fg='red'))
1387
+ click.echo(click.style("Engagement not found", fg="red"))
1256
1388
  click.pause()
1257
1389
  return
1258
1390
 
@@ -1276,13 +1408,15 @@ def view_exploit_suggestions(engagement_id):
1276
1408
  padding_total = width - 2 - text_display_width
1277
1409
  padding_left = padding_total // 2
1278
1410
  padding_right = padding_total - padding_left
1279
- console.print("│" + " " * padding_left + header_text + " " * padding_right + "│")
1411
+ console.print(
1412
+ "│" + " " * padding_left + header_text + " " * padding_right + "│"
1413
+ )
1280
1414
  console.print("└" + "─" * (width - 2) + "┘\n")
1281
1415
 
1282
1416
  # Get suggestions
1283
1417
  suggestions = engine.generate_suggestions(engagement_id)
1284
1418
 
1285
- if not suggestions or not suggestions.get('hosts'):
1419
+ if not suggestions or not suggestions.get("hosts"):
1286
1420
  # No suggestions available
1287
1421
  hm = HostManager()
1288
1422
  all_hosts = hm.list_hosts(engagement_id=engagement_id)
@@ -1297,22 +1431,28 @@ def view_exploit_suggestions(engagement_id):
1297
1431
  console.print(" • nmap for network scanning")
1298
1432
  else:
1299
1433
  console.print(" 💡 [cyan]Reason:[/cyan]")
1300
- console.print(f" • {len(all_hosts)} host(s) discovered but no exploits found")
1301
- console.print(" • Services may be patched or require manual research\n")
1434
+ console.print(
1435
+ f" • {len(all_hosts)} host(s) discovered but no exploits found"
1436
+ )
1437
+ console.print(
1438
+ " • Services may be patched or require manual research\n"
1439
+ )
1302
1440
 
1303
1441
  console.print()
1304
1442
  else:
1305
1443
  if selected_host is None:
1306
1444
  # Summary view
1307
1445
  console.print(f" Engagement: [cyan]{current['name']}[/cyan]")
1308
- console.print(f" Filter: {'[yellow]Untried Only[/yellow]' if filter_untried else 'All Exploits'}\n")
1446
+ console.print(
1447
+ f" Filter: {'[yellow]Untried Only[/yellow]' if filter_untried else 'All Exploits'}\n"
1448
+ )
1309
1449
 
1310
1450
  # Summary table
1311
1451
  table = Table(
1312
1452
  show_header=True,
1313
1453
  header_style="bold cyan",
1314
1454
  box=DesignSystem.TABLE_BOX,
1315
- padding=(0, 1)
1455
+ padding=(0, 1),
1316
1456
  )
1317
1457
  table.add_column("#", justify="right", width=4)
1318
1458
  table.add_column("Host", style="cyan")
@@ -1321,16 +1461,20 @@ def view_exploit_suggestions(engagement_id):
1321
1461
  table.add_column("High", justify="center", width=6, style="yellow")
1322
1462
  table.add_column("Med", justify="center", width=6)
1323
1463
  table.add_column("Low", justify="center", width=6, style="dim")
1324
- table.add_column("⚪", justify="center", width=4, style="dim") # Untried
1325
- table.add_column("", justify="center", width=4, style="green") # Success
1326
-
1327
- for idx, host in enumerate(suggestions['hosts'], 1):
1328
- ip = host.get('ip', 'Unknown')
1329
- hostname = host.get('hostname', '')
1464
+ table.add_column(
1465
+ "", justify="center", width=4, style="dim"
1466
+ ) # Untried
1467
+ table.add_column(
1468
+ "✅", justify="center", width=4, style="green"
1469
+ ) # Success
1470
+
1471
+ for idx, host in enumerate(suggestions["hosts"], 1):
1472
+ ip = host.get("ip", "Unknown")
1473
+ hostname = host.get("hostname", "")
1330
1474
  display_name = f"{ip} ({hostname})" if hostname else ip
1331
1475
 
1332
- services = host.get('services', [])
1333
- services_with_exploits = [s for s in services if s.get('exploits')]
1476
+ services = host.get("services", [])
1477
+ services_with_exploits = [s for s in services if s.get("exploits")]
1334
1478
 
1335
1479
  # Count by severity and status
1336
1480
  crit_count = 0
@@ -1341,22 +1485,22 @@ def view_exploit_suggestions(engagement_id):
1341
1485
  success_count = 0
1342
1486
 
1343
1487
  for service in services_with_exploits:
1344
- for exploit in service.get('exploits', []):
1345
- severity = exploit.get('severity', '')
1346
- status = exploit.get('attempt_status', 'not_tried')
1488
+ for exploit in service.get("exploits", []):
1489
+ severity = exploit.get("severity", "")
1490
+ status = exploit.get("attempt_status", "not_tried")
1347
1491
 
1348
- if severity == 'critical':
1492
+ if severity == "critical":
1349
1493
  crit_count += 1
1350
- elif severity == 'high':
1494
+ elif severity == "high":
1351
1495
  high_count += 1
1352
- elif severity == 'medium':
1496
+ elif severity == "medium":
1353
1497
  med_count += 1
1354
- elif severity == 'low':
1498
+ elif severity == "low":
1355
1499
  low_count += 1
1356
1500
 
1357
- if status == 'not_tried':
1501
+ if status == "not_tried":
1358
1502
  untried_count += 1
1359
- elif status == 'success':
1503
+ elif status == "success":
1360
1504
  success_count += 1
1361
1505
 
1362
1506
  table.add_row(
@@ -1368,7 +1512,7 @@ def view_exploit_suggestions(engagement_id):
1368
1512
  str(med_count) if med_count > 0 else "-",
1369
1513
  str(low_count) if low_count > 0 else "-",
1370
1514
  str(untried_count) if untried_count > 0 else "-",
1371
- str(success_count) if success_count > 0 else "-"
1515
+ str(success_count) if success_count > 0 else "-",
1372
1516
  )
1373
1517
 
1374
1518
  console.print(table)
@@ -1376,24 +1520,30 @@ def view_exploit_suggestions(engagement_id):
1376
1520
 
1377
1521
  else:
1378
1522
  # Detail view for selected host
1379
- host = suggestions['hosts'][selected_host - 1]
1380
- ip = host.get('ip') or host.get('host') or 'Unknown'
1381
- hostname = host.get('hostname', '')
1523
+ host = suggestions["hosts"][selected_host - 1]
1524
+ ip = host.get("ip") or host.get("host") or "Unknown"
1525
+ hostname = host.get("hostname", "")
1382
1526
  display_name = f"{ip} ({hostname})" if hostname else ip
1383
1527
 
1384
1528
  console.print(f" 🎯 [cyan bold]{display_name}[/cyan bold]")
1385
- console.print(f" Filter: {'[yellow]Untried Only[/yellow]' if filter_untried else 'All Exploits'}")
1529
+ console.print(
1530
+ f" Filter: {'[yellow]Untried Only[/yellow]' if filter_untried else 'All Exploits'}"
1531
+ )
1386
1532
 
1387
1533
  # Show selection count if any
1388
1534
  if selected_exploits:
1389
- console.print(f" [cyan bold]Selected:[/cyan bold] {len(selected_exploits)} exploit(s) [dim](\\[x] to manage)[/dim]")
1535
+ console.print(
1536
+ f" [cyan bold]Selected:[/cyan bold] {len(selected_exploits)} exploit(s) [dim](\\[x] to manage)[/dim]"
1537
+ )
1390
1538
  console.print()
1391
1539
 
1392
1540
  # Quick Wins section
1393
1541
  _render_quick_wins(host, width, selected_exploits)
1394
1542
 
1395
1543
  # Service groups with exploits
1396
- _render_service_groups(host, width, collapsed_services, filter_untried, selected_exploits)
1544
+ _render_service_groups(
1545
+ host, width, collapsed_services, filter_untried, selected_exploits
1546
+ )
1397
1547
 
1398
1548
  # Additional attack vectors
1399
1549
  if show_additional_vectors:
@@ -1407,10 +1557,18 @@ def view_exploit_suggestions(engagement_id):
1407
1557
  console.print()
1408
1558
 
1409
1559
  if selected_host is None:
1410
- console.print(" [cyan]\\[1-9][/cyan] Select Host - View exploit suggestions for specific target")
1411
- console.print(" [cyan]\\[u][/cyan] Toggle Untried Filter - Show only unexploited vulnerabilities")
1412
- console.print(" [cyan]\\[r][/cyan] Refresh - Reload exploit suggestions from database")
1413
- console.print(" [cyan]\\[h][/cyan] Help - Show keyboard shortcuts and usage guide")
1560
+ console.print(
1561
+ " [cyan]\\[1-9][/cyan] Select Host - View exploit suggestions for specific target"
1562
+ )
1563
+ console.print(
1564
+ " [cyan]\\[u][/cyan] Toggle Untried Filter - Show only unexploited vulnerabilities"
1565
+ )
1566
+ console.print(
1567
+ " [cyan]\\[r][/cyan] Refresh - Reload exploit suggestions from database"
1568
+ )
1569
+ console.print(
1570
+ " [cyan]\\[h][/cyan] Help - Show keyboard shortcuts and usage guide"
1571
+ )
1414
1572
  console.print()
1415
1573
  console.print(" " + "═" * (width - 4))
1416
1574
  console.print()
@@ -1418,19 +1576,41 @@ def view_exploit_suggestions(engagement_id):
1418
1576
  else:
1419
1577
  # Multi-select options
1420
1578
  if selected_exploits:
1421
- console.print(f" [cyan]\\[x][/cyan] Bulk Actions - Manage {len(selected_exploits)} selected exploit(s)")
1422
- console.print(" [cyan]\\[i][/cyan] Interactive Select - Multi-select exploits with keyboard")
1423
- console.print(" [cyan]\\[c][/cyan] Clear Selection - Deselect all exploits")
1579
+ console.print(
1580
+ f" [cyan]\\[x][/cyan] Bulk Actions - Manage {len(selected_exploits)} selected exploit(s)"
1581
+ )
1582
+ console.print(
1583
+ " [cyan]\\[i][/cyan] Interactive Select - Multi-select exploits with keyboard"
1584
+ )
1585
+ console.print(
1586
+ " [cyan]\\[c][/cyan] Clear Selection - Deselect all exploits"
1587
+ )
1424
1588
  console.print()
1425
- console.print(" [cyan]\\[m][/cyan] Mark Exploit - Record attempt status (tried/failed/success)")
1426
- console.print(" [cyan]\\[v][/cyan] View Details - Full exploit information and prerequisites")
1427
- console.print(" [cyan]\\[e][/cyan] Execute Exploit - Launch MSF module for selected exploit")
1428
- console.print(" [cyan]\\[a][/cyan] Execute All Critical - Batch execute all critical exploits")
1429
- console.print(" [cyan]\\[g][/cyan] Toggle Vectors - Show/hide additional attack techniques")
1589
+ console.print(
1590
+ " [cyan]\\[m][/cyan] Mark Exploit - Record attempt status (tried/failed/success)"
1591
+ )
1592
+ console.print(
1593
+ " [cyan]\\[v][/cyan] View Details - Full exploit information and prerequisites"
1594
+ )
1595
+ console.print(
1596
+ " [cyan]\\[e][/cyan] Execute Exploit - Launch MSF module for selected exploit"
1597
+ )
1598
+ console.print(
1599
+ " [cyan]\\[a][/cyan] Execute All Critical - Batch execute all critical exploits"
1600
+ )
1601
+ console.print(
1602
+ " [cyan]\\[g][/cyan] Toggle Vectors - Show/hide additional attack techniques"
1603
+ )
1430
1604
  console.print()
1431
- console.print(" [cyan]\\[u][/cyan] Toggle Untried Filter - Show only unexploited vulnerabilities")
1432
- console.print(" [cyan]\\[b][/cyan] Back to Host List - Return to target selection")
1433
- console.print(" [cyan]\\[h][/cyan] Help - Show keyboard shortcuts and usage guide")
1605
+ console.print(
1606
+ " [cyan]\\[u][/cyan] Toggle Untried Filter - Show only unexploited vulnerabilities"
1607
+ )
1608
+ console.print(
1609
+ " [cyan]\\[b][/cyan] Back to Host List - Return to target selection"
1610
+ )
1611
+ console.print(
1612
+ " [cyan]\\[h][/cyan] Help - Show keyboard shortcuts and usage guide"
1613
+ )
1434
1614
  console.print()
1435
1615
  console.print(" " + "═" * (width - 4))
1436
1616
  console.print()
@@ -1439,56 +1619,70 @@ def view_exploit_suggestions(engagement_id):
1439
1619
  console.print()
1440
1620
 
1441
1621
  try:
1442
- choice = click.prompt(" Select option", type=str, default='q', show_default=False).strip().lower()
1622
+ choice = (
1623
+ click.prompt(
1624
+ " Select option", type=str, default="q", show_default=False
1625
+ )
1626
+ .strip()
1627
+ .lower()
1628
+ )
1443
1629
 
1444
- if choice == 'q':
1630
+ if choice == "q":
1445
1631
  return
1446
- elif choice == 'h' or choice == '?':
1632
+ elif choice == "h" or choice == "?":
1447
1633
  _show_help_overlay()
1448
- elif choice == 'r':
1634
+ elif choice == "r":
1449
1635
  continue # Refresh
1450
- elif choice == 'u':
1636
+ elif choice == "u":
1451
1637
  filter_untried = not filter_untried
1452
- elif choice == 'b' and selected_host is not None:
1638
+ elif choice == "b" and selected_host is not None:
1453
1639
  selected_host = None
1454
1640
  collapsed_services.clear()
1455
1641
  selected_exploits.clear() # Clear selection when leaving host
1456
- elif choice == 'g' and selected_host is not None:
1642
+ elif choice == "g" and selected_host is not None:
1457
1643
  show_additional_vectors = not show_additional_vectors
1458
1644
  elif choice.isdigit() and selected_host is None:
1459
1645
  host_num = int(choice)
1460
- if 1 <= host_num <= len(suggestions.get('hosts', [])):
1646
+ if 1 <= host_num <= len(suggestions.get("hosts", [])):
1461
1647
  selected_host = host_num
1462
1648
  selected_exploits.clear() # Clear selection when switching hosts
1463
1649
  else:
1464
1650
  console.print("[red]Invalid host number[/red]")
1465
1651
  click.pause()
1466
- elif choice == 'm' and selected_host is not None:
1652
+ elif choice == "m" and selected_host is not None:
1467
1653
  # Mark exploit interactively
1468
- host = suggestions['hosts'][selected_host - 1]
1654
+ host = suggestions["hosts"][selected_host - 1]
1469
1655
  if _mark_exploit_interactive(engagement_id, host):
1470
1656
  # Refresh suggestions after marking
1471
- from souleyez.intelligence.exploit_suggestions import _SUGGESTION_CACHE
1657
+ from souleyez.intelligence.exploit_suggestions import (
1658
+ _SUGGESTION_CACHE,
1659
+ )
1660
+
1472
1661
  _SUGGESTION_CACHE.clear() # Clear cache to force refresh
1473
- elif choice == 'i' and selected_host is not None:
1662
+ elif choice == "i" and selected_host is not None:
1474
1663
  # Interactive mode - launch interactive selector
1475
- host = suggestions['hosts'][selected_host - 1]
1664
+ host = suggestions["hosts"][selected_host - 1]
1476
1665
  _interactive_exploit_select(host, selected_exploits)
1477
- elif choice == 'x' and selected_host is not None and selected_exploits:
1666
+ elif choice == "x" and selected_host is not None and selected_exploits:
1478
1667
  # Bulk actions menu
1479
- host = suggestions['hosts'][selected_host - 1]
1480
- result = _exploit_bulk_action_menu(engagement_id, host, selected_exploits)
1481
- if result == 'clear':
1668
+ host = suggestions["hosts"][selected_host - 1]
1669
+ result = _exploit_bulk_action_menu(
1670
+ engagement_id, host, selected_exploits
1671
+ )
1672
+ if result == "clear":
1482
1673
  selected_exploits.clear()
1483
- from souleyez.intelligence.exploit_suggestions import _SUGGESTION_CACHE
1674
+ from souleyez.intelligence.exploit_suggestions import (
1675
+ _SUGGESTION_CACHE,
1676
+ )
1677
+
1484
1678
  _SUGGESTION_CACHE.clear()
1485
- elif choice == 'c' and selected_host is not None:
1679
+ elif choice == "c" and selected_host is not None:
1486
1680
  # Clear selection
1487
1681
  selected_exploits.clear()
1488
1682
  console.print(" [dim]Selection cleared[/dim]")
1489
- elif choice == 'v' and selected_host is not None:
1683
+ elif choice == "v" and selected_host is not None:
1490
1684
  # Show detailed info for an exploit (renamed from [i])
1491
- host = suggestions['hosts'][selected_host - 1]
1685
+ host = suggestions["hosts"][selected_host - 1]
1492
1686
  exploit_index = _build_exploit_index(host)
1493
1687
 
1494
1688
  if not exploit_index:
@@ -1509,9 +1703,9 @@ def view_exploit_suggestions(engagement_id):
1509
1703
  click.pause()
1510
1704
  except (ValueError, click.Abort):
1511
1705
  pass
1512
- elif choice == 'e' and selected_host is not None:
1706
+ elif choice == "e" and selected_host is not None:
1513
1707
  # Execute MSF module
1514
- host = suggestions['hosts'][selected_host - 1]
1708
+ host = suggestions["hosts"][selected_host - 1]
1515
1709
  exploit_index = _build_exploit_index(host)
1516
1710
 
1517
1711
  if not exploit_index:
@@ -1527,26 +1721,38 @@ def view_exploit_suggestions(engagement_id):
1527
1721
  if 1 <= exploit_num <= len(exploit_index):
1528
1722
  exploit, service = exploit_index[exploit_num - 1]
1529
1723
 
1530
- if not exploit.get('msf_module'):
1531
- console.print(" [red]This exploit doesn't have an MSF module[/red]")
1532
- console.print(f" [dim]Actionability: {_render_actionability_badge(_determine_actionability(exploit))}[/dim]")
1724
+ if not exploit.get("msf_module"):
1725
+ console.print(
1726
+ " [red]This exploit doesn't have an MSF module[/red]"
1727
+ )
1728
+ console.print(
1729
+ f" [dim]Actionability: {_render_actionability_badge(_determine_actionability(exploit))}[/dim]"
1730
+ )
1533
1731
  click.pause()
1534
1732
  else:
1535
- if _execute_msf_module(exploit, host, service, engagement_id):
1733
+ if _execute_msf_module(
1734
+ exploit, host, service, engagement_id
1735
+ ):
1536
1736
  # Refresh after execution
1537
- from souleyez.intelligence.exploit_suggestions import _SUGGESTION_CACHE
1737
+ from souleyez.intelligence.exploit_suggestions import (
1738
+ _SUGGESTION_CACHE,
1739
+ )
1740
+
1538
1741
  _SUGGESTION_CACHE.clear()
1539
1742
  else:
1540
1743
  console.print(" [red]Invalid ID[/red]")
1541
1744
  click.pause()
1542
1745
  except (ValueError, click.Abort):
1543
1746
  pass
1544
- elif choice == 'a' and selected_host is not None:
1747
+ elif choice == "a" and selected_host is not None:
1545
1748
  # Batch execute all critical
1546
- host = suggestions['hosts'][selected_host - 1]
1749
+ host = suggestions["hosts"][selected_host - 1]
1547
1750
  if _batch_execute_critical(engagement_id, host) > 0:
1548
1751
  # Refresh after batch execution
1549
- from souleyez.intelligence.exploit_suggestions import _SUGGESTION_CACHE
1752
+ from souleyez.intelligence.exploit_suggestions import (
1753
+ _SUGGESTION_CACHE,
1754
+ )
1755
+
1550
1756
  _SUGGESTION_CACHE.clear()
1551
1757
  else:
1552
1758
  console.print("[red]Invalid command[/red]")