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
@@ -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 (format: username:RID:LM:NT:::)
34
- hash_pattern = r'([^:\s]+):(\d+):([0-9a-fA-F]{32}):([0-9a-fA-F]{32}):::'
35
- for match in re.finditer(hash_pattern, content):
36
- username, rid, lm_hash, nt_hash = match.groups()
37
-
38
- # Skip empty hashes
39
- if nt_hash.lower() == '31d6cfe0d16ae931b73c59d7e0c089c0':
40
- continue
41
-
42
- hashes.append({
43
- 'username': username,
44
- 'rid': rid,
45
- 'lm_hash': lm_hash,
46
- 'nt_hash': nt_hash,
47
- 'hash_type': 'NTLM'
48
- })
49
-
50
- # Parse plaintext passwords (format: DOMAIN\username:password)
51
- plaintext_pattern = r'([^\\:\s]+)\\([^:\s]+):(.+?)(?:\n|$)'
52
- for match in re.finditer(plaintext_pattern, content):
53
- domain, username, password = match.groups()
54
-
55
- if password and not password.startswith('(null)'):
56
- credentials.append({
57
- 'domain': domain,
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
- 'password': password,
60
- 'credential_type': 'plaintext'
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 (format: $krb5asrep$...)
117
- hash_pattern = r'\$krb5asrep\$23\$([^@]+)@([^:]+):([^\s]+)'
118
- for match in re.finditer(hash_pattern, content):
119
- username, domain, hash_value = match.groups()
120
-
121
- hashes.append({
122
- 'username': username,
123
- 'domain': domain,
124
- 'hash': f'$krb5asrep$23${username}@{domain}:{hash_value}',
125
- 'hash_type': 'AS-REP',
126
- 'crackable': True
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
- if '[*] Requesting shares on' in content or 'C:\\Windows\\system32>' in content:
179
- success = True
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()]
@@ -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
- # Look for cracked passwords in format: password (username)
39
- match = re.match(r'^(\S+)\s+\((\S+)\)\s*$', line.strip())
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
- # Check session status
50
- if 'Session completed' in output:
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' in output:
74
+ elif any(x in output for x in ['Session aborted', 'session aborted', 'Interrupted']):
53
75
  results['session_status'] = 'aborted'
54
-
55
- # Parse summary line
56
- # Format: "2g 0:00:00:01 DONE..."
57
- summary_match = re.search(r'(\d+)g\s+[\d:]+\s+(DONE|Session)', output)
58
- if summary_match:
59
- results['total_cracked'] = int(summary_match.group(1))
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):
@@ -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
- ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
12
- return ansi_escape.sub('', text)
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)
@@ -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
- # Remove nmap metadata: "syn-ack ttl XX"
453
- cleaned = raw_version
454
- if cleaned.startswith('syn-ack'):
455
- parts_ver = cleaned.split()
456
- # Skip "syn-ack", "ttl", and the TTL number
457
- if 'ttl' in parts_ver:
458
- ttl_idx = parts_ver.index('ttl')
459
- cleaned = ' '.join(parts_ver[ttl_idx+2:]) # Skip "ttl XX"
460
- else:
461
- cleaned = ' '.join(parts_ver[1:]) # Skip "syn-ack"
462
-
463
- # Extract product and version
464
- # Pattern: "ProductName version.number rest of string"
465
- # Examples:
466
- # "ProFTPD 1.3.5" product="ProFTPD", version="1.3.5"
467
- # "Apache httpd 2.4.7 ((Ubuntu))" → product="Apache httpd", version="2.4.7"
468
- # "OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13" product="OpenSSH", version="6.6.1p1"
469
-
470
- version_pattern = r'([A-Za-z][\w\s\-\.]+?)\s+(v?\d+[\.\d]+[\w\-\.]*)'
471
- match = re.search(version_pattern, cleaned)
472
-
473
- if match:
474
- product = match.group(1).strip()
475
- version = match.group(2).strip()
476
- else:
477
- # Fallback: use cleaned string as version, service as product
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 = cleaned if cleaned else None
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;]*m', '', line)
69
- line = re.sub(r'[\[\]\|/\\-]', '', line, count=1) # Remove progress indicators
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) >= 3:
104
- # Tab-separated format
137
+ if len(parts) >= 2:
105
138
  share_name = parts[0].strip()
106
- permissions = parts[1].strip()
107
- comment = parts[2].strip() if len(parts) > 2 else ''
108
- elif len(parts) == 2:
109
- # Only 2 parts (share + permissions, no comment)
110
- share_name = parts[0].strip()
111
- permissions = parts[1].strip()
112
- comment = ''
113
- else:
114
- # No tabs - try space-based parsing
115
- # Match pattern: SHARENAME (spaces) PERMISSIONS (spaces) COMMENT
116
- # Need at least 2+ spaces to separate fields
117
- match = re.match(r'^\s*(\S+)\s{2,}(READ, WRITE|NO ACCESS|READ|WRITE)(?:\s{2,}(.*))?$', line)
118
- if match:
119
- share_name = match.group(1).strip()
120
- permissions = match.group(2).strip()
121
- comment = match.group(3).strip() if match.group(3) else ''
122
- else:
123
- continue
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', '', '*']: