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
@@ -80,15 +80,22 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
80
80
  current_method = 'GET'
81
81
  current_post_data = None
82
82
 
83
+ # Track POST form URLs separately to prevent GET URL testing from overwriting them
84
+ # This fixes bug where chain rules get wrong URL when SQLMap tests multiple URLs
85
+ last_post_form_url = None
86
+ last_post_form_data = None
87
+
83
88
  for i, line in enumerate(lines):
84
89
  line = line.strip()
85
90
 
86
- # Extract URL being tested
87
- if 'testing URL' in line:
88
- url_match = re.search(r"testing URL '([^']+)'", line)
91
+ # Extract URL being tested (GET requests typically)
92
+ # Format variations: "testing URL 'http://...'" or 'testing URL "http://..."' or testing URL http://...
93
+ if 'testing URL' in line or 'testing url' in line.lower():
94
+ # Try single quotes first
95
+ url_match = re.search(r"testing URL ['\"]?([^'\"]+)['\"]?", line, re.IGNORECASE)
89
96
  if url_match:
90
- current_url = url_match.group(1)
91
- if current_url not in result['urls_tested']:
97
+ current_url = url_match.group(1).strip()
98
+ if current_url and current_url not in result['urls_tested']:
92
99
  result['urls_tested'].append(current_url)
93
100
 
94
101
  # Extract POST/GET URLs from form testing (crawl mode)
@@ -100,6 +107,9 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
100
107
  current_url = url_match.group(2)
101
108
  if current_url not in result['urls_tested']:
102
109
  result['urls_tested'].append(current_url)
110
+ # Save POST form URL separately for later use
111
+ if current_method == 'POST':
112
+ last_post_form_url = current_url
103
113
 
104
114
  # Extract POST data (appears after "POST http://..." line)
105
115
  # Format: "POST data: username=&password=&submit=Login"
@@ -107,6 +117,8 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
107
117
  post_data_match = re.search(r'^POST data:\s*(.+)$', line)
108
118
  if post_data_match:
109
119
  current_post_data = post_data_match.group(1).strip()
120
+ # Associate POST data with the POST form URL
121
+ last_post_form_data = current_post_data
110
122
 
111
123
  # Handle resumed injection points from stored session
112
124
  # Pattern: "sqlmap resumed the following injection point(s) from stored session:"
@@ -129,15 +141,23 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
129
141
  else:
130
142
  method = current_method # Use current context
131
143
 
144
+ # For POST parameters, use the saved POST form URL instead of current_url
145
+ if method == 'POST' and last_post_form_url:
146
+ effective_url = last_post_form_url
147
+ effective_post_data = last_post_form_data or current_post_data
148
+ else:
149
+ effective_url = current_url or target
150
+ effective_post_data = current_post_data if method == 'POST' else None
151
+
132
152
  # Mark as confirmed injection
133
153
  result['sql_injection_confirmed'] = True
134
154
  result['injectable_parameter'] = param
135
- result['injectable_url'] = current_url or target
155
+ result['injectable_url'] = effective_url
136
156
  result['injectable_method'] = method
137
157
 
138
158
  # Add vulnerability entry
