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
|
@@ -30,35 +30,80 @@ def parse_secretsdump(log_path: str, target: str) -> Dict[str, Any]:
|
|
|
30
30
|
with open(log_path, 'r', encoding='utf-8') as f:
|
|
31
31
|
content = f.read()
|
|
32
32
|
|
|
33
|
-
# Parse NTLM hashes
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
33
|
+
# Parse NTLM hashes with multiple format support
|
|
34
|
+
# Format 1: username:RID:LM:NT::: (standard secretsdump)
|
|
35
|
+
# Format 2: username:RID:LM:NT:: (without trailing colon)
|
|
36
|
+
# Format 3: DOMAIN\username:RID:LM:NT::: (with domain prefix)
|
|
37
|
+
# Format 4: username:$NT$hash (simplified format)
|
|
38
|
+
|
|
39
|
+
# Standard format with 32-char hashes and trailing colons
|
|
40
|
+
hash_patterns = [
|
|
41
|
+
r'([^:\s\\]+):(\d+):([0-9a-fA-F]{32}):([0-9a-fA-F]{32}):::?', # Standard
|
|
42
|
+
r'([^:\s]+)\\([^:\s]+):(\d+):([0-9a-fA-F]{32}):([0-9a-fA-F]{32}):::?', # Domain\user
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
for pattern in hash_patterns:
|
|
46
|
+
for match in re.finditer(pattern, content):
|
|
47
|
+
groups = match.groups()
|
|
48
|
+
if len(groups) == 4:
|
|
49
|
+
username, rid, lm_hash, nt_hash = groups
|
|
50
|
+
elif len(groups) == 5:
|
|
51
|
+
# Domain\username format
|
|
52
|
+
domain, username, rid, lm_hash, nt_hash = groups
|
|
53
|
+
username = f"{domain}\\{username}"
|
|
54
|
+
else:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
# Skip empty hashes (blank password indicator)
|
|
58
|
+
if nt_hash.lower() == '31d6cfe0d16ae931b73c59d7e0c089c0':
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
hashes.append({
|
|
58
62
|
'username': username,
|
|
59
|
-
'
|
|
60
|
-
'
|
|
63
|
+
'rid': rid,
|
|
64
|
+
'lm_hash': lm_hash,
|
|
65
|
+
'nt_hash': nt_hash,
|
|
66
|
+
'hash_type': 'NTLM'
|
|
61
67
|
})
|
|
68
|
+
|
|
69
|
+
# Parse plaintext passwords with multiple format support
|
|
70
|
+
# Format 1: DOMAIN\username:password
|
|
71
|
+
# Format 2: DOMAIN\\username:password (escaped backslash)
|
|
72
|
+
# Format 3: username@DOMAIN:password
|
|
73
|
+
# Format 4: [*] DOMAIN\username:password (with prefix)
|
|
74
|
+
|
|
75
|
+
plaintext_patterns = [
|
|
76
|
+
r'([^\\:\s]+)[\\]+([^:\s]+):([^\n\r]+)', # DOMAIN\user:pass
|
|
77
|
+
r'([^@:\s]+)@([^:\s]+):([^\n\r]+)', # user@DOMAIN:pass
|
|
78
|
+
r'\[\*\]\s*([^\\:\s]+)[\\]+([^:\s]+):([^\n\r]+)', # [*] DOMAIN\user:pass
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
for pattern in plaintext_patterns:
|
|
82
|
+
for match in re.finditer(pattern, content):
|
|
83
|
+
groups = match.groups()
|
|
84
|
+
if len(groups) == 3:
|
|
85
|
+
part1, part2, password = groups
|
|
86
|
+
password = password.strip()
|
|
87
|
+
|
|
88
|
+
# Skip null/empty passwords and hash-like values
|
|
89
|
+
if not password or password.startswith('(null)'):
|
|
90
|
+
continue
|
|
91
|
+
# Skip if password looks like a hash (32+ hex chars)
|
|
92
|
+
if re.match(r'^[0-9a-fA-F]{32,}$', password):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
# Determine domain/username based on pattern
|
|
96
|
+
if '@' in match.group(0):
|
|
97
|
+
username, domain = part1, part2
|
|
98
|
+
else:
|
|
99
|
+
domain, username = part1, part2
|
|
100
|
+
|
|
101
|
+
credentials.append({
|
|
102
|
+
'domain': domain,
|
|
103
|
+
'username': username,
|
|
104
|
+
'password': password,
|
|
105
|
+
'credential_type': 'plaintext'
|
|
106
|
+
})
|
|
62
107
|
|
|
63
108
|
# Parse Kerberos keys (format: username:$krb5...)
|
|
64
109
|
krb_pattern = r'([^:\s]+):(\$krb5[^\s]+)'
|
|
@@ -113,25 +158,46 @@ def parse_getnpusers(log_path: str, target: str) -> Dict[str, Any]:
|
|
|
113
158
|
with open(log_path, 'r', encoding='utf-8') as f:
|
|
114
159
|
content = f.read()
|
|
115
160
|
|
|
116
|
-
# Parse AS-REP hashes
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
# Parse AS-REP hashes with multiple format support
|
|
162
|
+
# Format 1: $krb5asrep$23$user@DOMAIN:hash (etype 23)
|
|
163
|
+
# Format 2: $krb5asrep$18$user@DOMAIN:hash (etype 18)
|
|
164
|
+
# Format 3: $krb5asrep$user@DOMAIN:hash (no etype)
|
|
165
|
+
# Format 4: username:$krb5asrep... (username:hash format)
|
|
166
|
+
|
|
167
|
+
# Full format with etype: $krb5asrep$ETYPE$user@DOMAIN:hash
|
|
168
|
+
hash_patterns = [
|
|
169
|
+
r'\$krb5asrep\$(\d+)\$([^@]+)@([^:]+):([^\s]+)', # With etype
|
|
170
|
+
r'\$krb5asrep\$([^@$]+)@([^:]+):([^\s]+)', # Without etype
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
for pattern in hash_patterns:
|
|
174
|
+
for match in re.finditer(pattern, content):
|
|
175
|
+
groups = match.groups()
|
|
176
|
+
if len(groups) == 4:
|
|
177
|
+
etype, username, domain, hash_value = groups
|
|
178
|
+
full_hash = f'$krb5asrep${etype}${username}@{domain}:{hash_value}'
|
|
179
|
+
elif len(groups) == 3:
|
|
180
|
+
username, domain, hash_value = groups
|
|
181
|
+
etype = '23' # Default etype
|
|
182
|
+
full_hash = f'$krb5asrep${username}@{domain}:{hash_value}'
|
|
183
|
+
else:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
hashes.append({
|
|
187
|
+
'username': username,
|
|
188
|
+
'domain': domain,
|
|
189
|
+
'hash': full_hash,
|
|
190
|
+
'hash_type': 'AS-REP',
|
|
191
|
+
'etype': etype,
|
|
192
|
+
'crackable': True
|
|
193
|
+
})
|
|
194
|
+
|
|
129
195
|
# Also check for simple format (username:hash)
|
|
130
196
|
if not hashes:
|
|
131
197
|
simple_pattern = r'^([^:\s]+):(\$krb5asrep[^\s]+)'
|
|
132
198
|
for match in re.finditer(simple_pattern, content, re.MULTILINE):
|
|
133
199
|
username, hash_value = match.groups()
|
|
134
|
-
|
|
200
|
+
|
|
135
201
|
hashes.append({
|
|
136
202
|
'username': username,
|
|
137
203
|
'hash': hash_value,
|
|
@@ -174,9 +240,22 @@ def parse_psexec(log_path: str, target: str) -> Dict[str, Any]:
|
|
|
174
240
|
with open(log_path, 'r', encoding='utf-8') as f:
|
|
175
241
|
content = f.read()
|
|
176
242
|
|
|
177
|
-
# Check for successful connection
|
|
178
|
-
|
|
179
|
-
|
|
243
|
+
# Check for successful connection with multiple indicators
|
|
244
|
+
success_indicators = [
|
|
245
|
+
'[*] Requesting shares on',
|
|
246
|
+
'C:\\Windows\\system32>',
|
|
247
|
+
'C:\\WINDOWS\\system32>',
|
|
248
|
+
'[*] Uploading',
|
|
249
|
+
'[*] Opening SVCManager',
|
|
250
|
+
'Microsoft Windows', # Version banner
|
|
251
|
+
'[*] Starting service',
|
|
252
|
+
'Process .+ created', # Process creation message
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
for indicator in success_indicators:
|
|
256
|
+
if re.search(indicator, content, re.IGNORECASE):
|
|
257
|
+
success = True
|
|
258
|
+
break
|
|
180
259
|
|
|
181
260
|
# Extract command output (everything after the prompt)
|
|
182
261
|
output_lines = [line for line in content.split('\n') if line.strip()]
|
souleyez/parsers/john_parser.py
CHANGED
|
@@ -32,11 +32,18 @@ def parse_john_output(output: str, hash_file: str = None) -> Dict:
|
|
|
32
32
|
if loaded_match:
|
|
33
33
|
results['total_loaded'] = int(loaded_match.group(1))
|
|
34
34
|
|
|
35
|
-
# Parse cracked passwords from live output
|
|
36
|
-
# Format: "password (username)"
|
|
35
|
+
# Parse cracked passwords from live output with multiple format support
|
|
36
|
+
# Format 1: "password (username)"
|
|
37
|
+
# Format 2: "password (username)"
|
|
38
|
+
# Format 3: "username:password"
|
|
39
|
+
# Format 4: "password (username) [hash_type]"
|
|
37
40
|
for line in output.split('\n'):
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
line = line.strip()
|
|
42
|
+
if not line or line.startswith('#') or line.startswith('['):
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
# Try format: password (username) with optional hash type
|
|
46
|
+
match = re.match(r'^(\S+)\s+\(([^)]+)\)(?:\s+\[.+\])?\s*$', line)
|
|
40
47
|
if match:
|
|
41
48
|
password = match.group(1)
|
|
42
49
|
username = match.group(2)
|
|
@@ -45,18 +52,44 @@ def parse_john_output(output: str, hash_file: str = None) -> Dict:
|
|
|
45
52
|
'password': password,
|
|
46
53
|
'source': 'john_live'
|
|
47
54
|
})
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
# Try format: username:password (from --show output)
|
|
58
|
+
if ':' in line and not line.startswith('Loaded'):
|
|
59
|
+
parts = line.split(':')
|
|
60
|
+
if len(parts) >= 2 and len(parts[0]) > 0 and len(parts[-1]) > 0:
|
|
61
|
+
# Skip if it looks like a hash (32+ hex chars)
|
|
62
|
+
if not re.match(r'^[0-9a-fA-F]{32,}$', parts[-1]):
|
|
63
|
+
username = parts[0]
|
|
64
|
+
password = parts[-1]
|
|
65
|
+
results['cracked'].append({
|
|
66
|
+
'username': username,
|
|
67
|
+
'password': password,
|
|
68
|
+
'source': 'john_live'
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
# Check session status with multiple format support
|
|
72
|
+
if any(x in output for x in ['Session completed', 'session completed', 'Proceeding with next']):
|
|
51
73
|
results['session_status'] = 'completed'
|
|
52
|
-
elif 'Session aborted'
|
|
74
|
+
elif any(x in output for x in ['Session aborted', 'session aborted', 'Interrupted']):
|
|
53
75
|
results['session_status'] = 'aborted'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
elif 'No password hashes left to crack' in output:
|
|
77
|
+
results['session_status'] = 'completed'
|
|
78
|
+
|
|
79
|
+
# Parse summary line with multiple formats
|
|
80
|
+
# Format 1: "2g 0:00:00:01 DONE..."
|
|
81
|
+
# Format 2: "2g 0:00:00:01 100% DONE..."
|
|
82
|
+
# Format 3: "Session completed, 2g"
|
|
83
|
+
summary_patterns = [
|
|
84
|
+
r'(\d+)g\s+[\d:]+\s+(?:\d+%\s+)?(DONE|Session)',
|
|
85
|
+
r'Session completed[,\s]+(\d+)g',
|
|
86
|
+
r'(\d+)\s+password hashes? cracked',
|
|
87
|
+
]
|
|
88
|
+
for pattern in summary_patterns:
|
|
89
|
+
summary_match = re.search(pattern, output, re.IGNORECASE)
|
|
90
|
+
if summary_match:
|
|
91
|
+
results['total_cracked'] = int(summary_match.group(1))
|
|
92
|
+
break
|
|
60
93
|
|
|
61
94
|
# If hash_file provided, also parse john.pot or run --show
|
|
62
95
|
if hash_file and os.path.isfile(hash_file):
|
souleyez/parsers/msf_parser.py
CHANGED
|
@@ -7,9 +7,16 @@ from typing import Dict, Any
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def strip_ansi_codes(text: str) -> str:
|
|
10
|
-
"""Remove ANSI escape codes from text."""
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
"""Remove ANSI escape codes and other terminal control sequences from text."""
|
|
11
|
+
# Pattern 1: Standard ANSI escape sequences
|
|
12
|
+
text = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', text)
|
|
13
|
+
# Pattern 2: OSC sequences (Operating System Command)
|
|
14
|
+
text = re.sub(r'\x1B\].*?\x07', '', text)
|
|
15
|
+
# Pattern 3: Simple color codes
|
|
16
|
+
text = re.sub(r'\x1b\[[0-9;]*m', '', text)
|
|
17
|
+
# Pattern 4: Carriage returns and other control chars (except newlines)
|
|
18
|
+
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', text)
|
|
19
|
+
return text
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
def parse_msf_ssh_version(output: str, target: str) -> Dict[str, Any]:
|
|
@@ -254,6 +261,7 @@ def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str,
|
|
|
254
261
|
seen_creds = set() # Avoid duplicates
|
|
255
262
|
|
|
256
263
|
# Pattern 1: [+] 10.0.0.82:22 - Success: 'username:password' 'additional info'
|
|
264
|
+
# Also handles: [+] 10.0.0.82:22 - Success: "username:password"
|
|
257
265
|
success_pattern1 = r'\[\+\]\s+[\d.]+:(\d+)\s+-\s+Success:\s+[\'"]([^:]+):([^\'\"]+)[\'"]'
|
|
258
266
|
|
|
259
267
|
# Pattern 2: [+] IP:PORT - IP:PORT - Login Successful: user:pass@database
|
|
@@ -268,6 +276,13 @@ def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str,
|
|
|
268
276
|
# MSF telnet_login uses "username:password login: Login OK" format
|
|
269
277
|
success_pattern_telnet = r'\[\+\]\s+[\d.]+:(\d+).*-\s+([^:\s]+):([^\s]+)\s+login:\s+Login OK'
|
|
270
278
|
|
|
279
|
+
# Pattern 5: Flexible [+] with credentials anywhere (fallback)
|
|
280
|
+
# Handles: [+] 10.0.0.82:22 Found credentials: user:pass
|
|
281
|
+
success_pattern_flexible = r'\[\+\]\s+[\d.]+:(\d+).*(?:credential|found|valid).*?[\'"]?([^:\s\'\"]+):([^\'\"@\s]+)[\'"]?'
|
|
282
|
+
|
|
283
|
+
# Pattern 6: RDP format [+] 10.0.0.82:3389 - DOMAIN\user:password - Success
|
|
284
|
+
success_pattern_rdp = r'\[\+\]\s+[\d.]+:(\d+).*?([^\\:\s]+\\)?([^:\s]+):([^\s-]+)\s*-\s*Success'
|
|
285
|
+
|
|
271
286
|
# Try pattern 3 first (VNC with empty username)
|
|
272
287
|
for match in re.finditer(success_pattern3, clean_output):
|
|
273
288
|
port = int(match.group(1))
|
|
@@ -295,8 +310,8 @@ def parse_msf_login_success(output: str, target: str, module: str) -> Dict[str,
|
|
|
295
310
|
})
|
|
296
311
|
|
|
297
312
|
# Try other patterns (username:password style)
|
|
298
|
-
for pattern in [success_pattern1, success_pattern2, success_pattern_telnet]:
|
|
299
|
-
for match in re.finditer(pattern, clean_output):
|
|
313
|
+
for pattern in [success_pattern1, success_pattern2, success_pattern_telnet, success_pattern_flexible]:
|
|
314
|
+
for match in re.finditer(pattern, clean_output, re.IGNORECASE):
|
|
300
315
|
port = int(match.group(1))
|
|
301
316
|
username = match.group(2)
|
|
302
317
|
password = match.group(3)
|
souleyez/parsers/nmap_parser.py
CHANGED
|
@@ -449,34 +449,55 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
449
449
|
version = None
|
|
450
450
|
|
|
451
451
|
if raw_version:
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
product =
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
452
|
+
try:
|
|
453
|
+
# Remove nmap metadata: "syn-ack ttl XX", "reset ttl XX", etc.
|
|
454
|
+
cleaned = raw_version
|
|
455
|
+
# Handle various nmap scan type prefixes
|
|
456
|
+
metadata_prefixes = ['syn-ack', 'reset', 'conn-refused', 'no-response']
|
|
457
|
+
for prefix in metadata_prefixes:
|
|
458
|
+
if cleaned.lower().startswith(prefix):
|
|
459
|
+
parts_ver = cleaned.split()
|
|
460
|
+
# Skip prefix and "ttl XX" if present
|
|
461
|
+
if len(parts_ver) > 1 and 'ttl' in parts_ver:
|
|
462
|
+
try:
|
|
463
|
+
ttl_idx = parts_ver.index('ttl')
|
|
464
|
+
cleaned = ' '.join(parts_ver[ttl_idx+2:]) # Skip "ttl XX"
|
|
465
|
+
except (ValueError, IndexError):
|
|
466
|
+
cleaned = ' '.join(parts_ver[1:]) # Skip just prefix
|
|
467
|
+
else:
|
|
468
|
+
cleaned = ' '.join(parts_ver[1:]) # Skip just prefix
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
# Extract product and version with multiple patterns
|
|
472
|
+
# Pattern: "ProductName version.number rest of string"
|
|
473
|
+
# Examples:
|
|
474
|
+
# "ProFTPD 1.3.5" → product="ProFTPD", version="1.3.5"
|
|
475
|
+
# "Apache httpd 2.4.7 ((Ubuntu))" → product="Apache httpd", version="2.4.7"
|
|
476
|
+
# "OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13" → product="OpenSSH", version="6.6.1p1"
|
|
477
|
+
|
|
478
|
+
version_patterns = [
|
|
479
|
+
r'([A-Za-z][\w\s\-\.]+?)\s+(v?\d+[\.\d]+[\w\-\.]*)', # Standard
|
|
480
|
+
r'^([A-Za-z][\w\-]+)\s+(\d[\w\.\-]+)', # ProductName vX.Y.Z
|
|
481
|
+
r'^([A-Za-z][\w\s]+?)\s+v?(\d+(?:\.\d+)+)', # "Product Name 1.2.3"
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
matched = False
|
|
485
|
+
for pattern in version_patterns:
|
|
486
|
+
match = re.search(pattern, cleaned)
|
|
487
|
+
if match:
|
|
488
|
+
product = match.group(1).strip()
|
|
489
|
+
version = match.group(2).strip()
|
|
490
|
+
matched = True
|
|
491
|
+
break
|
|
492
|
+
|
|
493
|
+
if not matched:
|
|
494
|
+
# Fallback: use cleaned string as version, service as product
|
|
495
|
+
product = service_name
|
|
496
|
+
version = cleaned.strip() if cleaned.strip() else None
|
|
497
|
+
except Exception:
|
|
498
|
+
# If version parsing fails, use raw values
|
|
478
499
|
product = service_name
|
|
479
|
-
version =
|
|
500
|
+
version = raw_version
|
|
480
501
|
|
|
481
502
|
# Fallback: If service is unknown but port is a common web port, assume HTTP
|
|
482
503
|
# This handles cases where nmap misidentifies or can't fingerprint web apps
|
|
@@ -580,12 +601,108 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
580
601
|
# Parse vulnerability scripts (--script vuln output)
|
|
581
602
|
vulnerabilities = parse_nmap_vuln_scripts(output)
|
|
582
603
|
|
|
604
|
+
# Parse info scripts (vnc-info, ssh-hostkey, etc.)
|
|
605
|
+
info_scripts = parse_nmap_info_scripts(output)
|
|
606
|
+
|
|
583
607
|
return {
|
|
584
608
|
"hosts": hosts,
|
|
585
|
-
"vulnerabilities": vulnerabilities
|
|
609
|
+
"vulnerabilities": vulnerabilities,
|
|
610
|
+
"info_scripts": info_scripts
|
|
586
611
|
}
|
|
587
612
|
|
|
588
613
|
|
|
614
|
+
def parse_nmap_info_scripts(output: str) -> List[Dict[str, Any]]:
|
|
615
|
+
"""
|
|
616
|
+
Parse nmap info script output (non-vulnerability scripts).
|
|
617
|
+
|
|
618
|
+
Extracts results from scripts like vnc-info, ssh-hostkey, etc.
|
|
619
|
+
These provide useful information that should be captured as findings.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
List of info findings with:
|
|
623
|
+
- host_ip: IP address
|
|
624
|
+
- port: Port number
|
|
625
|
+
- script: Script name
|
|
626
|
+
- title: Finding title
|
|
627
|
+
- severity: Always 'info' for info scripts
|
|
628
|
+
- description: Script output content
|
|
629
|
+
"""
|
|
630
|
+
findings = []
|
|
631
|
+
current_host_ip = None
|
|
632
|
+
current_port = None
|
|
633
|
+
|
|
634
|
+
lines = output.split('\n')
|
|
635
|
+
i = 0
|
|
636
|
+
|
|
637
|
+
# Info scripts to capture (add more as needed)
|
|
638
|
+
info_scripts = {
|
|
639
|
+
'vnc-info': 'VNC Server Information',
|
|
640
|
+
'ssh-hostkey': 'SSH Host Key',
|
|
641
|
+
'http-server-header': 'HTTP Server Header',
|
|
642
|
+
'ssl-cert': 'SSL Certificate',
|
|
643
|
+
'http-title': 'HTTP Page Title',
|
|
644
|
+
'smb-os-discovery': 'SMB OS Discovery',
|
|
645
|
+
'rdp-ntlm-info': 'RDP NTLM Information',
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
while i < len(lines):
|
|
649
|
+
line = lines[i]
|
|
650
|
+
|
|
651
|
+
# Track current host - "Nmap scan report for 10.0.0.73"
|
|
652
|
+
if line.startswith("Nmap scan report for"):
|
|
653
|
+
match = re.search(r'for (\d+\.\d+\.\d+\.\d+)', line)
|
|
654
|
+
if match:
|
|
655
|
+
current_host_ip = match.group(1)
|
|
656
|
+
else:
|
|
657
|
+
# Try hostname (IP in parens)
|
|
658
|
+
match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\)', line)
|
|
659
|
+
if match:
|
|
660
|
+
current_host_ip = match.group(1)
|
|
661
|
+
|
|
662
|
+
# Track current port - "80/tcp open http"
|
|
663
|
+
elif re.match(r'^\d+/(tcp|udp)', line):
|
|
664
|
+
parts = line.split()
|
|
665
|
+
if parts:
|
|
666
|
+
port_proto = parts[0].split('/')
|
|
667
|
+
current_port = int(port_proto[0])
|
|
668
|
+
|
|
669
|
+
# Parse info script blocks
|
|
670
|
+
elif line.startswith('| ') and ':' in line and current_host_ip:
|
|
671
|
+
# Could be start of a script block like "| vnc-info:"
|
|
672
|
+
script_match = re.match(r'\|\s*([a-zA-Z0-9_-]+):\s*$', line)
|
|
673
|
+
if script_match:
|
|
674
|
+
script_name = script_match.group(1)
|
|
675
|
+
|
|
676
|
+
# Only process info scripts we care about
|
|
677
|
+
if script_name in info_scripts:
|
|
678
|
+
# Collect all lines of this script block
|
|
679
|
+
script_lines = []
|
|
680
|
+
i += 1
|
|
681
|
+
while i < len(lines) and (lines[i].startswith('|') or lines[i].startswith('|_')):
|
|
682
|
+
# Clean up the line
|
|
683
|
+
clean_line = lines[i].lstrip('|').lstrip('_').strip()
|
|
684
|
+
if clean_line:
|
|
685
|
+
script_lines.append(clean_line)
|
|
686
|
+
if lines[i].startswith('|_'):
|
|
687
|
+
break
|
|
688
|
+
i += 1
|
|
689
|
+
|
|
690
|
+
if script_lines:
|
|
691
|
+
findings.append({
|
|
692
|
+
'host_ip': current_host_ip,
|
|
693
|
+
'port': current_port,
|
|
694
|
+
'script': script_name,
|
|
695
|
+
'title': info_scripts[script_name],
|
|
696
|
+
'severity': 'info',
|
|
697
|
+
'description': '\n'.join(script_lines)
|
|
698
|
+
})
|
|
699
|
+
continue
|
|
700
|
+
|
|
701
|
+
i += 1
|
|
702
|
+
|
|
703
|
+
return findings
|
|
704
|
+
|
|
705
|
+
|
|
589
706
|
def parse_nmap_output(content: str, target: str = "") -> Dict[str, Any]:
|
|
590
707
|
"""
|
|
591
708
|
Wrapper for parse_nmap_text that matches the display interface.
|
|
@@ -49,24 +49,54 @@ def parse_smbmap_output(output: str, target: str = "") -> Dict[str, Any]:
|
|
|
49
49
|
'timestamp': str
|
|
50
50
|
},
|
|
51
51
|
...
|
|
52
|
-
]
|
|
52
|
+
],
|
|
53
|
+
'smb_detected': bool, # True if SMB service was detected
|
|
54
|
+
'hosts_count': int, # Number of hosts serving SMB
|
|
55
|
+
'error': str # Error message if tool crashed
|
|
53
56
|
}
|
|
54
57
|
"""
|
|
55
58
|
result = {
|
|
56
59
|
'target': target,
|
|
57
60
|
'status': None,
|
|
58
61
|
'shares': [],
|
|
59
|
-
'files': []
|
|
62
|
+
'files': [],
|
|
63
|
+
'smb_detected': False,
|
|
64
|
+
'hosts_count': 0,
|
|
65
|
+
'error': None
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
# Check for SMB detection (even if tool crashes later)
|
|
69
|
+
# [*] Detected 1 hosts serving SMB
|
|
70
|
+
smb_detected_match = re.search(r'\[\*\]\s*Detected\s+(\d+)\s+hosts?\s+serving\s+SMB', output)
|
|
71
|
+
if smb_detected_match:
|
|
72
|
+
result['smb_detected'] = True
|
|
73
|
+
result['hosts_count'] = int(smb_detected_match.group(1))
|
|
74
|
+
|
|
75
|
+
# Check for Python traceback (tool crash)
|
|
76
|
+
if 'Traceback (most recent call last):' in output:
|
|
77
|
+
# Extract error message from traceback
|
|
78
|
+
error_match = re.search(r'(?:Error|Exception).*?[\'"]([^\'"]+)[\'"]', output, re.DOTALL)
|
|
79
|
+
if error_match:
|
|
80
|
+
result['error'] = error_match.group(1)
|
|
81
|
+
else:
|
|
82
|
+
# Try to get the last line of the traceback
|
|
83
|
+
traceback_lines = output.split('Traceback (most recent call last):')[-1].strip().split('\n')
|
|
84
|
+
for line in reversed(traceback_lines):
|
|
85
|
+
line = line.strip()
|
|
86
|
+
if line and not line.startswith('File') and not line.startswith('raise'):
|
|
87
|
+
result['error'] = line[:200] # Limit length
|
|
88
|
+
break
|
|
89
|
+
|
|
62
90
|
lines = output.split('\n')
|
|
63
91
|
in_share_table = False
|
|
64
92
|
current_share = None
|
|
65
93
|
|
|
66
94
|
for i, line in enumerate(lines):
|
|
67
|
-
# Remove ANSI color codes and control characters
|
|
68
|
-
line = re.sub(r'\x1b\[[0-9;]*
|
|
69
|
-
line = re.sub(r'
|
|
95
|
+
# Remove ANSI color codes and control characters more thoroughly
|
|
96
|
+
line = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', line) # All ANSI escape sequences
|
|
97
|
+
line = re.sub(r'\x1b\].*?\x07', '', line) # OSC sequences
|
|
98
|
+
# Only remove leading progress indicators, not all brackets
|
|
99
|
+
line = re.sub(r'^[\[\]\|/\\-]+\s*', '', line)
|
|
70
100
|
line = line.strip()
|
|
71
101
|
|
|
72
102
|
# Extract target and status
|
|
@@ -98,29 +128,43 @@ def parse_smbmap_output(output: str, target: str = "") -> Dict[str, Any]:
|
|
|
98
128
|
# Format: sharename <tabs/spaces> permissions <tabs/spaces> comment
|
|
99
129
|
# tmp READ, WRITE oh noes!
|
|
100
130
|
|
|
131
|
+
share_name = None
|
|
132
|
+
permissions = None
|
|
133
|
+
comment = ''
|
|
134
|
+
|
|
101
135
|
# Try tab split first
|
|
102
136
|
parts = re.split(r'\t+', line)
|
|
103
|
-
if len(parts) >=
|
|
104
|
-
# Tab-separated format
|
|
137
|
+
if len(parts) >= 2:
|
|
105
138
|
share_name = parts[0].strip()
|
|
106
|
-
permissions
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
# Find permissions in remaining parts
|
|
140
|
+
for p in parts[1:]:
|
|
141
|
+
p = p.strip().upper()
|
|
142
|
+
if any(x in p for x in ['READ', 'WRITE', 'NO ACCESS', 'NOACCESS']):
|
|
143
|
+
permissions = p
|
|
144
|
+
break
|
|
145
|
+
# Comment is everything after permissions
|
|
146
|
+
if permissions and len(parts) > 2:
|
|
147
|
+
perm_idx = next((i for i, p in enumerate(parts) if permissions in p.upper()), -1)
|
|
148
|
+
if perm_idx >= 0 and perm_idx + 1 < len(parts):
|
|
149
|
+
comment = ' '.join(parts[perm_idx + 1:]).strip()
|
|
150
|
+
|
|
151
|
+
# No tabs or tab parse failed - try space-based parsing
|
|
152
|
+
if not permissions:
|
|
153
|
+
# Match patterns with flexible spacing and permission variations
|
|
154
|
+
permission_patterns = [
|
|
155
|
+
r'^\s*(\S+)\s{2,}(READ,?\s*WRITE|READ\s*ONLY|WRITE\s*ONLY|NO\s*ACCESS|READ|WRITE)(?:\s{2,}(.*))?$',
|
|
156
|
+
r'^\s*(\S+)\s+(READ,?\s*WRITE|READ\s*ONLY|WRITE\s*ONLY|NO\s*ACCESS|READ|WRITE)\s*(.*)$',
|
|
157
|
+
]
|
|
158
|
+
for pattern in permission_patterns:
|
|
159
|
+
match = re.match(pattern, line, re.IGNORECASE)
|
|
160
|
+
if match:
|
|
161
|
+
share_name = match.group(1).strip()
|
|
162
|
+
permissions = match.group(2).strip().upper()
|
|
163
|
+
comment = match.group(3).strip() if match.group(3) else ''
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
if not share_name or not permissions:
|
|
167
|
+
continue
|
|
124
168
|
|
|
125
169
|
# Skip empty lines or non-share lines
|
|
126
170
|
if not share_name or share_name in ['Disk', 'IPC', '', '*']:
|