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.
- souleyez/__init__.py +1 -1
- souleyez/assets/__init__.py +1 -0
- souleyez/assets/souleyez-icon.png +0 -0
- souleyez/core/msf_sync_manager.py +15 -5
- souleyez/core/tool_chaining.py +221 -29
- souleyez/detection/validator.py +4 -2
- souleyez/docs/README.md +2 -2
- souleyez/docs/user-guide/installation.md +14 -1
- souleyez/engine/background.py +25 -1
- souleyez/engine/result_handler.py +129 -0
- souleyez/integrations/siem/splunk.py +58 -11
- souleyez/main.py +103 -4
- souleyez/parsers/crackmapexec_parser.py +101 -43
- souleyez/parsers/dnsrecon_parser.py +50 -35
- souleyez/parsers/enum4linux_parser.py +101 -21
- souleyez/parsers/http_fingerprint_parser.py +319 -0
- souleyez/parsers/hydra_parser.py +56 -5
- souleyez/parsers/impacket_parser.py +123 -44
- souleyez/parsers/john_parser.py +47 -14
- souleyez/parsers/msf_parser.py +20 -5
- souleyez/parsers/nmap_parser.py +145 -28
- souleyez/parsers/smbmap_parser.py +69 -25
- souleyez/parsers/sqlmap_parser.py +72 -26
- souleyez/parsers/theharvester_parser.py +21 -13
- souleyez/plugins/gobuster.py +96 -3
- souleyez/plugins/http_fingerprint.py +592 -0
- souleyez/plugins/msf_exploit.py +6 -3
- souleyez/plugins/nuclei.py +41 -17
- souleyez/ui/interactive.py +130 -20
- souleyez/ui/setup_wizard.py +424 -58
- souleyez/ui/tool_setup.py +52 -52
- souleyez/utils/tool_checker.py +75 -13
- {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/METADATA +16 -3
- {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/RECORD +38 -34
- {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/WHEEL +0 -0
- {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.16.0.dist-info → souleyez-2.26.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
88
|
-
|
|
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'] =
|
|
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':
|
|
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':
|
|
170
|
+
'url': effective_url,
|
|
151
171
|
'parameter': param,
|
|
152
172
|
'method': method,
|
|
153
|
-
'post_data':
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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':
|
|
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'] =
|
|
371
|
+
result['injectable_url'] = effective_url
|
|
336
372
|
result['injectable_method'] = param_method # GET, POST, etc.
|
|
337
|
-
if param_method == 'POST' and
|
|
338
|
-
result['injectable_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':
|
|
378
|
+
'url': effective_url,
|
|
343
379
|
'parameter': param,
|
|
344
380
|
'method': param_method,
|
|
345
|
-
'post_data':
|
|
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':
|
|
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'] =
|
|
425
|
+
result['injectable_url'] = effective_url
|
|
380
426
|
result['injectable_method'] = method
|
|
381
|
-
if method == 'POST' and
|
|
382
|
-
result['injectable_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':
|
|
432
|
+
'url': effective_url,
|
|
387
433
|
'parameter': param,
|
|
388
434
|
'method': method,
|
|
389
|
-
'post_data':
|
|
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
|
-
|
|
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
|
|
74
|
+
elif any(x in line_lower for x in ['urls found', 'interesting urls', 'url found']):
|
|
74
75
|
current_section = 'urls'
|
|
75
|
-
elif '
|
|
76
|
+
elif any(x in line_lower for x in ['ips found', 'ip found', 'ip addresses']):
|
|
76
77
|
current_section = 'ips'
|
|
77
|
-
elif
|
|
78
|
+
elif any(x in line_lower for x in ['emails found', 'email found', 'email addresses']):
|
|
78
79
|
current_section = 'emails'
|
|
79
|
-
elif
|
|
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
|
|
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
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
#
|
|
131
|
-
|
|
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
|
|
souleyez/plugins/gobuster.py
CHANGED
|
@@ -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:
|