139
159
  result['vulnerabilities'].append({
140
- 'url': current_url or target,
160
+ 'url': effective_url,
141
161
  'parameter': param,
142
162
  'vuln_type': 'sqli',
143
163
  'injectable': True,
@@ -147,10 +167,10 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
147
167
 
148
168
  # Collect injection point
149
169
  injection_point = {
150
- 'url': current_url or target,
170
+ 'url': effective_url,
151
171
  'parameter': param,
152
172
  'method': method,
153
- 'post_data': current_post_data if method == 'POST' else None,
173
+ 'post_data': effective_post_data,
154
174
  'techniques': []
155
175
  }
156
176
  if not any(
@@ -166,12 +186,19 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
166
186
  if next_line.startswith('[') or next_line.startswith('back-end'):
167
187
  break
168
188
 
169
- # Extract DBMS type
170
- if 'back-end DBMS:' in line:
171
- # Pattern: "back-end DBMS: MySQL >= 5.0.12"
172
- dbms_match = re.search(r"back-end DBMS:\s*([^\s]+)", line)
189
+ # Extract DBMS type with full version info
190
+ # Format variations:
191
+ # "back-end DBMS: MySQL >= 5.0.12"
192
+ # "back-end DBMS: Microsoft SQL Server 2019"
193
+ # "back-end DBMS: PostgreSQL"
194
+ if 'back-end DBMS:' in line or 'back-end dbms:' in line.lower():
195
+ dbms_match = re.search(r"back-end DBMS:\s*(.+)", line, re.IGNORECASE)
173
196
  if dbms_match and not result['dbms']:
174
- result['dbms'] = dbms_match.group(1)
197
+ dbms_full = dbms_match.group(1).strip()
198
+ # Extract just the DBMS name for the main field (first word)
199
+ # but store full version in a separate field
200
+ result['dbms'] = dbms_full.split()[0] if dbms_full else None
201
+ result['dbms_full'] = dbms_full # Keep full string
175
202
 
176
203
  # Extract web server OS
177
204
  if 'web server operating system:' in line.lower():
@@ -318,8 +345,17 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
318
345
  )
319
346
 
320
347
  if not already_added:
348
+ # For POST parameters, use the saved POST form URL instead of current_url
349
+ # This prevents bug where GET URL testing overwrites the correct POST form URL
350
+ if param_method == 'POST' and last_post_form_url:
351
+ effective_url = last_post_form_url
352
+ effective_post_data = last_post_form_data or current_post_data
353
+ else:
354
+ effective_url = current_url or target
355
+ effective_post_data = current_post_data if param_method == 'POST' else None
356
+
321
357
  result['vulnerabilities'].append({
322
- 'url': current_url or target,
358
+ 'url': effective_url,
323
359
  'parameter': param,
324
360
  'vuln_type': 'sqli',
325
361
  'injectable': True,
@@ -332,17 +368,17 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
332
368
  # Set confirmation flags
333
369
  result['sql_injection_confirmed'] = True
334
370
  result['injectable_parameter'] = param
335
- result['injectable_url'] = current_url or target
371
+ result['injectable_url'] = effective_url
336
372
  result['injectable_method'] = param_method # GET, POST, etc.
337
- if param_method == 'POST' and current_post_data:
338
- result['injectable_post_data'] = current_post_data
373
+ if param_method == 'POST' and effective_post_data:
374
+ result['injectable_post_data'] = effective_post_data
339
375
 
340
376
  # Collect ALL injection points for fallback
341
377
  injection_point = {
342
- 'url': current_url or target,
378
+ 'url': effective_url,
343
379
  'parameter': param,
344
380
  'method': param_method,
345
- 'post_data': current_post_data if param_method == 'POST' else None,
381
+ 'post_data': effective_post_data,
346
382
  'techniques': techniques
347
383
  }
348
384
  # Avoid duplicates
@@ -364,8 +400,18 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
364
400
  if param_match:
365
401
  method = param_match.group(1) or current_method
366
402
  param = param_match.group(2)
403
+
404
+ # For POST parameters, use the saved POST form URL instead of current_url
405
+ # This prevents bug where GET URL testing overwrites the correct POST form URL
406
+ if method == 'POST' and last_post_form_url:
407
+ effective_url = last_post_form_url
408
+ effective_post_data = last_post_form_data or current_post_data
409
+ else:
410
+ effective_url = current_url or target
411
+ effective_post_data = current_post_data if method == 'POST' else None
412
+
367
413
  result['vulnerabilities'].append({
368
- 'url': current_url or target,
414
+ 'url': effective_url,
369
415
  'parameter': param,
370
416
  'vuln_type': 'sqli',
371
417
  'injectable': True,
@@ -376,17 +422,17 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
376
422
  # Set confirmation flags
377
423
  result['sql_injection_confirmed'] = True
378
424
  result['injectable_parameter'] = param
379
- result['injectable_url'] = current_url or target
425
+ result['injectable_url'] = effective_url
380
426
  result['injectable_method'] = method
381
- if method == 'POST' and current_post_data:
382
- result['injectable_post_data'] = current_post_data
427
+ if method == 'POST' and effective_post_data:
428
+ result['injectable_post_data'] = effective_post_data
383
429
 
384
430
  # Collect ALL injection points for fallback
385
431
  injection_point = {
386
- 'url': current_url or target,
432
+ 'url': effective_url,
387
433
  'parameter': param,
388
434
  'method': method,
389
- 'post_data': current_post_data if method == 'POST' else None,
435
+ 'post_data': effective_post_data,
390
436
  'techniques': [] # Technique details not available at this detection point
391
437
  }
392
438
  # Avoid duplicates
@@ -67,18 +67,19 @@ def parse_theharvester_output(output: str, target: str = "") -> Dict[str, Any]:
67
67
  if target_match:
68
68
  result['target'] = target_match.group(1)
69
69
 
70
- # Detect section headers
71
- elif '[*] ASNS found:' in line or 'ASNs found:' in line:
70
+ # Detect section headers (case-insensitive, multiple format variations)
71
+ line_lower = line.lower()
72
+ if any(x in line_lower for x in ['asns found', 'asn found', 'autonomous system']):
72
73
  current_section = 'asns'
73
- elif '[*] Interesting Urls found:' in line or '[*] URLs found:' in line:
74
+ elif any(x in line_lower for x in ['urls found', 'interesting urls', 'url found']):
74
75
  current_section = 'urls'
75
- elif '[*] IPs found:' in line:
76
+ elif any(x in line_lower for x in ['ips found', 'ip found', 'ip addresses']):
76
77
  current_section = 'ips'
77
- elif '[*] Emails found:' in line or 'Email addresses found:' in line:
78
+ elif any(x in line_lower for x in ['emails found', 'email found', 'email addresses']):
78
79
  current_section = 'emails'
79
- elif '[*] Hosts found:' in line or 'Hosts found:' in line:
80
+ elif any(x in line_lower for x in ['hosts found', 'host found', 'subdomains found', 'subdomain found']):
80
81
  current_section = 'hosts'
81
- elif '[*] People found:' in line or '[*] No people found' in line:
82
+ elif any(x in line_lower for x in ['people found', 'no people found', 'linkedin']):
82
83
  current_section = 'people' # We'll skip this for now
83
84
 
84
85
  # Skip separator lines and empty lines
@@ -117,18 +118,25 @@ def parse_theharvester_output(output: str, target: str = "") -> Dict[str, Any]:
117
118
  elif current_section == 'emails':
118
119
  # Email format: user@domain
119
120
  if '@' in line and '.' in line:
120
- # Basic email validation
121
- if re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', line):
122
- if line not in result['emails']:
123
- result['emails'].append(line)
121
+ # More permissive email validation (supports international domains)
122
+ # Pattern allows: standard emails, plus-addressing, dots, underscores
123
+ email = line.strip().lower()
124
+ # Remove any leading/trailing brackets or quotes
125
+ email = re.sub(r'^[\[\(<\'\"]+|[\]\)>\'\"]$', '', email)
126
+ if re.match(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$', email):
127
+ if email not in result['emails']:
128
+ result['emails'].append(email)
124
129
 
125
130
  elif current_section == 'hosts':
126
131
  # Host format: subdomain.domain.tld
127
132
  if '.' in line and not line.startswith('http'):
128
133
  # Clean and validate hostname
129
134
  host = line.strip().lower()
130
- # Basic validation: has at least one dot and no invalid chars
131
- if re.match(r'^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', host):
135
+ # Remove any leading/trailing brackets, quotes, or trailing dots
136
+ host = re.sub(r'^[\[\(<\'\"]+|[\]\)>\'\".]+$', '', host)
137
+ # More permissive validation: allows underscores (common in some hosts)
138
+ # and longer TLDs (some are 4+ chars)
139
+ if re.match(r'^[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$', host) and len(host) > 3:
132
140
  if host not in result['hosts']:
133
141
  result['hosts'].append(host)
134
142
 
@@ -154,6 +154,83 @@ class GobusterPlugin(PluginBase):
154
154
  category = "scanning"
155
155
  HELP = HELP
156
156
 
157
+ # Minimum required version (v3.x uses subcommands like 'dir', 'dns', 'vhost')
158
+ MIN_VERSION = "3.0.0"
159
+
160
+ def _check_version(self) -> tuple:
161
+ """
162
+ Check gobuster version meets minimum requirements.
163
+
164
+ Returns:
165
+ (meets_requirement: bool, version: str, error_msg: str or None)
166
+ """
167
+ try:
168
+ # Use -v flag (not 'version' subcommand) - works on v3.x
169
+ result = subprocess.run(
170
+ ["gobuster", "-v"],
171
+ capture_output=True,
172
+ text=True,
173
+ timeout=10
174
+ )
175
+ output = result.stdout + result.stderr
176
+
177
+ # Parse version from output like "gobuster version 3.8.2"
178
+ version_match = re.search(r'version\s+(\d+\.\d+\.\d+)', output, re.IGNORECASE)
179
+ if version_match:
180
+ version = version_match.group(1)
181
+ major = int(version.split('.')[0])
182
+ if major >= 3:
183
+ return (True, version, None)
184
+ else:
185
+ return (False, version, self._upgrade_message(version))
186
+
187
+ # Also try --version flag as fallback
188
+ result2 = subprocess.run(
189
+ ["gobuster", "--version"],
190
+ capture_output=True,
191
+ text=True,
192
+ timeout=10
193
+ )
194
+ output2 = result2.stdout + result2.stderr
195
+ version_match2 = re.search(r'version\s+(\d+\.\d+\.\d+)', output2, re.IGNORECASE)
196
+ if version_match2:
197
+ version = version_match2.group(1)
198
+ major = int(version.split('.')[0])
199
+ if major >= 3:
200
+ return (True, version, None)
201
+ else:
202
+ return (False, version, self._upgrade_message(version))
203
+
204
+ # If neither flag shows version info, check if v2.x by looking for subcommand error
205
+ # v2.x will show "Usage: gobuster [OPTIONS] ..." without subcommands
206
+ if "dir" not in output.lower() and "dns" not in output.lower():
207
+ return (False, "2.x", self._upgrade_message("2.x"))
208
+
209
+ # If we see dir/dns subcommands mentioned, assume v3.x
210
+ return (True, "3.x", None)
211
+
212
+ except FileNotFoundError:
213
+ return (False, None, "ERROR: gobuster not found. Install with: sudo apt install gobuster")
214
+ except subprocess.TimeoutExpired:
215
+ return (True, "unknown", None) # Assume it works
216
+ except Exception as e:
217
+ return (True, "unknown", None) # Assume it works
218
+
219
+ def _upgrade_message(self, current_version: str) -> str:
220
+ """Generate upgrade instructions for old gobuster versions."""
221
+ return (
222
+ f"ERROR: gobuster {current_version} is too old. Version 3.0.0+ required.\n\n"
223
+ f"Gobuster v2.x doesn't support the 'dir/dns/vhost' subcommands used by SoulEyez.\n\n"
224
+ f"UPGRADE OPTIONS:\n"
225
+ f" Option 1 - Install from Go (recommended, gets latest):\n"
226
+ f" go install github.com/OJ/gobuster/v3@latest\n\n"
227
+ f" Option 2 - Download binary from GitHub:\n"
228
+ f" https://github.com/OJ/gobuster/releases\n\n"
229
+ f" Option 3 - On Kali Linux:\n"
230
+ f" sudo apt update && sudo apt install gobuster\n\n"
231
+ f"After upgrading, verify with: gobuster version\n"
232
+ )
233
+
157
234
  def _preflight_check(self, base_url: str, timeout: float = 5.0, log_path: str = None) -> Dict[str, Optional[str]]:
158
235
  """
159
236
  Probe target with random UUID path to detect false positive responses.
@@ -225,7 +302,15 @@ class GobusterPlugin(PluginBase):
225
302
  def build_command(self, target: str, args: List[str] = None, label: str = "", log_path: str = None):
226
303
  """Build gobuster command for background execution with PID tracking."""
227
304
  args = args or []
228
-
305
+
306
+ # Check gobuster version meets requirements (v3.x+ required for subcommands)
307
+ meets_req, version, error_msg = self._check_version()
308
+ if not meets_req:
309
+ if log_path:
310
+ with open(log_path, 'w') as f:
311
+ f.write(error_msg)
312
+ return None
313
+
229
314
  # Detect the mode from args
230
315
  mode = None
231
316
  if 'dir' in args:
@@ -323,9 +408,17 @@ class GobusterPlugin(PluginBase):
323
408
 
324
409
  def run(self, target: str, args: List[str] = None, label: str = "", log_path: str = None) -> int:
325
410
  """Execute gobuster scan and write output to log_path."""
326
-
411
+
327
412
  args = args or []
328
-
413
+
414
+ # Check gobuster version meets requirements (v3.x+ required for subcommands)
415
+ meets_req, version, error_msg = self._check_version()
416
+ if not meets_req:
417
+ if log_path:
418
+ with open(log_path, 'w') as f:
419
+ f.write(error_msg)
420
+ return 1
421
+
329
422
  # Detect the mode from args
330
423
  mode = None
331
424
  if 'dir' in args: