souleyez 2.16.0__py3-none-any.whl → 2.26.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 (38) hide show
  1. souleyez/__init__.py +1 -1
  2. souleyez/assets/__init__.py +1 -0
  3. souleyez/assets/souleyez-icon.png +0 -0
  4. souleyez/core/msf_sync_manager.py +15 -5
  5. souleyez/core/tool_chaining.py +221 -29
  6. souleyez/detection/validator.py +4 -2
  7. souleyez/docs/README.md +2 -2
  8. souleyez/docs/user-guide/installation.md +14 -1
  9. souleyez/engine/background.py +25 -1
  10. souleyez/engine/result_handler.py +129 -0
  11. souleyez/integrations/siem/splunk.py +58 -11
  12. souleyez/main.py +103 -4
  13. souleyez/parsers/crackmapexec_parser.py +101 -43
  14. souleyez/parsers/dnsrecon_parser.py +50 -35
  15. souleyez/parsers/enum4linux_parser.py +101 -21
  16. souleyez/parsers/http_fingerprint_parser.py +319 -0
  17. souleyez/parsers/hydra_parser.py +56 -5
  18. souleyez/parsers/impacket_parser.py +123 -44
  19. souleyez/parsers/john_parser.py +47 -14
  20. souleyez/parsers/msf_parser.py +20 -5
  21. souleyez/parsers/nmap_parser.py +145 -28
  22. souleyez/parsers/smbmap_parser.py +69 -25
  23. souleyez/parsers/sqlmap_parser.py +72 -26
  24. souleyez/parsers/theharvester_parser.py +21 -13
  25. souleyez/plugins/gobuster.py +96 -3
  26. souleyez/plugins/http_fingerprint.py +592 -0
  27. souleyez/plugins/msf_exploit.py +6 -3
  28. souleyez/plugins/nuclei.py +41 -17
  29. souleyez/ui/interactive.py +130 -20
  30. souleyez/ui/setup_wizard.py +424 -58
  31. souleyez/ui/tool_setup.py +52 -52
  32. souleyez/utils/tool_checker.py +75 -13
  33. {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/METADATA +16 -3
  34. {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/RECORD +38 -34
  35. {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/WHEEL +0 -0
  36. {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/entry_points.txt +0 -0
  37. {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/licenses/LICENSE +0 -0
  38. {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,9 @@ import time
7
7
  import click
8
8
  import getpass
9
9
  import shutil
10
+ import subprocess
10
11
  from pathlib import Path
12
+ from typing import List, Dict, Optional
11
13
  from souleyez.ui.design_system import DesignSystem
12
14
 
13
15
 
@@ -26,16 +28,336 @@ def mark_wizard_completed():
26
28
  WIZARD_STATE_FILE.touch()
27
29
 
28
30
 
31
+ def _check_disk_space(min_mb: int = 500) -> bool:
32
+ """Check if there's enough disk space for installs."""
33
+ try:
34
+ usage = shutil.disk_usage('/')
35
+ free_mb = usage.free // (1024 * 1024)
36
+ return free_mb >= min_mb
37
+ except Exception:
38
+ return True # Assume OK if we can't check
39
+
40
+
41
+ def _configure_sudoers(tool_name: str, tool_path: str) -> bool:
42
+ """
43
+ Add NOPASSWD sudoers entry for privileged tool.
44
+
45
+ Args:
46
+ tool_name: Name of the tool (used for sudoers filename)
47
+ tool_path: Absolute path to tool binary
48
+
49
+ Returns:
50
+ True if configured successfully, False otherwise
51
+ """
52
+ # Skip if running as root
53
+ if os.geteuid() == 0:
54
+ return True
55
+
56
+ username = getpass.getuser()
57
+ sudoers_file = f"/etc/sudoers.d/{tool_name}"
58
+ sudoers_line = f"{username} ALL=(ALL) NOPASSWD: {tool_path}"
59
+ tmp_file = f"/tmp/sudoers_{tool_name}_{os.getpid()}"
60
+
61
+ try:
62
+ # Write to temp file first
63
+ with open(tmp_file, 'w') as f:
64
+ f.write(sudoers_line + '\n')
65
+
66
+ # Validate syntax with visudo before applying
67
+ result = subprocess.run(
68
+ ['sudo', 'visudo', '-c', '-f', tmp_file],
69
+ capture_output=True,
70
+ timeout=30
71
+ )
72
+
73
+ if result.returncode != 0:
74
+ click.echo(f" {click.style('!', fg='yellow')} Invalid sudoers syntax for {tool_name}, skipping")
75
+ os.unlink(tmp_file)
76
+ return False
77
+
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)
81
+
82
+ return True
83
+
84
+ except subprocess.TimeoutExpired:
85
+ click.echo(f" {click.style('!', fg='yellow')} Timeout configuring sudoers for {tool_name}")
86
+ return False
87
+ except Exception as e:
88
+ click.echo(f" {click.style('!', fg='yellow')} Error configuring sudoers for {tool_name}: {e}")
89
+ # Clean up temp file if it exists
90
+ if os.path.exists(tmp_file):
91
+ try:
92
+ os.unlink(tmp_file)
93
+ except Exception:
94
+ pass
95
+ return False
96
+
97
+
98
+ def _install_desktop_shortcut():
99
+ """
100
+ Install desktop shortcut for SoulEyez in Applications menu.
101
+
102
+ This runs silently during setup - any errors are ignored to not
103
+ disrupt the setup flow.
104
+ """
105
+ 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
+
111
+ # Skip if already installed
112
+ if desktop_file.exists():
113
+ return
114
+
115
+ # Create directories
116
+ applications_dir.mkdir(parents=True, exist_ok=True)
117
+ icons_dir.mkdir(parents=True, exist_ok=True)
118
+
119
+ # Find and copy icon
120
+ try:
121
+ # Try importlib.resources first (Python 3.9+)
122
+ try:
123
+ from importlib.resources import files
124
+ icon_source = files('souleyez.assets').joinpath('souleyez-icon.png')
125
+ with open(icon_source, 'rb') as src:
126
+ icon_data = src.read()
127
+ except (ImportError, TypeError, FileNotFoundError):
128
+ # 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:
131
+ icon_data = src.read()
132
+
133
+ with open(icon_dest, 'wb') as dst:
134
+ dst.write(icon_data)
135
+ except Exception:
136
+ icon_dest = "utilities-terminal" # Fallback to system icon
137
+
138
+ # Create .desktop file
139
+ desktop_content = f"""[Desktop Entry]
140
+ Name=SoulEyez
141
+ Comment=AI-Powered Penetration Testing Platform
142
+ Exec=souleyez interactive
143
+ Icon={icon_dest}
144
+ Terminal=true
145
+ Type=Application
146
+ Categories=Security;System;Network;
147
+ Keywords=pentest;security;hacking;nmap;metasploit;
148
+ """
149
+
150
+ desktop_file.write_text(desktop_content)
151
+
152
+ # Update desktop database (optional)
153
+ try:
154
+ subprocess.run(['update-desktop-database', str(applications_dir)],
155
+ capture_output=True, check=False, timeout=5)
156
+ except Exception:
157
+ pass
158
+
159
+ click.echo(f" {click.style('✓', fg='green')} Desktop shortcut: Added to Applications menu")
160
+
161
+ except Exception:
162
+ # Silently ignore errors - desktop shortcut is nice-to-have
163
+ pass
164
+
165
+
166
+ def _run_tool_installs(
167
+ missing_tools: List[Dict],
168
+ wrong_version_tools: List[Dict],
169
+ distro: str
170
+ ) -> bool:
171
+ """
172
+ Prompt user and run install/upgrade commands.
173
+
174
+ Args:
175
+ missing_tools: List of missing tool dicts with 'name', 'install', 'tool_info'
176
+ wrong_version_tools: List of wrong version tool dicts
177
+ distro: Detected distribution
178
+
179
+ Returns:
180
+ True if any installs were run
181
+ """
182
+ from souleyez.utils.tool_checker import EXTERNAL_TOOLS, get_upgrade_command
183
+
184
+ all_installs = []
185
+
186
+ # Add wrong version tools (upgrades)
187
+ for tool in wrong_version_tools:
188
+ # Try to get upgrade command, fall back to install command
189
+ tool_info = tool.get('tool_info', {})
190
+ 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
+ })
199
+
200
+ # Add missing tools (installs)
201
+ 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
+ })
208
+
209
+ if not all_installs:
210
+ return False
211
+
212
+ # Check disk space
213
+ if not _check_disk_space(500):
214
+ click.echo()
215
+ click.echo(f" {click.style('!', fg='yellow')} Low disk space (<500MB free). Installs may fail.")
216
+ if not click.confirm(" Continue anyway?", default=False):
217
+ return False
218
+
219
+ # Show what will be installed/upgraded
220
+ click.echo()
221
+ click.echo(f" {len(all_installs)} tool(s) to install/upgrade:")
222
+ 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)})")
225
+
226
+ click.echo()
227
+ if not click.confirm(" Install/upgrade now?", default=False):
228
+ click.echo(" Skipped. You can run 'souleyez setup' later to install tools.")
229
+ return False
230
+
231
+ # Request sudo upfront
232
+ click.echo()
233
+ click.echo(" Requesting sudo access...")
234
+ result = subprocess.run(['sudo', '-v'], check=False)
235
+ if result.returncode != 0:
236
+ click.echo(f" {click.style('x', fg='red')} Could not obtain sudo. Aborting installs.")
237
+ return False
238
+
239
+ # Update apt cache first (for apt-based installs)
240
+ has_apt_installs = any('apt' in item['cmd'] for item in all_installs)
241
+ if has_apt_installs:
242
+ click.echo()
243
+ click.echo(" Updating package lists...")
244
+ result = subprocess.run(['sudo', 'apt', 'update'], capture_output=True)
245
+ if result.returncode != 0:
246
+ click.echo(f" {click.style('!', fg='yellow')} apt update had issues, continuing anyway...")
247
+
248
+ # Track results for summary
249
+ results = []
250
+
251
+ # Run each install command
252
+ for item in all_installs:
253
+ click.echo()
254
+ click.echo(f" {click.style(item['action'].capitalize() + 'ing', fg='cyan')} {item['name']}...")
255
+
256
+ try:
257
+ # Run install command
258
+ proc = subprocess.run(
259
+ item['cmd'],
260
+ shell=True, # nosec B602 - commands from trusted EXTERNAL_TOOLS
261
+ timeout=600 # 10 minute timeout per tool
262
+ )
263
+
264
+ 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']})
267
+ 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']})
270
+
271
+ except subprocess.TimeoutExpired:
272
+ click.echo(f" {click.style('✗', fg='red')} {item['name']} timed out")
273
+ results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
274
+ except Exception as e:
275
+ click.echo(f" {click.style('✗', fg='red')} {item['name']} error: {e}")
276
+ results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
277
+
278
+ # Configure sudoers for privileged tools that installed successfully
279
+ click.echo()
280
+ click.echo(" Configuring permissions for privileged tools...")
281
+
282
+ sudoers_configured = False
283
+ for res in results:
284
+ if not res['success']:
285
+ continue
286
+
287
+ tool_info = res['tool_info']
288
+ if not tool_info.get('needs_sudo'):
289
+ continue
290
+
291
+ sudoers_configured = True
292
+ # Find the actual binary path
293
+ command = tool_info.get('command', res['name'])
294
+ tool_path = shutil.which(command)
295
+
296
+ # 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']:
299
+ tool_path = shutil.which(alt)
300
+ if tool_path:
301
+ break
302
+
303
+ 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")
306
+ else:
307
+ click.echo(f" {click.style('!', fg='yellow')} {res['name']} sudoers config failed")
308
+ else:
309
+ click.echo(f" {click.style('!', fg='yellow')} {res['name']} binary not found, skipping sudoers")
310
+
311
+ if not sudoers_configured:
312
+ click.echo(" No privileged tools needed configuration.")
313
+
314
+ # Re-verify installed tools
315
+ click.echo()
316
+ click.echo(" Verifying installations...")
317
+
318
+ success_count = 0
319
+ fail_count = 0
320
+
321
+ 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')
325
+
326
+ # Check if tool is now available
327
+ found = shutil.which(command) is not None
328
+ if not found and alt_commands:
329
+ for alt in alt_commands:
330
+ if shutil.which(alt):
331
+ found = True
332
+ break
333
+
334
+ if found:
335
+ click.echo(f" {click.style('✓', fg='green')} {res['name']} verified")
336
+ success_count += 1
337
+ else:
338
+ click.echo(f" {click.style('✗', fg='red')} {res['name']} not found after install")
339
+ fail_count += 1
340
+
341
+ # Summary
342
+ click.echo()
343
+ if fail_count == 0:
344
+ click.echo(f" {click.style('✓', fg='green')} All {success_count} tool(s) installed successfully!")
345
+ else:
346
+ click.echo(f" {click.style('!', fg='yellow')} {success_count} succeeded, {fail_count} failed")
347
+ click.echo(" Failed tools may need manual installation.")
348
+
349
+ return True
350
+
29
351
 
30
352
  def _show_wizard_banner():
31
353
  """Display the SoulEyez ASCII banner for wizard steps."""
32
354
  click.echo()
33
- click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True))
34
- click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True))
35
- click.echo(click.style(" ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ", fg='bright_cyan', bold=True))
36
- click.echo(click.style(" ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ", fg='bright_cyan', bold=True))
37
- click.echo(click.style(" ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗", fg='bright_cyan', bold=True))
38
- click.echo(click.style(" ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝", fg='bright_cyan', bold=True))
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))
39
361
  click.echo()
40
362
  click.echo(click.style(" Created by CyberSoul SecurITy", fg='bright_blue'))
41
363
  click.echo()
@@ -44,7 +366,7 @@ def _show_wizard_banner():
44
366
  def run_setup_wizard() -> bool:
45
367
  """
46
368
  Run the setup wizard for new users.
47
-
369
+
48
370
  Returns:
49
371
  bool: True if wizard completed, False if skipped/cancelled
50
372
  """
@@ -53,20 +375,20 @@ def run_setup_wizard() -> bool:
53
375
  from souleyez.auth import get_current_user, Tier
54
376
  user = get_current_user()
55
377
  is_pro = user and user.tier == Tier.PRO
56
-
378
+
57
379
  # Step 1: Welcome
58
380
  if not _wizard_welcome(is_pro):
59
381
  mark_wizard_completed() # Mark as completed even if skipped
60
382
  return False
61
-
383
+
62
384
  # Step 2: Encryption Setup
63
385
  encryption_enabled = _wizard_encryption_setup()
64
-
386
+
65
387
  # Step 3: Create First Engagement
66
388
  engagement_info = _wizard_create_engagement()
67
389
  if not engagement_info:
68
390
  return False
69
-
391
+
70
392
  # Step 4: Tool Check
71
393
  tool_status = _wizard_tool_check()
72
394
 
@@ -78,7 +400,7 @@ def run_setup_wizard() -> bool:
78
400
  automation_prefs = _wizard_automation_prefs()
79
401
  else:
80
402
  automation_prefs = {'enabled': False, 'mode': None}
81
-
403
+
82
404
  # Step 7: Deliverable Templates (Pro only)
83
405
  if is_pro:
84
406
  templates = _wizard_deliverables(engagement_info.get('type'))
@@ -95,10 +417,10 @@ def run_setup_wizard() -> bool:
95
417
  templates,
96
418
  is_pro
97
419
  )
98
-
420
+
99
421
  mark_wizard_completed()
100
422
  return True
101
-
423
+
102
424
  except (KeyboardInterrupt, click.Abort):
103
425
  click.echo(click.style("\n\n Setup wizard cancelled.", fg='yellow'))
104
426
  mark_wizard_completed() # Don't show wizard again
@@ -110,13 +432,13 @@ def _wizard_welcome(is_pro: bool = False) -> bool:
110
432
  """Show welcome screen with ASCII banner."""
111
433
  DesignSystem.clear_screen()
112
434
  width = DesignSystem.get_terminal_width()
113
-
435
+
114
436
  # Header
115
437
  click.echo("\n┌" + "─" * (width - 2) + "┐")
116
438
  click.echo("│" + click.style(" WELCOME TO SOULEYEZ - SETUP WIZARD ".center(width - 2), bold=True, fg='cyan') + "│")
117
439
  click.echo("└" + "─" * (width - 2) + "┘")
118
440
  click.echo()
119
-
441
+
120
442
  # ASCII Art Banner - SOULEYEZ with all-seeing eye on the right
121
443
  click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▄██▄", fg='bright_blue', bold=True))
122
444
  click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True) + click.style(" ▄█▀ ▀█▄", fg='bright_blue', bold=True))
@@ -166,15 +488,15 @@ def _wizard_encryption_setup() -> bool:
166
488
  click.echo("│" + click.style(" STEP 2: ENCRYPTION SETUP ".center(width - 2), bold=True, fg='cyan') + "│")
167
489
  click.echo("└" + "─" * (width - 2) + "┘")
168
490
  click.echo()
169
-
491
+
170
492
  crypto = CryptoManager()
171
-
493
+
172
494
  if crypto.is_encryption_enabled():
173
495
  click.echo(" " + click.style("✓ Encryption already configured", fg='green'))
174
496
  click.echo()
175
497
  click.pause(" Press any key to continue...")
176
498
  return True
177
-
499
+
178
500
  click.echo(" SoulEyez encrypts all credentials (passwords, API keys, tokens)")
179
501
  click.echo(" with a master password. This is " + click.style("required", fg='green', bold=True) + " for security.")
180
502
  click.echo()
@@ -204,46 +526,46 @@ def _wizard_encryption_setup() -> bool:
204
526
  click.echo(" • At least one number")
205
527
  click.echo(" • At least one special character (!@#$%^&*)")
206
528
  click.echo()
207
- click.echo(" " + click.style("⚠️ If you lose this password, encrypted credentials cannot be recovered!",
529
+ click.echo(" " + click.style("⚠️ If you lose this password, encrypted credentials cannot be recovered!",
208
530
  fg='yellow', bold=True))
209
531
  click.echo()
210
-
532
+
211
533
  import re
212
534
  password_set = False
213
535
  while not password_set:
214
536
  password = getpass.getpass(" Enter master password: ")
215
-
537
+
216
538
  # Validate password strength
217
539
  if len(password) < 12:
218
540
  click.echo(click.style(" ✗ Password must be at least 12 characters.", fg='red'))
219
541
  continue
220
-
542
+
221
543
  if not re.search(r'[a-z]', password):
222
544
  click.echo(click.style(" ✗ Password must contain at least one lowercase letter.", fg='red'))
223
545
  continue
224
-
546
+
225
547
  if not re.search(r'[A-Z]', password):
226
548
  click.echo(click.style(" ✗ Password must contain at least one uppercase letter.", fg='red'))
227
549
  continue
228
-
550
+
229
551
  if not re.search(r'\d', password):
230
552
  click.echo(click.style(" ✗ Password must contain at least one number.", fg='red'))
231
553
  continue
232
-
554
+
233
555
  if not re.search(r'[!@#$%^&*()_+\-=\[\]{};:\'",.<>?/\\|`~]', password):
234
556
  click.echo(click.style(" ✗ Password must contain at least one special character.", fg='red'))
235
557
  continue
236
-
558
+
237
559
  password_confirm = getpass.getpass(" Confirm master password: ")
238
560
  if password != password_confirm:
239
561
  click.echo(click.style(" ✗ Passwords don't match!", fg='red'))
240
562
  continue
241
-
563
+
242
564
  password_set = True
243
-
565
+
244
566
  click.echo()
245
567
  click.echo(" Enabling encryption...")
246
-
568
+
247
569
  try:
248
570
  if crypto.enable_encryption(password):
249
571
  click.echo(" " + click.style("✓ Encryption enabled!", fg='green'))
@@ -255,7 +577,7 @@ def _wizard_encryption_setup() -> bool:
255
577
  click.echo(" " + click.style(f"✗ Error: {e}", fg='red'))
256
578
  click.pause(" Press any key to continue...")
257
579
  return False
258
-
580
+
259
581
  click.pause(" Press any key to continue...")
260
582
  return True
261
583
 
@@ -293,9 +615,9 @@ def _wizard_create_engagement() -> dict:
293
615
  click.echo()
294
616
  click.echo(" " + click.style("NOTE:", fg='yellow') + " Type affects default automation and scan aggressiveness")
295
617
  click.echo()
296
-
618
+
297
619
  type_choice = click.prompt(" Select option", type=click.IntRange(1, 5), default=1, show_default=False)
298
-
620
+
299
621
  engagement_types = {
300
622
  1: 'penetration_test',
301
623
  2: 'bug_bounty',
@@ -303,7 +625,7 @@ def _wizard_create_engagement() -> dict:
303
625
  4: 'red_team',
304
626
  5: 'custom'
305
627
  }
306
-
628
+
307
629
  engagement_type = engagement_types[type_choice]
308
630
 
309
631
  # Create engagement and set it as active
@@ -338,7 +660,8 @@ def _wizard_create_engagement() -> dict:
338
660
  def _wizard_tool_check() -> dict:
339
661
  """Check installed tools using the centralized tool_checker module."""
340
662
  from souleyez.utils.tool_checker import (
341
- check_tool_version, EXTERNAL_TOOLS, get_install_command, detect_distro
663
+ check_tool_version, EXTERNAL_TOOLS, get_install_command, detect_distro,
664
+ get_tool_version, get_upgrade_command
342
665
  )
343
666
 
344
667
  DesignSystem.clear_screen()
@@ -373,6 +696,7 @@ def _wizard_tool_check() -> dict:
373
696
  total = len(wizard_tools)
374
697
  tool_status = {}
375
698
  wrong_version_tools = []
699
+ missing_tools = []
376
700
 
377
701
  for display_name, (category, tool_key) in wizard_tools.items():
378
702
  tool_info = EXTERNAL_TOOLS[category][tool_key]
@@ -390,7 +714,6 @@ def _wizard_tool_check() -> dict:
390
714
  version_str = version_status['version']
391
715
  if not version_str:
392
716
  # Try to get version using the actual found command
393
- from souleyez.utils.tool_checker import get_tool_version
394
717
  version_str = get_tool_version(actual_cmd)
395
718
 
396
719
  # Check if version is OK
@@ -410,8 +733,9 @@ def _wizard_tool_check() -> dict:
410
733
  'name': display_name,
411
734
  'installed': ver_display,
412
735
  'required': min_ver,
413
- 'install': get_install_command(tool_info, distro),
414
- 'note': version_status.get('version_note')
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
415
739
  })
416
740
  found += 1 # Still counts as found, just needs upgrade
417
741
  else:
@@ -423,6 +747,11 @@ def _wizard_tool_check() -> dict:
423
747
  else:
424
748
  click.echo(f" {click.style('✗', fg='red')} {display_name:<15} NOT FOUND")
425
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
+ })
426
755
 
427
756
  click.echo()
428
757
  click.echo(f" Found {found}/{total} recommended tools")
@@ -435,17 +764,34 @@ def _wizard_tool_check() -> dict:
435
764
  click.echo(f" - {tool['name']}: installed v{tool['installed']}, needs v{tool['required']}+")
436
765
  if tool.get('note'):
437
766
  click.echo(f" {click.style(tool['note'], fg='bright_black')}")
767
+
768
+ # Offer to install/upgrade tools
769
+ if missing_tools or wrong_version_tools:
438
770
  click.echo()
439
- click.echo(" " + click.style("To upgrade:", fg='cyan'))
440
- for tool in wrong_version_tools:
441
- click.echo(f" {tool['install']}")
771
+ # Run the install flow (will prompt user)
772
+ if _run_tool_installs(missing_tools, wrong_version_tools, distro):
773
+ # Re-check tools after install
774
+ click.echo()
775
+ click.echo(" Re-checking tool availability...")
776
+ click.echo()
442
777
 
443
- click.echo()
778
+ found = 0
779
+ wrong_version_tools = []
780
+ for display_name, (category, tool_key) in wizard_tools.items():
781
+ tool_info = EXTERNAL_TOOLS[category][tool_key]
782
+ version_status = check_tool_version(tool_info)
783
+
784
+ if version_status['installed'] and not version_status['needs_upgrade']:
785
+ tool_status[display_name] = {'found': True, 'path': shutil.which(tool_info['command'])}
786
+ 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})
790
+ found += 1
791
+ else:
792
+ tool_status[display_name] = {'found': False, 'path': None}
444
793
 
445
- if found < total:
446
- click.echo(" " + click.style("TIP:", fg='yellow', bold=True) +
447
- " You can install missing tools later. SoulEyez will work")
448
- click.echo(" with the tools you have available.")
794
+ click.echo(f" Now have {found}/{total} tools available")
449
795
 
450
796
  click.echo()
451
797
  click.pause(" Press any key to continue...")
@@ -556,14 +902,16 @@ def _install_ollama() -> bool:
556
902
 
557
903
  click.echo()
558
904
  click.echo(" Installing Ollama...")
559
- click.echo(" " + click.style("(This may take a minute)", fg='bright_black'))
905
+ click.echo(" " + click.style("(This may take a minute - downloading ~100MB)", fg='bright_black'))
560
906
  click.echo()
561
907
 
562
908
  try:
563
- # Run the official Ollama install script - let it output directly to terminal
909
+ # Run the official Ollama install script with timeout
910
+ # Add curl timeouts to fail faster on network issues
564
911
  result = subprocess.run(
565
- ['bash', '-c', 'curl -fsSL https://ollama.ai/install.sh | sh'],
566
- check=False
912
+ ['bash', '-c', 'curl --connect-timeout 30 --max-time 300 -fsSL https://ollama.ai/install.sh | sh'],
913
+ check=False,
914
+ timeout=360 # 6 minute total timeout
567
915
  )
568
916
 
569
917
  if result.returncode == 0:
@@ -575,10 +923,25 @@ def _install_ollama() -> bool:
575
923
  else:
576
924
  click.echo()
577
925
  click.echo(" " + click.style("✗ Installation failed", fg='red'))
578
- click.echo(" You can install manually from https://ollama.ai")
926
+ click.echo(" " + click.style("This is usually a network issue (slow connection or timeout).", fg='yellow'))
927
+ click.echo()
928
+ click.echo(" To install manually:")
929
+ click.echo(" curl -fsSL https://ollama.ai/install.sh | sh")
930
+ click.echo()
931
+ click.echo(" Or visit: https://ollama.ai")
579
932
  click.pause(" Press any key to continue...")
580
933
  return False
581
934
 
