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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/ui/tool_setup.py CHANGED
@@ -29,48 +29,53 @@ def _reset_terminal():
29
29
  """Reset terminal to sane state after interrupt."""
30
30
  try:
31
31
  # Reset terminal using stty
32
- subprocess.run(['stty', 'sane'], check=False, timeout=5)
32
+ subprocess.run(["stty", "sane"], check=False, timeout=5)
33
33
  # Also try the reset command for good measure
34
- subprocess.run(['reset', '-I'], check=False, timeout=5,
35
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
34
+ subprocess.run(
35
+ ["reset", "-I"],
36
+ check=False,
37
+ timeout=5,
38
+ stdout=subprocess.DEVNULL,
39
+ stderr=subprocess.DEVNULL,
40
+ )
36
41
  except Exception:
37
42
  pass
38
43
 
39
44
 
40
45
  # Prerequisites needed for various install methods
41
46
  PREREQUISITES = {
42
- 'build-deps': {
43
- 'check': None, # Always install to ensure all deps present
44
- 'install': 'sudo apt install -y build-essential python3-dev libxml2-dev libxslt1-dev libuv1-dev libffi-dev libssl-dev rustc cargo',
45
- 'description': 'Build dependencies for Python packages with native extensions',
46
- 'always_install': True # Flag to always run this
47
+ "build-deps": {
48
+ "check": None, # Always install to ensure all deps present
49
+ "install": "sudo apt install -y build-essential python3-dev libxml2-dev libxslt1-dev libuv1-dev libffi-dev libssl-dev rustc cargo",
50
+ "description": "Build dependencies for Python packages with native extensions",
51
+ "always_install": True, # Flag to always run this
47
52
  },
48
- 'pipx': {
49
- 'check': 'pipx',
50
- 'install': 'sudo apt install -y pipx && pipx ensurepath',
51
- 'description': 'Python application installer (for theHarvester, NetExec, etc.)',
52
- 'path_additions': ['~/.local/bin']
53
+ "pipx": {
54
+ "check": "pipx",
55
+ "install": "sudo apt install -y pipx && pipx ensurepath",
56
+ "description": "Python application installer (for theHarvester, NetExec, etc.)",
57
+ "path_additions": ["~/.local/bin"],
53
58
  },
54
- 'golang': {
55
- 'check': 'go',
56
- 'install': 'sudo apt install -y golang-go',
57
- 'description': 'Go programming language (for nuclei, ffuf)',
58
- 'path_additions': ['~/go/bin']
59
+ "golang": {
60
+ "check": "go",
61
+ "install": "sudo apt install -y golang-go",
62
+ "description": "Go programming language (for nuclei, ffuf)",
63
+ "path_additions": ["~/go/bin"],
59
64
  },
60
- 'ruby': {
61
- 'check': 'gem',
62
- 'install': 'sudo apt install -y ruby-full ruby-dev build-essential',
63
- 'description': 'Ruby programming language (for wpscan)'
65
+ "ruby": {
66
+ "check": "gem",
67
+ "install": "sudo apt install -y ruby-full ruby-dev build-essential",
68
+ "description": "Ruby programming language (for wpscan)",
64
69
  },
65
- 'snap': {
66
- 'check': 'snap',
67
- 'install': 'sudo apt install -y snapd',
68
- 'description': 'Snap package manager (for enum4linux)'
70
+ "snap": {
71
+ "check": "snap",
72
+ "install": "sudo apt install -y snapd",
73
+ "description": "Snap package manager (for enum4linux)",
69
74
  },
70
- 'git': {
71
- 'check': 'git',
72
- 'install': 'sudo apt install -y git',
73
- 'description': 'Git version control (for exploitdb, Responder)'
75
+ "git": {
76
+ "check": "git",
77
+ "install": "sudo apt install -y git",
78
+ "description": "Git version control (for exploitdb, Responder)",
74
79
  },
75
80
  }
76
81
 
@@ -129,12 +134,15 @@ def _add_paths_to_shell_rc():
129
134
  pass # Don't fail on PATH configuration issues
130
135
 
131
136
 
132
- def _run_command(cmd: str, console, description: str = "", capture: bool = False) -> tuple:
137
+ def _run_command(
138
+ cmd: str, console, description: str = "", capture: bool = False
139
+ ) -> tuple:
133
140
  """Run a command with proper error handling."""
134
141
  import sys
142
+
135
143
  try:
136
144
  # Never capture sudo commands - password prompt needs to be visible
137
- if cmd.strip().startswith('sudo'):
145
+ if cmd.strip().startswith("sudo"):
138
146
  capture = False
139
147
  # Flush output and print newline so sudo prompt appears on new line
140
148
  sys.stdout.flush()
@@ -147,7 +155,7 @@ def _run_command(cmd: str, console, description: str = "", capture: bool = False
147
155
  shell=True,
148
156
  capture_output=True,
149
157
  text=True,
150
- timeout=600 # 10 minute timeout
158
+ timeout=600, # 10 minute timeout
151
159
  )
152
160
  return result.returncode == 0, result.stdout, result.stderr
153
161
  else:
@@ -155,7 +163,7 @@ def _run_command(cmd: str, console, description: str = "", capture: bool = False
155
163
  cmd,
156
164
  shell=True,
157
165
  stdin=sys.stdin, # Ensure stdin is connected for password input
158
- timeout=600
166
+ timeout=600,
159
167
  )
160
168
  return result.returncode == 0, "", ""
161
169
  except subprocess.TimeoutExpired:
@@ -166,19 +174,19 @@ def _run_command(cmd: str, console, description: str = "", capture: bool = False
166
174
 
167
175
  # Tools that need privileged access
168
176
  # Binary tools: simple executables that need sudo
169
- PRIVILEGED_BINARY_TOOLS = ['nmap']
177
+ PRIVILEGED_BINARY_TOOLS = ["nmap"]
170
178
 
171
179
  # Script-based tools: need sudo to run interpreter + script
172
180
  # Format: {'name': {'interpreter': path, 'script_paths': [possible locations], 'description': str}}
173
181
  PRIVILEGED_SCRIPT_TOOLS = {
174
- 'responder': {
175
- 'interpreter': '/usr/bin/python3',
176
- 'script_paths': [
177
- '/usr/share/responder/Responder.py',
178
- '/opt/Responder/Responder.py',
179
- str(Path.home() / 'tools/Responder/Responder.py'),
182
+ "responder": {
183
+ "interpreter": "/usr/bin/python3",
184
+ "script_paths": [
185
+ "/usr/share/responder/Responder.py",
186
+ "/opt/Responder/Responder.py",
187
+ str(Path.home() / "tools/Responder/Responder.py"),
180
188
  ],
181
- 'description': 'LLMNR/NBT-NS credential capture'
189
+ "description": "LLMNR/NBT-NS credential capture",
182
190
  }
183
191
  }
184
192
 
@@ -199,11 +207,9 @@ def _check_sudoers_configured_binary(tool_name: str) -> bool:
199
207
 
200
208
  # Try running sudo -n (non-interactive) to see if NOPASSWD is set
201
209
  try:
202
- subprocess.run(['sudo', '-k'], capture_output=True, timeout=5)
210
+ subprocess.run(["sudo", "-k"], capture_output=True, timeout=5)
203
211
  result = subprocess.run(
204
- ['sudo', '-n', tool_path, '--version'],
205
- capture_output=True,
206
- timeout=5
212
+ ["sudo", "-n", tool_path, "--version"], capture_output=True, timeout=5
207
213
  )
208
214
  return result.returncode == 0
209
215
  except Exception:
@@ -217,11 +223,11 @@ def _check_sudoers_configured_script(interpreter: str, script_path: str) -> bool
217
223
 
218
224
  # Try running sudo -n with interpreter + script
219
225
  try:
220
- subprocess.run(['sudo', '-k'], capture_output=True, timeout=5)
226
+ subprocess.run(["sudo", "-k"], capture_output=True, timeout=5)
221
227
  result = subprocess.run(
222
- ['sudo', '-n', interpreter, script_path, '--help'],
228
+ ["sudo", "-n", interpreter, script_path, "--help"],
223
229
  capture_output=True,
224
- timeout=5
230
+ timeout=5,
225
231
  )
226
232
  return result.returncode == 0
227
233
  except Exception:
@@ -243,11 +249,13 @@ def _configure_sudoers(console):
243
249
 
244
250
  # Check script-based tools
245
251
  for tool_name, tool_info in PRIVILEGED_SCRIPT_TOOLS.items():
246
- script_path = _find_script_path(tool_info['script_paths'])
252
+ script_path = _find_script_path(tool_info["script_paths"])
247
253
  if script_path:
248
- interpreter = tool_info['interpreter']
254
+ interpreter = tool_info["interpreter"]
249
255
  if not _check_sudoers_configured_script(interpreter, script_path):
250
- script_tools_to_configure.append((tool_name, interpreter, script_path, tool_info['description']))
256
+ script_tools_to_configure.append(
257
+ (tool_name, interpreter, script_path, tool_info["description"])
258
+ )
251
259
 
252
260
  if not binary_tools_to_configure and not script_tools_to_configure:
253
261
  return # All configured or not installed
@@ -256,7 +264,9 @@ def _configure_sudoers(console):
256
264
  console.print(" " + "═" * 60)
257
265
  console.print("[bold cyan] PRIVILEGED SCAN SETUP[/bold cyan]")
258
266
  console.print()
259
- console.print(" Some scans require root privileges (SYN scans, credential capture).")
267
+ console.print(
268
+ " Some scans require root privileges (SYN scans, credential capture)."
269
+ )
260
270
  console.print(" SoulEyez can configure passwordless sudo so these scans")
261
271
  console.print(" work automatically without running as root.")
262
272
  console.print()
@@ -267,9 +277,13 @@ def _configure_sudoers(console):
267
277
  console.print(f" • {tool_name} ({description})")
268
278
  console.print()
269
279
 
270
- if not click.confirm(" Configure passwordless sudo for these tools?", default=True):
280
+ if not click.confirm(
281
+ " Configure passwordless sudo for these tools?", default=True
282
+ ):
271
283
  console.print()
272
- console.print(" [yellow]Skipped.[/yellow] Privileged scans will require running as root.")
284
+ console.print(
285
+ " [yellow]Skipped.[/yellow] Privileged scans will require running as root."
286
+ )
273
287
  console.print(" Run 'souleyez setup --fix-permissions' later to configure.")
274
288
  return
275
289
 
@@ -287,7 +301,9 @@ def _configure_sudoers(console):
287
301
  proc = subprocess.run(cmd, shell=True, timeout=60) # nosec B602
288
302
 
289
303
  if proc.returncode == 0:
290
- console.print(f" [green]✓[/green] {tool_name} - configured for privileged scans")
304
+ console.print(
305
+ f" [green]✓[/green] {tool_name} - configured for privileged scans"
306
+ )
291
307
  else:
292
308
  console.print(f" [red]✗[/red] {tool_name} - failed to configure")
293
309
  except subprocess.TimeoutExpired:
@@ -306,7 +322,9 @@ def _configure_sudoers(console):
306
322
  proc = subprocess.run(cmd, shell=True, timeout=60) # nosec B602
307
323
 
308
324
  if proc.returncode == 0:
309
- console.print(f" [green]✓[/green] {tool_name} - configured for privileged scans")
325
+ console.print(
326
+ f" [green]✓[/green] {tool_name} - configured for privileged scans"
327
+ )
310
328
  else:
311
329
  console.print(f" [red]✗[/red] {tool_name} - failed to configure")
312
330
  except subprocess.TimeoutExpired:
@@ -322,15 +340,15 @@ def _ensure_msfdb_initialized(console):
322
340
  from pathlib import Path
323
341
 
324
342
  # Check if msfdb exists (either apt install or official installer)
325
- msfdb_path = shutil.which('msfdb')
343
+ msfdb_path = shutil.which("msfdb")
326
344
  if not msfdb_path:
327
345
  # Try the official installer path
328
- msfdb_path = '/opt/metasploit-framework/bin/msfdb'
346
+ msfdb_path = "/opt/metasploit-framework/bin/msfdb"
329
347
  if not Path(msfdb_path).exists():
330
348
  return # Metasploit not installed
331
349
 
332
350
  # Check if database is already initialized by looking for database.yml
333
- user_db_yml = Path.home() / '.msf4' / 'database.yml'
351
+ user_db_yml = Path.home() / ".msf4" / "database.yml"
334
352
  if user_db_yml.exists():
335
353
  return # Already initialized
336
354
 
@@ -344,7 +362,9 @@ def _ensure_msfdb_initialized(console):
344
362
 
345
363
  if not click.confirm(" Initialize MSF database now?", default=True):
346
364
  console.print()
347
- console.print(" [yellow]Skipped.[/yellow] Run 'sudo msfdb init' manually when needed.")
365
+ console.print(
366
+ " [yellow]Skipped.[/yellow] Run 'sudo msfdb init' manually when needed."
367
+ )
348
368
  return
349
369
 
350
370
  console.print()
@@ -353,30 +373,36 @@ def _ensure_msfdb_initialized(console):
353
373
  try:
354
374
  # Use sudo for msfdb init (required on Kali and some other distros)
355
375
  result = subprocess.run(
356
- ['sudo', msfdb_path, 'init'],
376
+ ["sudo", msfdb_path, "init"],
357
377
  capture_output=True,
358
- timeout=300 # 5 minute timeout
378
+ timeout=300, # 5 minute timeout
359
379
  )
360
380
 
361
381
  if result.returncode == 0:
362
382
  console.print(" [green]✓ MSF database initialized[/green]")
363
383
 
364
384
  # Copy database.yml to /root/.msf4/ so sudo msfconsole can connect
365
- user_db_yml = Path.home() / '.msf4' / 'database.yml'
385
+ user_db_yml = Path.home() / ".msf4" / "database.yml"
366
386
  if user_db_yml.exists():
367
387
  console.print(" [dim]Configuring sudo access to MSF database...[/dim]")
368
388
  try:
369
- subprocess.run(['sudo', 'mkdir', '-p', '/root/.msf4'], capture_output=True)
389
+ subprocess.run(
390
+ ["sudo", "mkdir", "-p", "/root/.msf4"], capture_output=True
391
+ )
370
392
  copy_result = subprocess.run(
371
- ['sudo', 'cp', str(user_db_yml), '/root/.msf4/database.yml'],
372
- capture_output=True
393
+ ["sudo", "cp", str(user_db_yml), "/root/.msf4/database.yml"],
394
+ capture_output=True,
373
395
  )
374
396
  if copy_result.returncode == 0:
375
397
  console.print(" [green]✓ Sudo MSF access configured[/green]")
376
398
  else:
377
- console.print(" [yellow]⚠ Run: sudo cp ~/.msf4/database.yml /root/.msf4/[/yellow]")
399
+ console.print(
400
+ " [yellow]⚠ Run: sudo cp ~/.msf4/database.yml /root/.msf4/[/yellow]"
401
+ )
378
402
  except Exception:
379
- console.print(" [yellow]⚠ Run: sudo cp ~/.msf4/database.yml /root/.msf4/[/yellow]")
403
+ console.print(
404
+ " [yellow]⚠ Run: sudo cp ~/.msf4/database.yml /root/.msf4/[/yellow]"
405
+ )
380
406
  else:
381
407
  stderr = result.stderr.decode() if result.stderr else ""
382
408
  if "already" in stderr.lower():
@@ -386,7 +412,9 @@ def _ensure_msfdb_initialized(console):
386
412
  except subprocess.TimeoutExpired:
387
413
  console.print(" [yellow]⚠ msfdb init timed out (run manually)[/yellow]")
388
414
  except Exception as e:
389
- console.print(f" [yellow]⚠ Could not init MSF database: {str(e)[:40]}[/yellow]")
415
+ console.print(
416
+ f" [yellow]⚠ Could not init MSF database: {str(e)[:40]}[/yellow]"
417
+ )
390
418
 
391
419
  console.print()
392
420
 
@@ -413,30 +441,42 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
413
441
  # Header
414
442
  DesignSystem.clear_screen()
415
443
  console.print()
416
- console.print("[bold cyan]╔══════════════════════════════════════════════════════════════╗[/bold cyan]")
417
- console.print("[bold cyan]║ SOULEYEZ TOOL SETUP WIZARD ║[/bold cyan]")
418
- console.print("[bold cyan]╚══════════════════════════════════════════════════════════════╝[/bold cyan]")
444
+ console.print(
445
+ "[bold cyan]╔══════════════════════════════════════════════════════════════╗[/bold cyan]"
446
+ )
447
+ console.print(
448
+ "[bold cyan]║ SOULEYEZ TOOL SETUP WIZARD ║[/bold cyan]"
449
+ )
450
+ console.print(
451
+ "[bold cyan]╚══════════════════════════════════════════════════════════════╝[/bold cyan]"
452
+ )
419
453
  console.print()
420
454
 
421
455
  # Distro detection
422
456
  distro_names = {
423
- 'kali': 'Kali Linux',
424
- 'parrot': 'Parrot OS',
425
- 'ubuntu': 'Ubuntu',
426
- 'debian': 'Debian',
427
- 'unknown': 'Unknown Linux'
457
+ "kali": "Kali Linux",
458
+ "parrot": "Parrot OS",
459
+ "ubuntu": "Ubuntu",
460
+ "debian": "Debian",
461
+ "unknown": "Unknown Linux",
428
462
  }
429
463
  console.print(f" Detected OS: [bold]{distro_names.get(distro, distro)}[/bold]")
430
464
  console.print()
431
465
 
432
466
  # Show distro-specific messaging
433
- if distro in ('kali', 'parrot'):
467
+ if distro in ("kali", "parrot"):
434
468
  console.print(" [green]✓ You're on a pentesting distro![/green]")
435
- console.print(" Most tools are available via apt. Some may use pipx or direct download.")
469
+ console.print(
470
+ " Most tools are available via apt. Some may use pipx or direct download."
471
+ )
436
472
  console.print()
437
- elif distro in ('ubuntu', 'debian'):
438
- console.print(" [yellow]Note:[/yellow] Some pentesting tools aren't in Ubuntu/Debian repos.")
439
- console.print(" This wizard will install them using pipx, go, snap, or from source.")
473
+ elif distro in ("ubuntu", "debian"):
474
+ console.print(
475
+ " [yellow]Note:[/yellow] Some pentesting tools aren't in Ubuntu/Debian repos."
476
+ )
477
+ console.print(
478
+ " This wizard will install them using pipx, go, snap, or from source."
479
+ )
440
480
  console.print()
441
481
 
442
482
  _show_tool_status(console)
@@ -465,13 +505,18 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
465
505
  _check_prerequisites(console, missing, distro)
466
506
 
467
507
  # Group tools by install method for smarter ordering
468
- apt_tools = [t for t in missing if t['install_method'] == 'apt']
469
- pipx_tools = [t for t in missing if 'pipx' in t['install']]
470
- go_tools = [t for t in missing if 'go install' in t['install']]
471
- gem_tools = [t for t in missing if 'gem install' in t['install']]
472
- snap_tools = [t for t in missing if 'snap install' in t['install']]
473
- git_tools = [t for t in missing if 'git clone' in t['install']]
474
- other_tools = [t for t in missing if t not in apt_tools + pipx_tools + go_tools + gem_tools + snap_tools + git_tools]
508
+ apt_tools = [t for t in missing if t["install_method"] == "apt"]
509
+ pipx_tools = [t for t in missing if "pipx" in t["install"]]
510
+ go_tools = [t for t in missing if "go install" in t["install"]]
511
+ gem_tools = [t for t in missing if "gem install" in t["install"]]
512
+ snap_tools = [t for t in missing if "snap install" in t["install"]]
513
+ git_tools = [t for t in missing if "git clone" in t["install"]]
514
+ other_tools = [
515
+ t
516
+ for t in missing
517
+ if t
518
+ not in apt_tools + pipx_tools + go_tools + gem_tools + snap_tools + git_tools
519
+ ]
475
520
 
476
521
  total_to_install = len(missing)
477
522
  installed_count = 0
@@ -485,7 +530,7 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
485
530
  if success:
486
531
  installed_count += len(apt_tools)
487
532
  else:
488
- failed_tools.extend([t['name'] for t in apt_tools])
533
+ failed_tools.extend([t["name"] for t in apt_tools])
489
534
 
490
535
  # Install pipx tools
491
536
  if pipx_tools:
@@ -495,7 +540,7 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
495
540
  if _install_pipx_tool(console, tool):
496
541
  installed_count += 1
497
542
  else:
498
- failed_tools.append(tool['name'])
543
+ failed_tools.append(tool["name"])
499
544
 
500
545
  # Install go tools
501
546
  if go_tools:
@@ -505,7 +550,7 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
505
550
  if _install_go_tool(console, tool):
506
551
  installed_count += 1
507
552
  else:
508
- failed_tools.append(tool['name'])
553
+ failed_tools.append(tool["name"])
509
554
 
510
555
  # Install gem tools
511
556
  if gem_tools:
@@ -515,7 +560,7 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
515
560
  if _install_gem_tool(console, tool):
516
561
  installed_count += 1
517
562
  else:
518
- failed_tools.append(tool['name'])
563
+ failed_tools.append(tool["name"])
519
564
 
520
565
  # Install snap tools
521
566
  if snap_tools:
@@ -525,7 +570,7 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
525
570
  if _install_snap_tool(console, tool):
526
571
  installed_count += 1
527
572
  else:
528
- failed_tools.append(tool['name'])
573
+ failed_tools.append(tool["name"])
529
574
 
530
575
  # Install git-based tools
531
576
  if git_tools:
@@ -535,7 +580,7 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
535
580
  if _install_git_tool(console, tool):
536
581
  installed_count += 1
537
582
  else:
538
- failed_tools.append(tool['name'])
583
+ failed_tools.append(tool["name"])
539
584
 
540
585
  # Install other tools (like metasploit)
541
586
  if other_tools:
@@ -545,7 +590,7 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
545
590
  if _install_other_tool(console, tool):
546
591
  installed_count += 1
547
592
  else:
548
- failed_tools.append(tool['name'])
593
+ failed_tools.append(tool["name"])
549
594
 
550
595
  # Configure PATH in shell rc files (bash and zsh)
551
596
  _add_paths_to_shell_rc()
@@ -557,7 +602,9 @@ def _run_tool_setup_impl(check_only: bool = False, install_all: bool = False):
557
602
  console.print()
558
603
 
559
604
  if failed_tools:
560
- console.print(f" [yellow]⚠ {len(failed_tools)} tools failed to install:[/yellow]")
605
+ console.print(
606
+ f" [yellow]⚠ {len(failed_tools)} tools failed to install:[/yellow]"
607
+ )
561
608
  for name in failed_tools:
562
609
  console.print(f" • {name}")
563
610
  console.print()
@@ -585,7 +632,7 @@ def _run_post_install_tasks(console, distro: str):
585
632
  console.print()
586
633
  console.print(" [yellow]Important:[/yellow] To use newly installed tools, either:")
587
634
  console.print(" 1. Restart your terminal, OR")
588
- if distro in ('kali', 'parrot'):
635
+ if distro in ("kali", "parrot"):
589
636
  console.print(" 2. Run: [cyan]source ~/.zshrc[/cyan] (Kali uses zsh)")
590
637
  else:
591
638
  console.print(" 2. Run: [cyan]source ~/.bashrc[/cyan]")
@@ -597,8 +644,12 @@ def _show_tool_status(console):
597
644
  tools_by_cat = get_tools_by_category()
598
645
  installed, total = get_tool_stats()
599
646
 
600
- status_color = "green" if installed == total else "yellow" if installed > 0 else "red"
601
- console.print(f" [bold]Tool Status:[/bold] [{status_color}]{installed}/{total} installed[/{status_color}]")
647
+ status_color = (
648
+ "green" if installed == total else "yellow" if installed > 0 else "red"
649
+ )
650
+ console.print(
651
+ f" [bold]Tool Status:[/bold] [{status_color}]{installed}/{total} installed[/{status_color}]"
652
+ )
602
653
  console.print()
603
654
 
604
655
  for category, tools in tools_by_cat.items():
@@ -606,12 +657,14 @@ def _show_tool_status(console):
606
657
  console.print(f" [bold]{cat_name}[/bold]")
607
658
 
608
659
  for tool in tools:
609
- if tool['installed']:
660
+ if tool["installed"]:
610
661
  status = "[green]✓[/green]"
611
662
  else:
612
663
  status = "[red]✗[/red]"
613
664
 
614
- console.print(f" {status} {tool['name']:<18} - {tool['description'][:40]}")
665
+ console.print(
666
+ f" {status} {tool['name']:<18} - {tool['description'][:40]}"
667
+ )
615
668
 
616
669
  console.print()
617
670
 
@@ -621,32 +674,41 @@ def _check_prerequisites(console, missing_tools: List[Dict], distro: str):
621
674
  needed_prereqs = set()
622
675
 
623
676
  for tool in missing_tools:
624
- install_cmd = tool['install']
625
- if 'pipx' in install_cmd:
626
- needed_prereqs.add('pipx')
677
+ install_cmd = tool["install"]
678
+ if "pipx" in install_cmd:
679
+ needed_prereqs.add("pipx")
627
680
  # pipx tools with native extensions need build dependencies
628
- needed_prereqs.add('build-deps')
629
- if 'go install' in install_cmd:
630
- needed_prereqs.add('golang')
631
- if 'gem install' in install_cmd:
632
- needed_prereqs.add('ruby')
633
- if 'snap install' in install_cmd:
634
- needed_prereqs.add('snap')
635
- if 'git clone' in install_cmd:
636
- needed_prereqs.add('git')
681
+ needed_prereqs.add("build-deps")
682
+ if "go install" in install_cmd:
683
+ needed_prereqs.add("golang")
684
+ if "gem install" in install_cmd:
685
+ needed_prereqs.add("ruby")
686
+ if "snap install" in install_cmd:
687
+ needed_prereqs.add("snap")
688
+ if "git clone" in install_cmd:
689
+ needed_prereqs.add("git")
637
690
 
638
691
  missing_prereqs = []
639
692
  for prereq in needed_prereqs:
640
693
  info = PREREQUISITES[prereq]
641
694
  # Check if always_install flag is set, or if tool check fails
642
- if info.get('always_install') or (info.get('check') and not check_tool(info['check'])):
695
+ if info.get("always_install") or (
696
+ info.get("check") and not check_tool(info["check"])
697
+ ):
643
698
  missing_prereqs.append((prereq, info))
644
699
 
645
700
  if not missing_prereqs:
646
701
  return
647
702
 
648
703
  # Sort to ensure build-deps comes first (needed before pipx installs)
649
- prereq_order = {'build-deps': 0, 'pipx': 1, 'golang': 2, 'ruby': 3, 'snap': 4, 'git': 5}
704
+ prereq_order = {
705
+ "build-deps": 0,
706
+ "pipx": 1,
707
+ "golang": 2,
708
+ "ruby": 3,
709
+ "snap": 4,
710
+ "git": 5,
711
+ }
650
712
  missing_prereqs.sort(key=lambda x: prereq_order.get(x[0], 99))
651
713
 
652
714
  console.print()
@@ -654,12 +716,12 @@ def _check_prerequisites(console, missing_tools: List[Dict], distro: str):
654
716
 
655
717
  for prereq, info in missing_prereqs:
656
718
  console.print(f" Installing {prereq}...", end=" ")
657
- success, _, stderr = _run_command(info['install'], console, capture=True)
719
+ success, _, stderr = _run_command(info["install"], console, capture=True)
658
720
  if success:
659
721
  console.print("[green]✓[/green]")
660
722
 
661
723
  # Run pipx ensurepath if we just installed pipx
662
- if prereq == 'pipx':
724
+ if prereq == "pipx":
663
725
  subprocess.run("pipx ensurepath", shell=True, capture_output=True)
664
726
  else:
665
727
  console.print(f"[red]✗[/red] {stderr[:50]}")
@@ -672,14 +734,14 @@ def _install_apt_tools(console, tools: List[Dict]) -> bool:
672
734
  """Install tools available via apt."""
673
735
  packages = []
674
736
  for tool in tools:
675
- cmd = tool['install']
676
- if 'apt install' in cmd:
737
+ cmd = tool["install"]
738
+ if "apt install" in cmd:
677
739
  # Extract package name
678
740
  parts = cmd.split()
679
741
  for i, part in enumerate(parts):
680
- if part == 'install' and i + 1 < len(parts):
742
+ if part == "install" and i + 1 < len(parts):
681
743
  pkg = parts[i + 1]
682
- if not pkg.startswith('-'):
744
+ if not pkg.startswith("-"):
683
745
  packages.append(pkg)
684
746
  break
685
747
 
@@ -700,8 +762,8 @@ def _install_apt_tools(console, tools: List[Dict]) -> bool:
700
762
 
701
763
  def _install_pipx_tool(console, tool: Dict) -> bool:
702
764
  """Install a tool using pipx."""
703
- name = tool['name']
704
- cmd = tool['install']
765
+ name = tool["name"]
766
+ cmd = tool["install"]
705
767
 
706
768
  console.print(f" {name}...", end=" ")
707
769
 
@@ -724,8 +786,8 @@ def _install_pipx_tool(console, tool: Dict) -> bool:
724
786
 
725
787
  def _install_go_tool(console, tool: Dict) -> bool:
726
788
  """Install a tool using go install."""
727
- name = tool['name']
728
- cmd = tool['install']
789
+ name = tool["name"]
790
+ cmd = tool["install"]
729
791
 
730
792
  console.print(f" {name}...", end=" ")
731
793
 
@@ -742,7 +804,7 @@ def _install_go_tool(console, tool: Dict) -> bool:
742
804
  capture_output=True,
743
805
  text=True,
744
806
  env=env,
745
- timeout=300 # 5 minute timeout for go installs
807
+ timeout=300, # 5 minute timeout for go installs
746
808
  )
747
809
  if result.returncode == 0:
748
810
  console.print("[green]✓[/green]")
@@ -759,8 +821,8 @@ def _install_go_tool(console, tool: Dict) -> bool:
759
821
 
760
822
  def _install_gem_tool(console, tool: Dict) -> bool:
761
823
  """Install a tool using gem."""
762
- name = tool['name']
763
- cmd = tool['install']
824
+ name = tool["name"]
825
+ cmd = tool["install"]
764
826
 
765
827
  console.print(f" {name}...", end=" ")
766
828
 
@@ -778,8 +840,8 @@ def _install_gem_tool(console, tool: Dict) -> bool:
778
840
 
779
841
  def _install_snap_tool(console, tool: Dict) -> bool:
780
842
  """Install a tool using snap."""
781
- name = tool['name']
782
- cmd = tool['install']
843
+ name = tool["name"]
844
+ cmd = tool["install"]
783
845
 
784
846
  console.print(f" {name}...", end=" ")
785
847
 
@@ -799,36 +861,52 @@ def _install_git_tool(console, tool: Dict) -> bool:
799
861
  """Install a tool from git (requires sudo for /opt)."""
800
862
  import re
801
863
 
802
- name = tool['name']
803
- cmd = tool['install']
864
+ name = tool["name"]
865
+ cmd = tool["install"]
804
866
 
805
867
  console.print(f" {name}...")
806
868
 
807
869
  # Parse the command to handle existing directories properly
808
- # Commands are typically: git clone <url> <dir> && pip install ... && ln -sf ...
809
- commands = [c.strip() for c in cmd.split('&&')]
870
+ # Commands are typically: prereq && git clone <url> <dir> && pip install ... && ln -sf ...
871
+ commands = [c.strip() for c in cmd.split("&&")]
810
872
 
811
873
  clone_cmd = None
874
+ pre_clone_cmds = []
812
875
  post_clone_cmds = []
813
876
  target_dir = None
814
877
 
815
878
  for i, c in enumerate(commands):
816
- if 'git clone' in c:
879
+ if "git clone" in c:
817
880
  clone_cmd = c
818
- post_clone_cmds = commands[i + 1:]
881
+ pre_clone_cmds = commands[:i] # Commands before git clone (e.g., cpan)
882
+ post_clone_cmds = commands[i + 1 :]
819
883
  # Extract target directory from clone command
820
884
  # Pattern: git clone <url> <directory>
821
- match = re.search(r'git clone\s+\S+\s+(\S+)', c)
885
+ match = re.search(r"git clone\s+\S+\s+(\S+)", c)
822
886
  if match:
823
887
  target_dir = match.group(1)
824
888
  break
825
889
 
826
890
  # If we found a git clone command and target directory
827
891
  if clone_cmd and target_dir:
892
+ # Run pre-clone commands first (e.g., installing dependencies like cpan)
893
+ for pre_cmd in pre_clone_cmds:
894
+ pre_cmd = pre_cmd.strip()
895
+ if not pre_cmd:
896
+ continue
897
+ console.print(f" [dim]Running: {pre_cmd[:50]}...[/dim]")
898
+ success, _, stderr = _run_command(pre_cmd, console, capture=True)
899
+ if not success:
900
+ console.print(f"[red]✗[/red]")
901
+ if stderr:
902
+ console.print(f" [dim]{stderr[:80]}[/dim]")
903
+ return False
828
904
  dir_exists = Path(target_dir).exists()
829
905
 
830
906
  if dir_exists:
831
- console.print(f" [dim]Directory {target_dir} exists, updating...[/dim]")
907
+ console.print(
908
+ f" [dim]Directory {target_dir} exists, updating...[/dim]"
909
+ )
832
910
  # Try to update with git pull
833
911
  pull_cmd = f"sudo git -C {target_dir} pull"
834
912
  success, _, stderr = _run_command(pull_cmd, console, capture=True)
@@ -846,7 +924,9 @@ def _install_git_tool(console, tool: Dict) -> bool:
846
924
  return False
847
925
  elif not success:
848
926
  # Pull failed for other reasons, try to continue anyway
849
- console.print(f" [yellow]⚠ git pull failed, continuing with existing files[/yellow]")
927
+ console.print(
928
+ f" [yellow]⚠ git pull failed, continuing with existing files[/yellow]"
929
+ )
850
930
  else:
851
931
  # Directory doesn't exist, run the clone
852
932
  success, _, stderr = _run_command(clone_cmd, console, capture=True)
@@ -886,23 +966,25 @@ def _install_git_tool(console, tool: Dict) -> bool:
886
966
 
887
967
  def _install_other_tool(console, tool: Dict) -> bool:
888
968
  """Install tools with custom installation methods (like metasploit)."""
889
- name = tool['name']
890
- cmd = tool['install']
969
+ name = tool["name"]
970
+ cmd = tool["install"]
891
971
 
892
972
  console.print(f" {name}...", end=" ")
893
973
 
894
974
  # Special handling for metasploit installer (takes a while)
895
- if 'msfinstall' in cmd:
975
+ if "msfinstall" in cmd:
896
976
  console.print()
897
- console.print(" [yellow]Installing Metasploit (this may take several minutes)...[/yellow]")
898
- console.print(" [dim]Installing postgresql and downloading Metasploit installer...[/dim]")
977
+ console.print(
978
+ " [yellow]Installing Metasploit (this may take several minutes)...[/yellow]"
979
+ )
980
+ console.print(
981
+ " [dim]Installing postgresql and downloading Metasploit installer...[/dim]"
982
+ )
899
983
 
900
984
  # Run the full install command (includes postgresql setup)
901
985
  try:
902
986
  result = subprocess.run(
903
- cmd,
904
- shell=True,
905
- timeout=1800 # 30 minute timeout for metasploit
987
+ cmd, shell=True, timeout=1800 # 30 minute timeout for metasploit
906
988
  )
907
989
  if result.returncode == 0:
908
990
  console.print(" [green]✓ Metasploit installed[/green]")
@@ -911,11 +993,11 @@ def _install_other_tool(console, tool: Dict) -> bool:
911
993
  console.print(" [dim]Initializing MSF database...[/dim]")
912
994
  try:
913
995
  # Use full path since /opt/metasploit-framework/bin isn't in PATH yet
914
- msfdb_path = '/opt/metasploit-framework/bin/msfdb'
996
+ msfdb_path = "/opt/metasploit-framework/bin/msfdb"
915
997
  init_result = subprocess.run(
916
- [msfdb_path, 'init'],
998
+ [msfdb_path, "init"],
917
999
  capture_output=True,
918
- timeout=300 # 5 minute timeout
1000
+ timeout=300, # 5 minute timeout
919
1001
  )
920
1002
  if init_result.returncode == 0:
921
1003
  console.print(" [green]✓ MSF database initialized[/green]")
@@ -924,30 +1006,48 @@ def _install_other_tool(console, tool: Dict) -> bool:
924
1006
  # msfdb init creates config in ~/.msf4/, but sudo looks in /root/.msf4/
925
1007
  import os
926
1008
  from pathlib import Path
927
- user_msf4 = Path.home() / '.msf4'
928
- user_db_yml = user_msf4 / 'database.yml'
1009
+
1010
+ user_msf4 = Path.home() / ".msf4"
1011
+ user_db_yml = user_msf4 / "database.yml"
929
1012
  if user_db_yml.exists():
930
- console.print(" [dim]Configuring sudo access to MSF database...[/dim]")
1013
+ console.print(
1014
+ " [dim]Configuring sudo access to MSF database...[/dim]"
1015
+ )
931
1016
  try:
932
1017
  # Create /root/.msf4/ directory and copy database.yml
933
1018
  copy_result = subprocess.run(
934
- ['sudo', 'mkdir', '-p', '/root/.msf4'],
935
- capture_output=True
1019
+ ["sudo", "mkdir", "-p", "/root/.msf4"],
1020
+ capture_output=True,
936
1021
  )
937
1022
  copy_result = subprocess.run(
938
- ['sudo', 'cp', str(user_db_yml), '/root/.msf4/database.yml'],
939
- capture_output=True
1023
+ [
1024
+ "sudo",
1025
+ "cp",
1026
+ str(user_db_yml),
1027
+ "/root/.msf4/database.yml",
1028
+ ],
1029
+ capture_output=True,
940
1030
  )
941
1031
  if copy_result.returncode == 0:
942
- console.print(" [green]✓ Sudo MSF access configured[/green]")
1032
+ console.print(
1033
+ " [green]✓ Sudo MSF access configured[/green]"
1034
+ )
943
1035
  else:
944
- console.print(" [yellow]⚠ Could not configure sudo access (run manually: sudo cp ~/.msf4/database.yml /root/.msf4/)[/yellow]")
1036
+ console.print(
1037
+ " [yellow]⚠ Could not configure sudo access (run manually: sudo cp ~/.msf4/database.yml /root/.msf4/)[/yellow]"
1038
+ )
945
1039
  except Exception as e:
946
- console.print(f" [yellow]⚠ Could not configure sudo access: {str(e)[:30]}[/yellow]")
1040
+ console.print(
1041
+ f" [yellow]⚠ Could not configure sudo access: {str(e)[:30]}[/yellow]"
1042
+ )
947
1043
  else:
948
- console.print(" [yellow]⚠ MSF database init returned non-zero (may already be initialized)[/yellow]")
1044
+ console.print(
1045
+ " [yellow]⚠ MSF database init returned non-zero (may already be initialized)[/yellow]"
1046
+ )
949
1047
  except Exception as e:
950
- console.print(f" [yellow]⚠ Could not init MSF database: {str(e)[:40]}[/yellow]")
1048
+ console.print(
1049
+ f" [yellow]⚠ Could not init MSF database: {str(e)[:40]}[/yellow]"
1050
+ )
951
1051
 
952
1052
  return True
953
1053
  else: