souleyez 2.16.0__py3-none-any.whl → 2.22.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.
@@ -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,268 @@ 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 _run_tool_installs(
99
+ missing_tools: List[Dict],
100
+ wrong_version_tools: List[Dict],
101
+ distro: str
102
+ ) -> bool:
103
+ """
104
+ Prompt user and run install/upgrade commands.
105
+
106
+ Args:
107
+ missing_tools: List of missing tool dicts with 'name', 'install', 'tool_info'
108
+ wrong_version_tools: List of wrong version tool dicts
109
+ distro: Detected distribution
110
+
111
+ Returns:
112
+ True if any installs were run
113
+ """
114
+ from souleyez.utils.tool_checker import EXTERNAL_TOOLS, get_upgrade_command
115
+
116
+ all_installs = []
117
+
118
+ # Add wrong version tools (upgrades)
119
+ for tool in wrong_version_tools:
120
+ # Try to get upgrade command, fall back to install command
121
+ tool_info = tool.get('tool_info', {})
122
+ upgrade_cmd = get_upgrade_command(tool_info, distro) if tool_info else None
123
+ install_cmd = upgrade_cmd or tool.get('install', '')
124
+
125
+ all_installs.append({
126
+ 'name': tool['name'],
127
+ 'cmd': install_cmd,
128
+ 'action': 'upgrade',
129
+ 'tool_info': tool_info
130
+ })
131
+
132
+ # Add missing tools (installs)
133
+ for tool in missing_tools:
134
+ all_installs.append({
135
+ 'name': tool['name'],
136
+ 'cmd': tool['install'],
137
+ 'action': 'install',
138
+ 'tool_info': tool.get('tool_info', {})
139
+ })
140
+
141
+ if not all_installs:
142
+ return False
143
+
144
+ # Check disk space
145
+ if not _check_disk_space(500):
146
+ click.echo()
147
+ click.echo(f" {click.style('!', fg='yellow')} Low disk space (<500MB free). Installs may fail.")
148
+ if not click.confirm(" Continue anyway?", default=False):
149
+ return False
150
+
151
+ # Show what will be installed/upgraded
152
+ click.echo()
153
+ click.echo(f" {len(all_installs)} tool(s) to install/upgrade:")
154
+ for item in all_installs:
155
+ action_color = 'cyan' if item['action'] == 'upgrade' else 'green'
156
+ click.echo(f" - {item['name']} ({click.style(item['action'], fg=action_color)})")
157
+
158
+ click.echo()
159
+ if not click.confirm(" Install/upgrade now?", default=False):
160
+ click.echo(" Skipped. You can run 'souleyez setup' later to install tools.")
161
+ return False
162
+
163
+ # Request sudo upfront
164
+ click.echo()
165
+ click.echo(" Requesting sudo access...")
166
+ result = subprocess.run(['sudo', '-v'], check=False)
167
+ if result.returncode != 0:
168
+ click.echo(f" {click.style('x', fg='red')} Could not obtain sudo. Aborting installs.")
169
+ return False
170
+
171
+ # Update apt cache first (for apt-based installs)
172
+ has_apt_installs = any('apt' in item['cmd'] for item in all_installs)
173
+ if has_apt_installs:
174
+ click.echo()
175
+ click.echo(" Updating package lists...")
176
+ result = subprocess.run(['sudo', 'apt', 'update'], capture_output=True)
177
+ if result.returncode != 0:
178
+ click.echo(f" {click.style('!', fg='yellow')} apt update had issues, continuing anyway...")
179
+
180
+ # Track results for summary
181
+ results = []
182
+
183
+ # Run each install command
184
+ for item in all_installs:
185
+ click.echo()
186
+ click.echo(f" {click.style(item['action'].capitalize() + 'ing', fg='cyan')} {item['name']}...")
187
+
188
+ try:
189
+ # Run install command
190
+ proc = subprocess.run(
191
+ item['cmd'],
192
+ shell=True, # nosec B602 - commands from trusted EXTERNAL_TOOLS
193
+ timeout=600 # 10 minute timeout per tool
194
+ )
195
+
196
+ if proc.returncode == 0:
197
+ click.echo(f" {click.style('✓', fg='green')} {item['name']} {item['action']} complete")
198
+ results.append({'name': item['name'], 'success': True, 'tool_info': item['tool_info']})
199
+ else:
200
+ click.echo(f" {click.style('✗', fg='red')} {item['name']} failed (exit {proc.returncode})")
201
+ results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
202
+
203
+ except subprocess.TimeoutExpired:
204
+ click.echo(f" {click.style('✗', fg='red')} {item['name']} timed out")
205
+ results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
206
+ except Exception as e:
207
+ click.echo(f" {click.style('✗', fg='red')} {item['name']} error: {e}")
208
+ results.append({'name': item['name'], 'success': False, 'tool_info': item['tool_info']})
209
+
210
+ # Configure sudoers for privileged tools that installed successfully
211
+ click.echo()
212
+ click.echo(" Configuring permissions for privileged tools...")
213
+
214
+ sudoers_configured = False
215
+ for res in results:
216
+ if not res['success']:
217
+ continue
218
+
219
+ tool_info = res['tool_info']
220
+ if not tool_info.get('needs_sudo'):
221
+ continue
222
+
223
+ sudoers_configured = True
224
+ # Find the actual binary path
225
+ command = tool_info.get('command', res['name'])
226
+ tool_path = shutil.which(command)
227
+
228
+ # Check alt_commands if primary not found
229
+ if not tool_path and tool_info.get('alt_commands'):
230
+ for alt in tool_info['alt_commands']:
231
+ tool_path = shutil.which(alt)
232
+ if tool_path:
233
+ break
234
+
235
+ if tool_path:
236
+ if _configure_sudoers(res['name'], tool_path):
237
+ click.echo(f" {click.style('✓', fg='green')} {res['name']} configured for passwordless sudo")
238
+ else:
239
+ click.echo(f" {click.style('!', fg='yellow')} {res['name']} sudoers config failed")
240
+ else:
241
+ click.echo(f" {click.style('!', fg='yellow')} {res['name']} binary not found, skipping sudoers")
242
+
243
+ if not sudoers_configured:
244
+ click.echo(" No privileged tools needed configuration.")
245
+
246
+ # Re-verify installed tools
247
+ click.echo()
248
+ click.echo(" Verifying installations...")
249
+
250
+ success_count = 0
251
+ fail_count = 0
252
+
253
+ for res in results:
254
+ tool_info = res['tool_info']
255
+ command = tool_info.get('command', res['name'])
256
+ alt_commands = tool_info.get('alt_commands')
257
+
258
+ # Check if tool is now available
259
+ found = shutil.which(command) is not None
260
+ if not found and alt_commands:
261
+ for alt in alt_commands:
262
+ if shutil.which(alt):
263
+ found = True
264
+ break
265
+
266
+ if found:
267
+ click.echo(f" {click.style('✓', fg='green')} {res['name']} verified")
268
+ success_count += 1
269
+ else:
270
+ click.echo(f" {click.style('✗', fg='red')} {res['name']} not found after install")
271
+ fail_count += 1
272
+
273
+ # Summary
274
+ click.echo()
275
+ if fail_count == 0:
276
+ click.echo(f" {click.style('✓', fg='green')} All {success_count} tool(s) installed successfully!")
277
+ else:
278
+ click.echo(f" {click.style('!', fg='yellow')} {success_count} succeeded, {fail_count} failed")
279
+ click.echo(" Failed tools may need manual installation.")
280
+
281
+ return True
282
+
29
283
 
30
284
  def _show_wizard_banner():
31
285
  """Display the SoulEyez ASCII banner for wizard steps."""
32
286
  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))
287
+ click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▄██▄", fg='bright_blue', bold=True))
288
+ click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True) + click.style(" ▄█▀ ▀█▄", fg='bright_blue', bold=True))
289
+ click.echo(click.style(" ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" █ ◉ █", fg='bright_blue', bold=True))
290
+ click.echo(click.style(" ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" █ ═══ █", fg='bright_blue', bold=True))
291
+ click.echo(click.style(" ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▀█▄ ▄█▀", fg='bright_blue', bold=True))
292
+ click.echo(click.style(" ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝", fg='bright_cyan', bold=True) + click.style(" ▀██▀", fg='bright_blue', bold=True))
39
293
  click.echo()
40
294
  click.echo(click.style(" Created by CyberSoul SecurITy", fg='bright_blue'))
41
295
  click.echo()
@@ -44,7 +298,7 @@ def _show_wizard_banner():
44
298
  def run_setup_wizard() -> bool:
45
299
  """
46
300
  Run the setup wizard for new users.
47
-
301
+
48
302
  Returns:
49
303
  bool: True if wizard completed, False if skipped/cancelled
50
304
  """
@@ -53,20 +307,20 @@ def run_setup_wizard() -> bool:
53
307
  from souleyez.auth import get_current_user, Tier
54
308
  user = get_current_user()
55
309
  is_pro = user and user.tier == Tier.PRO
56
-
310
+
57
311
  # Step 1: Welcome
58
312
  if not _wizard_welcome(is_pro):
59
313
  mark_wizard_completed() # Mark as completed even if skipped
60
314
  return False
61
-
315
+
62
316
  # Step 2: Encryption Setup
63
317
  encryption_enabled = _wizard_encryption_setup()
64
-
318
+
65
319
  # Step 3: Create First Engagement
66
320
  engagement_info = _wizard_create_engagement()
67
321
  if not engagement_info:
68
322
  return False
69
-
323
+
70
324
  # Step 4: Tool Check
71
325
  tool_status = _wizard_tool_check()
72
326
 
@@ -78,7 +332,7 @@ def run_setup_wizard() -> bool:
78
332
  automation_prefs = _wizard_automation_prefs()
79
333
  else:
80
334
  automation_prefs = {'enabled': False, 'mode': None}
81
-
335
+
82
336
  # Step 7: Deliverable Templates (Pro only)
83
337
  if is_pro:
84
338
  templates = _wizard_deliverables(engagement_info.get('type'))
@@ -95,10 +349,10 @@ def run_setup_wizard() -> bool:
95
349
  templates,
96
350
  is_pro
97
351
  )
98
-
352
+
99
353
  mark_wizard_completed()
100
354
  return True
101
-
355
+
102
356
  except (KeyboardInterrupt, click.Abort):
103
357
  click.echo(click.style("\n\n Setup wizard cancelled.", fg='yellow'))
104
358
  mark_wizard_completed() # Don't show wizard again
@@ -110,13 +364,13 @@ def _wizard_welcome(is_pro: bool = False) -> bool:
110
364
  """Show welcome screen with ASCII banner."""
111
365
  DesignSystem.clear_screen()
112
366
  width = DesignSystem.get_terminal_width()
113
-
367
+
114
368
  # Header
115
369
  click.echo("\n┌" + "─" * (width - 2) + "┐")
116
370
  click.echo("│" + click.style(" WELCOME TO SOULEYEZ - SETUP WIZARD ".center(width - 2), bold=True, fg='cyan') + "│")
117
371
  click.echo("└" + "─" * (width - 2) + "┘")
118
372
  click.echo()
119
-
373
+
120
374
  # ASCII Art Banner - SOULEYEZ with all-seeing eye on the right
121
375
  click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▄██▄", fg='bright_blue', bold=True))
122
376
  click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True) + click.style(" ▄█▀ ▀█▄", fg='bright_blue', bold=True))
@@ -166,15 +420,15 @@ def _wizard_encryption_setup() -> bool:
166
420
  click.echo("│" + click.style(" STEP 2: ENCRYPTION SETUP ".center(width - 2), bold=True, fg='cyan') + "│")
167
421
  click.echo("└" + "─" * (width - 2) + "┘")
168
422
  click.echo()
169
-
423
+
170
424
  crypto = CryptoManager()
171
-
425
+
172
426
  if crypto.is_encryption_enabled():
173
427
  click.echo(" " + click.style("✓ Encryption already configured", fg='green'))
174
428
  click.echo()
175
429
  click.pause(" Press any key to continue...")
176
430
  return True
177
-
431
+
178
432
  click.echo(" SoulEyez encrypts all credentials (passwords, API keys, tokens)")
179
433
  click.echo(" with a master password. This is " + click.style("required", fg='green', bold=True) + " for security.")
180
434
  click.echo()
@@ -204,46 +458,46 @@ def _wizard_encryption_setup() -> bool:
204
458
  click.echo(" • At least one number")
205
459
  click.echo(" • At least one special character (!@#$%^&*)")
206
460
  click.echo()
207
- click.echo(" " + click.style("⚠️ If you lose this password, encrypted credentials cannot be recovered!",
461
+ click.echo(" " + click.style("⚠️ If you lose this password, encrypted credentials cannot be recovered!",
208
462
  fg='yellow', bold=True))
209
463
  click.echo()
210
-
464
+
211
465
  import re
212
466
  password_set = False
213
467
  while not password_set:
214
468
  password = getpass.getpass(" Enter master password: ")
215
-
469
+
216
470
  # Validate password strength
217
471
  if len(password) < 12:
218
472
  click.echo(click.style(" ✗ Password must be at least 12 characters.", fg='red'))
219
473
  continue
220
-
474
+
221
475
  if not re.search(r'[a-z]', password):
222
476
  click.echo(click.style(" ✗ Password must contain at least one lowercase letter.", fg='red'))
223
477
  continue
224
-
478
+
225
479
  if not re.search(r'[A-Z]', password):
226
480
  click.echo(click.style(" ✗ Password must contain at least one uppercase letter.", fg='red'))
227
481
  continue
228
-
482
+
229
483
  if not re.search(r'\d', password):
230
484
  click.echo(click.style(" ✗ Password must contain at least one number.", fg='red'))
231
485
  continue
232
-
486
+
233
487
  if not re.search(r'[!@#$%^&*()_+\-=\[\]{};:\'",.<>?/\\|`~]', password):
234
488
  click.echo(click.style(" ✗ Password must contain at least one special character.", fg='red'))
235
489
  continue
236
-
490
+
237
491
  password_confirm = getpass.getpass(" Confirm master password: ")
238
492
  if password != password_confirm:
239
493
  click.echo(click.style(" ✗ Passwords don't match!", fg='red'))
240
494
  continue
241
-
495
+
242
496
  password_set = True
243
-
497
+
244
498
  click.echo()
245
499
  click.echo(" Enabling encryption...")
246
-
500
+
247
501
  try:
248
502
  if crypto.enable_encryption(password):
249
503
  click.echo(" " + click.style("✓ Encryption enabled!", fg='green'))
@@ -255,7 +509,7 @@ def _wizard_encryption_setup() -> bool:
255
509
  click.echo(" " + click.style(f"✗ Error: {e}", fg='red'))
256
510
  click.pause(" Press any key to continue...")
257
511
  return False
258
-
512
+
259
513
  click.pause(" Press any key to continue...")
260
514
  return True
261
515
 
@@ -293,9 +547,9 @@ def _wizard_create_engagement() -> dict:
293
547
  click.echo()
294
548
  click.echo(" " + click.style("NOTE:", fg='yellow') + " Type affects default automation and scan aggressiveness")
295
549
  click.echo()
296
-
550
+
297
551
  type_choice = click.prompt(" Select option", type=click.IntRange(1, 5), default=1, show_default=False)
298
-
552
+
299
553
  engagement_types = {
300
554
  1: 'penetration_test',
301
555
  2: 'bug_bounty',
@@ -303,7 +557,7 @@ def _wizard_create_engagement() -> dict:
303
557
  4: 'red_team',
304
558
  5: 'custom'
305
559
  }
306
-
560
+
307
561
  engagement_type = engagement_types[type_choice]
308
562
 
309
563
  # Create engagement and set it as active
@@ -338,7 +592,8 @@ def _wizard_create_engagement() -> dict:
338
592
  def _wizard_tool_check() -> dict:
339
593
  """Check installed tools using the centralized tool_checker module."""
340
594
  from souleyez.utils.tool_checker import (
341
- check_tool_version, EXTERNAL_TOOLS, get_install_command, detect_distro
595
+ check_tool_version, EXTERNAL_TOOLS, get_install_command, detect_distro,
596
+ get_tool_version, get_upgrade_command
342
597
  )
343
598
 
344
599
  DesignSystem.clear_screen()
@@ -373,6 +628,7 @@ def _wizard_tool_check() -> dict:
373
628
  total = len(wizard_tools)
374
629
  tool_status = {}
375
630
  wrong_version_tools = []
631
+ missing_tools = []
376
632
 
377
633
  for display_name, (category, tool_key) in wizard_tools.items():
378
634
  tool_info = EXTERNAL_TOOLS[category][tool_key]
@@ -390,7 +646,6 @@ def _wizard_tool_check() -> dict:
390
646
  version_str = version_status['version']
391
647
  if not version_str:
392
648
  # Try to get version using the actual found command
393
- from souleyez.utils.tool_checker import get_tool_version
394
649
  version_str = get_tool_version(actual_cmd)
395
650
 
396
651
  # Check if version is OK
@@ -410,8 +665,9 @@ def _wizard_tool_check() -> dict:
410
665
  'name': display_name,
411
666
  'installed': ver_display,
412
667
  'required': min_ver,
413
- 'install': get_install_command(tool_info, distro),
414
- 'note': version_status.get('version_note')
668
+ 'install': get_upgrade_command(tool_info, distro) or get_install_command(tool_info, distro),
669
+ 'note': version_status.get('version_note'),
670
+ 'tool_info': tool_info
415
671
  })
416
672
  found += 1 # Still counts as found, just needs upgrade
417
673
  else:
@@ -423,6 +679,11 @@ def _wizard_tool_check() -> dict:
423
679
  else:
424
680
  click.echo(f" {click.style('✗', fg='red')} {display_name:<15} NOT FOUND")
425
681
  tool_status[display_name] = {'found': False, 'path': None}
682
+ missing_tools.append({
683
+ 'name': display_name,
684
+ 'install': get_install_command(tool_info, distro),
685
+ 'tool_info': tool_info
686
+ })
426
687
 
427
688
  click.echo()
428
689
  click.echo(f" Found {found}/{total} recommended tools")
@@ -435,17 +696,34 @@ def _wizard_tool_check() -> dict:
435
696
  click.echo(f" - {tool['name']}: installed v{tool['installed']}, needs v{tool['required']}+")
436
697
  if tool.get('note'):
437
698
  click.echo(f" {click.style(tool['note'], fg='bright_black')}")
699
+
700
+ # Offer to install/upgrade tools
701
+ if missing_tools or wrong_version_tools:
438
702
  click.echo()
439
- click.echo(" " + click.style("To upgrade:", fg='cyan'))
440
- for tool in wrong_version_tools:
441
- click.echo(f" {tool['install']}")
703
+ # Run the install flow (will prompt user)
704
+ if _run_tool_installs(missing_tools, wrong_version_tools, distro):
705
+ # Re-check tools after install
706
+ click.echo()
707
+ click.echo(" Re-checking tool availability...")
708
+ click.echo()
442
709
 
443
- click.echo()
710
+ found = 0
711
+ wrong_version_tools = []
712
+ for display_name, (category, tool_key) in wizard_tools.items():
713
+ tool_info = EXTERNAL_TOOLS[category][tool_key]
714
+ version_status = check_tool_version(tool_info)
715
+
716
+ if version_status['installed'] and not version_status['needs_upgrade']:
717
+ tool_status[display_name] = {'found': True, 'path': shutil.which(tool_info['command'])}
718
+ found += 1
719
+ elif version_status['installed'] and version_status['needs_upgrade']:
720
+ tool_status[display_name] = {'found': True, 'needs_upgrade': True}
721
+ wrong_version_tools.append({'name': display_name})
722
+ found += 1
723
+ else:
724
+ tool_status[display_name] = {'found': False, 'path': None}
444
725
 
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.")
726
+ click.echo(f" Now have {found}/{total} tools available")
449
727
 
450
728
  click.echo()
451
729
  click.pause(" Press any key to continue...")
@@ -722,7 +1000,7 @@ def _wizard_deliverables(engagement_type: str) -> list:
722
1000
  DesignSystem.clear_screen()
723
1001
  _show_wizard_banner()
724
1002
  width = 60
725
-
1003
+
726
1004
  click.echo("┌" + "─" * (width - 2) + "┐")
727
1005
  click.echo("│" + click.style(" STEP 7: REPORT TEMPLATES ".center(width - 2), bold=True, fg='cyan') + "│")
728
1006
  click.echo("└" + "─" * (width - 2) + "┘")
@@ -732,7 +1010,7 @@ def _wizard_deliverables(engagement_type: str) -> list:
732
1010
  click.echo()
733
1011
  click.echo(" " + click.style("NOTE:", fg='yellow') + " Templates help generate professional reports from your findings")
734
1012
  click.echo()
735
-
1013
+
736
1014
  # Default templates based on engagement type
737
1015
  type_templates = {
738
1016
  'penetration_test': ['Executive Summary', 'Technical Findings Report', 'Vulnerability Details'],
@@ -741,9 +1019,9 @@ def _wizard_deliverables(engagement_type: str) -> list:
741
1019
  'red_team': ['Attack Narrative', 'Remediation Roadmap'],
742
1020
  'custom': ['Technical Findings Report']
743
1021
  }
744
-
1022
+
745
1023
  recommended = type_templates.get(engagement_type, ['Technical Findings Report'])
746
-
1024
+
747
1025
  click.echo(f" Recommended for {engagement_type.replace('_', ' ').title()}:")
748
1026
  for template in recommended:
749
1027
  click.echo(f" {click.style('✓', fg='green')} {template}")
@@ -785,7 +1063,7 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
785
1063
  click.echo(f" {click.style('✓', fg='green')} AI Features: Enabled (Ollama)")
786
1064
  else:
787
1065
  click.echo(f" {click.style('○', fg='bright_black')} AI Features: Not configured")
788
-
1066
+
789
1067
  # Show tier status
790
1068
  if is_pro:
791
1069
  click.echo(f" {click.style('💎', fg='magenta')} License: PRO")
@@ -794,7 +1072,7 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
794
1072
  click.echo(f" {click.style('✓', fg='green')} Auto-Chain: ON ({auto_mode} mode)")
795
1073
  else:
796
1074
  click.echo(f" {click.style('✓', fg='green')} Auto-Chain: OFF")
797
-
1075
+
798
1076
  if templates:
799
1077
  click.echo(f" {click.style('✓', fg='green')} Templates: {len(templates)} selected")
800
1078
  else:
@@ -806,7 +1084,7 @@ def _wizard_summary(encryption_enabled, engagement_info, tool_status, ai_enabled
806
1084
  click.echo(" • MSF Integration - Advanced attack chains")
807
1085
  click.echo(" • Reports - Professional deliverables")
808
1086
  click.echo(f" {click.style('→ cybersoulsecurity.com/upgrade', fg='cyan')}")
809
-
1087
+
810
1088
  click.echo()
811
1089
  click.echo(" " + click.style("You're ready to start!", bold=True, fg='cyan'))
812
1090
  click.echo()