935
+ except subprocess.TimeoutExpired:
936
+ 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'))
939
+ click.echo()
940
+ click.echo(" To install manually:")
941
+ click.echo(" curl -fsSL https://ollama.ai/install.sh | sh")
942
+ click.pause(" Press any key to continue...")
943
+ return False
944
+
582
945
  except Exception as e:
583
946
  click.echo()
584
947
  click.echo(click.style(f" ✗ Error: {e}", fg='red'))
@@ -722,7 +1085,7 @@ def _wizard_deliverables(engagement_type: str) -> list:
722
1085
  DesignSystem.clear_screen()
723
1086
  _show_wizard_banner()
724
1087
  width = 60
725
-
1088
+
726
1089
  click.echo("┌" + "─" * (width - 2) + "┐")
727
1090
  click.echo("│" + click.style(" STEP 7: REPORT TEMPLATES ".center(width - 2), bold=True, fg='cyan') + "│")
728
1091
  click.echo("└" + "─" * (width - 2) + "┘")
@@ -732,7 +1095,7 @@ def _wizard_deliverables(engagement_type: str) -> list:
732
1095
  click.echo()
733
1096
  click.echo(" " + click.style("NOTE:", fg='yellow') + " Templates help generate professional reports from your findings")
734
1097
  click.echo()
735
-
1098
+
736
1099
  # Default templates based on engagement type
737
1100
  type_templates = {
738
1101
  'penetration_test': ['Executive Summary', 'Technical Findings Report', 'Vulnerability Details'],
@@ -741,9 +1104,9 @@ def _wizard_deliverables(engagement_type: str) -> list:
741
1104
  'red_team': ['Attack Narrative', 'Remediation Roadmap'],
742
1105
  'custom': ['Technical Findings Report']
743
1106
  }
744
-
1107
+
745
1108
  recommended = type_templates.get(engagement_type, ['Technical Findings Report'])
746
-
1109
+
747
1110
  click.echo(f" Recommended for {engagement_type.replace('_', ' ').title()}:")
748
1111
  for template in recommended:
749
1112
  click.echo(f" {click.style('✓', fg='green')} {template}")
@@ -785,7 +1148,7 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
785
1148
  click.echo(f" {click.style('✓', fg='green')} AI Features: Enabled (Ollama)")
786
1149
  else:
787
1150
  click.echo(f" {click.style('○', fg='bright_black')} AI Features: Not configured")
788
-
1151
+
789
1152
  # Show tier status
790
1153
  if is_pro:
791
1154
  click.echo(f" {click.style('💎', fg='magenta')} License: PRO")
@@ -794,7 +1157,7 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
794
1157
  click.echo(f" {click.style('✓', fg='green')} Auto-Chain: ON ({auto_mode} mode)")
795
1158
  else:
796
1159
  click.echo(f" {click.style('✓', fg='green')} Auto-Chain: OFF")
797
-
1160
+
798
1161
  if templates:
799
1162
  click.echo(f" {click.style('✓', fg='green')} Templates: {len(templates)} selected")
800
1163
  else:
@@ -806,11 +1169,14 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
806
1169
  click.echo(" • MSF Integration - Advanced attack chains")
807
1170
  click.echo(" • Reports - Professional deliverables")
808
1171
  click.echo(f" {click.style('→ cybersoulsecurity.com/upgrade', fg='cyan')}")
809
-
1172
+
810
1173
  click.echo()
811
1174
  click.echo(" " + click.style("You're ready to start!", bold=True, fg='cyan'))
812
1175
  click.echo()
813
1176
 
1177
+ # Install desktop shortcut automatically
1178
+ _install_desktop_shortcut()
1179
+
814
1180
  # Prompt for interactive tutorial
815
1181
  click.echo(" ┌" + "─" * 56 + "┐")
816
1182
  click.echo(" │" + click.style(" Would you like to run the interactive tutorial?", fg='cyan').center(65) + "│")