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.
@@ -80,10 +80,15 @@ 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
91
+ # Extract URL being tested (GET requests typically)
87
92
  if 'testing URL' in line:
88
93
  url_match = re.search(r"testing URL '([^']+)'", line)
89
94
  if url_match:
@@ -100,6 +105,9 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
100
105
  current_url = url_match.group(2)
101
106
  if current_url not in result['urls_tested']:
102
107
  result['urls_tested'].append(current_url)
108
+ # Save POST form URL separately for later use
109
+ if current_method == 'POST':
110
+ last_post_form_url = current_url
103
111
 
104
112
  # Extract POST data (appears after "POST http://..." line)
105
113
  # Format: "POST data: username=&password=&submit=Login"
@@ -107,6 +115,8 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
107
115
  post_data_match = re.search(r'^POST data:\s*(.+)$', line)
108
116
  if post_data_match:
109
117
  current_post_data = post_data_match.group(1).strip()
118
+ # Associate POST data with the POST form URL
119
+ last_post_form_data = current_post_data
110
120
 
111
121
  # Handle resumed injection points from stored session
112
122
  # Pattern: "sqlmap resumed the following injection point(s) from stored session:"
@@ -129,15 +139,23 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
129
139
  else:
130
140
  method = current_method # Use current context
131
141
 
142
+ # For POST parameters, use the saved POST form URL instead of current_url
143
+ if method == 'POST' and last_post_form_url:
144
+ effective_url = last_post_form_url
145
+ effective_post_data = last_post_form_data or current_post_data
146
+ else:
147
+ effective_url = current_url or target
148
+ effective_post_data = current_post_data if method == 'POST' else None
149
+
132
150
  # Mark as confirmed injection
133
151
  result['sql_injection_confirmed'] = True
134
152
  result['injectable_parameter'] = param
135
- result['injectable_url'] = current_url or target
153
+ result['injectable_url'] = effective_url
136
154
  result['injectable_method'] = method
137
155
 
138
156
  # Add vulnerability entry
139
157
  result['vulnerabilities'].append({
140
- 'url': current_url or target,
158
+ 'url': effective_url,
141
159
  'parameter': param,
142
160
  'vuln_type': 'sqli',
143
161
  'injectable': True,
@@ -147,10 +165,10 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
147
165
 
148
166
  # Collect injection point
149
167
  injection_point = {
150
- 'url': current_url or target,
168
+ 'url': effective_url,
151
169
  'parameter': param,
152
170
  'method': method,
153
- 'post_data': current_post_data if method == 'POST' else None,
171
+ 'post_data': effective_post_data,
154
172
  'techniques': []
155
173
  }
156
174
  if not any(
@@ -318,8 +336,17 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
318
336
  )
319
337
 
320
338
  if not already_added:
339
+ # For POST parameters, use the saved POST form URL instead of current_url
340
+ # This prevents bug where GET URL testing overwrites the correct POST form URL
341
+ if param_method == 'POST' and last_post_form_url:
342
+ effective_url = last_post_form_url
343
+ effective_post_data = last_post_form_data or current_post_data
344
+ else:
345
+ effective_url = current_url or target
346
+ effective_post_data = current_post_data if param_method == 'POST' else None
347
+
321
348
  result['vulnerabilities'].append({
322
- 'url': current_url or target,
349
+ 'url': effective_url,
323
350
  'parameter': param,
324
351
  'vuln_type': 'sqli',
325
352
  'injectable': True,
@@ -332,17 +359,17 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
332
359
  # Set confirmation flags
333
360
  result['sql_injection_confirmed'] = True
334
361
  result['injectable_parameter'] = param
335
- result['injectable_url'] = current_url or target
362
+ result['injectable_url'] = effective_url
336
363
  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
364
+ if param_method == 'POST' and effective_post_data:
365
+ result['injectable_post_data'] = effective_post_data
339
366
 
340
367
  # Collect ALL injection points for fallback
341
368
  injection_point = {
342
- 'url': current_url or target,
369
+ 'url': effective_url,
343
370
  'parameter': param,
344
371
  'method': param_method,
345
- 'post_data': current_post_data if param_method == 'POST' else None,
372
+ 'post_data': effective_post_data,
346
373
  'techniques': techniques
347
374
  }
348
375
  # Avoid duplicates
@@ -364,8 +391,18 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
364
391
  if param_match:
365
392
  method = param_match.group(1) or current_method
366
393
  param = param_match.group(2)
394
+
395
+ # For POST parameters, use the saved POST form URL instead of current_url
396
+ # This prevents bug where GET URL testing overwrites the correct POST form URL
397
+ if method == 'POST' and last_post_form_url:
398
+ effective_url = last_post_form_url
399
+ effective_post_data = last_post_form_data or current_post_data
400
+ else:
401
+ effective_url = current_url or target
402
+ effective_post_data = current_post_data if method == 'POST' else None
403
+
367
404
  result['vulnerabilities'].append({
368
- 'url': current_url or target,
405
+ 'url': effective_url,
369
406
  'parameter': param,
370
407
  'vuln_type': 'sqli',
371
408
  'injectable': True,
@@ -376,17 +413,17 @@ def parse_sqlmap_output(output: str, target: str = "") -> Dict[str, Any]:
376
413
  # Set confirmation flags
377
414
  result['sql_injection_confirmed'] = True
378
415
  result['injectable_parameter'] = param
379
- result['injectable_url'] = current_url or target
416
+ result['injectable_url'] = effective_url
380
417
  result['injectable_method'] = method
381
- if method == 'POST' and current_post_data:
382
- result['injectable_post_data'] = current_post_data
418
+ if method == 'POST' and effective_post_data:
419
+ result['injectable_post_data'] = effective_post_data
383
420
 
384
421
  # Collect ALL injection points for fallback
385
422
  injection_point = {
386
- 'url': current_url or target,
423
+ 'url': effective_url,
387
424
  'parameter': param,
388
425
  'method': method,
389
- 'post_data': current_post_data if method == 'POST' else None,
426
+ 'post_data': effective_post_data,
390
427
  'techniques': [] # Technique details not available at this detection point
391
428
  }
392
429
  # Avoid duplicates
@@ -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:
@@ -295,13 +295,16 @@ class MsfExploitPlugin(PluginBase):
295
295
  with open(log_path, 'a') as f:
296
296
  f.write(f"[!] Poll error: {e}\n")
297
297
 
298
- # Timeout - no session opened
298
+ # Timeout - no session opened (not an error, just means target likely not vulnerable)
299
299
  if log_path:
300
300
  with open(log_path, 'a') as f:
301
- f.write(f"\n[-] No session opened after {max_poll}s\n")
301
+ f.write(f"\n[*] No session opened after {max_poll}s\n")
302
+ f.write(f"[*] Target may not be vulnerable or exploit conditions not met\n")
303
+ f.write(f"[*] Try re-running the exploit if needed\n")
302
304
  f.write(f"Completed: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n")
303
305
 
304
- return {'success': False, 'error': f'No session after {max_poll}s'}
306
+ # Return success=False but no 'error' key - this is a "no results" case, not an error
307
+ return {'success': False, 'no_session': True, 'reason': f'No session after {max_poll}s'}
305
308
 
306
309
  def _get_local_ip(self, target: str) -> str:
307
310
  """Get local IP that can reach the target."""
@@ -9694,9 +9694,19 @@ def _view_all_job_alerts(item: dict):
9694
9694
  return f"{icon} {severity[:6].upper()}"
9695
9695
  elif key == 'rule_id':
9696
9696
  if is_wazuh_style:
9697
- return str(alert.get('rule', {}).get('id', 'N/A'))
9697
+ # Wazuh: show first rule group (more descriptive) or rule ID
9698
+ rule_data = alert.get('rule', {})
9699
+ groups = rule_data.get('groups', [])
9700
+ if groups:
9701
+ # Get most specific group (often last is most specific)
9702
+ return str(groups[-1])[:12]
9703
+ return str(rule_data.get('id', 'N/A'))[:12]
9698
9704
  else:
9699
- return str(alert.get('rule_id', alert.get('id', 'N/A')))[:15]
9705
+ # Splunk: show MITRE tactic if available, else sourcetype
9706
+ mitre_tactics = alert.get('mitre_tactics', [])
9707
+ if mitre_tactics:
9708
+ return str(mitre_tactics[0])[:12]
9709
+ return str(alert.get('rule_id', 'N/A'))[:12]
9700
9710
  elif key == 'agent_name':
9701
9711
  if is_wazuh_style:
9702
9712
  return alert.get('agent', {}).get('name', 'N/A')
@@ -9704,9 +9714,17 @@ def _view_all_job_alerts(item: dict):
9704
9714
  return str(alert.get('source_ip', alert.get('host', 'N/A')))[:15]
9705
9715
  elif key == 'description':
9706
9716
  if is_wazuh_style:
9707
- return alert.get('rule', {}).get('description', 'No description')[:45]
9717
+ # Wazuh: use rule description, or rule groups if more descriptive
9718
+ rule_data = alert.get('rule', {})
9719
+ desc = rule_data.get('description', '')
9720
+ if not desc:
9721
+ groups = rule_data.get('groups', [])
9722
+ if groups:
9723
+ desc = ', '.join(groups[:2])
9724
+ return str(desc)[:45] if desc else 'No description'
9708
9725
  else:
9709
- desc = alert.get('rule_name', alert.get('description', 'No description'))
9726
+ # Splunk: prefer actual description (log content) over rule_name
9727
+ desc = alert.get('description', '') or alert.get('rule_name', '')
9710
9728
  return str(desc)[:45] if desc else 'No description'
9711
9729
  elif key == 'timestamp':
9712
9730
  ts = alert.get('timestamp', 'N/A')
@@ -9718,9 +9736,9 @@ def _view_all_job_alerts(item: dict):
9718
9736
  columns = [
9719
9737
  {'name': '#', 'width': 5, 'key': '_idx'},
9720
9738
  {'name': 'Level', 'width': 10, 'key': 'level_display'},
9721
- {'name': 'Rule', 'width': 8, 'key': 'rule_id'},
9739
+ {'name': 'Type', 'width': 14, 'key': 'rule_id'},
9722
9740
  {'name': 'Agent', 'width': 15, 'key': 'agent_name'},
9723
- {'name': 'Description', 'width': 45, 'key': 'description'},
9741
+ {'name': 'Description', 'width': 42, 'key': 'description'},
9724
9742
  {'name': 'Time', 'width': 20, 'key': 'timestamp'},
9725
9743
  ]
9726
9744
 
@@ -31347,13 +31365,13 @@ def run_interactive_menu():
31347
31365
  click.echo("└" + "─" * (width - 2) + "┘")
31348
31366
  click.echo("\n")
31349
31367
 
31350
- # ASCII Art Banner - SOULEYEZ
31351
- click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True))
31352
- click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True))
31353
- click.echo(click.style(" ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ", fg='bright_cyan', bold=True))
31354
- click.echo(click.style(" ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ", fg='bright_cyan', bold=True))
31355
- click.echo(click.style(" ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗", fg='bright_cyan', bold=True))
31356
- click.echo(click.style(" ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝", fg='bright_cyan', bold=True))
31368
+ # ASCII Art Banner - SOULEYEZ with all-seeing eye on the right
31369
+ click.echo(click.style(" ███████╗ ██████╗ ██╗ ██╗██╗ ███████╗██╗ ██╗███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▄██▄", fg='bright_blue', bold=True))
31370
+ click.echo(click.style(" ██╔════╝██╔═══██╗██║ ██║██║ ██╔════╝╚██╗ ██╔╝██╔════╝╚══███╔╝", fg='bright_cyan', bold=True) + click.style(" ▄█▀ ▀█▄", fg='bright_blue', bold=True))
31371
+ click.echo(click.style(" ███████╗██║ ██║██║ ██║██║ █████╗ ╚████╔╝ █████╗ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" █ ◉ █", fg='bright_blue', bold=True))
31372
+ click.echo(click.style(" ╚════██║██║ ██║██║ ██║██║ ██╔══╝ ╚██╔╝ ██╔══╝ ███╔╝ ", fg='bright_cyan', bold=True) + click.style(" █ ═══ █", fg='bright_blue', bold=True))
31373
+ click.echo(click.style(" ███████║╚██████╔╝╚██████╔╝███████╗███████╗ ██║ ███████╗███████╗", fg='bright_cyan', bold=True) + click.style(" ▀█▄ ▄█▀", fg='bright_blue', bold=True))
31374
+ click.echo(click.style(" ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝╚══════╝", fg='bright_cyan', bold=True) + click.style(" ▀██▀", fg='bright_blue', bold=True))
31357
31375
  click.echo()
31358
31376
 
31359
31377
  # Tagline and description