souleyez 2.43.12__py3-none-any.whl → 2.43.15__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.

Potentially problematic release.


This version of souleyez might be problematic. Click here for more details.

souleyez/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = '2.43.12'
1
+ __version__ = '2.43.15'
2
2
 
@@ -4451,6 +4451,58 @@ class ToolChaining:
4451
4451
  return job_ids
4452
4452
  # === END Wildcard Auto-Retry ===
4453
4453
 
4454
+ # === Host Redirect Auto-Retry Logic ===
4455
+ # If gobuster detected a host-level redirect (e.g., non-www to www), auto-retry with corrected target
4456
+ if parse_results.get('host_redirect_detected') and parse_results.get('redirect_target'):
4457
+ # Check if this is already a retry attempt (prevent loops)
4458
+ is_retry = job.get('metadata', {}).get('redirect_retry', False)
4459
+
4460
+ if not is_retry:
4461
+ redirect_target = parse_results['redirect_target']
4462
+ original_target = target
4463
+ logger.info(f"🔄 Host redirect detected ({original_target} → {redirect_target}), creating auto-retry with corrected target")
4464
+
4465
+ # Get original args and update the -u parameter with new target
4466
+ args = job.get('args', [])
4467
+ retry_args = []
4468
+ for i, arg in enumerate(args):
4469
+ if arg == '-u' and i + 1 < len(args):
4470
+ # Next arg is the URL, replace it
4471
+ retry_args.append(arg)
4472
+ retry_args.append(redirect_target)
4473
+ # Skip the next iteration since we handled it
4474
+ elif i > 0 and args[i - 1] == '-u':
4475
+ # This is the URL after -u, skip it (already handled)
4476
+ continue
4477
+ else:
4478
+ # Replace <target> placeholder if present
4479
+ retry_args.append(arg.replace(original_target, redirect_target) if original_target in arg else arg)
4480
+
4481
+ # Build retry job command with corrected target
4482
+ retry_command = {
4483
+ 'tool': 'gobuster',
4484
+ 'target': redirect_target,
4485
+ 'args': retry_args,
4486
+ 'reason': f'Auto-retry: gobuster (host redirect detected: {original_target} → {redirect_target})',
4487
+ 'metadata': {
4488
+ 'redirect_retry': True,
4489
+ 'retry_parent_job_id': job.get('id'),
4490
+ 'original_target': original_target,
4491
+ 'redirect_target': redirect_target
4492
+ }
4493
+ }
4494
+
4495
+ # Enqueue the retry job
4496
+ retry_job_ids = self._enqueue_commands([retry_command], tool, engagement_id, redirect_target, parent_job_id=job.get('id'))
4497
+ if retry_job_ids:
4498
+ job_ids.extend(retry_job_ids)
4499
+ logger.info(f"✓ Retry job created with corrected target: {redirect_target}")
4500
+
4501
+ # Skip further processing of this redirect scan
4502
+ # Let the retry job complete and process properly
4503
+ return job_ids
4504
+ # === END Host Redirect Auto-Retry ===
4505
+
4454
4506
  # PHP/ASP files found → trigger SQLMap intelligently
4455
4507
  # High-value targets get direct testing, others get base URL crawl
4456
4508
  php_files = parse_results.get('php_files', [])
@@ -5041,6 +5093,57 @@ class ToolChaining:
5041
5093
  return job_ids
5042
5094
  # === END Gobuster wildcard retry ===
5043
5095
 
5096
+ # === Special handling for Gobuster host redirect retry ===
5097
+ if tool == 'gobuster' and parse_results.get('host_redirect_detected'):
5098
+ # Gobuster detected host-level redirect, auto-retry with corrected target
5099
+ from souleyez.engine.background import enqueue_job
5100
+ from souleyez.log_config import get_logger
5101
+ logger = get_logger(__name__)
5102
+
5103
+ # Check if this is already a redirect retry (prevent loops)
5104
+ is_redirect_retry = job.get('metadata', {}).get('redirect_retry', False)
5105
+ if is_redirect_retry:
5106
+ logger.info(f"Gobuster host redirect: Already attempted redirect retry, skipping")
5107
+ return job_ids
5108
+
5109
+ # Get the redirect target
5110
+ redirect_target = parse_results.get('redirect_target')
5111
+ if not redirect_target:
5112
+ logger.warning(f"Gobuster host redirect detected but no redirect_target found")
5113
+ return job_ids
5114
+
5115
+ original_target = target
5116
+ logger.info(f"Gobuster host redirect: Auto-retrying with corrected target {redirect_target}")
5117
+
5118
+ # Get original args and update the -u parameter with new target
5119
+ job_args = job.get('args', [])
5120
+ retry_args = []
5121
+ for i, arg in enumerate(job_args):
5122
+ if arg == '-u' and i + 1 < len(job_args):
5123
+ retry_args.append(arg)
5124
+ retry_args.append(redirect_target)
5125
+ elif i > 0 and job_args[i - 1] == '-u':
5126
+ continue
5127
+ else:
5128
+ retry_args.append(arg.replace(original_target, redirect_target) if original_target in arg else arg)
5129
+
5130
+ retry_job_id = enqueue_job(
5131
+ tool='gobuster',
5132
+ target=redirect_target,
5133
+ args=retry_args,
5134
+ label=f"Auto-retry: gobuster (redirect → {redirect_target})",
5135
+ engagement_id=engagement_id,
5136
+ parent_id=job.get('id'),
5137
+ reason=f"Auto-triggered by gobuster: Host redirect detected ({original_target} → {redirect_target})",
5138
+ metadata={'redirect_retry': True, 'retry_parent_job_id': job.get('id'), 'original_target': original_target}
5139
+ )
5140
+
5141
+ job_ids.append(retry_job_id)
5142
+ logger.info(f"Created gobuster redirect retry job #{retry_job_id}")
5143
+
5144
+ return job_ids
5145
+ # === END Gobuster host redirect retry ===
5146
+
5044
5147
  # === Special handling for ffuf: Create SQLMap jobs and recursive ffuf scans ===
5045
5148
  if tool == 'ffuf':
5046
5149
  from souleyez.engine.background import enqueue_job
souleyez/docs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # SoulEyez Documentation
2
2
 
3
- **Version:** 2.43.12
3
+ **Version:** 2.43.15
4
4
  **Last Updated:** January 12, 2026
5
5
  **Organization:** CyberSoul Security
6
6
 
@@ -1218,12 +1218,27 @@ def parse_gobuster_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -
1218
1218
  exclude_length = length_match.group(1)
1219
1219
  logger.info(f"Gobuster wildcard detected: Length {exclude_length}b")
1220
1220
 
1221
+ # Check for host-level redirect (for auto-retry with corrected target)
1222
+ host_redirect_detected = False
1223
+ redirect_target = None
1224
+
1225
+ if "HOST_REDIRECT_TARGET:" in log_content:
1226
+ host_redirect_detected = True
1227
+ import re
1228
+ redirect_match = re.search(r'HOST_REDIRECT_TARGET:\s*(\S+)', log_content)
1229
+ if redirect_match:
1230
+ redirect_target = redirect_match.group(1)
1231
+ logger.info(f"Gobuster host redirect detected: {redirect_target}")
1232
+
1221
1233
  # Check for gobuster errors
1222
1234
  gobuster_error = detect_tool_error(log_content, 'gobuster')
1223
1235
 
1224
1236
  # Determine status based on results
1225
1237
  if gobuster_error:
1226
1238
  status = STATUS_ERROR # Tool failed to connect
1239
+ elif host_redirect_detected:
1240
+ # Host redirect detected - warning status (triggers auto-retry with corrected target)
1241
+ status = STATUS_WARNING
1227
1242
  elif wildcard_detected:
1228
1243
  # Wildcard detected - warning status (triggers auto-retry)
1229
1244
  status = STATUS_WARNING
@@ -1255,6 +1270,12 @@ def parse_gobuster_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -
1255
1270
  if exclude_length:
1256
1271
  result['exclude_length'] = exclude_length
1257
1272
 
1273
+ # Add host redirect info if detected
1274
+ if host_redirect_detected:
1275
+ result['host_redirect_detected'] = True
1276
+ if redirect_target:
1277
+ result['redirect_target'] = redirect_target
1278
+
1258
1279
  return result
1259
1280
  except Exception as e:
1260
1281
  return {'error': str(e)}
souleyez/main.py CHANGED
@@ -173,7 +173,7 @@ def _check_privileged_tools():
173
173
 
174
174
 
175
175
  @click.group()
176
- @click.version_option(version='2.43.12')
176
+ @click.version_option(version='2.43.15')
177
177
  def cli():
178
178
  """SoulEyez - AI-Powered Pentesting Platform by CyberSoul Security"""
179
179
  from souleyez.log_config import init_logging
@@ -57,7 +57,8 @@ def parse_dnsrecon_output(output: str, target: str = "") -> Dict[str, Any]:
57
57
  continue
58
58
 
59
59
  # Parse DNS records from dnsrecon output
60
- # Old format: [*] A cybersoulsecurity.com 198.185.159.144
60
+ # Format: [*] A cybersoulsecurity.com 198.185.159.144 (info)
61
+ # Format: [+] A www.vulnweb.com 44.228.249.3 (found records)
61
62
  # New format: 2026-01-08T13:50:16.302153-1000 INFO SOA dns1.p01.nsone.net 198.51.44.1
62
63
  # New format: 2026-01-08T13:50:17.112742-1000 INFO NS dns4.p01.nsone.net 198.51.45.65
63
64
 
@@ -65,8 +66,9 @@ def parse_dnsrecon_output(output: str, target: str = "") -> Dict[str, Any]:
65
66
  hostname = None
66
67
  ip = None
67
68
 
68
- if line_stripped.startswith('[*]'):
69
- # Old format: [*] <type> <hostname> <ip>
69
+ if line_stripped.startswith('[*]') or line_stripped.startswith('[+]'):
70
+ # Format: [*] or [+] <type> <hostname> <ip>
71
+ # [*] = informational, [+] = found/success
70
72
  parts = line_stripped.split()
71
73
  if len(parts) >= 4:
72
74
  record_type = parts[1]
@@ -239,18 +239,25 @@ class GobusterPlugin(PluginBase):
239
239
  This causes gobuster to fail or produce false positives. We detect this
240
240
  upfront and auto-add --exclude-length to filter them out.
241
241
 
242
+ Also detects host-level redirects (e.g., non-www to www) and warns the user.
243
+
242
244
  Returns:
243
245
  dict with keys:
244
246
  - exclude_length: str or None (response length to exclude)
245
247
  - exclude_status: str or None (status code detected)
246
248
  - reason: str or None (explanation)
