souleyez 2.43.26__py3-none-any.whl → 2.43.34__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 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23434 -10286
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/parsers/nmap_parser.py
CHANGED
|
@@ -38,7 +38,7 @@ def parse_nmap_vuln_scripts(output: str) -> List[Dict[str, Any]]:
|
|
|
38
38
|
current_host_ip = None
|
|
39
39
|
current_port = None
|
|
40
40
|
|
|
41
|
-
lines = output.split(
|
|
41
|
+
lines = output.split("\n")
|
|
42
42
|
i = 0
|
|
43
43
|
|
|
44
44
|
while i < len(lines):
|
|
@@ -46,47 +46,53 @@ def parse_nmap_vuln_scripts(output: str) -> List[Dict[str, Any]]:
|
|
|
46
46
|
|
|
47
47
|
# Track current host - "Nmap scan report for 10.0.0.73"
|
|
48
48
|
if line.startswith("Nmap scan report for"):
|
|
49
|
-
match = re.search(r
|
|
49
|
+
match = re.search(r"for (\d+\.\d+\.\d+\.\d+)", line)
|
|
50
50
|
if match:
|
|
51
51
|
current_host_ip = match.group(1)
|
|
52
52
|
else:
|
|
53
53
|
# Try hostname (IP in parens)
|
|
54
|
-
match = re.search(r
|
|
54
|
+
match = re.search(r"\((\d+\.\d+\.\d+\.\d+)\)", line)
|
|
55
55
|
if match:
|
|
56
56
|
current_host_ip = match.group(1)
|
|
57
57
|
|
|
58
58
|
# Track current port - "80/tcp open http"
|
|
59
|
-
elif re.match(r
|
|
59
|
+
elif re.match(r"^\d+/(tcp|udp)", line):
|
|
60
60
|
parts = line.split()
|
|
61
61
|
if parts:
|
|
62
|
-
port_proto = parts[0].split(
|
|
62
|
+
port_proto = parts[0].split("/")
|
|
63
63
|
current_port = int(port_proto[0])
|
|
64
64
|
|
|
65
65
|
# Parse vulnerability script blocks
|
|
66
|
-
elif line.startswith(
|
|
66
|
+
elif line.startswith("| ") and ":" in line and current_host_ip:
|
|
67
67
|
# Could be start of a script block like "| ssl-poodle:"
|
|
68
|
-
script_match = re.match(r
|
|
68
|
+
script_match = re.match(r"\|\s*([a-zA-Z0-9_-]+):\s*$", line)
|
|
69
69
|
if script_match:
|
|
70
70
|
script_name = script_match.group(1)
|
|
71
71
|
|
|
72
72
|
# Collect all lines of this script block
|
|
73
73
|
script_lines = []
|
|
74
74
|
i += 1
|
|
75
|
-
while i < len(lines) and (
|
|
75
|
+
while i < len(lines) and (
|
|
76
|
+
lines[i].startswith("|") or lines[i].startswith("|_")
|
|
77
|
+
):
|
|
76
78
|
script_lines.append(lines[i])
|
|
77
|
-
if lines[i].startswith(
|
|
79
|
+
if lines[i].startswith("|_"):
|
|
78
80
|
break
|
|
79
81
|
i += 1
|
|
80
82
|
|
|
81
|
-
script_block =
|
|
83
|
+
script_block = "\n".join(script_lines)
|
|
82
84
|
|
|
83
85
|
# Parse vulners output specially (CVE lists with scores)
|
|
84
|
-
if script_name ==
|
|
85
|
-
vulns = _parse_vulners_block(
|
|
86
|
+
if script_name == "vulners":
|
|
87
|
+
vulns = _parse_vulners_block(
|
|
88
|
+
script_block, current_host_ip, current_port
|
|
89
|
+
)
|
|
86
90
|
vulnerabilities.extend(vulns)
|
|
87
91
|
else:
|
|
88
92
|
# Parse standard vuln script output
|
|
89
|
-
vuln = _parse_vuln_script_block(
|
|
93
|
+
vuln = _parse_vuln_script_block(
|
|
94
|
+
script_name, script_block, current_host_ip, current_port
|
|
95
|
+
)
|
|
90
96
|
if vuln:
|
|
91
97
|
vulnerabilities.extend(vuln)
|
|
92
98
|
|
|
@@ -112,14 +118,16 @@ def _parse_vulners_block(block: str, host_ip: str, port: int) -> List[Dict[str,
|
|
|
112
118
|
vulnerabilities = []
|
|
113
119
|
current_software = None
|
|
114
120
|
|
|
115
|
-
for line in block.split(
|
|
116
|
-
line = line.strip().lstrip(
|
|
121
|
+
for line in block.split("\n"):
|
|
122
|
+
line = line.strip().lstrip("|").lstrip("_").strip()
|
|
117
123
|
if not line:
|
|
118
124
|
continue
|
|
119
125
|
|
|
120
126
|
# Track CPE or software version (e.g., "nginx 1.19.0:")
|
|
121
|
-
if line.startswith(
|
|
122
|
-
|
|
127
|
+
if line.startswith("cpe:/") or (
|
|
128
|
+
line.endswith(":") and not line.startswith("http")
|
|
129
|
+
):
|
|
130
|
+
current_software = line.rstrip(":")
|
|
123
131
|
continue
|
|
124
132
|
|
|
125
133
|
# Parse vulners line - multiple formats:
|
|
@@ -129,21 +137,21 @@ def _parse_vulners_block(block: str, host_ip: str, port: int) -> List[Dict[str,
|
|
|
129
137
|
# 4. "EDB-ID:50973 7.7 https://..."
|
|
130
138
|
|
|
131
139
|
# Generic pattern: ID SCORE URL [*EXPLOIT*]
|
|
132
|
-
vuln_match = re.match(r
|
|
140
|
+
vuln_match = re.match(r"(\S+)\s+(\d+\.?\d*)\s+(https?://\S+)", line)
|
|
133
141
|
if vuln_match:
|
|
134
142
|
vuln_id = vuln_match.group(1)
|
|
135
143
|
cvss_score = float(vuln_match.group(2))
|
|
136
144
|
ref_url = vuln_match.group(3)
|
|
137
|
-
is_exploit =
|
|
145
|
+
is_exploit = "*EXPLOIT*" in line
|
|
138
146
|
|
|
139
147
|
# Extract CVE from various formats
|
|
140
148
|
cve_ids = []
|
|
141
|
-
cve_in_id = re.search(r
|
|
149
|
+
cve_in_id = re.search(r"CVE-\d{4}-\d+", vuln_id, re.IGNORECASE)
|
|
142
150
|
if cve_in_id:
|
|
143
151
|
cve_ids.append(cve_in_id.group(0).upper())
|
|
144
152
|
|
|
145
153
|
# Also check the URL for CVE
|
|
146
|
-
cve_in_url = re.search(r
|
|
154
|
+
cve_in_url = re.search(r"CVE-\d{4}-\d+", ref_url, re.IGNORECASE)
|
|
147
155
|
if cve_in_url and cve_in_url.group(0).upper() not in cve_ids:
|
|
148
156
|
cve_ids.append(cve_in_url.group(0).upper())
|
|
149
157
|
|
|
@@ -157,24 +165,28 @@ def _parse_vulners_block(block: str, host_ip: str, port: int) -> List[Dict[str,
|
|
|
157
165
|
|
|
158
166
|
# Include high-severity vulns (CVSS >= 7.0) and all exploits
|
|
159
167
|
if cvss_score >= 7.0 or is_exploit:
|
|
160
|
-
vulnerabilities.append(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
vulnerabilities.append(
|
|
169
|
+
{
|
|
170
|
+
"host_ip": host_ip,
|
|
171
|
+
"port": port,
|
|
172
|
+
"script": "vulners",
|
|
173
|
+
"title": title,
|
|
174
|
+
"state": "VULNERABLE",
|
|
175
|
+
"cve_ids": cve_ids,
|
|
176
|
+
"cvss_score": cvss_score,
|
|
177
|
+
"references": [ref_url],
|
|
178
|
+
"description": f"{'Exploit available: ' if is_exploit else ''}{vuln_id} detected via vulners database",
|
|
179
|
+
"software": current_software,
|
|
180
|
+
"is_exploit": is_exploit,
|
|
181
|
+
}
|
|
182
|
+
)
|
|
173
183
|
|
|
174
184
|
return vulnerabilities
|
|
175
185
|
|
|
176
186
|
|
|
177
|
-
def _parse_vuln_script_block(
|
|
187
|
+
def _parse_vuln_script_block(
|
|
188
|
+
script_name: str, block: str, host_ip: str, port: int
|
|
189
|
+
) -> List[Dict[str, Any]]:
|
|
178
190
|
"""
|
|
179
191
|
Parse a standard nmap vuln script block.
|
|
180
192
|
|
|
@@ -183,76 +195,91 @@ def _parse_vuln_script_block(script_name: str, block: str, host_ip: str, port: i
|
|
|
183
195
|
vulnerabilities = []
|
|
184
196
|
|
|
185
197
|
# Check if this block indicates a vulnerability
|
|
186
|
-
if
|
|
198
|
+
if "VULNERABLE" not in block.upper():
|
|
187
199
|
return []
|
|
188
200
|
|
|
189
201
|
# Split into individual vulnerability entries (some scripts report multiple)
|
|
190
202
|
# Each vuln typically starts with a title line after VULNERABLE:
|
|
191
|
-
vuln_sections = re.split(r
|
|
203
|
+
vuln_sections = re.split(r"\|\s+VULNERABLE:", block, flags=re.IGNORECASE)
|
|
192
204
|
|
|
193
205
|
for section in vuln_sections[1:]: # Skip first empty part
|
|
194
206
|
vuln = {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
"host_ip": host_ip,
|
|
208
|
+
"port": port,
|
|
209
|
+
"script": script_name,
|
|
210
|
+
"title": None,
|
|
211
|
+
"state": "VULNERABLE",
|
|
212
|
+
"cve_ids": [],
|
|
213
|
+
"cvss_score": None,
|
|
214
|
+
"references": [],
|
|
215
|
+
"description": "",
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
lines = section.strip().split(
|
|
218
|
+
lines = section.strip().split("\n")
|
|
207
219
|
description_parts = []
|
|
208
220
|
|
|
209
221
|
for line in lines:
|
|
210
|
-
line = line.strip().lstrip(
|
|
222
|
+
line = line.strip().lstrip("|").lstrip("_").strip()
|
|
211
223
|
|
|
212
224
|
if not line:
|
|
213
225
|
continue
|
|
214
226
|
|
|
215
227
|
# Extract title (first non-empty line after VULNERABLE:)
|
|
216
|
-
if
|
|
217
|
-
vuln[
|
|
228
|
+
if (
|
|
229
|
+
not vuln["title"]
|
|
230
|
+
and line
|
|
231
|
+
and not line.startswith("State:")
|
|
232
|
+
and not line.startswith("IDs:")
|
|
233
|
+
):
|
|
234
|
+
vuln["title"] = line
|
|
218
235
|
|
|
219
236
|
# Extract CVE IDs
|
|
220
|
-
cve_matches = re.findall(
|
|
237
|
+
cve_matches = re.findall(
|
|
238
|
+
r"CVE[:\s-]*(CVE-\d{4}-\d+|(\d{4}-\d+))", line, re.IGNORECASE
|
|
239
|
+
)
|
|
221
240
|
for match in cve_matches:
|
|
222
|
-
cve = match[0] if match[0].startswith(
|
|
223
|
-
if cve not in vuln[
|
|
224
|
-
vuln[
|
|
241
|
+
cve = match[0] if match[0].startswith("CVE-") else f"CVE-{match[1]}"
|
|
242
|
+
if cve not in vuln["cve_ids"]:
|
|
243
|
+
vuln["cve_ids"].append(cve)
|
|
225
244
|
|
|
226
245
|
# Also check for CVE format without prefix
|
|
227
|
-
simple_cve = re.findall(r
|
|
246
|
+
simple_cve = re.findall(r"\bCVE-(\d{4}-\d+)\b", line)
|
|
228
247
|
for cve_num in simple_cve:
|
|
229
248
|
cve = f"CVE-{cve_num}"
|
|
230
|
-
if cve not in vuln[
|
|
231
|
-
vuln[
|
|
249
|
+
if cve not in vuln["cve_ids"]:
|
|
250
|
+
vuln["cve_ids"].append(cve)
|
|
232
251
|
|
|
233
252
|
# Extract references
|
|
234
|
-
if
|
|
235
|
-
urls = re.findall(r
|
|
236
|
-
vuln[
|
|
253
|
+
if "http" in line.lower():
|
|
254
|
+
urls = re.findall(r"https?://\S+", line)
|
|
255
|
+
vuln["references"].extend(urls)
|
|
237
256
|
|
|
238
257
|
# Build description (skip metadata lines)
|
|
239
|
-
if
|
|
240
|
-
|
|
258
|
+
if (
|
|
259
|
+
not line.startswith("State:")
|
|
260
|
+
and not line.startswith("IDs:")
|
|
261
|
+
and not line.startswith("References:")
|
|
262
|
+
):
|
|
263
|
+
if line.startswith("Disclosure date:") or line.startswith(
|
|
264
|
+
"Check results:"
|
|
265
|
+
):
|
|
241
266
|
description_parts.append(line)
|
|
242
|
-
elif vuln[
|
|
267
|
+
elif vuln["title"] and line != vuln["title"]:
|
|
243
268
|
description_parts.append(line)
|
|
244
269
|
|
|
245
|
-
vuln[
|
|
270
|
+
vuln["description"] = " ".join(
|
|
271
|
+
description_parts[:3]
|
|
272
|
+
) # First 3 lines of description
|
|
246
273
|
|
|
247
274
|
# Generate title from script name if not found
|
|
248
|
-
if not vuln[
|
|
249
|
-
vuln[
|
|
275
|
+
if not vuln["title"]:
|
|
276
|
+
vuln["title"] = script_name.replace("-", " ").replace("_", " ").title()
|
|
250
277
|
|
|
251
278
|
# Add CVE to title if found
|
|
252
|
-
if vuln[
|
|
253
|
-
vuln[
|
|
279
|
+
if vuln["cve_ids"] and vuln["cve_ids"][0] not in vuln["title"]:
|
|
280
|
+
vuln["title"] = f"{vuln['cve_ids'][0]} - {vuln['title']}"
|
|
254
281
|
|
|
255
|
-
if vuln[
|
|
282
|
+
if vuln["title"]:
|
|
256
283
|
vulnerabilities.append(vuln)
|
|
257
284
|
|
|
258
285
|
return vulnerabilities
|
|
@@ -278,13 +305,33 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
278
305
|
}
|
|
279
306
|
"""
|
|
280
307
|
hosts = []
|
|
308
|
+
domains = [] # Track AD domains discovered from LDAP/SMB banners
|
|
281
309
|
current_host = None
|
|
282
310
|
|
|
283
|
-
lines = output.split(
|
|
311
|
+
lines = output.split("\n")
|
|
284
312
|
|
|
285
313
|
for line in lines:
|
|
286
314
|
line = line.strip()
|
|
287
315
|
|
|
316
|
+
# Extract AD domain from LDAP/SMB banners like:
|
|
317
|
+
# "Microsoft Windows Active Directory LDAP (Domain: active.htb, Site: Default-First-Site-Name)"
|
|
318
|
+
# Note: Nmap's LDAP scripts sometimes incorrectly append digits to domain names
|
|
319
|
+
# (e.g., "baby.vl0." instead of "baby.vl"). Clean up these artifacts.
|
|
320
|
+
domain_match = re.search(r"\(Domain:\s*([^,\)]+)", line)
|
|
321
|
+
if domain_match:
|
|
322
|
+
domain_name = domain_match.group(1).strip()
|
|
323
|
+
# Strip trailing dots (FQDN format) and trailing digits that are nmap artifacts
|
|
324
|
+
# Pattern: strip trailing digits followed by optional dot (e.g., "baby.vl0." → "baby.vl")
|
|
325
|
+
domain_name = re.sub(r"\d+\.?$", "", domain_name).rstrip(".")
|
|
326
|
+
if domain_name and not any(d["domain"] == domain_name for d in domains):
|
|
327
|
+
domains.append(
|
|
328
|
+
{
|
|
329
|
+
"domain": domain_name,
|
|
330
|
+
"ip": current_host.get("ip") if current_host else None,
|
|
331
|
+
"source": "nmap_banner",
|
|
332
|
+
}
|
|
333
|
+
)
|
|
334
|
+
|
|
288
335
|
# Parse host line: "Nmap scan report for 10.0.0.5" or "Nmap scan report for example.com (10.0.0.5)"
|
|
289
336
|
# Also handles: "Nmap scan report for 10.0.0.5 [host down, received no-response]"
|
|
290
337
|
if line.startswith("Nmap scan report for"):
|
|
@@ -293,16 +340,16 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
293
340
|
|
|
294
341
|
# Extract IP and hostname, removing any trailing status info like [host down...]
|
|
295
342
|
# Remove the [host down...] part first
|
|
296
|
-
clean_line = re.sub(r
|
|
343
|
+
clean_line = re.sub(r"\s*\[host.*?\].*$", "", line)
|
|
297
344
|
|
|
298
345
|
# Extract IP and hostname
|
|
299
|
-
match = re.search(r
|
|
346
|
+
match = re.search(r"for (.+?)(?:\s+\((.+?)\))?$", clean_line)
|
|
300
347
|
if match:
|
|
301
348
|
target = match.group(1)
|
|
302
349
|
paren_content = match.group(2)
|
|
303
350
|
|
|
304
351
|
# Determine if target is IP or hostname
|
|
305
|
-
if re.match(r
|
|
352
|
+
if re.match(r"^\d+\.\d+\.\d+\.\d+$", target):
|
|
306
353
|
ip = target
|
|
307
354
|
hostname = paren_content if paren_content else None
|
|
308
355
|
else:
|
|
@@ -312,15 +359,15 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
312
359
|
# Check if this host already exists (e.g., from "Discovered open port" lines)
|
|
313
360
|
existing_host = None
|
|
314
361
|
for h in hosts:
|
|
315
|
-
if h.get(
|
|
362
|
+
if h.get("ip") == ip:
|
|
316
363
|
existing_host = h
|
|
317
364
|
break
|
|
318
365
|
|
|
319
366
|
if existing_host:
|
|
320
367
|
# Use existing host and update hostname if needed
|
|
321
368
|
current_host = existing_host
|
|
322
|
-
if hostname and not current_host.get(
|
|
323
|
-
current_host[
|
|
369
|
+
if hostname and not current_host.get("hostname"):
|
|
370
|
+
current_host["hostname"] = hostname
|
|
324
371
|
else:
|
|
325
372
|
current_host = {
|
|
326
373
|
"ip": ip,
|
|
@@ -329,11 +376,11 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
329
376
|
"os": None,
|
|
330
377
|
"mac_address": None,
|
|
331
378
|
"os_accuracy": None,
|
|
332
|
-
"services": []
|
|
379
|
+
"services": [],
|
|
333
380
|
}
|
|
334
381
|
|
|
335
382
|
# Check if the line indicates host is down
|
|
336
|
-
if
|
|
383
|
+
if "[host down" in line and current_host:
|
|
337
384
|
current_host["status"] = "down"
|
|
338
385
|
|
|
339
386
|
# Parse host status
|
|
@@ -346,21 +393,23 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
346
393
|
# Parse "Skipping host" (host timeout) - still mark as up since we got some response
|
|
347
394
|
elif "Skipping host" in line and "due to host timeout" in line:
|
|
348
395
|
# Extract the IP from the line
|
|
349
|
-
match = re.search(r
|
|
396
|
+
match = re.search(r"Skipping host (\d+\.\d+\.\d+\.\d+)", line)
|
|
350
397
|
if match:
|
|
351
398
|
skip_ip = match.group(1)
|
|
352
399
|
# Find the host with this IP and mark it (it may have discovered ports)
|
|
353
400
|
for h in hosts:
|
|
354
|
-
if h.get(
|
|
355
|
-
h[
|
|
401
|
+
if h.get("ip") == skip_ip:
|
|
402
|
+
h["status"] = "up" # Host responded, just timed out
|
|
356
403
|
break
|
|
357
|
-
if current_host and current_host.get(
|
|
358
|
-
current_host[
|
|
404
|
+
if current_host and current_host.get("ip") == skip_ip:
|
|
405
|
+
current_host["status"] = "up"
|
|
359
406
|
|
|
360
407
|
# Parse "Discovered open port" lines (shown during scan, before timeout)
|
|
361
408
|
# Format: "Discovered open port 443/tcp on 10.0.0.48"
|
|
362
409
|
elif "Discovered open port" in line:
|
|
363
|
-
match = re.search(
|
|
410
|
+
match = re.search(
|
|
411
|
+
r"Discovered open port (\d+)/(tcp|udp) on (\d+\.\d+\.\d+\.\d+)", line
|
|
412
|
+
)
|
|
364
413
|
if match:
|
|
365
414
|
port = int(match.group(1))
|
|
366
415
|
protocol = match.group(2)
|
|
@@ -369,11 +418,15 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
369
418
|
# Find or create host entry for this IP
|
|
370
419
|
target_host = None
|
|
371
420
|
for h in hosts:
|
|
372
|
-
if h.get(
|
|
421
|
+
if h.get("ip") == host_ip:
|
|
373
422
|
target_host = h
|
|
374
423
|
break
|
|
375
424
|
|
|
376
|
-
if
|
|
425
|
+
if (
|
|
426
|
+
not target_host
|
|
427
|
+
and current_host
|
|
428
|
+
and current_host.get("ip") == host_ip
|
|
429
|
+
):
|
|
377
430
|
target_host = current_host
|
|
378
431
|
elif not target_host:
|
|
379
432
|
# Create a new host entry
|
|
@@ -384,7 +437,7 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
384
437
|
"os": None,
|
|
385
438
|
"mac_address": None,
|
|
386
439
|
"os_accuracy": None,
|
|
387
|
-
"services": []
|
|
440
|
+
"services": [],
|
|
388
441
|
}
|
|
389
442
|
hosts.append(target_host)
|
|
390
443
|
|
|
@@ -399,73 +452,86 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
399
452
|
# Infer service name from common ports
|
|
400
453
|
service_name = None
|
|
401
454
|
if port == 80:
|
|
402
|
-
service_name =
|
|
455
|
+
service_name = "http"
|
|
403
456
|
elif port == 443:
|
|
404
|
-
service_name =
|
|
457
|
+
service_name = "https"
|
|
405
458
|
elif port == 22:
|
|
406
|
-
service_name =
|
|
459
|
+
service_name = "ssh"
|
|
407
460
|
elif port == 21:
|
|
408
|
-
service_name =
|
|
461
|
+
service_name = "ftp"
|
|
409
462
|
elif port == 23:
|
|
410
|
-
service_name =
|
|
463
|
+
service_name = "telnet"
|
|
411
464
|
elif port == 25:
|
|
412
|
-
service_name =
|
|
465
|
+
service_name = "smtp"
|
|
413
466
|
elif port == 53:
|
|
414
|
-
service_name =
|
|
467
|
+
service_name = "domain"
|
|
415
468
|
elif port == 445:
|
|
416
|
-
service_name =
|
|
469
|
+
service_name = "microsoft-ds"
|
|
417
470
|
elif port == 139:
|
|
418
|
-
service_name =
|
|
471
|
+
service_name = "netbios-ssn"
|
|
419
472
|
elif port == 3306:
|
|
420
|
-
service_name =
|
|
473
|
+
service_name = "mysql"
|
|
421
474
|
elif port == 5432:
|
|
422
|
-
service_name =
|
|
423
|
-
|
|
424
|
-
target_host["services"].append(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
475
|
+
service_name = "postgresql"
|
|
476
|
+
|
|
477
|
+
target_host["services"].append(
|
|
478
|
+
{
|
|
479
|
+
"port": port,
|
|
480
|
+
"protocol": protocol,
|
|
481
|
+
"state": "open",
|
|
482
|
+
"service": service_name,
|
|
483
|
+
"product": None,
|
|
484
|
+
"version": None,
|
|
485
|
+
"raw_version": None,
|
|
486
|
+
}
|
|
487
|
+
)
|
|
433
488
|
|
|
434
489
|
# Parse service line: "22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.1"
|
|
435
|
-
elif re.match(r
|
|
490
|
+
elif re.match(r"^\d+/(tcp|udp)", line) and current_host:
|
|
436
491
|
parts = line.split(None, 4) # Split on whitespace, max 5 parts
|
|
437
492
|
if len(parts) >= 3:
|
|
438
|
-
port_proto = parts[0].split(
|
|
493
|
+
port_proto = parts[0].split("/")
|
|
439
494
|
port = int(port_proto[0])
|
|
440
|
-
protocol = port_proto[1] if len(port_proto) > 1 else
|
|
495
|
+
protocol = port_proto[1] if len(port_proto) > 1 else "tcp"
|
|
441
496
|
state = parts[1]
|
|
442
497
|
service_name = parts[2] if len(parts) > 2 else None
|
|
443
498
|
|
|
444
499
|
# Everything after service name is version info
|
|
445
|
-
raw_version =
|
|
446
|
-
|
|
500
|
+
raw_version = " ".join(parts[3:]) if len(parts) > 3 else None
|
|
501
|
+
|
|
447
502
|
# Parse product and version from raw version string
|
|
448
503
|
product = None
|
|
449
504
|
version = None
|
|
450
|
-
|
|
505
|
+
|
|
451
506
|
if raw_version:
|
|
452
507
|
try:
|
|
453
508
|
# Remove nmap metadata: "syn-ack ttl XX", "reset ttl XX", etc.
|
|
454
509
|
cleaned = raw_version
|
|
455
510
|
# Handle various nmap scan type prefixes
|
|
456
|
-
metadata_prefixes = [
|
|
511
|
+
metadata_prefixes = [
|
|
512
|
+
"syn-ack",
|
|
513
|
+
"reset",
|
|
514
|
+
"conn-refused",
|
|
515
|
+
"no-response",
|
|
516
|
+
]
|
|
457
517
|
for prefix in metadata_prefixes:
|
|
458
518
|
if cleaned.lower().startswith(prefix):
|
|
459
519
|
parts_ver = cleaned.split()
|
|
460
520
|
# Skip prefix and "ttl XX" if present
|
|
461
|
-
if len(parts_ver) > 1 and
|
|
521
|
+
if len(parts_ver) > 1 and "ttl" in parts_ver:
|
|
462
522
|
try:
|
|
463
|
-
ttl_idx = parts_ver.index(
|
|
464
|
-
cleaned =
|
|
523
|
+
ttl_idx = parts_ver.index("ttl")
|
|
524
|
+
cleaned = " ".join(
|
|
525
|
+
parts_ver[ttl_idx + 2 :]
|
|
526
|
+
) # Skip "ttl XX"
|
|
465
527
|
except (ValueError, IndexError):
|
|
466
|
-
cleaned =
|
|
528
|
+
cleaned = " ".join(
|
|
529
|
+
parts_ver[1:]
|
|
530
|
+
) # Skip just prefix
|
|
467
531
|
else:
|
|
468
|
-
cleaned =
|
|
532
|
+
cleaned = " ".join(
|
|
533
|
+
parts_ver[1:]
|
|
534
|
+
) # Skip just prefix
|
|
469
535
|
break
|
|
470
536
|
|
|
471
537
|
# Extract product and version with multiple patterns
|
|
@@ -476,9 +542,9 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
476
542
|
# "OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13" → product="OpenSSH", version="6.6.1p1"
|
|
477
543
|
|
|
478
544
|
version_patterns = [
|
|
479
|
-
r
|
|
480
|
-
r
|
|
481
|
-
r
|
|
545
|
+
r"([A-Za-z][\w\s\-\.]+?)\s+(v?\d+[\.\d]+[\w\-\.]*)", # Standard
|
|
546
|
+
r"^([A-Za-z][\w\-]+)\s+(\d[\w\.\-]+)", # ProductName vX.Y.Z
|
|
547
|
+
r"^([A-Za-z][\w\s]+?)\s+v?(\d+(?:\.\d+)+)", # "Product Name 1.2.3"
|
|
482
548
|
]
|
|
483
549
|
|
|
484
550
|
matched = False
|
|
@@ -503,11 +569,13 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
503
569
|
# This handles cases where nmap misidentifies or can't fingerprint web apps
|
|
504
570
|
# Common misidentifications: ppp (port 3000), upnp (port 8000), tcpwrapped
|
|
505
571
|
common_web_ports = [3000, 8080, 8000, 8888, 9090]
|
|
506
|
-
misidentified_services = [
|
|
507
|
-
if (
|
|
508
|
-
service_name
|
|
572
|
+
misidentified_services = ["unknown", "tcpwrapped", "ppp", "upnp", None]
|
|
573
|
+
if (
|
|
574
|
+
service_name in misidentified_services or not service_name
|
|
575
|
+
) and port in common_web_ports:
|
|
576
|
+
service_name = "http"
|
|
509
577
|
if not product:
|
|
510
|
-
product =
|
|
578
|
+
product = "http"
|
|
511
579
|
|
|
512
580
|
# Check if we already have this port (from "Discovered open port" lines)
|
|
513
581
|
existing_svc = None
|
|
@@ -528,37 +596,39 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
528
596
|
if raw_version:
|
|
529
597
|
existing_svc["raw_version"] = raw_version
|
|
530
598
|
else:
|
|
531
|
-
current_host["services"].append(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
599
|
+
current_host["services"].append(
|
|
600
|
+
{
|
|
601
|
+
"port": port,
|
|
602
|
+
"protocol": protocol,
|
|
603
|
+
"state": state,
|
|
604
|
+
"service": service_name,
|
|
605
|
+
"product": product,
|
|
606
|
+
"version": version,
|
|
607
|
+
"raw_version": raw_version,
|
|
608
|
+
}
|
|
609
|
+
)
|
|
540
610
|
|
|
541
611
|
# Parse OS detection: "Running: Linux 4.X|5.X" or "OS details: Linux 5.4"
|
|
542
612
|
elif ("Running:" in line or "OS details:" in line) and current_host:
|
|
543
|
-
os_info = line.split(
|
|
613
|
+
os_info = line.split(":", 1)[1].strip()
|
|
544
614
|
current_host["os"] = os_info
|
|
545
615
|
|
|
546
616
|
# Parse MAC address: "MAC Address: 00:11:22:33:44:55 (Vendor)"
|
|
547
617
|
elif "MAC Address:" in line and current_host:
|
|
548
|
-
match = re.search(r
|
|
618
|
+
match = re.search(r"MAC Address:\s+([0-9A-Fa-f:]{17})", line)
|
|
549
619
|
if match:
|
|
550
620
|
current_host["mac_address"] = match.group(1)
|
|
551
621
|
|
|
552
622
|
# Parse OS with confidence: "OS: Linux 5.x (95%)"
|
|
553
623
|
elif line.startswith("OS:") and current_host:
|
|
554
|
-
match = re.search(r
|
|
624
|
+
match = re.search(r"OS:\s+(.+?)\s+\((\d+)%\)", line)
|
|
555
625
|
if match:
|
|
556
626
|
current_host["os"] = match.group(1).strip()
|
|
557
627
|
current_host["os_accuracy"] = int(match.group(2))
|
|
558
628
|
|
|
559
629
|
# Parse OS from Service Info line: "Service Info: OSs: Windows, Windows Server 2008 R2 - 2012"
|
|
560
630
|
elif "Service Info:" in line and "OSs:" in line and current_host:
|
|
561
|
-
match = re.search(r
|
|
631
|
+
match = re.search(r"OSs?:\s+([^;]+)", line)
|
|
562
632
|
if match:
|
|
563
633
|
os_info = match.group(1).strip()
|
|
564
634
|
# Only update if we don't already have more specific OS info
|
|
@@ -566,8 +636,13 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
566
636
|
current_host["os"] = os_info
|
|
567
637
|
|
|
568
638
|
# Parse OS from Service Info line: "Service Info: OS: Linux"
|
|
569
|
-
elif
|
|
570
|
-
|
|
639
|
+
elif (
|
|
640
|
+
"Service Info:" in line
|
|
641
|
+
and "OS:" in line
|
|
642
|
+
and "OSs:" not in line
|
|
643
|
+
and current_host
|
|
644
|
+
):
|
|
645
|
+
match = re.search(r"OS:\s+([^;,]+)", line)
|
|
571
646
|
if match:
|
|
572
647
|
os_info = match.group(1).strip()
|
|
573
648
|
# Only update if we don't already have more specific OS info
|
|
@@ -576,7 +651,7 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
576
651
|
|
|
577
652
|
# Parse hostname from Service Info: "Service Info: Host: VAGRANT-2008R2"
|
|
578
653
|
elif "Service Info:" in line and "Host:" in line and current_host:
|
|
579
|
-
match = re.search(r
|
|
654
|
+
match = re.search(r"Host:\s+([^\s;,]+)", line)
|
|
580
655
|
if match:
|
|
581
656
|
hostname = match.group(1).strip()
|
|
582
657
|
# Only update if we don't already have a hostname
|
|
@@ -586,13 +661,17 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
586
661
|
# Parse OS from SMB script: "| OS: Windows Server 2008 R2 Standard 7601 Service Pack 1"
|
|
587
662
|
elif line.strip().startswith("|") and "OS:" in line and current_host:
|
|
588
663
|
# This is from smb-os-discovery script - more detailed than Service Info
|
|
589
|
-
match = re.search(r
|
|
664
|
+
match = re.search(r"\|\s+OS:\s+(.+?)(?:\s+\(|$)", line)
|
|
590
665
|
if match:
|
|
591
666
|
os_info = match.group(1).strip()
|
|
592
667
|
# SMB OS discovery is very accurate, so always update
|
|
593
|
-
if
|
|
668
|
+
if (
|
|
669
|
+
os_info and os_info != "Windows"
|
|
670
|
+
): # Don't overwrite with generic "Windows"
|
|
594
671
|
current_host["os"] = os_info
|
|
595
|
-
current_host["os_accuracy"] =
|
|
672
|
+
current_host["os_accuracy"] = (
|
|
673
|
+
95 # High confidence for SMB detection
|
|
674
|
+
)
|
|
596
675
|
|
|
597
676
|
# Don't forget the last host (if not already added)
|
|
598
677
|
if current_host and current_host not in hosts:
|
|
@@ -607,7 +686,8 @@ def parse_nmap_text(output: str) -> Dict[str, Any]:
|
|
|
607
686
|
return {
|
|
608
687
|
"hosts": hosts,
|
|
609
688
|
"vulnerabilities": vulnerabilities,
|
|
610
|
-
"info_scripts": info_scripts
|
|
689
|
+
"info_scripts": info_scripts,
|
|
690
|
+
"domains": domains,
|
|
611
691
|
}
|
|
612
692
|
|
|
613
693
|
|
|
@@ -631,18 +711,18 @@ def parse_nmap_info_scripts(output: str) -> List[Dict[str, Any]]:
|
|
|
631
711
|
current_host_ip = None
|
|
632
712
|
current_port = None
|
|
633
713
|
|
|
634
|
-
lines = output.split(
|
|
714
|
+
lines = output.split("\n")
|
|
635
715
|
i = 0
|
|
636
716
|
|
|
637
717
|
# Info scripts to capture (add more as needed)
|
|
638
718
|
info_scripts = {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
719
|
+
"vnc-info": "VNC Server Information",
|
|
720
|
+
"ssh-hostkey": "SSH Host Key",
|
|
721
|
+
"http-server-header": "HTTP Server Header",
|
|
722
|
+
"ssl-cert": "SSL Certificate",
|
|
723
|
+
"http-title": "HTTP Page Title",
|
|
724
|
+
"smb-os-discovery": "SMB OS Discovery",
|
|
725
|
+
"rdp-ntlm-info": "RDP NTLM Information",
|
|
646
726
|
}
|
|
647
727
|
|
|
648
728
|
while i < len(lines):
|
|
@@ -650,26 +730,26 @@ def parse_nmap_info_scripts(output: str) -> List[Dict[str, Any]]:
|
|
|
650
730
|
|
|
651
731
|
# Track current host - "Nmap scan report for 10.0.0.73"
|
|
652
732
|
if line.startswith("Nmap scan report for"):
|
|
653
|
-
match = re.search(r
|
|
733
|
+
match = re.search(r"for (\d+\.\d+\.\d+\.\d+)", line)
|
|
654
734
|
if match:
|
|
655
735
|
current_host_ip = match.group(1)
|
|
656
736
|
else:
|
|
657
737
|
# Try hostname (IP in parens)
|
|
658
|
-
match = re.search(r
|
|
738
|
+
match = re.search(r"\((\d+\.\d+\.\d+\.\d+)\)", line)
|
|
659
739
|
if match:
|
|
660
740
|
current_host_ip = match.group(1)
|
|
661
741
|
|
|
662
742
|
# Track current port - "80/tcp open http"
|
|
663
|
-
elif re.match(r
|
|
743
|
+
elif re.match(r"^\d+/(tcp|udp)", line):
|
|
664
744
|
parts = line.split()
|
|
665
745
|
if parts:
|
|
666
|
-
port_proto = parts[0].split(
|
|
746
|
+
port_proto = parts[0].split("/")
|
|
667
747
|
current_port = int(port_proto[0])
|
|
668
748
|
|
|
669
749
|
# Parse info script blocks
|
|
670
|
-
elif line.startswith(
|
|
750
|
+
elif line.startswith("| ") and ":" in line and current_host_ip:
|
|
671
751
|
# Could be start of a script block like "| vnc-info:"
|
|
672
|
-
script_match = re.match(r
|
|
752
|
+
script_match = re.match(r"\|\s*([a-zA-Z0-9_-]+):\s*$", line)
|
|
673
753
|
if script_match:
|
|
674
754
|
script_name = script_match.group(1)
|
|
675
755
|
|
|
@@ -678,24 +758,28 @@ def parse_nmap_info_scripts(output: str) -> List[Dict[str, Any]]:
|
|
|
678
758
|
# Collect all lines of this script block
|
|
679
759
|
script_lines = []
|
|
680
760
|
i += 1
|
|
681
|
-
while i < len(lines) and (
|
|
761
|
+
while i < len(lines) and (
|
|
762
|
+
lines[i].startswith("|") or lines[i].startswith("|_")
|
|
763
|
+
):
|
|
682
764
|
# Clean up the line
|
|
683
|
-
clean_line = lines[i].lstrip(
|
|
765
|
+
clean_line = lines[i].lstrip("|").lstrip("_").strip()
|
|
684
766
|
if clean_line:
|
|
685
767
|
script_lines.append(clean_line)
|
|
686
|
-
if lines[i].startswith(
|
|
768
|
+
if lines[i].startswith("|_"):
|
|
687
769
|
break
|
|
688
770
|
i += 1
|
|
689
771
|
|
|
690
772
|
if script_lines:
|
|
691
|
-
findings.append(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
773
|
+
findings.append(
|
|
774
|
+
{
|
|
775
|
+
"host_ip": current_host_ip,
|
|
776
|
+
"port": current_port,
|
|
777
|
+
"script": script_name,
|
|
778
|
+
"title": info_scripts[script_name],
|
|
779
|
+
"severity": "info",
|
|
780
|
+
"description": "\n".join(script_lines),
|
|
781
|
+
}
|
|
782
|
+
)
|
|
699
783
|
continue
|
|
700
784
|
|
|
701
785
|
i += 1
|
|
@@ -728,11 +812,15 @@ def parse_nmap_log(log_path: str) -> Dict[str, Any]:
|
|
|
728
812
|
Parsed nmap data with hosts, services, and vulnerabilities
|
|
729
813
|
"""
|
|
730
814
|
try:
|
|
731
|
-
with open(log_path,
|
|
815
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
732
816
|
content = f.read()
|
|
733
817
|
|
|
734
818
|
return parse_nmap_text(content)
|
|
735
819
|
except FileNotFoundError:
|
|
736
|
-
return {
|
|
820
|
+
return {
|
|
821
|
+
"hosts": [],
|
|
822
|
+
"vulnerabilities": [],
|
|
823
|
+
"error": f"File not found: {log_path}",
|
|
824
|
+
}
|
|
737
825
|
except Exception as e:
|
|
738
826
|
return {"hosts": [], "vulnerabilities": [], "error": str(e)}
|