souleyez 2.43.29__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9564 -2881
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +564 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +409 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +417 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +913 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +219 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +237 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23034 -10679
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
  353. souleyez-3.0.0.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ from souleyez.ui.design_system import DesignSystem
14
14
 
15
15
 
16
16
  # Wizard state file
17
- WIZARD_STATE_FILE = Path.home() / '.souleyez' / '.wizard_completed'
17
+ WIZARD_STATE_FILE = Path.home() / ".souleyez" / ".wizard_completed"
18
18
 
19
19
 
20
20
  def is_wizard_completed() -> bool:
@@ -31,7 +31,7 @@ def mark_wizard_completed():
31
31
  def _check_disk_space(min_mb: int = 500) -> bool:
32
32
  """Check if there's enough disk space for installs."""
33
33
  try:
34
- usage = shutil.disk_usage('/')
34
+ usage = shutil.disk_usage("/")
35
35
  free_mb = usage.free // (1024 * 1024)
36
36
  return free_mb >= min_mb
37
37
  except Exception:
@@ -60,32 +60,36 @@ def _configure_sudoers(tool_name: str, tool_path: str) -> bool:
60
60
 
61
61
  try:
62
62
  # Write to temp file first
63
- with open(tmp_file, 'w') as f:
64
- f.write(sudoers_line + '\n')
63
+ with open(tmp_file, "w") as f:
64
+ f.write(sudoers_line + "\n")
65
65
 
66
66
  # Validate syntax with visudo before applying
67
67
  result = subprocess.run(
68
- ['sudo', 'visudo', '-c', '-f', tmp_file],
69
- capture_output=True,
70
- timeout=30
68
+ ["sudo", "visudo", "-c", "-f", tmp_file], capture_output=True, timeout=30
71
69
  )
72
70
 
73
71
  if result.returncode != 0:
74
- click.echo(f" {click.style('!', fg='yellow')} Invalid sudoers syntax for {tool_name}, skipping")
72
+ click.echo(
73
+ f" {click.style('!', fg='yellow')} Invalid sudoers syntax for {tool_name}, skipping"
74
+ )
75
75
  os.unlink(tmp_file)
76
76
  return False
77
77
 
78
78
  # Safe to move to sudoers.d
79
- subprocess.run(['sudo', 'mv', tmp_file, sudoers_file], check=True, timeout=30)
80
- subprocess.run(['sudo', 'chmod', '0440', sudoers_file], check=True, timeout=30)
79
+ subprocess.run(["sudo", "mv", tmp_file, sudoers_file], check=True, timeout=30)
80
+ subprocess.run(["sudo", "chmod", "0440", sudoers_file], check=True, timeout=30)
81
81
 
82
82
  return True
83
83
 
84
84
  except subprocess.TimeoutExpired:
85
- click.echo(f" {click.style('!', fg='yellow')} Timeout configuring sudoers for {tool_name}")
85
+ click.echo(
86
+ f" {click.style('!', fg='yellow')} Timeout configuring sudoers for {tool_name}"
87
+ )
86
88
  return False
87
89
  except Exception as e:
88
- click.echo(f" {click.style('!', fg='yellow')} Error configuring sudoers for {tool_name}: {e}")
90
+ click.echo(
91
+ f" {click.style('!', fg='yellow')} Error configuring sudoers for {tool_name}: {e}"
92
+ )
89
93
  # Clean up temp file if it exists
90
94
  if os.path.exists(tmp_file):
91
95
  try:
@@ -103,10 +107,10 @@ def _install_desktop_shortcut():
103
107
  disrupt the setup flow.
104
108
  """
105
109
  try:
106
- applications_dir = Path.home() / '.local' / 'share' / 'applications'
107
- icons_dir = Path.home() / '.local' / 'share' / 'icons'
108
- desktop_file = applications_dir / 'souleyez.desktop'
109
- icon_dest = icons_dir / 'souleyez.png'
110
+ applications_dir = Path.home() / ".local" / "share" / "applications"
111
+ icons_dir = Path.home() / ".local" / "share" / "icons"
112
+ desktop_file = applications_dir / "souleyez.desktop"
113
+ icon_dest = icons_dir / "souleyez.png"
110
114
 
111
115
  # Skip if already installed
112
116
  if desktop_file.exists():
@@ -121,16 +125,19 @@ def _install_desktop_shortcut():
121
125
  # Try importlib.resources first (Python 3.9+)
122
126
  try:
123
127
  from importlib.resources import files
124
- icon_source = files('souleyez.assets').joinpath('souleyez-icon.png')
125
- with open(icon_source, 'rb') as src:
128
+
129
+ icon_source = files("souleyez.assets").joinpath("souleyez-icon.png")
130
+ with open(icon_source, "rb") as src:
126
131
  icon_data = src.read()
127
132
  except (ImportError, TypeError, FileNotFoundError):
128
133
  # Fallback: find icon relative to this file
129
- icon_source = Path(__file__).parent.parent / 'assets' / 'souleyez-icon.png'
130
- with open(icon_source, 'rb') as src:
134
+ icon_source = (
135
+ Path(__file__).parent.parent / "assets" / "souleyez-icon.png"
136
+ )
137
+ with open(icon_source, "rb") as src:
131
138
  icon_data = src.read()
132
139
 
133
- with open(icon_dest, 'wb') as dst:
140
+ with open(icon_dest, "wb") as dst:
134
141
  dst.write(icon_data)
135
142
  except Exception:
136
143
  icon_dest = "utilities-terminal" # Fallback to system icon
@@ -151,12 +158,18 @@ Keywords=pentest;security;hacking;nmap;metasploit;
151
158
 
152
159
  # Update desktop database (optional)
153
160
  try:
154
- subprocess.run(['update-desktop-database', str(applications_dir)],
155
- capture_output=True, check=False, timeout=5)
161
+ subprocess.run(
162
+ ["update-desktop-database", str(applications_dir)],
163
+ capture_output=True,
164
+ check=False,
165
+ timeout=5,
166
+ )
156
167
  except Exception:
157
168
  pass
158
169
 
159
- click.echo(f" {click.style('✓', fg='green')} Desktop shortcut: Added to Applications menu")
170
+ click.echo(
171
+ f" {click.style('✓', fg='green')} Desktop shortcut: Added to Applications menu"
172
+ )
160
173
 
161
174
  except Exception:
162
175
  # Silently ignore errors - desktop shortcut is nice-to-have
@@ -164,9 +177,7 @@ Keywords=pentest;security;hacking;nmap;metasploit;
164
177
 
165
178
 
166
179
  def _run_tool_installs(
167
- missing_tools: List[Dict],
168
- wrong_version_tools: List[Dict],
169
- distro: str
180
+ missing_tools: List[Dict], wrong_version_tools: List[Dict], distro: str
170
181
  ) -> bool:
171
182
  """
172
183
  Prompt user and run install/upgrade commands.
@@ -186,25 +197,29 @@ def _run_tool_installs(
186
197
  # Add wrong version tools (upgrades)
187
198
  for tool in wrong_version_tools:
188
199
  # Try to get upgrade command, fall back to install command
189
- tool_info = tool.get('tool_info', {})
200
+ tool_info = tool.get("tool_info", {})
190
201
  upgrade_cmd = get_upgrade_command(tool_info, distro) if tool_info else None
191
- install_cmd = upgrade_cmd or tool.get('install', '')
192
-
193
- all_installs.append({
194
- 'name': tool['name'],
195
- 'cmd': install_cmd,
196
- 'action': 'upgrade',
197
- 'tool_info': tool_info
198
- })
202
+ install_cmd = upgrade_cmd or tool.get("install", "")
203
+
204
+ all_installs.append(
205
+ {
206
+ "name": tool["name"],
207
+ "cmd": install_cmd,
208
+ "action": "upgrade",
209
+ "tool_info": tool_info,
210
+ }
211
+ )
199
212
 
200
213
  # Add missing tools (installs)
201
214
  for tool in missing_tools:
202
- all_installs.append({
203
- 'name': tool['name'],
204
- 'cmd': tool['install'],
205
- 'action': 'install',
206
- 'tool_info': tool.get('tool_info', {})
207
- })
215
+ all_installs.append(
216
+ {
217
+ "name": tool["name"],
218
+ "cmd": tool["install"],
219
+ "action": "install",
220
+ "tool_info": tool.get("tool_info", {}),
221
+ }
222
+ )
208
223
 
209
224
  if not all_installs:
210
225
  return False
@@ -212,7 +227,9 @@ def _run_tool_installs(
212
227
  # Check disk space
213
228
  if not _check_disk_space(500):
214
229
  click.echo()
215
- click.echo(f" {click.style('!', fg='yellow')} Low disk space (<500MB free). Installs may fail.")
230
+ click.echo(
231
+ f" {click.style('!', fg='yellow')} Low disk space (<500MB free). Installs may fail."
232
+ )
216
233
  if not click.confirm(" Continue anyway?", default=False):
217
234
  return False
218
235
 
@@ -220,8 +237,10 @@ def _run_tool_installs(
220
237
  click.echo()
221
238
  click.echo(f" {len(all_installs)} tool(s) to install/upgrade:")
222
239
  for item in all_installs:
223
- action_color = 'cyan' if item['action'] == 'upgrade' else 'green'
224
- click.echo(f" - {item['name']} ({click.style(item['action'], fg=action_color)})")
240
+ action_color = "cyan" if item["action"] == "upgrade" else "green"
241
+ click.echo(
242
+ f" - {item['name']} ({click.style(item['action'], fg=action_color)})"
243
+ )
225
244
 
226
245
  click.echo()
227
246
  if not click.confirm(" Install/upgrade now?", default=False):
@@ -231,19 +250,23 @@ def _run_tool_installs(
231
250
  # Request sudo upfront
232
251
  click.echo()
233
252
  click.echo(" Requesting sudo access...")
234
- result = subprocess.run(['sudo', '-v'], check=False)
253
+ result = subprocess.run(["sudo", "-v"], check=False)
235
254
  if result.returncode != 0:
236
- click.echo(f" {click.style('x', fg='red')} Could not obtain sudo. Aborting installs.")
255
+ click.echo(
256
+ f" {click.style('x', fg='red')} Could not obtain sudo. Aborting installs."
257
+ )
237
258
  return False
238
259
 
239
260
  # Update apt cache first (for apt-based installs)
240
- has_apt_installs = any('apt' in item['cmd'] for item in all_installs)
261
+ has_apt_installs = any("apt" in item["cmd"] for item in all_installs)
241
262
  if has_apt_installs:
242
263
  click.echo()
243
264
  click.echo(" Updating package lists...")
244
- result = subprocess.run(['sudo', 'apt', 'update'], capture_output=True)
265
+ result = subprocess.run(["sudo", "apt", "update"], capture_output=True)
245
266
  if result.returncode != 0:
246
- click.echo(f" {click.style('!', fg='yellow')} apt update had issues, continuing anyway...")
267
+ click.echo(
268
+ f" {click.style('!', fg='yellow')} apt update had issues, continuing anyway..."
269
+ )
247
270
 
248
271
  # Track results for summary
249
272
  results = []
@@ -251,29 +274,51 @@ def _run_tool_installs(
251
274
  # Run each install command
252
275
  for item in all_installs:
253
276
  click.echo()
254
- click.echo(f" {click.style(item['action'].capitalize() + 'ing', fg='cyan')} {item['name']}...")
277
+ click.echo(
278
+ f" {click.style(item['action'].capitalize() + 'ing', fg='cyan')} {item['name']}..."
279
+ )
255
280
 
256
281
  try:
257
282
  # Run install command
258
283
  proc = subprocess.run(
259
- item['cmd'],
284
+ item["cmd"],
260
285
  shell=True, # nosec B602 - commands from trusted EXTERNAL_TOOLS
261
- timeout=600 # 10 minute timeout per tool
286
+ timeout=600, # 10 minute timeout per tool
262
287
  )
263
288
 
264
289
  if proc.returncode == 0:
265
- click.echo(f" {click.style('✓', fg='green')} {item['name']} {item['action']} complete")
266
- results.append({'name': item['name'], 'success': True, 'tool_info': item['tool_info']})
290
+ click.echo(
291
+ f" {click.style('', fg='green')} {item['name']} {item['action']} complete"
292
+ )
293
+ results.append(
294
+ {
295
+ "name": item["name"],
296
+ "success": True,
297
+ "tool_info": item["tool_info"],
298
+ }
299
+ )
267
300
  else:
268
- click.echo(f" {click.style('✗', fg='red')} {item['name']} failed (exit {proc.returncode})")
269
- results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
301
+ click.echo(
302
+ f" {click.style('', fg='red')} {item['name']} failed (exit {proc.returncode})"
303
+ )
304
+ results.append(
305
+ {
306
+ "name": item["name"],
307
+ "success": False,
308
+ "tool_info": item["tool_info"],
309
+ }
310
+ )
270
311
 
271
312
  except subprocess.TimeoutExpired:
272
313
  click.echo(f" {click.style('✗', fg='red')} {item['name']} timed out")
273
- results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
314
+ results.append(
315
+ {"name": item["name"], "success": False, "tool_info": item["tool_info"]}
316
+ )
274
317
  except Exception as e:
275
318
  click.echo(f" {click.style('✗', fg='red')} {item['name']} error: {e}")
276
- results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
319
+ results.append(
320
+ {"name": item["name"], "success": False, "tool_info": item["tool_info"]}
321
+ )
277
322
 
278
323
  # Configure sudoers for privileged tools that installed successfully
279
324
  click.echo()
@@ -281,32 +326,38 @@ def _run_tool_installs(
281
326
 
282
327
  sudoers_configured = False
283
328
  for res in results:
284
- if not res['success']:
329
+ if not res["success"]:
285
330
  continue
286
331
 
287
- tool_info = res['tool_info']
288
- if not tool_info.get('needs_sudo'):
332
+ tool_info = res["tool_info"]
333
+ if not tool_info.get("needs_sudo"):
289
334
  continue
290
335
 
291
336
  sudoers_configured = True
292
337
  # Find the actual binary path
293
- command = tool_info.get('command', res['name'])
338
+ command = tool_info.get("command", res["name"])
294
339
  tool_path = shutil.which(command)
295
340
 
296
341
  # Check alt_commands if primary not found
297
- if not tool_path and tool_info.get('alt_commands'):
298
- for alt in tool_info['alt_commands']:
342
+ if not tool_path and tool_info.get("alt_commands"):
343
+ for alt in tool_info["alt_commands"]:
299
344
  tool_path = shutil.which(alt)
300
345
  if tool_path:
301
346
  break
302
347
 
303
348
  if tool_path:
304
- if _configure_sudoers(res['name'], tool_path):
305
- click.echo(f" {click.style('✓', fg='green')} {res['name']} configured for passwordless sudo")
349
+ if _configure_sudoers(res["name"], tool_path):
350
+ click.echo(
351
+ f" {click.style('✓', fg='green')} {res['name']} configured for passwordless sudo"
352
+ )
306
353
  else:
307
- click.echo(f" {click.style('!', fg='yellow')} {res['name']} sudoers config failed")
354
+ click.echo(
355
+ f" {click.style('!', fg='yellow')} {res['name']} sudoers config failed"
356
+ )
308
357
  else:
309
- click.echo(f" {click.style('!', fg='yellow')} {res['name']} binary not found, skipping sudoers")
358
+ click.echo(
359
+ f" {click.style('!', fg='yellow')} {res['name']} binary not found, skipping sudoers"
360
+ )
310
361
 
311
362
  if not sudoers_configured:
312
363
  click.echo(" No privileged tools needed configuration.")
@@ -319,9 +370,9 @@ def _run_tool_installs(
319
370
  fail_count = 0
320
371
 
321
372
  for res in results:
322
- tool_info = res['tool_info']
323
- command = tool_info.get('command', res['name'])
324
- alt_commands = tool_info.get('alt_commands')
373
+ tool_info = res["tool_info"]
374
+ command = tool_info.get("command", res["name"])
375
+ alt_commands = tool_info.get("alt_commands")
325
376
 
326
377
  # Check if tool is now available
327
378
  found = shutil.which(command) is not None
@@ -335,15 +386,21 @@ def _run_tool_installs(
335
386
  click.echo(f" {click.style('✓', fg='green')} {res['name']} verified")
336
387
  success_count += 1
337
388
  else:
338
- click.echo(f" {click.style('✗', fg='red')} {res['name']} not found after install")
389
+ click.echo(
390
+ f" {click.style('✗', fg='red')} {res['name']} not found after install"
391
+ )
339
392
  fail_count += 1
340
393
 
341
394
  # Summary
342
395
  click.echo()
343
396
  if fail_count == 0:
344
- click.echo(f" {click.style('✓', fg='green')} All {success_count} tool(s) installed successfully!")
397
+ click.echo(
398
+ f" {click.style('✓', fg='green')} All {success_count} tool(s) installed successfully!"
399
+ )
345
400
  else:
346
- click.echo(f" {click.style('!', fg='yellow')} {success_count} succeeded, {fail_count} failed")
401
+ click.echo(
402
+ f" {click.style('!', fg='yellow')} {success_count} succeeded, {fail_count} failed"
403
+ )
347
404
  click.echo(" Failed tools may need manual installation.")
348
405
 
349
406
  return True
@@ -352,14 +409,56 @@ def _run_tool_installs(
352
409
  def _show_wizard_banner():
353
410
  """Display the SoulEyez ASCII banner for wizard steps."""
354
411
  click.echo()
355
- click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▄██▄", fg='bright_blue', bold=True))
356
- click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True) + click.style(" ▄█▀ ▀█▄", fg='bright_blue', bold=True))
357
- click.echo(click.style(" ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" ◉ █", fg='bright_blue', bold=True))
358
- click.echo(click.style(" ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" █ ═══ █", fg='bright_blue', bold=True))
359
- click.echo(click.style(" ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▀█▄ ▄█▀", fg='bright_blue', bold=True))
360
- click.echo(click.style(" ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝", fg='bright_cyan', bold=True) + click.style(" ▀██▀", fg='bright_blue', bold=True))
412
+ click.echo(
413
+ click.style(
414
+ " ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗",
415
+ fg="bright_cyan",
416
+ bold=True,
417
+ )
418
+ + click.style(" ▄██▄", fg="bright_blue", bold=True)
419
+ )
420
+ click.echo(
421
+ click.style(
422
+ " ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝",
423
+ fg="bright_cyan",
424
+ bold=True,
425
+ )
426
+ + click.style(" ▄█▀ ▀█▄", fg="bright_blue", bold=True)
427
+ )
428
+ click.echo(
429
+ click.style(
430
+ " ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ",
431
+ fg="bright_cyan",
432
+ bold=True,
433
+ )
434
+ + click.style(" █ ◉ █", fg="bright_blue", bold=True)
435
+ )
436
+ click.echo(
437
+ click.style(
438
+ " ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ",
439
+ fg="bright_cyan",
440
+ bold=True,
441
+ )
442
+ + click.style(" █ ═══ █", fg="bright_blue", bold=True)
443
+ )
444
+ click.echo(
445
+ click.style(
446
+ " ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗",
447
+ fg="bright_cyan",
448
+ bold=True,
449
+ )
450
+ + click.style(" ▀█▄ ▄█▀", fg="bright_blue", bold=True)
451
+ )
452
+ click.echo(
453
+ click.style(
454
+ " ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝",
455
+ fg="bright_cyan",
456
+ bold=True,
457
+ )
458
+ + click.style(" ▀██▀", fg="bright_blue", bold=True)
459
+ )
361
460
  click.echo()
362
- click.echo(click.style(" Created by CyberSoul SecurITy", fg='bright_blue'))
461
+ click.echo(click.style(" Created by CyberSoul SecurITy", fg="bright_blue"))
363
462
  click.echo()
364
463
 
365
464
 
@@ -373,6 +472,7 @@ def run_setup_wizard() -> bool:
373
472
  try:
374
473
  # Check if user has Pro tier
375
474
  from souleyez.auth import get_current_user, Tier
475
+
376
476
  user = get_current_user()
377
477
  is_pro = user and user.tier == Tier.PRO
378
478
 
@@ -399,11 +499,11 @@ def run_setup_wizard() -> bool:
399
499
  if is_pro:
400
500
  automation_prefs = _wizard_automation_prefs()
401
501
  else:
402
- automation_prefs = {'enabled': False, 'mode': None}
502
+ automation_prefs = {"enabled": False, "mode": None}
403
503
 
404
504
  # Step 7: Deliverable Templates (Pro only)
405
505
  if is_pro:
406
- templates = _wizard_deliverables(engagement_info.get('type'))
506
+ templates = _wizard_deliverables(engagement_info.get("type"))
407
507
  else:
408
508
  templates = []
409
509
 
@@ -415,14 +515,14 @@ def run_setup_wizard() -> bool:
415
515
  ai_enabled,
416
516
  automation_prefs,
417
517
  templates,
418
- is_pro
518
+ is_pro,
419
519
  )
420
520
 
421
521
  mark_wizard_completed()
422
522
  return True
423
523
 
424
524
  except (KeyboardInterrupt, click.Abort):
425
- click.echo(click.style("\n\n Setup wizard cancelled.", fg='yellow'))
525
+ click.echo(click.style("\n\n Setup wizard cancelled.", fg="yellow"))
426
526
  mark_wizard_completed() # Don't show wizard again
427
527
  click.pause()
428
528
  return False
@@ -435,40 +535,100 @@ def _wizard_welcome(is_pro: bool = False) -> bool:
435
535
 
436
536
  # Header
437
537
  click.echo("\n┌" + "─" * (width - 2) + "┐")
438
- click.echo("│" + click.style(" WELCOME TO SOULEYEZ - SETUP WIZARD ".center(width - 2), bold=True, fg='cyan') + "│")
538
+ click.echo(
539
+ "│"
540
+ + click.style(
541
+ " WELCOME TO SOULEYEZ - SETUP WIZARD ".center(width - 2),
542
+ bold=True,
543
+ fg="cyan",
544
+ )
545
+ + "│"
546
+ )
439
547
  click.echo("└" + "─" * (width - 2) + "┘")
440
548
  click.echo()
441
549
 
442
550
  # ASCII Art Banner - SOULEYEZ with all-seeing eye on the right
443
- click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▄██▄", fg='bright_blue', bold=True))
444
- click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True) + click.style(" ▄█▀ ▀█▄", fg='bright_blue', bold=True))
445
- click.echo(click.style(" ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" ◉ █", fg='bright_blue', bold=True))
446
- click.echo(click.style(" ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" █ ═══ █", fg='bright_blue', bold=True))
447
- click.echo(click.style(" ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▀█▄ ▄█▀", fg='bright_blue', bold=True))
448
- click.echo(click.style(" ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝", fg='bright_cyan', bold=True) + click.style(" ▀██▀", fg='bright_blue', bold=True))
551
+ click.echo(
552
+ click.style(
553
+ " ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗",
554
+ fg="bright_cyan",
555
+ bold=True,
556
+ )
557
+ + click.style(" ▄██▄", fg="bright_blue", bold=True)
558
+ )
559
+ click.echo(
560
+ click.style(
561
+ " ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝",
562
+ fg="bright_cyan",
563
+ bold=True,
564
+ )
565
+ + click.style(" ▄█▀ ▀█▄", fg="bright_blue", bold=True)
566
+ )
567
+ click.echo(
568
+ click.style(
569
+ " ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ",
570
+ fg="bright_cyan",
571
+ bold=True,
572
+ )
573
+ + click.style(" █ ◉ █", fg="bright_blue", bold=True)
574
+ )
575
+ click.echo(
576
+ click.style(
577
+ " ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ",
578
+ fg="bright_cyan",
579
+ bold=True,
580
+ )
581
+ + click.style(" █ ═══ █", fg="bright_blue", bold=True)
582
+ )
583
+ click.echo(
584
+ click.style(
585
+ " ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗",
586
+ fg="bright_cyan",
587
+ bold=True,
588
+ )
589
+ + click.style(" ▀█▄ ▄█▀", fg="bright_blue", bold=True)
590
+ )
591
+ click.echo(
592
+ click.style(
593
+ " ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝",
594
+ fg="bright_cyan",
595
+ bold=True,
596
+ )
597
+ + click.style(" ▀██▀", fg="bright_blue", bold=True)
598
+ )
449
599
  click.echo()
450
600
 
451
601
  # Tagline and description
452
- click.echo(click.style(" Created by CyberSoul SecurITy", fg='bright_blue'))
602
+ click.echo(click.style(" Created by CyberSoul SecurITy", fg="bright_blue"))
453
603
  click.echo()
454
- click.echo(" SoulEyez brings your hacking tools together so you can spend less time switching windows")
455
- click.echo(" and more time breaking things (ethically, of course). Launch scans with Nmap, Metasploit,")
456
- click.echo(" Gobuster, theHarvester, and many more. Manage engagements, review findings, generate reports,")
604
+ click.echo(
605
+ " SoulEyez brings your hacking tools together so you can spend less time switching windows"
606
+ )
607
+ click.echo(
608
+ " and more time breaking things (ethically, of course). Launch scans with Nmap, Metasploit,"
609
+ )
610
+ click.echo(
611
+ " Gobuster, theHarvester, and many more. Manage engagements, review findings, generate reports,"
612
+ )
457
613
  click.echo(" and let AI recommend your next moves — all in one place.")
458
614
  click.echo()
459
- click.echo(click.style(" SETUP WIZARD", bold=True, fg='cyan'))
615
+ click.echo(click.style(" SETUP WIZARD", bold=True, fg="cyan"))
460
616
  click.echo(" ─────────────")
461
617
  click.echo()
462
618
  click.echo(" This wizard will help you get started:")
463
- click.echo(" " + click.style("✓", fg='green') + " Set up encryption for credentials")
464
- click.echo(" " + click.style("✓", fg='green') + " Create your first engagement")
465
- click.echo(" " + click.style("✓", fg='green') + " Check installed tools")
466
- click.echo(" " + click.style("✓", fg='green') + " Set up AI features (optional)")
619
+ click.echo(
620
+ " " + click.style("✓", fg="green") + " Set up encryption for credentials"
621
+ )
622
+ click.echo(" " + click.style("✓", fg="green") + " Create your first engagement")
623
+ click.echo(" " + click.style("✓", fg="green") + " Check installed tools")
624
+ click.echo(" " + click.style("✓", fg="green") + " Set up AI features (optional)")
467
625
 
468
626
  # Only show Pro steps if user has Pro tier
469
627
  if is_pro:
470
- click.echo(" " + click.style("✓", fg='green') + " Configure automation preferences")
471
- click.echo(" " + click.style("✓", fg='green') + " Select report templates")
628
+ click.echo(
629
+ " " + click.style("✓", fg="green") + " Configure automation preferences"
630
+ )
631
+ click.echo(" " + click.style("✓", fg="green") + " Select report templates")
472
632
 
473
633
  click.echo()
474
634
  click.pause(" Press ENTER to continue...")
@@ -485,38 +645,54 @@ def _wizard_encryption_setup() -> bool:
485
645
  width = 60
486
646
 
487
647
  click.echo("┌" + "─" * (width - 2) + "┐")
488
- click.echo("│" + click.style(" STEP 2: ENCRYPTION SETUP ".center(width - 2), bold=True, fg='cyan') + "│")
648
+ click.echo(
649
+ "│"
650
+ + click.style(
651
+ " STEP 2: ENCRYPTION SETUP ".center(width - 2), bold=True, fg="cyan"
652
+ )
653
+ + "│"
654
+ )
489
655
  click.echo("└" + "─" * (width - 2) + "┘")
490
656
  click.echo()
491
657
 
492
658
  crypto = CryptoManager()
493
659
 
494
660
  if crypto.is_encryption_enabled():
495
- click.echo(" " + click.style("✓ Encryption already configured", fg='green'))
661
+ click.echo(" " + click.style("✓ Encryption already configured", fg="green"))
496
662
  click.echo()
497
663
  click.pause(" Press any key to continue...")
498
664
  return True
499
665
 
500
666
  click.echo(" SoulEyez encrypts all credentials (passwords, API keys, tokens)")
501
- click.echo(" with a master password. This is " + click.style("required", fg='green', bold=True) + " for security.")
667
+ click.echo(
668
+ " with a master password. This is "
669
+ + click.style("required", fg="green", bold=True)
670
+ + " for security."
671
+ )
502
672
  click.echo()
503
- click.echo(" " + click.style("[y]", fg='green') + " Enable encryption (recommended)")
504
- click.echo(" " + click.style("[n]", fg='bright_black') + " Skip encryption")
673
+ click.echo(
674
+ " " + click.style("[y]", fg="green") + " Enable encryption (recommended)"
675
+ )
676
+ click.echo(" " + click.style("[n]", fg="bright_black") + " Skip encryption")
505
677
  click.echo()
506
678
 
507
- choice = click.prompt(" Enable encryption? [y/n]", default='y').strip().lower()
679
+ choice = click.prompt(" Enable encryption? [y/n]", default="y").strip().lower()
508
680
 
509
- if choice in ('n', 's', 'no', 'skip'):
681
+ if choice in ("n", "s", "no", "skip"):
510
682
  click.echo()
511
683
  time.sleep(0.5)
512
- click.echo(" " + click.style("SIKE! 😏", fg='magenta', bold=True))
684
+ click.echo(" " + click.style("SIKE! 😏", fg="magenta", bold=True))
513
685
  time.sleep(0.8)
514
686
  click.echo()
515
- click.echo(" " + click.style("Security is not optional.", fg='cyan', bold=True))
687
+ click.echo(
688
+ " " + click.style("Security is not optional.", fg="cyan", bold=True)
689
+ )
516
690
  click.echo(" " + "We're a security company. Encryption is mandatory.")
517
691
  click.echo()
518
692
  time.sleep(1)
519
- click.echo(" " + click.style("Let's set up your vault password...", fg='green'))
693
+ click.echo(
694
+ " " + click.style("Let's set up your vault password...", fg="green")
695
+ )
520
696
  click.pause(" Press any key to continue...")
521
697
 
522
698
  click.echo()
@@ -526,39 +702,63 @@ def _wizard_encryption_setup() -> bool:
526
702
  click.echo(" • At least one number")
527
703
  click.echo(" • At least one special character (!@#$%^&*)")
528
704
  click.echo()
529
- click.echo(" " + click.style("⚠️ If you lose this password, encrypted credentials cannot be recovered!",
530
- fg='yellow', bold=True))
705
+ click.echo(
706
+ " "
707
+ + click.style(
708
+ "⚠️ If you lose this password, encrypted credentials cannot be recovered!",
709
+ fg="yellow",
710
+ bold=True,
711
+ )
712
+ )
531
713
  click.echo()
532
714
 
533
715
  import re
716
+
534
717
  password_set = False
535
718
  while not password_set:
536
719
  password = getpass.getpass(" Enter master password: ")
537
720
 
538
721
  # Validate password strength
539
722
  if len(password) < 12:
540
- click.echo(click.style(" ✗ Password must be at least 12 characters.", fg='red'))
723
+ click.echo(
724
+ click.style(" ✗ Password must be at least 12 characters.", fg="red")
725
+ )
541
726
  continue
542
727
 
543
- if not re.search(r'[a-z]', password):
544
- click.echo(click.style(" ✗ Password must contain at least one lowercase letter.", fg='red'))
728
+ if not re.search(r"[a-z]", password):
729
+ click.echo(
730
+ click.style(
731
+ " ✗ Password must contain at least one lowercase letter.", fg="red"
732
+ )
733
+ )
545
734
  continue
546
735
 
547
- if not re.search(r'[A-Z]', password):
548
- click.echo(click.style(" ✗ Password must contain at least one uppercase letter.", fg='red'))
736
+ if not re.search(r"[A-Z]", password):
737
+ click.echo(
738
+ click.style(
739
+ " ✗ Password must contain at least one uppercase letter.", fg="red"
740
+ )
741
+ )
549
742
  continue
550
743
 
551
- if not re.search(r'\d', password):
552
- click.echo(click.style(" ✗ Password must contain at least one number.", fg='red'))
744
+ if not re.search(r"\d", password):
745
+ click.echo(
746
+ click.style(" ✗ Password must contain at least one number.", fg="red")
747
+ )
553
748
  continue
554
749
 
555
750
  if not re.search(r'[!@#$%^&*()_+\-=\[\]{};:\'",.<>?/\\|`~]', password):
556
- click.echo(click.style(" ✗ Password must contain at least one special character.", fg='red'))
751
+ click.echo(
752
+ click.style(
753
+ " ✗ Password must contain at least one special character.",
754
+ fg="red",
755
+ )
756
+ )
557
757
  continue
558
758
 
559
759
  password_confirm = getpass.getpass(" Confirm master password: ")
560
760
  if password != password_confirm:
561
- click.echo(click.style(" ✗ Passwords don't match!", fg='red'))
761
+ click.echo(click.style(" ✗ Passwords don't match!", fg="red"))
562
762
  continue
563
763
 
564
764
  password_set = True
@@ -568,13 +768,13 @@ def _wizard_encryption_setup() -> bool:
568
768
 
569
769
  try:
570
770
  if crypto.enable_encryption(password):
571
- click.echo(" " + click.style("✓ Encryption enabled!", fg='green'))
771
+ click.echo(" " + click.style("✓ Encryption enabled!", fg="green"))
572
772
  else:
573
- click.echo(" " + click.style("✗ Failed to enable encryption!", fg='red'))
773
+ click.echo(" " + click.style("✗ Failed to enable encryption!", fg="red"))
574
774
  click.pause(" Press any key to continue...")
575
775
  return False
576
776
  except Exception as e:
577
- click.echo(" " + click.style(f"✗ Error: {e}", fg='red'))
777
+ click.echo(" " + click.style(f"✗ Error: {e}", fg="red"))
578
778
  click.pause(" Press any key to continue...")
579
779
  return False
580
780
 
@@ -591,39 +791,55 @@ def _wizard_create_engagement() -> dict:
591
791
  width = 60
592
792
 
593
793
  click.echo("┌" + "─" * (width - 2) + "┐")
594
- click.echo("│" + click.style(" STEP 3: CREATE YOUR ENGAGEMENT ".center(width - 2), bold=True, fg='cyan') + "│")
794
+ click.echo(
795
+ "│"
796
+ + click.style(
797
+ " STEP 3: CREATE YOUR ENGAGEMENT ".center(width - 2), bold=True, fg="cyan"
798
+ )
799
+ + "│"
800
+ )
595
801
  click.echo("└" + "─" * (width - 2) + "┘")
596
802
  click.echo()
597
803
  click.echo(" An engagement organizes all your work for a specific target.")
598
- click.echo(" Give it a descriptive name like 'Acme Corp Pentest' or 'HackTheBox Lab'.")
804
+ click.echo(
805
+ " Give it a descriptive name like 'Acme Corp Pentest' or 'HackTheBox Lab'."
806
+ )
599
807
  click.echo()
600
808
 
601
809
  name = click.prompt(" Engagement Name").strip()
602
810
 
603
811
  # Require a name
604
812
  while not name:
605
- click.echo(click.style(" ✗ Name is required!", fg='red'))
813
+ click.echo(click.style(" ✗ Name is required!", fg="red"))
606
814
  name = click.prompt(" Engagement Name").strip()
607
815
 
608
816
  click.echo()
609
817
  click.echo(" " + click.style("Engagement Type:", bold=True))
610
818
  click.echo(" [1] Penetration Test - Full-scope security assessment")
611
819
  click.echo(" [2] Bug Bounty - Vulnerability hunting with defined scope")
612
- click.echo(" [3] CTF / Lab - Practice environment, aggressive scanning OK")
820
+ click.echo(
821
+ " [3] CTF / Lab - Practice environment, aggressive scanning OK"
822
+ )
613
823
  click.echo(" [4] Red Team - Adversary simulation, stealth preferred")
614
824
  click.echo(" [5] Custom - Define your own")
615
825
  click.echo()
616
- click.echo(" " + click.style("NOTE:", fg='yellow') + " Type affects default automation and scan aggressiveness")
826
+ click.echo(
827
+ " "
828
+ + click.style("NOTE:", fg="yellow")
829
+ + " Type affects default automation and scan aggressiveness"
830
+ )
617
831
  click.echo()
618
832
 
619
- type_choice = click.prompt(" Select option", type=click.IntRange(1, 5), default=1, show_default=False)
833
+ type_choice = click.prompt(
834
+ " Select option", type=click.IntRange(1, 5), default=1, show_default=False
835
+ )
620
836
 
621
837
  engagement_types = {
622
- 1: 'penetration_test',
623
- 2: 'bug_bounty',
624
- 3: 'ctf',
625
- 4: 'red_team',
626
- 5: 'custom'
838
+ 1: "penetration_test",
839
+ 2: "bug_bounty",
840
+ 3: "ctf",
841
+ 4: "red_team",
842
+ 5: "custom",
627
843
  }
628
844
 
629
845
  engagement_type = engagement_types[type_choice]
@@ -633,35 +849,44 @@ def _wizard_create_engagement() -> dict:
633
849
 
634
850
  # Handle duplicate engagement names
635
851
  try:
636
- engagement_id = em.create_engagement(name, f"Created via Setup Wizard - Type: {engagement_type}")
852
+ engagement_id = em.create_engagement(
853
+ name, f"Created via Setup Wizard - Type: {engagement_type}"
854
+ )
637
855
  click.echo()
638
- click.echo(" " + click.style(f"✓ Engagement '{name}' created and set as active!", fg='green'))
856
+ click.echo(
857
+ " "
858
+ + click.style(
859
+ f"✓ Engagement '{name}' created and set as active!", fg="green"
860
+ )
861
+ )
639
862
  except ValueError as e:
640
863
  if "already exists" in str(e):
641
864
  click.echo()
642
- click.echo(" " + click.style(f"Engagement '{name}' already exists.", fg='yellow'))
643
- click.echo(" " + click.style(" Using existing engagement.", fg='green'))
865
+ click.echo(
866
+ " " + click.style(f"Engagement '{name}' already exists.", fg="yellow")
867
+ )
868
+ click.echo(" " + click.style("✓ Using existing engagement.", fg="green"))
644
869
  # Get existing engagement ID
645
870
  existing = em.get_engagement(name)
646
- engagement_id = existing['id'] if existing else None
871
+ engagement_id = existing["id"] if existing else None
647
872
  else:
648
873
  raise
649
874
 
650
875
  em.set_current(name) # Set as active so user drops into this engagement
651
876
  click.pause(" Press any key to continue...")
652
877
 
653
- return {
654
- 'id': engagement_id,
655
- 'name': name,
656
- 'type': engagement_type
657
- }
878
+ return {"id": engagement_id, "name": name, "type": engagement_type}
658
879
 
659
880
 
660
881
  def _wizard_tool_check() -> dict:
661
882
  """Check installed tools using the centralized tool_checker module."""
662
883
  from souleyez.utils.tool_checker import (
663
- check_tool_version, EXTERNAL_TOOLS, get_install_command, detect_distro,
664
- get_tool_version, get_upgrade_command
884
+ check_tool_version,
885
+ EXTERNAL_TOOLS,
886
+ get_install_command,
887
+ detect_distro,
888
+ get_tool_version,
889
+ get_upgrade_command,
665
890
  )
666
891
 
667
892
  DesignSystem.clear_screen()
@@ -670,7 +895,13 @@ def _wizard_tool_check() -> dict:
670
895
  distro = detect_distro()
671
896
 
672
897
  click.echo("┌" + "─" * (width - 2) + "┐")
673
- click.echo("│" + click.style(" STEP 4: TOOL AVAILABILITY ".center(width - 2), bold=True, fg='cyan') + "│")
898
+ click.echo(
899
+ "│"
900
+ + click.style(
901
+ " STEP 4: TOOL AVAILABILITY ".center(width - 2), bold=True, fg="cyan"
902
+ )
903
+ + "│"
904
+ )
674
905
  click.echo("└" + "─" * (width - 2) + "┘")
675
906
  click.echo()
676
907
  click.echo(" Checking installed tools...")
@@ -679,17 +910,17 @@ def _wizard_tool_check() -> dict:
679
910
  # Core tools to check in the wizard (subset of full tool list)
680
911
  # Maps display name -> tool_checker key (category, tool_name)
681
912
  wizard_tools = {
682
- 'nmap': ('reconnaissance', 'nmap'),
683
- 'gobuster': ('web_scanning', 'gobuster'),
684
- 'sqlmap': ('exploitation', 'sqlmap'),
685
- 'nuclei': ('web_scanning', 'nuclei'),
686
- 'hydra': ('credential_attacks', 'hydra'),
687
- 'theHarvester': ('reconnaissance', 'theharvester'),
688
- 'wpscan': ('web_scanning', 'wpscan'),
689
- 'netexec': ('windows_ad', 'netexec'),
690
- 'enum4linux': ('windows_ad', 'enum4linux'),
691
- 'dnsrecon': ('reconnaissance', 'dnsrecon'),
692
- 'whois': ('reconnaissance', 'whois'),
913
+ "nmap": ("reconnaissance", "nmap"),
914
+ "gobuster": ("web_scanning", "gobuster"),
915
+ "sqlmap": ("exploitation", "sqlmap"),
916
+ "nuclei": ("web_scanning", "nuclei"),
917
+ "hydra": ("credential_attacks", "hydra"),
918
+ "theHarvester": ("reconnaissance", "theharvester"),
919
+ "wpscan": ("web_scanning", "wpscan"),
920
+ "netexec": ("windows_ad", "netexec"),
921
+ "enum4linux": ("windows_ad", "enum4linux"),
922
+ "dnsrecon": ("reconnaissance", "dnsrecon"),
923
+ "whois": ("reconnaissance", "whois"),
693
924
  }
694
925
 
695
926
  found = 0
@@ -700,58 +931,71 @@ def _wizard_tool_check() -> dict:
700
931
 
701
932
  for display_name, (category, tool_key) in wizard_tools.items():
702
933
  tool_info = EXTERNAL_TOOLS[category][tool_key]
703
- command = tool_info['command']
934
+ command = tool_info["command"]
704
935
 
705
936
  # Use version-aware check
706
937
  version_status = check_tool_version(tool_info)
707
938
 
708
- if version_status['installed']:
939
+ if version_status["installed"]:
709
940
  # Use the actual command that was found (may be primary or alt)
710
- actual_cmd = version_status.get('actual_command') or command
941
+ actual_cmd = version_status.get("actual_command") or command
711
942
  path = shutil.which(actual_cmd)
712
943
 
713
944
  # Try to get version for display (even if no min_version requirement)
714
- version_str = version_status['version']
945
+ version_str = version_status["version"]
715
946
  if not version_str:
716
947
  # Try to get version using the actual found command
717
948
  version_str = get_tool_version(actual_cmd)
718
949
 
719
950
  # Check if version is OK
720
- if version_status['needs_upgrade']:
951
+ if version_status["needs_upgrade"]:
721
952
  # Tool installed but wrong version
722
- ver_display = version_str or 'unknown'
723
- min_ver = version_status['min_version']
724
- click.echo(f" {click.style('!', fg='yellow')} {display_name:<15} v{ver_display} " +
725
- click.style(f"(needs v{min_ver}+)", fg='yellow'))
953
+ ver_display = version_str or "unknown"
954
+ min_ver = version_status["min_version"]
955
+ click.echo(
956
+ f" {click.style('!', fg='yellow')} {display_name:<15} v{ver_display} "
957
+ + click.style(f"(needs v{min_ver}+)", fg="yellow")
958
+ )
726
959
  tool_status[display_name] = {
727
- 'found': True,
728
- 'path': path,
729
- 'version': ver_display,
730
- 'needs_upgrade': True
960
+ "found": True,
961
+ "path": path,
962
+ "version": ver_display,
963
+ "needs_upgrade": True,
731
964
  }
732
- wrong_version_tools.append({
733
- 'name': display_name,
734
- 'installed': ver_display,
735
- 'required': min_ver,
736
- 'install': get_upgrade_command(tool_info, distro) or get_install_command(tool_info, distro),
737
- 'note': version_status.get('version_note'),
738
- 'tool_info': tool_info
739
- })
965
+ wrong_version_tools.append(
966
+ {
967
+ "name": display_name,
968
+ "installed": ver_display,
969
+ "required": min_ver,
970
+ "install": get_upgrade_command(tool_info, distro)
971
+ or get_install_command(tool_info, distro),
972
+ "note": version_status.get("version_note"),
973
+ "tool_info": tool_info,
974
+ }
975
+ )
740
976
  found += 1 # Still counts as found, just needs upgrade
741
977
  else:
742
978
  # Tool installed with correct version
743
979
  ver_display = f"v{version_str}" if version_str else ""
744
- click.echo(f" {click.style('✓', fg='green')} {display_name:<15} Found {ver_display}")
745
- tool_status[display_name] = {'found': True, 'path': path, 'version': version_str}
980
+ click.echo(
981
+ f" {click.style('', fg='green')} {display_name:<15} Found {ver_display}"
982
+ )
983
+ tool_status[display_name] = {
984
+ "found": True,
985
+ "path": path,
986
+ "version": version_str,
987
+ }
746
988
  found += 1
747
989
  else:
748
990
  click.echo(f" {click.style('✗', fg='red')} {display_name:<15} NOT FOUND")
749
- tool_status[display_name] = {'found': False, 'path': None}
750
- missing_tools.append({
751
- 'name': display_name,
752
- 'install': get_install_command(tool_info, distro),
753
- 'tool_info': tool_info
754
- })
991
+ tool_status[display_name] = {"found": False, "path": None}
992
+ missing_tools.append(
993
+ {
994
+ "name": display_name,
995
+ "install": get_install_command(tool_info, distro),
996
+ "tool_info": tool_info,
997
+ }
998
+ )
755
999
 
756
1000
  click.echo()
757
1001
  click.echo(f" Found {found}/{total} recommended tools")
@@ -759,10 +1003,12 @@ def _wizard_tool_check() -> dict:
759
1003
  # Show version warnings
760
1004
  if wrong_version_tools:
761
1005
  click.echo()
762
- click.echo(" " + click.style("VERSION ISSUES:", fg='yellow', bold=True))
1006
+ click.echo(" " + click.style("VERSION ISSUES:", fg="yellow", bold=True))
763
1007
  for tool in wrong_version_tools:
764
- click.echo(f" - {tool['name']}: installed v{tool['installed']}, needs v{tool['required']}+")
765
- if tool.get('note'):
1008
+ click.echo(
1009
+ f" - {tool['name']}: installed v{tool['installed']}, needs v{tool['required']}+"
1010
+ )
1011
+ if tool.get("note"):
766
1012
  click.echo(f" {click.style(tool['note'], fg='bright_black')}")
767
1013
 
768
1014
  # Offer to install/upgrade tools
@@ -781,22 +1027,30 @@ def _wizard_tool_check() -> dict:
781
1027
  tool_info = EXTERNAL_TOOLS[category][tool_key]
782
1028
  version_status = check_tool_version(tool_info)
783
1029
 
784
- if version_status['installed'] and not version_status['needs_upgrade']:
785
- tool_status[display_name] = {'found': True, 'path': shutil.which(tool_info['command'])}
1030
+ if version_status["installed"] and not version_status["needs_upgrade"]:
1031
+ tool_status[display_name] = {
1032
+ "found": True,
1033
+ "path": shutil.which(tool_info["command"]),
1034
+ }
786
1035
  found += 1
787
- elif version_status['installed'] and version_status['needs_upgrade']:
788
- tool_status[display_name] = {'found': True, 'needs_upgrade': True}
789
- wrong_version_tools.append({'name': display_name})
1036
+ elif version_status["installed"] and version_status["needs_upgrade"]:
1037
+ tool_status[display_name] = {"found": True, "needs_upgrade": True}
1038
+ wrong_version_tools.append({"name": display_name})
790
1039
  found += 1
791
1040
  else:
792
- tool_status[display_name] = {'found': False, 'path': None}
1041
+ tool_status[display_name] = {"found": False, "path": None}
793
1042
 
794
1043
  click.echo(f" Now have {found}/{total} tools available")
795
1044
 
796
1045
  click.echo()
797
1046
  click.pause(" Press any key to continue...")
798
1047
 
799
- return {'found': found, 'total': total, 'tools': tool_status, 'wrong_version': wrong_version_tools}
1048
+ return {
1049
+ "found": found,
1050
+ "total": total,
1051
+ "tools": tool_status,
1052
+ "wrong_version": wrong_version_tools,
1053
+ }
800
1054
 
801
1055
 
802
1056
  def _wizard_ai_setup() -> bool:
@@ -809,54 +1063,83 @@ def _wizard_ai_setup() -> bool:
809
1063
  width = 60
810
1064
 
811
1065
  click.echo("┌" + "─" * (width - 2) + "┐")
812
- click.echo("│" + click.style(" STEP 5: AI FEATURES (OPTIONAL) ".center(width - 2), bold=True, fg='cyan') + "│")
1066
+ click.echo(
1067
+ "│"
1068
+ + click.style(
1069
+ " STEP 5: AI FEATURES (OPTIONAL) ".center(width - 2), bold=True, fg="cyan"
1070
+ )
1071
+ + "│"
1072
+ )
813
1073
  click.echo("└" + "─" * (width - 2) + "┘")
814
1074
  click.echo()
815
1075
  click.echo(" SoulEyez can use AI to suggest scanning strategies and")
816
- click.echo(" prioritize targets. This requires " + click.style("Ollama", bold=True) + " (local AI runtime).")
1076
+ click.echo(
1077
+ " prioritize targets. This requires "
1078
+ + click.style("Ollama", bold=True)
1079
+ + " (local AI runtime)."
1080
+ )
817
1081
  click.echo()
818
1082
 
819
1083
  # Check if Ollama is installed
820
- ollama_path = shutil.which('ollama')
1084
+ ollama_path = shutil.which("ollama")
821
1085
 
822
1086
  if ollama_path:
823
- click.echo(" " + click.style("✓", fg='green') + " Ollama is installed")
1087
+ click.echo(" " + click.style("✓", fg="green") + " Ollama is installed")
824
1088
 
825
1089
  # Check if it's running
826
1090
  try:
827
1091
  result = subprocess.run(
828
- ['curl', '-s', 'http://localhost:11434/api/tags'],
829
- capture_output=True, timeout=5
1092
+ ["curl", "-s", "http://localhost:11434/api/tags"],
1093
+ capture_output=True,
1094
+ timeout=5,
830
1095
  )
831
1096
  if result.returncode == 0:
832
- click.echo(" " + click.style("✓", fg='green') + " Ollama is running")
1097
+ click.echo(" " + click.style("✓", fg="green") + " Ollama is running")
833
1098
 
834
1099
  # Check for models
835
1100
  import json
1101
+
836
1102
  try:
837
1103
  data = json.loads(result.stdout)
838
- models = data.get('models', [])
1104
+ models = data.get("models", [])
839
1105
  if models:
840
- click.echo(f" " + click.style("✓", fg='green') + f" {len(models)} model(s) available")
1106
+ click.echo(
1107
+ f" "
1108
+ + click.style("✓", fg="green")
1109
+ + f" {len(models)} model(s) available"
1110
+ )
841
1111
  click.echo()
842
- click.echo(" " + click.style("AI features are ready!", fg='green', bold=True))
1112
+ click.echo(
1113
+ " "
1114
+ + click.style(
1115
+ "AI features are ready!", fg="green", bold=True
1116
+ )
1117
+ )
843
1118
  click.echo()
844
1119
  click.pause(" Press any key to continue...")
845
1120
  return True
846
1121
  else:
847
- click.echo(" " + click.style("!", fg='yellow') + " No models installed")
1122
+ click.echo(
1123
+ " "
1124
+ + click.style("!", fg="yellow")
1125
+ + " No models installed"
1126
+ )
848
1127
  except:
849
1128
  pass
850
1129
 
851
1130
  # Offer to pull a model
852
1131
  click.echo()
853
1132
  click.echo(" Would you like to download a model now?")
854
- click.echo(" Recommended: " + click.style("llama3.1:8b", fg='cyan') + " (~4.7GB)")
1133
+ click.echo(
1134
+ " Recommended: "
1135
+ + click.style("llama3.1:8b", fg="cyan")
1136
+ + " (~4.7GB)"
1137
+ )
855
1138
  click.echo()
856
1139
 
857
1140
  try:
858
1141
  response = input(" Download model? (y/n): ").strip().lower()
859
- if response == 'y':
1142
+ if response == "y":
860
1143
  return _pull_ollama_model()
861
1144
  except (KeyboardInterrupt, EOFError):
862
1145
  click.echo("\n Skipped.")
@@ -865,11 +1148,15 @@ def _wizard_ai_setup() -> bool:
865
1148
  return False
866
1149
  else:
867
1150
  # Ollama installed but not running
868
- click.echo(" " + click.style("!", fg='yellow') + " Ollama is not running")
1151
+ click.echo(
1152
+ " " + click.style("!", fg="yellow") + " Ollama is not running"
1153
+ )
869
1154
  return _start_ollama_and_setup()
870
1155
 
871
1156
  except subprocess.TimeoutExpired:
872
- click.echo(" " + click.style("!", fg='yellow') + " Ollama is not responding")
1157
+ click.echo(
1158
+ " " + click.style("!", fg="yellow") + " Ollama is not responding"
1159
+ )
873
1160
  return _start_ollama_and_setup()
874
1161
  except FileNotFoundError:
875
1162
  # curl not found, try another method
@@ -877,7 +1164,7 @@ def _wizard_ai_setup() -> bool:
877
1164
 
878
1165
  else:
879
1166
  # Ollama not installed - offer to install
880
- click.echo(" " + click.style("!", fg='yellow') + " Ollama is not installed")
1167
+ click.echo(" " + click.style("!", fg="yellow") + " Ollama is not installed")
881
1168
  click.echo()
882
1169
  click.echo(" Would you like to install Ollama now?")
883
1170
  click.echo(" This will download and install the Ollama runtime (~100MB).")
@@ -885,13 +1172,17 @@ def _wizard_ai_setup() -> bool:
885
1172
 
886
1173
  try:
887
1174
  response = input(" Install Ollama? (y/n): ").strip().lower()
888
- if response == 'y':
1175
+ if response == "y":
889
1176
  return _install_ollama()
890
1177
  except (KeyboardInterrupt, EOFError):
891
1178
  click.echo("\n Skipped.")
892
1179
 
893
1180
  click.echo()
894
- click.echo(" " + click.style("Skipping AI setup.", fg='bright_black') + " You can enable it later in Settings.")
1181
+ click.echo(
1182
+ " "
1183
+ + click.style("Skipping AI setup.", fg="bright_black")
1184
+ + " You can enable it later in Settings."
1185
+ )
895
1186
  click.pause(" Press any key to continue...")
896
1187
  return False
897
1188
 
@@ -902,28 +1193,45 @@ def _install_ollama() -> bool:
902
1193
 
903
1194
  click.echo()
904
1195
  click.echo(" Installing Ollama...")
905
- click.echo(" " + click.style("(This may take a minute - downloading ~100MB)", fg='bright_black'))
1196
+ click.echo(
1197
+ " "
1198
+ + click.style(
1199
+ "(This may take a minute - downloading ~100MB)", fg="bright_black"
1200
+ )
1201
+ )
906
1202
  click.echo()
907
1203
 
908
1204
  try:
909
1205
  # Run the official Ollama install script with timeout
910
1206
  # Add curl timeouts to fail faster on network issues
911
1207
  result = subprocess.run(
912
- ['bash', '-c', 'curl --connect-timeout 30 --max-time 300 -fsSL https://ollama.ai/install.sh | sh'],
1208
+ [
1209
+ "bash",
1210
+ "-c",
1211
+ "curl --connect-timeout 30 --max-time 300 -fsSL https://ollama.ai/install.sh | sh",
1212
+ ],
913
1213
  check=False,
914
- timeout=360 # 6 minute total timeout
1214
+ timeout=360, # 6 minute total timeout
915
1215
  )
916
1216
 
917
1217
  if result.returncode == 0:
918
1218
  click.echo()
919
- click.echo(" " + click.style("✓ Ollama installed successfully!", fg='green'))
1219
+ click.echo(
1220
+ " " + click.style("✓ Ollama installed successfully!", fg="green")
1221
+ )
920
1222
 
921
1223
  # Start Ollama and set up model
922
1224
  return _start_ollama_and_setup()
923
1225
  else:
924
1226
  click.echo()
925
- click.echo(" " + click.style("✗ Installation failed", fg='red'))
926
- click.echo(" " + click.style("This is usually a network issue (slow connection or timeout).", fg='yellow'))
1227
+ click.echo(" " + click.style("✗ Installation failed", fg="red"))
1228
+ click.echo(
1229
+ " "
1230
+ + click.style(
1231
+ "This is usually a network issue (slow connection or timeout).",
1232
+ fg="yellow",
1233
+ )
1234
+ )
927
1235
  click.echo()
928
1236
  click.echo(" To install manually:")
929
1237
  click.echo(" curl -fsSL https://ollama.ai/install.sh | sh")
@@ -934,8 +1242,14 @@ def _install_ollama() -> bool:
934
1242
 
935
1243
  except subprocess.TimeoutExpired:
936
1244
  click.echo()
937
- click.echo(" " + click.style("✗ Installation timed out", fg='red'))
938
- click.echo(" " + click.style("The download took too long. Check your internet connection.", fg='yellow'))
1245
+ click.echo(" " + click.style("✗ Installation timed out", fg="red"))
1246
+ click.echo(
1247
+ " "
1248
+ + click.style(
1249
+ "The download took too long. Check your internet connection.",
1250
+ fg="yellow",
1251
+ )
1252
+ )
939
1253
  click.echo()
940
1254
  click.echo(" To install manually:")
941
1255
  click.echo(" curl -fsSL https://ollama.ai/install.sh | sh")
@@ -944,7 +1258,7 @@ def _install_ollama() -> bool:
944
1258
 
945
1259
  except Exception as e:
946
1260
  click.echo()
947
- click.echo(click.style(f" ✗ Error: {e}", fg='red'))
1261
+ click.echo(click.style(f" ✗ Error: {e}", fg="red"))
948
1262
  click.echo(" You can install manually from https://ollama.ai")
949
1263
  click.pause(" Press any key to continue...")
950
1264
  return False
@@ -961,10 +1275,10 @@ def _start_ollama_and_setup() -> bool:
961
1275
  try:
962
1276
  # Start ollama serve in background
963
1277
  subprocess.Popen(
964
- ['ollama', 'serve'],
1278
+ ["ollama", "serve"],
965
1279
  stdout=subprocess.DEVNULL,
966
1280
  stderr=subprocess.DEVNULL,
967
- start_new_session=True
1281
+ start_new_session=True,
968
1282
  )
969
1283
 
970
1284
  # Wait for it to start
@@ -973,22 +1287,25 @@ def _start_ollama_and_setup() -> bool:
973
1287
 
974
1288
  # Verify it's running
975
1289
  result = subprocess.run(
976
- ['curl', '-s', 'http://localhost:11434/api/tags'],
977
- capture_output=True, timeout=5
1290
+ ["curl", "-s", "http://localhost:11434/api/tags"],
1291
+ capture_output=True,
1292
+ timeout=5,
978
1293
  )
979
1294
 
980
1295
  if result.returncode == 0:
981
- click.echo(" " + click.style("✓ Ollama started!", fg='green'))
1296
+ click.echo(" " + click.style("✓ Ollama started!", fg="green"))
982
1297
 
983
1298
  # Offer to pull model
984
1299
  click.echo()
985
1300
  click.echo(" Would you like to download a model now?")
986
- click.echo(" Recommended: " + click.style("llama3.1:8b", fg='cyan') + " (~4.7GB)")
1301
+ click.echo(
1302
+ " Recommended: " + click.style("llama3.1:8b", fg="cyan") + " (~4.7GB)"
1303
+ )
987
1304
  click.echo()
988
1305
 
989
1306
  try:
990
1307
  response = input(" Download model? (y/n): ").strip().lower()
991
- if response == 'y':
1308
+ if response == "y":
992
1309
  return _pull_ollama_model()
993
1310
  except (KeyboardInterrupt, EOFError):
994
1311
  click.echo("\n Skipped.")
@@ -996,12 +1313,12 @@ def _start_ollama_and_setup() -> bool:
996
1313
  click.pause(" Press any key to continue...")
997
1314
  return True
998
1315
  else:
999
- click.echo(" " + click.style("✗ Failed to start Ollama", fg='red'))
1316
+ click.echo(" " + click.style("✗ Failed to start Ollama", fg="red"))
1000
1317
  click.pause(" Press any key to continue...")
1001
1318
  return False
1002
1319
 
1003
1320
  except Exception as e:
1004
- click.echo(click.style(f" ✗ Error: {e}", fg='red'))
1321
+ click.echo(click.style(f" ✗ Error: {e}", fg="red"))
1005
1322
  click.pause(" Press any key to continue...")
1006
1323
  return False
1007
1324
 
@@ -1014,32 +1331,38 @@ def _pull_ollama_model() -> bool:
1014
1331
 
1015
1332
  click.echo()
1016
1333
  click.echo(f" Downloading {model}...")
1017
- click.echo(" " + click.style("(This may take several minutes depending on your connection)", fg='bright_black'))
1334
+ click.echo(
1335
+ " "
1336
+ + click.style(
1337
+ "(This may take several minutes depending on your connection)",
1338
+ fg="bright_black",
1339
+ )
1340
+ )
1018
1341
  click.echo()
1019
1342
 
1020
1343
  try:
1021
1344
  # Run ollama pull and let it output directly to terminal for proper progress bar
1022
- result = subprocess.run(
1023
- ['ollama', 'pull', model],
1024
- check=False
1025
- )
1345
+ result = subprocess.run(["ollama", "pull", model], check=False)
1026
1346
 
1027
1347
  if result.returncode == 0:
1028
1348
  click.echo()
1029
- click.echo(" " + click.style(f"✓ Model {model} ready!", fg='green'))
1349
+ click.echo(" " + click.style(f"✓ Model {model} ready!", fg="green"))
1030
1350
  click.echo()
1031
- click.echo(" " + click.style("AI features are now enabled!", fg='green', bold=True))
1351
+ click.echo(
1352
+ " "
1353
+ + click.style("AI features are now enabled!", fg="green", bold=True)
1354
+ )
1032
1355
  click.pause(" Press any key to continue...")
1033
1356
  return True
1034
1357
  else:
1035
1358
  click.echo()
1036
- click.echo(" " + click.style("✗ Failed to download model", fg='red'))
1359
+ click.echo(" " + click.style("✗ Failed to download model", fg="red"))
1037
1360
  click.echo(" You can try later with: ollama pull " + model)
1038
1361
  click.pause(" Press any key to continue...")
1039
1362
  return False
1040
1363
 
1041
1364
  except Exception as e:
1042
- click.echo(click.style(f" ✗ Error: {e}", fg='red'))
1365
+ click.echo(click.style(f" ✗ Error: {e}", fg="red"))
1043
1366
  click.pause(" Press any key to continue...")
1044
1367
  return False
1045
1368
 
@@ -1051,7 +1374,13 @@ def _wizard_automation_prefs() -> dict:
1051
1374
  width = 60
1052
1375
 
1053
1376
  click.echo("┌" + "─" * (width - 2) + "┐")
1054
- click.echo("│" + click.style(" STEP 6: AUTOMATION PREFERENCES ".center(width - 2), bold=True, fg='cyan') + "│")
1377
+ click.echo(
1378
+ "│"
1379
+ + click.style(
1380
+ " STEP 6: AUTOMATION PREFERENCES ".center(width - 2), bold=True, fg="cyan"
1381
+ )
1382
+ + "│"
1383
+ )
1055
1384
  click.echo("└" + "─" * (width - 2) + "┘")
1056
1385
  click.echo()
1057
1386
  click.echo(" SoulEyez can automatically chain tools based on discoveries.")
@@ -1069,15 +1398,17 @@ def _wizard_automation_prefs() -> dict:
1069
1398
  click.echo()
1070
1399
  click.echo(f" {click.style('✓', fg='green')} Auto-chaining disabled")
1071
1400
  click.pause(" Press any key to continue...")
1072
- return {'enabled': False, 'mode': None}
1401
+ return {"enabled": False, "mode": None}
1073
1402
 
1074
- mode = 'auto' if mode_choice == 1 else 'approval'
1403
+ mode = "auto" if mode_choice == 1 else "approval"
1075
1404
 
1076
1405
  click.echo()
1077
- click.echo(f" {click.style('✓', fg='green')} Auto-chaining enabled in {mode.upper()} mode")
1406
+ click.echo(
1407
+ f" {click.style('✓', fg='green')} Auto-chaining enabled in {mode.upper()} mode"
1408
+ )
1078
1409
  click.pause(" Press any key to continue...")
1079
1410
 
1080
- return {'enabled': True, 'mode': mode}
1411
+ return {"enabled": True, "mode": mode}
1081
1412
 
1082
1413
 
1083
1414
  def _wizard_deliverables(engagement_type: str) -> list:
@@ -1087,25 +1418,39 @@ def _wizard_deliverables(engagement_type: str) -> list:
1087
1418
  width = 60
1088
1419
 
1089
1420
  click.echo("┌" + "─" * (width - 2) + "┐")
1090
- click.echo("│" + click.style(" STEP 7: REPORT TEMPLATES ".center(width - 2), bold=True, fg='cyan') + "│")
1421
+ click.echo(
1422
+ "│"
1423
+ + click.style(
1424
+ " STEP 7: REPORT TEMPLATES ".center(width - 2), bold=True, fg="cyan"
1425
+ )
1426
+ + "│"
1427
+ )
1091
1428
  click.echo("└" + "─" * (width - 2) + "┘")
1092
1429
  click.echo()
1093
1430
  click.echo(" Pre-select report templates that will be available for export.")
1094
1431
  click.echo(" You can change these later in Reports & Export menu.")
1095
1432
  click.echo()
1096
- click.echo(" " + click.style("NOTE:", fg='yellow') + " Templates help generate professional reports from your findings")
1433
+ click.echo(
1434
+ " "
1435
+ + click.style("NOTE:", fg="yellow")
1436
+ + " Templates help generate professional reports from your findings"
1437
+ )
1097
1438
  click.echo()
1098
1439
 
1099
1440
  # Default templates based on engagement type
1100
1441
  type_templates = {
1101
- 'penetration_test': ['Executive Summary', 'Technical Findings Report', 'Vulnerability Details'],
1102
- 'bug_bounty': ['Vulnerability Details', 'Proof of Concept'],
1103
- 'ctf': ['Technical Findings Report', 'Attack Narrative'],
1104
- 'red_team': ['Attack Narrative', 'Remediation Roadmap'],
1105
- 'custom': ['Technical Findings Report']
1442
+ "penetration_test": [
1443
+ "Executive Summary",
1444
+ "Technical Findings Report",
1445
+ "Vulnerability Details",
1446
+ ],
1447
+ "bug_bounty": ["Vulnerability Details", "Proof of Concept"],
1448
+ "ctf": ["Technical Findings Report", "Attack Narrative"],
1449
+ "red_team": ["Attack Narrative", "Remediation Roadmap"],
1450
+ "custom": ["Technical Findings Report"],
1106
1451
  }
1107
1452
 
1108
- recommended = type_templates.get(engagement_type, ['Technical Findings Report'])
1453
+ recommended = type_templates.get(engagement_type, ["Technical Findings Report"])
1109
1454
 
1110
1455
  click.echo(f" Recommended for {engagement_type.replace('_', ' ').title()}:")
1111
1456
  for template in recommended:
@@ -1118,7 +1463,15 @@ def _wizard_deliverables(engagement_type: str) -> list:
1118
1463
  return recommended
1119
1464
 
1120
1465
 
1121
- def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled, automation_prefs, templates, is_pro=False):
1466
+ def _wizard_summary(
1467
+ encryption_enabled,
1468
+ engagement_info,
1469
+ tool_status,
1470
+ ai_enabled,
1471
+ automation_prefs,
1472
+ templates,
1473
+ is_pro=False,
1474
+ ):
1122
1475
  """Show wizard summary."""
1123
1476
  DesignSystem.clear_screen()
1124
1477
  _show_wizard_banner()
@@ -1127,43 +1480,65 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
1127
1480
  # Adjust step number based on tier
1128
1481
  step_num = "8" if is_pro else "6"
1129
1482
  click.echo("┌" + "─" * (width - 2) + "┐")
1130
- click.echo("│" + click.style(f" STEP {step_num}: SETUP COMPLETE! ".center(width - 2), bold=True, fg='green') + "│")
1483
+ click.echo(
1484
+ "│"
1485
+ + click.style(
1486
+ f" STEP {step_num}: SETUP COMPLETE! ".center(width - 2),
1487
+ bold=True,
1488
+ fg="green",
1489
+ )
1490
+ + "│"
1491
+ )
1131
1492
  click.echo("└" + "─" * (width - 2) + "┘")
1132
1493
  click.echo()
1133
1494
 
1134
1495
  # Summary
1135
- enc_status = click.style("Enabled", fg='green') if encryption_enabled else click.style("Disabled", fg='yellow')
1496
+ enc_status = (
1497
+ click.style("Enabled", fg="green")
1498
+ if encryption_enabled
1499
+ else click.style("Disabled", fg="yellow")
1500
+ )
1136
1501
  click.echo(f" {click.style('✓', fg='green')} Encryption: {enc_status}")
1137
1502
 
1138
- eng_name = engagement_info['name']
1139
- eng_type = engagement_info['type'].replace('_', ' ').title()
1140
- click.echo(f" {click.style('✓', fg='green')} Engagement: \"{eng_name}\" ({eng_type})")
1503
+ eng_name = engagement_info["name"]
1504
+ eng_type = engagement_info["type"].replace("_", " ").title()
1505
+ click.echo(
1506
+ f" {click.style('✓', fg='green')} Engagement: \"{eng_name}\" ({eng_type})"
1507
+ )
1141
1508
 
1142
- tools_found = tool_status['found']
1143
- tools_total = tool_status['total']
1144
- click.echo(f" {click.style('✓', fg='green')} Tools: {tools_found}/{tools_total} available")
1509
+ tools_found = tool_status["found"]
1510
+ tools_total = tool_status["total"]
1511
+ click.echo(
1512
+ f" {click.style('✓', fg='green')} Tools: {tools_found}/{tools_total} available"
1513
+ )
1145
1514
 
1146
1515
  # AI status
1147
1516
  if ai_enabled:
1148
1517
  click.echo(f" {click.style('✓', fg='green')} AI Features: Enabled (Ollama)")
1149
1518
  else:
1150
- click.echo(f" {click.style('○', fg='bright_black')} AI Features: Not configured")
1519
+ click.echo(
1520
+ f" {click.style('○', fg='bright_black')} AI Features: Not configured"
1521
+ )
1151
1522
 
1152
1523
  # Show tier status
1153
1524
  if is_pro:
1154
1525
  click.echo(f" {click.style('💎', fg='magenta')} License: PRO")
1155
- if automation_prefs['enabled']:
1156
- auto_mode = automation_prefs['mode'].upper()
1157
- click.echo(f" {click.style('✓', fg='green')} Auto-Chain: ON ({auto_mode} mode)")
1526
+ if automation_prefs["enabled"]:
1527
+ auto_mode = automation_prefs["mode"].upper()
1528
+ click.echo(
1529
+ f" {click.style('✓', fg='green')} Auto-Chain: ON ({auto_mode} mode)"
1530
+ )
1158
1531
  else:
1159
1532
  click.echo(f" {click.style('✓', fg='green')} Auto-Chain: OFF")
1160
1533
 
1161
1534
  if templates:
1162
- click.echo(f" {click.style('✓', fg='green')} Templates: {len(templates)} selected")
1535
+ click.echo(
1536
+ f" {click.style('✓', fg='green')} Templates: {len(templates)} selected"
1537
+ )
1163
1538
  else:
1164
1539
  click.echo(f" {click.style('○', fg='bright_black')} License: FREE")
1165
1540
  click.echo()
1166
- click.echo(click.style(" Upgrade to Pro for:", fg='yellow'))
1541
+ click.echo(click.style(" Upgrade to Pro for:", fg="yellow"))
1167
1542
  click.echo(" • AI Execute - Autonomous exploitation")
1168
1543
  click.echo(" • Automation - Smart tool chaining")
1169
1544
  click.echo(" • MSF Integration - Advanced attack chains")
@@ -1171,7 +1546,7 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
1171
1546
  click.echo(f" {click.style('→ cybersoulsecurity.com/upgrade', fg='cyan')}")
1172
1547
 
1173
1548
  click.echo()
1174
- click.echo(" " + click.style("You're ready to start!", bold=True, fg='cyan'))
1549
+ click.echo(" " + click.style("You're ready to start!", bold=True, fg="cyan"))
1175
1550
  click.echo()
1176
1551
 
1177
1552
  # Install desktop shortcut automatically
@@ -1179,22 +1554,41 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
1179
1554
 
1180
1555
  # Prompt for interactive tutorial
1181
1556
  click.echo(" ┌" + "─" * 56 + "┐")
1182
- click.echo(" │" + click.style(" Would you like to run the interactive tutorial?", fg='cyan').center(65) + "│")
1183
- click.echo(" │" + " Recommended for new users - takes about 5 minutes".center(56) + "│")
1557
+ click.echo(
1558
+ " │"
1559
+ + click.style(
1560
+ " Would you like to run the interactive tutorial?", fg="cyan"
1561
+ ).center(65)
1562
+ + "│"
1563
+ )
1564
+ click.echo(
1565
+ " │" + " Recommended for new users - takes about 5 minutes".center(56) + "│"
1566
+ )
1184
1567
  click.echo(" └" + "─" * 56 + "┘")
1185
1568
  click.echo()
1186
- click.echo(" " + click.style("[Y]", fg='green', bold=True) + " Yes, show me around")
1187
- click.echo(" " + click.style("[n]", fg='bright_black') + " No, go to main menu")
1569
+ click.echo(
1570
+ " " + click.style("[Y]", fg="green", bold=True) + " Yes, show me around"
1571
+ )
1572
+ click.echo(" " + click.style("[n]", fg="bright_black") + " No, go to main menu")
1188
1573
  click.echo()
1189
1574
 
1190
- choice = click.prompt(" Run tutorial?", default='y', show_default=False).strip().lower()
1575
+ choice = (
1576
+ click.prompt(" Run tutorial?", default="y", show_default=False).strip().lower()
1577
+ )
1191
1578
 
1192
- if choice in ('y', 'yes', ''):
1579
+ if choice in ("y", "yes", ""):
1193
1580
  from souleyez.ui.tutorial import run_tutorial
1581
+
1194
1582
  run_tutorial()
1195
1583
  else:
1196
1584
  click.echo()
1197
- click.echo(" " + click.style("No problem!", fg='cyan') + " You can run the tutorial anytime from:")
1198
- click.echo(" " + click.style("Settings & Security → [t] Tutorial", fg='yellow'))
1585
+ click.echo(
1586
+ " "
1587
+ + click.style("No problem!", fg="cyan")
1588
+ + " You can run the tutorial anytime from:"
1589
+ )
1590
+ click.echo(
1591
+ " " + click.style("Settings & Security → [t] Tutorial", fg="yellow")
1592
+ )
1199
1593
  click.echo()
1200
1594
  click.pause(" Press any key to continue to main menu...")