249
+ - redirect_host: str or None (suggested target if host redirect detected)
247
250
  """
248
- result = {'exclude_length': None, 'exclude_status': None, 'reason': None}
251
+ result = {'exclude_length': None, 'exclude_status': None, 'reason': None, 'redirect_host': None}
249
252
 
250
253
  # Generate random UUID path that definitely doesn't exist
251
254
  test_path = str(uuid.uuid4())
252
255
  test_url = f"{base_url.rstrip('/')}/{test_path}"
253
256
 
257
+ # Parse the original target host for comparison
258
+ original_parsed = urlparse(base_url)
259
+ original_host = original_parsed.netloc.lower()
260
+
254
261
  try:
255
262
  resp = requests.get(
256
263
  test_url,
@@ -263,6 +270,46 @@ class GobusterPlugin(PluginBase):
263
270
  if resp.status_code == 404:
264
271
  return result
265
272
 
273
+ # Check for host-level redirects (301/302/307/308)
274
+ if resp.status_code in [301, 302, 307, 308]:
275
+ location = resp.headers.get('Location', '')
276
+ if location:
277
+ # Parse the redirect location
278
+ redirect_parsed = urlparse(location)
279
+ redirect_host = redirect_parsed.netloc.lower()
280
+
281
+ # If Location is relative, it's not a host redirect
282
+ if redirect_host and redirect_host != original_host:
283
+ # This is a host-level redirect (e.g., non-www to www)
284
+ suggested_url = f"{redirect_parsed.scheme or original_parsed.scheme}://{redirect_host}"
285
+ result['redirect_host'] = suggested_url
286
+ result['exclude_status'] = str(resp.status_code)
287
+ result['reason'] = (
288
+ f"Host redirect detected: {original_host} → {redirect_host}"
289
+ )
290
+
291
+ if log_path:
292
+ with open(log_path, 'a') as f:
293
+ f.write(f"\n{'=' * 70}\n")
294
+ f.write("⚠️ HOST-LEVEL REDIRECT DETECTED\n")
295
+ f.write(f"{'=' * 70}\n")
296
+ f.write(f"Target: {base_url}\n")
297
+ f.write(f"Redirects to: {redirect_host}\n")
298
+ f.write(f"Status: {resp.status_code}\n\n")
299
+ f.write("The server redirects ALL requests to a different host.\n")
300
+ f.write("This causes unreliable results because:\n")
301
+ f.write(" - Response sizes vary based on path length in redirect URL\n")
302
+ f.write(" - Gobuster may report false positives or miss real paths\n\n")
303
+ # Parseable marker for auto-retry
304
+ f.write(f"HOST_REDIRECT_TARGET: {suggested_url}\n\n")
305
+ f.write("Auto-retrying with corrected target...\n")
306
+ f.write(f"{'=' * 70}\n\n")
307
+
308
+ # Still try to exclude the response length for this scan
309
+ content_length = len(resp.content)
310
+ result['exclude_length'] = str(content_length)
311
+ return result
312
+
266
313
  # Any other status for a random UUID = false positive indicator
267
314
  # Common: 403 (blocked), 401 (auth required), 200 (catch-all), 500 (error page)
268
315
  if resp.status_code in [200, 301, 302, 400, 401, 403, 500, 503]:
@@ -0,0 +1,481 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ souleyez.ui.wordlist_browser - Interactive wordlist browser with keyboard navigation
4
+
5
+ Discovers wordlists from common directories (SecLists, Kali, etc.) and provides
6
+ an interactive browser for selection.
7
+ """
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Tuple
11
+
12
+ import click
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+
16
+ from souleyez.ui.design_system import DesignSystem
17
+ from souleyez.ui.interactive_selector import _get_key, KEY_UP, KEY_DOWN, KEY_ENTER, KEY_ESCAPE
18
+
19
+ # Common wordlist directories to scan
20
+ WORDLIST_DIRECTORIES: List[Tuple[str, str]] = [
21
+ # SoulEyez built-in (highest priority)
22
+ ('SoulEyez', '~/.souleyez/data/wordlists'),
23
+ # Kali/Parrot Linux
24
+ ('System', '/usr/share/wordlists'),
25
+ # SecLists - various install locations
26
+ ('SecLists', '/usr/share/seclists'),
27
+ ('SecLists', '/opt/SecLists'),
28
+ ('SecLists', '/opt/seclists'),
29
+ ('SecLists', '~/SecLists'),
30
+ # Metasploit
31
+ ('Metasploit', '/usr/share/metasploit-framework/data/wordlists'),
32
+ # Dirb
33
+ ('Dirb', '/usr/share/dirb/wordlists'),
34
+ # Dirbuster
35
+ ('Dirbuster', '/usr/share/dirbuster/wordlists'),
36
+ ]
37
+
38
+ # Category detection patterns - ORDER MATTERS (more specific first)
39
+ # Each pattern is checked against the full path + filename
40
+ CATEGORY_PATTERNS = {
41
+ # DNS/Subdomain - check first since "subdomain" is specific
42
+ 'dns': [
43
+ 'subdomain', 'dns', 'vhost', 'hostname'
44
+ ],
45
+ # Extensions
46
+ 'extensions': [
47
+ 'extension', 'ext.', 'suffix', 'filetype'
48
+ ],
49
+ # Fuzzing
50
+ 'fuzzing': [
51
+ 'fuzz', 'injection', 'xss', 'sqli', 'lfi', 'rfi', 'traversal'
52
+ ],
53
+ # Users - be specific to avoid matching "username" in paths
54
+ 'users': [
55
+ 'users.txt', 'usernames', 'user_', '_user', 'logins', 'accounts',
56
+ '/users/', '/usernames/'
57
+ ],
58
+ # Passwords - check before dirs
59
+ 'passwords': [
60
+ 'password', 'pass.txt', 'passwd', 'credential', 'rockyou',
61
+ 'darkweb', 'leaked', '/passwords/'
62
+ ],
63
+ # Directories - last since patterns are more generic
64
+ 'dirs': [
65
+ 'directory', 'dirs', 'dir-', 'web-content', 'web_content',
66
+ 'dirbuster', '/dirb/', 'raft-', 'apache.txt', 'iis.txt'
67
+ ],
68
+ }
69
+
70
+
71
+ def detect_category(path: str, name: str) -> str:
72
+ """
73
+ Detect wordlist category from path and filename.
74
+
75
+ Args:
76
+ path: Full path to wordlist
77
+ name: Filename
78
+
79
+ Returns:
80
+ Category string: 'dirs', 'dns', 'passwords', 'users', 'fuzzing', 'extensions', or 'other'
81
+ """
82
+ path_lower = path.lower()
83
+ name_lower = name.lower()
84
+ combined = f"{path_lower}/{name_lower}"
85
+
86
+ for category, patterns in CATEGORY_PATTERNS.items():
87
+ for pattern in patterns:
88
+ if pattern in combined:
89
+ return category
90
+
91
+ return 'other'
92
+
93
+
94
+ def count_lines(filepath: str, max_count: int = 1000000) -> int:
95
+ """
96
+ Count lines in a file efficiently.
97
+
98
+ Args:
99
+ filepath: Path to file
100
+ max_count: Stop counting after this many lines
101
+
102
+ Returns:
103
+ Line count (or max_count if exceeded)
104
+ """
105
+ try:
106
+ count = 0
107
+ with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
108
+ for _ in f:
109
+ count += 1
110
+ if count >= max_count:
111
+ return max_count
112
+ return count
113
+ except Exception:
114
+ return 0
115
+
116
+
117
+ def discover_all_wordlists(category_filter: Optional[str] = None) -> List[Dict[str, Any]]:
118
+ """
119
+ Discover all wordlists from known directories.
120
+
121
+ Args:
122
+ category_filter: Optional category to filter by ('dirs', 'dns', 'passwords', etc.)
123
+
124
+ Returns:
125
+ List of wordlist dicts with: path, name, source, category, entries, size_mb
126
+ """
127
+ wordlists = []
128
+ seen_paths = set()
129
+
130
+ for source, directory in WORDLIST_DIRECTORIES:
131
+ dir_path = os.path.expanduser(directory)
132
+
133
+ if not os.path.isdir(dir_path):
134
+ continue
135
+
136
+ # Walk directory tree
137
+ for root, _, files in os.walk(dir_path):
138
+ for filename in files:
139
+ # Skip non-text files
140
+ if not filename.endswith(('.txt', '.lst', '.dic', '.wordlist')):
141
+ # Also include files without extension if they look like wordlists
142
+ if '.' in filename:
143
+ continue
144
+
145
+ filepath = os.path.join(root, filename)
146
+
147
+ # Skip if already seen (handles duplicate paths)
148
+ if filepath in seen_paths:
149
+ continue
150
+ seen_paths.add(filepath)
151
+
152
+ # Get relative path from source directory for display
153
+ rel_path = os.path.relpath(filepath, dir_path)
154
+
155
+ # Detect category
156
+ category = detect_category(filepath, filename)
157
+
158
+ # Apply category filter
159
+ if category_filter and category != category_filter:
160
+ # Also check if filter matches 'other' for unmatched
161
+ if category_filter != 'all':
162
+ continue
163
+
164
+ # Get file stats
165
+ try:
166
+ stat = os.stat(filepath)
167
+ size_mb = stat.st_size / (1024 * 1024)
168
+ except Exception:
169
+ size_mb = 0
170
+
171
+ # Count entries (limit to avoid slow scans on huge files)
172
+ if size_mb < 50: # Only count for files under 50MB
173
+ entries = count_lines(filepath)
174
+ else:
175
+ entries = -1 # Will display as "large"
176
+
177
+ wordlists.append({
178
+ 'path': filepath,
179
+ 'name': filename,
180
+ 'rel_path': rel_path,
181
+ 'source': source,
182
+ 'category': category,
183
+ 'entries': entries,
184
+ 'size_mb': size_mb,
185
+ })
186
+
187
+ # Sort: SoulEyez first, then by source, then by name
188
+ def sort_key(w):
189
+ source_order = {
190
+ 'SoulEyez': 0,
191
+ 'SecLists': 1,
192
+ 'System': 2,
193
+ 'Dirb': 3,
194
+ 'Dirbuster': 4,
195
+ 'Metasploit': 5,
196
+ }
197
+ return (source_order.get(w['source'], 99), w['name'].lower())
198
+
199
+ wordlists.sort(key=sort_key)
200
+
201
+ return wordlists
202
+
203
+
204
+ class WordlistBrowser:
205
+ """
206
+ Interactive single-select browser for wordlists.
207
+
208
+ Based on InteractiveSelector but for single selection:
209
+ - Enter selects highlighted item (no checkboxes)
210
+ - Tab cycles category filter
211
+ - / starts search mode
212
+ """
213
+
214
+ CURSOR = '>'
215
+ CATEGORIES = ['all', 'dirs', 'dns', 'passwords', 'users', 'fuzzing', 'other']
216
+
217
+ def __init__(
218
+ self,
219
+ category_filter: Optional[str] = None,
220
+ title: str = 'WORDLIST BROWSER'
221
+ ):
222
+ """
223
+ Initialize the wordlist browser.
224
+
225
+ Args:
226
+ category_filter: Initial category filter
227
+ title: Title to display
228
+ """
229
+ self.title = title
230
+ self.suggested_category = category_filter # Remember suggested filter
231
+ self.category_idx = 0 # Start with 'all' so users see everything
232
+
233
+ self.search_query = ''
234
+ self.search_mode = False
235
+ self.all_wordlists = discover_all_wordlists()
236
+ self.filtered = self._apply_filter()
237
+ self.cursor_pos = 0
238
+ self.page_start = 0
239
+ self.page_size = 15
240
+ self.console = Console()
241
+ self.running = True
242
+
243
+ def _apply_filter(self) -> List[Dict[str, Any]]:
244
+ """Apply current category filter and search query."""
245
+ category = self.CATEGORIES[self.category_idx]
246
+ filtered = self.all_wordlists
247
+
248
+ # Apply category filter
249
+ if category != 'all':
250
+ filtered = [w for w in filtered if w['category'] == category]
251
+
252
+ # Apply search query
253
+ if self.search_query:
254
+ query = self.search_query.lower()
255
+ filtered = [w for w in filtered if query in w['name'].lower() or query in w['rel_path'].lower()]
256
+
257
+ return filtered
258
+
259
+ def run(self) -> Optional[str]:
260
+ """
261
+ Run the interactive browser.
262
+
263
+ Returns:
264
+ Selected wordlist path or None if cancelled
265
+ """
266
+ if not self.all_wordlists:
267
+ click.echo(click.style(" No wordlists found in common directories.", fg='yellow'))
268
+ click.pause()
269
+ return None
270
+
271
+ self.running = True
272
+
273
+ while self.running:
274
+ self._render()
275
+ key = _get_key()
276
+ result = self._handle_key(key)
277
+ if result is not None:
278
+ return result
279
+
280
+ return None
281
+
282
+ def _render(self):
283
+ """Render the browser UI."""
284
+ from rich.table import Table
285
+ from rich import box
286
+
287
+ DesignSystem.clear_screen()
288
+ width = DesignSystem.get_terminal_width()
289
+
290
+ # Title bar - box style like InteractiveSelector
291
+ click.echo()
292
+ click.echo("┌" + "─" * (width - 2) + "┐")
293
+ category = self.CATEGORIES[self.category_idx]
294
+ filter_text = f"Filter: {category}"
295
+ title_text = f" {self.title} "
296
+ padding = width - len(title_text) - len(filter_text) - 4
297
+ left_pad = padding // 2
298
+ right_pad = padding - left_pad
299
+ click.echo("│" + " " * left_pad + click.style(title_text, bold=True, fg='cyan') +
300
+ " " * right_pad + click.style(filter_text, fg='yellow') + " │")
301
+ click.echo("└" + "─" * (width - 2) + "┘")
302
+ click.echo()
303
+
304
+ # Search bar
305
+ if self.search_mode:
306
+ click.echo(f" Search: {click.style(self.search_query + '_', fg='cyan')}")
307
+ elif self.search_query:
308
+ click.echo(f" Search: {click.style(self.search_query, fg='cyan')} (press / to edit)")
309
+
310
+ # Stats
311
+ total = len(self.filtered)
312
+ click.echo(f" {click.style('Total:', bold=True)} {total} wordlists")
313
+ click.echo()
314
+
315
+ if not self.filtered:
316
+ click.echo(click.style(" No wordlists match current filter.", fg='yellow'))
317
+ click.echo()
318
+ else:
319
+ # Calculate visible items
320
+ page_end = min(self.page_start + self.page_size, len(self.filtered))
321
+ visible = self.filtered[self.page_start:page_end]
322
+
323
+ # Create table like InteractiveSelector
324
+ table = Table(
325
+ show_header=True,
326
+ header_style="bold cyan",
327
+ box=DesignSystem.TABLE_BOX,
328
+ padding=(0, 1),
329
+ expand=True
330
+ )
331
+
332
+ # Add columns
333
+ table.add_column(" ", width=2, justify="center", no_wrap=True) # Cursor
334
+ table.add_column("Name", no_wrap=True)
335
+ table.add_column("Entries", width=12, justify="right")
336
+ table.add_column("Source", width=12)
337
+ table.add_column("Category", width=12)
338
+
339
+ # Add rows
340
+ for idx, wordlist in enumerate(visible):
341
+ absolute_idx = self.page_start + idx
342
+ is_cursor = (absolute_idx == self.cursor_pos)
343
+
344
+ # Cursor indicator
345
+ cursor = "▶" if is_cursor else " "
346
+
347
+ # Name - use rel_path if it has subdirs
348
+ name = wordlist['rel_path'] if '/' in wordlist['rel_path'] else wordlist['name']
349
+ if len(name) > 45:
350
+ name = '...' + name[-42:]
351
+
352
+ # Entry count
353
+ if wordlist['entries'] == -1:
354
+ entries_str = 'large'
355
+ elif wordlist['entries'] >= 1000000:
356
+ entries_str = f"{wordlist['entries'] / 1000000:.1f}M"
357
+ elif wordlist['entries'] >= 1000:
358
+ entries_str = f"{wordlist['entries'] / 1000:.1f}K"
359
+ else:
360
+ entries_str = str(wordlist['entries'])
361
+
362
+ # Source color
363
+ source = wordlist['source']
364
+
365
+ # Category
366
+ cat = wordlist['category']
367
+
368
+ # Add row with highlight for cursor
369
+ if is_cursor:
370
+ table.add_row(cursor, name, entries_str, source, cat, style="reverse")
371
+ else:
372
+ table.add_row(cursor, name, entries_str, source, cat)
373
+
374
+ self.console.print(table)
375
+
376
+ # Pagination
377
+ if len(self.filtered) > self.page_size:
378
+ page_num = (self.page_start // self.page_size) + 1
379
+ total_pages = (len(self.filtered) + self.page_size - 1) // self.page_size
380
+ click.echo()
381
+ click.echo(f" Page {page_num}/{total_pages}")
382
+
383
+ # Help bar
384
+ click.echo()
385
+ click.echo(DesignSystem.separator())
386
+ help_text = (
387
+ f" {click.style('↑↓/jk:', bold=True)} Navigate | "
388
+ f"{click.style('Enter:', bold=True)} Select | "
389
+ f"{click.style('/:', bold=True)} Search | "
390
+ f"{click.style('Tab:', bold=True)} Filter | "
391
+ f"{click.style('q:', bold=True)} Back"
392
+ )
393
+ click.echo(help_text)
394
+ click.echo(DesignSystem.separator())
395
+
396
+ def _handle_key(self, key: str) -> Optional[str]:
397
+ """
398
+ Handle a keypress.
399
+
400
+ Returns:
401
+ Selected path if Enter pressed, None otherwise
402
+ """
403
+ # Search mode - capture text
404
+ if self.search_mode:
405
+ if key == KEY_ENTER or key == '\r' or key == '\n':
406
+ self.search_mode = False
407
+ self.filtered = self._apply_filter()
408
+ self.cursor_pos = 0
409
+ self.page_start = 0
410
+ elif key == KEY_ESCAPE:
411
+ self.search_mode = False
412
+ self.search_query = ''
413
+ self.filtered = self._apply_filter()
414
+ self.cursor_pos = 0
415
+ self.page_start = 0
416
+ elif key in ('\x7f', '\x08'): # Backspace
417
+ self.search_query = self.search_query[:-1]
418
+ self.filtered = self._apply_filter()
419
+ self.cursor_pos = 0
420
+ self.page_start = 0
421
+ elif len(key) == 1 and key.isprintable():
422
+ self.search_query += key
423
+ self.filtered = self._apply_filter()
424
+ self.cursor_pos = 0
425
+ self.page_start = 0
426
+ return None
427
+
428
+ # Navigation - Up
429
+ if key in (KEY_UP, 'k'):
430
+ if self.cursor_pos > 0:
431
+ self.cursor_pos -= 1
432
+ if self.cursor_pos < self.page_start:
433
+ self.page_start = max(0, self.page_start - self.page_size)
434
+
435
+ # Navigation - Down
436
+ elif key in (KEY_DOWN, 'j'):
437
+ if self.filtered and self.cursor_pos < len(self.filtered) - 1:
438
+ self.cursor_pos += 1
439
+ if self.cursor_pos >= self.page_start + self.page_size:
440
+ self.page_start += self.page_size
441
+
442
+ # Select - Enter
443
+ elif key in (KEY_ENTER, '\r', '\n'):
444
+ if self.filtered:
445
+ return self.filtered[self.cursor_pos]['path']
446
+
447
+ # Search mode
448
+ elif key == '/':
449
+ self.search_mode = True
450
+
451
+ # Cycle category filter - Tab
452
+ elif key == '\t':
453
+ self.category_idx = (self.category_idx + 1) % len(self.CATEGORIES)
454
+ self.filtered = self._apply_filter()
455
+ self.cursor_pos = 0
456
+ self.page_start = 0
457
+
458
+ # Exit
459
+ elif key in (KEY_ESCAPE, 'q', '\x03'): # \x03 is Ctrl+C
460
+ self.running = False
461
+
462
+ return None
463
+
464
+
465
+ def browse_wordlists(category_filter: Optional[str] = None, title: str = 'WORDLIST BROWSER') -> Optional[str]:
466
+ """
467
+ Launch the interactive wordlist browser.
468
+
469
+ Args:
470
+ category_filter: Optional initial category filter
471
+ title: Browser title
472
+
473
+ Returns:
474
+ Selected wordlist path or None if cancelled
475
+ """
476
+ browser = WordlistBrowser(category_filter=category_filter, title=title)
477
+ return browser.run()
478
+
479
+
480
+ # Convenience export
481
+ __all__ = ['browse_wordlists', 'discover_all_wordlists', 'WordlistBrowser', 'WORDLIST_DIRECTORIES']
souleyez/wordlists.py CHANGED
@@ -391,6 +391,11 @@ def display_wordlist_menu(tool_name, category, title="Wordlist Selection"):
391
391
  idx += 1
392
392
  click.echo()
393
393
 
394
+ # Browse all wordlists option
395
+ click.echo(f" {idx}. Browse all wordlists...")
396
+ browse_idx = idx
397
+ idx += 1
398
+
394
399
  # Dynamic label based on category
395
400
  single_label = "Enter single username" if category == 'users' else "Enter single password" if category == 'passwords' else "Enter single value"
396
401
 
@@ -411,6 +416,13 @@ def display_wordlist_menu(tool_name, category, title="Wordlist Selection"):
411
416
  selected = choice_map[choice]
412
417
  click.echo(click.style(f"✓ Selected: {selected}", fg='green'))
413
418
  return selected
419
+ elif choice == browse_idx:
420
+ # Launch interactive wordlist browser
421
+ from souleyez.ui.wordlist_browser import browse_wordlists
422
+ selected = browse_wordlists(category_filter=category, title=f'SELECT {category.upper()} WORDLIST')
423
+ if selected:
424
+ click.echo(click.style(f"✓ Selected: {selected}", fg='green'))
425
+ return selected
414
426
  elif choice == single_idx:
415
427
  # Single value entry - return tuple to indicate it's not a file
416
428
  value_prompt = "Enter username" if category == 'users' else "Enter password" if category == 'passwords' else "Enter value"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: souleyez
3
- Version: 2.43.12
3
+ Version: 2.43.15
4
4
  Summary: AI-Powered Penetration Testing Platform with 40+ integrated tools
5
5
  Author-email: CyberSoul Security <contact@cybersoulsecurity.com>
6
6
  Maintainer-email: CyberSoul Security <contact@cybersoulsecurity.com>
@@ -1,15 +1,15 @@
1
- souleyez/__init__.py,sha256=AqWvRvGm0eblSfTChQBR6xzyVh0aeRZjPSgASWfhVEk,25
1
+ souleyez/__init__.py,sha256=slnfvvkWKYSrEzjlUbRNn3eLdpvlqHX0-gUtgkJAmx4,25
2
2
  souleyez/config.py,sha256=av357I3GYRWAklv8Dto-9-5Db699Wq5znez7zo7241Q,11595
3
3
  souleyez/devtools.py,sha256=rptmUY4a5eVvYjdEc6273MSagL-D9xibPOFgohVqUno,3508
4
4
  souleyez/feature_flags.py,sha256=mo6YAq07lc6sR3lEFKmIwTKxXZ2JPxwa5X97uR_mu50,4642
5
5
  souleyez/history.py,sha256=gzs5I_j-3OigIP6yfmBChdqxaFmyUIxvTpzWUPe_Q6c,2853
6
6
  souleyez/log_config.py,sha256=MMhPAJOqgXDfuE-xm5g0RxAfWndcmbhFHvIEMm1a_Wo,5830
7
- souleyez/main.py,sha256=LfDxtOn9W1BOHPoIlAq9P46-hfl0m_YYiqSNpMhD3ng,129101
7
+ souleyez/main.py,sha256=MKgVu0VaYdNpZsq0MWFNz33uX3P8MWwennF_2gmDjPQ,129101
8
8
  souleyez/scanner.py,sha256=U3IWHRrJ5aQ32dSHiVAHB60w1R_z0E0QxfM99msYNlw,3124
9
9
  souleyez/security.py,sha256=S84m1QmnKz_6NgH2I6IBIAorMHxRPNYVFSnks5xjihQ,2479
10
10
  souleyez/ui.py,sha256=15pfsqoDPnojAqr5S0TZHJE2ZkSHzkHpNVfVvsRj66A,34301
11
11
  souleyez/utils.py,sha256=2IbIwzBmGm4_vVvPl9YV1ExS2dxhVS8SGVClvuWKuZI,2721
12
- souleyez/wordlists.py,sha256=u_2pcRqTKNfzg_w-1kUUOcX33IjyunptFtNmx6m8HGU,15815
12
+ souleyez/wordlists.py,sha256=xzWOOIPnfVlx3Zeq5dUYDZBW0gBxVX3BRxksTcYcinA,16336
13
13
  souleyez/ai/__init__.py,sha256=7C4wIQX8gL5HksjKvyh3yp-XMiMnqIH8UWAF5zn6B2I,908
14
14
  souleyez/ai/action_mapper.py,sha256=YdTNV1ylzovJKAROEhArlgGLsH6iyO6ll15My-ILVn8,17587
15
15
  souleyez/ai/chain_advisor.py,sha256=3I94rKIG3yGsJ-16MWAnWPMjIvR0Tl57JZjzSf58xDg,16617
@@ -59,7 +59,7 @@ souleyez/core/network_utils.py,sha256=-4WgUE91RBzyXDFgGTxMa0zsWowJ47cEOAKXNeVa-W
59
59
  souleyez/core/parser_handler.py,sha256=cyZtEDctqMdWgubsU0Jg6o4XqBgyfaJ_AeBHQmmv4hM,5564
60
60
  souleyez/core/pending_chains.py,sha256=Dnka7JK7A8gTWCGpTu6qrIgIDIXprkZmwJ0Rm2oWqRE,10972
61
61
  souleyez/core/templates.py,sha256=DzlXlAz8_lwAFjjUWPp3r81KCCzbNeK-bkN1IlgQBSU,18112
62
- souleyez/core/tool_chaining.py,sha256=x1cJ6icuWu5YBZJ31q_Jm2iahGC3mBoUCnfYrkT0Kcc,281598
62
+ souleyez/core/tool_chaining.py,sha256=LLrCUwVelZHP7N6yso-fcIJbDVbDJDcPKRQlFx6JIu0,287404
63
63
  souleyez/core/version_utils.py,sha256=UOrOa3qfUdLKdzWT6GAGNV9TauwinXyLyelS8sOk0eE,11769
64
64
  souleyez/core/vuln_correlation.py,sha256=U69MSI5I-AtiyOAbXohGDKMpEHRW9y4G_0M1ppRGX18,14765
65
65
  souleyez/core/web_utils.py,sha256=f-Dqa6tH8ROnygn6-k7J1y8Qz2f1FmeJnPjPE0WRn34,4902
@@ -104,7 +104,7 @@ souleyez/detection/__init__.py,sha256=QIhvXjFdjrquQ6A0VQ7GZQkK_EXB59t8Dv9PKXhEUe
104
104
  souleyez/detection/attack_signatures.py,sha256=akgWwiIkh6WYnghCuLhRV0y6FS0SQ0caGF8tZUc49oA,6965
105
105
  souleyez/detection/mitre_mappings.py,sha256=xejE80YK-g8kKaeQoo-vBl8P3t8RTTItbfN0NaVZw6s,20558
106
106
  souleyez/detection/validator.py,sha256=-AJ7QSJ3-6jFKLnPG_Rc34IXyF4JPyI82BFUgTA9zw0,15641
107
- souleyez/docs/README.md,sha256=1tbpTWjDcVQqP3DrVth28HR5bq9jAWccaUwtksG1lNk,7188
107
+ souleyez/docs/README.md,sha256=hjhVK2ZaTUOWQWCum2agdKB_VL5O9FRLe74xMT2k-Xw,7188
108
108
  souleyez/docs/api-reference/cli-commands.md,sha256=lTLFnILN3YRVdqCaag7WgsYXfDGglb1TuPexkxDsVdE,12917
109
109
  souleyez/docs/api-reference/engagement-api.md,sha256=nd-EvQMtiJrobg2bzFEADp853HP1Uhb9dmgok0_-neE,11672
110
110
  souleyez/docs/api-reference/integration-guide.md,sha256=c96uX79ukHyYotLa54wZ20Kx-EUZnrKegTeGkfLD-pw,16285
@@ -152,7 +152,7 @@ souleyez/engine/job_status.py,sha256=OAEf2rAzapm55m4tc3PSilotdA5ONX15JavUMLre0is
152
152
  souleyez/engine/loader.py,sha256=ke6QQVVWozDnqGNBotajC3RBYOa2_DZmv5DAnDZVgIc,2769
153
153
  souleyez/engine/log_sanitizer.py,sha256=QHF6zSms-wHo6SbL6fHXIh1GG-8G34lE7kl45nbPn70,7130
154
154
  souleyez/engine/manager.py,sha256=aBQMoib-VWNXtIp5Qn34tRj1P1jiLpwAIoo1fexAaLU,3629
155
- souleyez/engine/result_handler.py,sha256=eD5fqGAlnG0pr1gxhKB40HBw_uyzsFJZpFaTDeMXc1g,142535
155
+ souleyez/engine/result_handler.py,sha256=jioWzfRrSdRP7vfrcGIujrhh6-H1POkyiBGDnwjoW9I,143455
156
156
  souleyez/engine/worker_manager.py,sha256=B7b8RbkKTNofmiIyHTNgdikoZCLXpB-iIl1S4-U3q9o,6127
157
157
  souleyez/export/__init__.py,sha256=2kFHftSqqrRUG6PhtfhCyhnkpkjc-8Zb4utGo-Nb6B4,61
158
158
  souleyez/export/evidence_bundle.py,sha256=hqPn_h2CidhL-1VAT0qraZ8r1yfnUTnLZ3RfPPCK5Ds,9966
@@ -195,7 +195,7 @@ souleyez/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
195
195
  souleyez/parsers/bloodhound_parser.py,sha256=r1f5ePsuPc90CJOySnZ6WkfYQoeVJWraTiE7dnI8BbI,3134
196
196
  souleyez/parsers/crackmapexec_parser.py,sha256=HN3nCBMdCEoP1OuYPLmf080b2YmMphZ7Wtm1ByrKGu0,10849
197
197
  souleyez/parsers/dalfox_parser.py,sha256=w_eCeyL-Eu9zGyXqYmq0XKbnOIR7lnDD-zbRT7SPco0,6856
198
- souleyez/parsers/dnsrecon_parser.py,sha256=zy9_d-dxVSWSSNy7UgMQ2J_r8uoPacHiAMwkAjomXcM,7885
198
+ souleyez/parsers/dnsrecon_parser.py,sha256=_j_xE94OAEffhH8YZWQ-lKXlcUVlhTaPHwGcwDBjY5I,8052
199
199
  souleyez/parsers/enum4linux_parser.py,sha256=8C9VQ0f-VzKFBkRmScPM84Q8CYvPwidBuQFLH2TJ48c,17728
200
200
  souleyez/parsers/ffuf_parser.py,sha256=xoLicqbwjgJqMrkGEyUFmBIx1Jn7t2OG7OtXkN-wvUE,1706
201
201
  souleyez/parsers/gobuster_parser.py,sha256=-DS8fpA7uoSKhrGnoMdjowhl7OcitAPoEee-LXiogYs,5960
@@ -227,7 +227,7 @@ souleyez/plugins/dnsrecon.py,sha256=nxeVgwACUyw5VYEyD-5U277d1U72EkWBX9nR9_DMZrI,
227
227
  souleyez/plugins/enum4linux.py,sha256=VHkKPs8PWX90RLsGdYt5Ieuc3Sz52fbeWvKCL1KquIY,10876
228
228
  souleyez/plugins/ffuf.py,sha256=7c1-Q7xXTMmH_2wHXikjmZnSgZL13Hj5E_asBxZ6Y5U,11652
229
229
  souleyez/plugins/firmware_extract.py,sha256=_hZXx6cHb9noM6uVgi3hwrJLw8hE9mDUelTEHwoIdCU,6460
230
- souleyez/plugins/gobuster.py,sha256=GMTUyfkVnZ2gp3kh_R-KQ4EIGEBX5fxBIMfHZrwkVFo,29285
230
+ souleyez/plugins/gobuster.py,sha256=y8QeEjMR5_2tf-T63nxFRUtWmlCzrPez2I4nLqPNOfY,32114
231
231
  souleyez/plugins/hashcat.py,sha256=aigfwBu9IorXKgbyEIWx0qOCEdr1wnZaPqdYwh0PITc,10381
232
232
  souleyez/plugins/http_fingerprint.py,sha256=_D5UVAtDC0f-uy4pQcBI43c4jnsJ5wyvCIvttUiVurw,21202
233
233
  souleyez/plugins/hydra.py,sha256=kfVJwgh3x1DC0wEtA-lkoY7qhQH1qKViYexUECZSPY4,29520
@@ -368,11 +368,12 @@ souleyez/ui/tool_setup.py,sha256=HkRTjzN7FHUs_XKtNYnphkdXb5kC4P6ghpI8TeCuEjU,363
368
368
  souleyez/ui/tutorial.py,sha256=efDF6nWRek6fySppjmq3qMQ3J2WfW89LOcaeltqnWH4,14917
369
369
  souleyez/ui/tutorial_state.py,sha256=Thf7_qCj4VKjG7UqgJqa9kjIqiFUU-7Q7kG4v-u2B4A,8123
370
370
  souleyez/ui/wazuh_vulns_view.py,sha256=3vJJEmrjgS2wD6EDB7ZV7WxgytBHTm-1WqNDjp7lVEI,21830
371
+ souleyez/ui/wordlist_browser.py,sha256=iQ2YYxrVo8FGCfM-Bc0teVBijSAbd2rjbSQ2hOE7eiY,16110
371
372
  souleyez/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
372
373
  souleyez/utils/tool_checker.py,sha256=YzNajZpFyKJA5fp0Kq_gQ0YnKb7J1BaKJSZ8vP-IWj8,30868
373
- souleyez-2.43.12.dist-info/licenses/LICENSE,sha256=J7vDD5QMF4w2oSDm35eBgosATE70ah1M40u9W4EpTZs,1090
374
- souleyez-2.43.12.dist-info/METADATA,sha256=BqwnW_Lm4R-T_rhGCK7rBhZBfsXvgDSSjWbiClza5X4,10426
375
- souleyez-2.43.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
376
- souleyez-2.43.12.dist-info/entry_points.txt,sha256=bN5W1dhjDZJl3TKclMjRpfQvGPmyrJLwwDuCj_X39HE,48
377
- souleyez-2.43.12.dist-info/top_level.txt,sha256=afAMzS9p4lcdBNxhGo6jl3ipQE9HUvvNIPOdjtPjr_Q,9
378
- souleyez-2.43.12.dist-info/RECORD,,
374
+ souleyez-2.43.15.dist-info/licenses/LICENSE,sha256=J7vDD5QMF4w2oSDm35eBgosATE70ah1M40u9W4EpTZs,1090
375
+ souleyez-2.43.15.dist-info/METADATA,sha256=5Ax1maa8aJJjBmPh5hCgS1u6HQFmxRhn9DZ3wuf0IVQ,10426
376
+ souleyez-2.43.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
377
+ souleyez-2.43.15.dist-info/entry_points.txt,sha256=bN5W1dhjDZJl3TKclMjRpfQvGPmyrJLwwDuCj_X39HE,48
378
+ souleyez-2.43.15.dist-info/top_level.txt,sha256=afAMzS9p4lcdBNxhGo6jl3ipQE9HUvvNIPOdjtPjr_Q,9
379
+ souleyez-2.43.15.dist-info/RECORD,,