souleyez 2.43.29__py3-none-any.whl → 3.0.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.
Potentially problematic release.
This version of souleyez might be problematic. Click here for more details.
- 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 +9564 -2881
- 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 +564 -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 +409 -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 +417 -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 +913 -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 +219 -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 +237 -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 +23034 -10679
- 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.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
- souleyez-3.0.0.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -52,26 +52,28 @@ def parse_enum4linux_output(output: str, target: str = "") -> Dict[str, Any]:
|
|
|
52
52
|
def _is_enum4linux_ng_output(output: str) -> bool:
|
|
53
53
|
"""Detect if output is from enum4linux-ng (YAML-style format)."""
|
|
54
54
|
# Primary indicator - explicit version string (most reliable)
|
|
55
|
-
if re.search(r
|
|
55
|
+
if re.search(r"ENUM4LINUX\s*-\s*next\s*generation", output, re.IGNORECASE):
|
|
56
56
|
return True
|
|
57
|
-
if re.search(r
|
|
57
|
+
if re.search(r"enum4linux-ng", output, re.IGNORECASE):
|
|
58
58
|
return True
|
|
59
59
|
|
|
60
60
|
# Secondary indicators - look for YAML-style patterns unique to ng
|
|
61
61
|
ng_indicators = [
|
|
62
|
-
re.search(r
|
|
63
|
-
re.search(r
|
|
64
|
-
re.search(
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
re.search(r"After merging (user|share|group) results", output, re.IGNORECASE),
|
|
63
|
+
re.search(r"^\s{2,}username:\s+", output, re.MULTILINE), # Indented YAML-style
|
|
64
|
+
re.search(
|
|
65
|
+
r"^'?\d+'?:\s*$", output, re.MULTILINE
|
|
66
|
+
), # RID entries: '1000': or 1000:
|
|
67
|
+
re.search(r"^\s{2,}(groupname|name|type|comment):\s+", output, re.MULTILINE),
|
|
68
|
+
re.search(r"Trying to get SID from lsaquery", output, re.IGNORECASE),
|
|
67
69
|
]
|
|
68
70
|
|
|
69
71
|
# Classic enum4linux indicators (to confirm it's NOT ng)
|
|
70
72
|
classic_indicators = [
|
|
71
|
-
re.search(r
|
|
72
|
-
re.search(r
|
|
73
|
-
re.search(r
|
|
74
|
-
re.search(r
|
|
73
|
+
re.search(r"enum4linux v\d", output, re.IGNORECASE),
|
|
74
|
+
re.search(r"Starting enum4linux v", output, re.IGNORECASE),
|
|
75
|
+
re.search(r"Sharename\s+Type\s+Comment", output), # Table header
|
|
76
|
+
re.search(r"\|\s+Users on", output),
|
|
75
77
|
]
|
|
76
78
|
|
|
77
79
|
ng_count = sum(1 for ind in ng_indicators if ind)
|
|
@@ -102,17 +104,17 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
|
|
|
102
104
|
type: ...
|
|
103
105
|
"""
|
|
104
106
|
result = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
"target": target,
|
|
108
|
+
"workgroup": None,
|
|
109
|
+
"domain_sid": None,
|
|
110
|
+
"shares": [],
|
|
111
|
+
"users": [],
|
|
112
|
+
"groups": [],
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
# Remove ANSI color codes
|
|
114
|
-
output = re.sub(r
|
|
115
|
-
lines = output.split(
|
|
116
|
+
output = re.sub(r"\x1b\[[0-9;]*m", "", output)
|
|
117
|
+
lines = output.split("\n")
|
|
116
118
|
|
|
117
119
|
current_section = None
|
|
118
120
|
current_item = {}
|
|
@@ -121,16 +123,16 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
|
|
|
121
123
|
def save_current_item():
|
|
122
124
|
"""Save the current item to the appropriate result list."""
|
|
123
125
|
nonlocal current_item, in_item
|
|
124
|
-
if current_section ==
|
|
125
|
-
username = current_item[
|
|
126
|
-
if username not in result[
|
|
127
|
-
result[
|
|
128
|
-
elif current_section ==
|
|
129
|
-
result[
|
|
130
|
-
elif current_section ==
|
|
131
|
-
groupname = current_item[
|
|
132
|
-
if groupname not in result[
|
|
133
|
-
result[
|
|
126
|
+
if current_section == "users" and current_item.get("username"):
|
|
127
|
+
username = current_item["username"]
|
|
128
|
+
if username not in result["users"]:
|
|
129
|
+
result["users"].append(username)
|
|
130
|
+
elif current_section == "shares" and current_item.get("name"):
|
|
131
|
+
result["shares"].append(current_item.copy())
|
|
132
|
+
elif current_section == "groups" and current_item.get("groupname"):
|
|
133
|
+
groupname = current_item["groupname"]
|
|
134
|
+
if groupname not in result["groups"]:
|
|
135
|
+
result["groups"].append(groupname)
|
|
134
136
|
current_item = {}
|
|
135
137
|
in_item = False
|
|
136
138
|
|
|
@@ -138,31 +140,33 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
|
|
|
138
140
|
stripped = line.rstrip()
|
|
139
141
|
|
|
140
142
|
# Detect section headers from enum4linux-ng output
|
|
141
|
-
if
|
|
143
|
+
if "After merging user results" in stripped:
|
|
142
144
|
save_current_item()
|
|
143
|
-
current_section =
|
|
145
|
+
current_section = "users"
|
|
144
146
|
continue
|
|
145
|
-
elif
|
|
147
|
+
elif "After merging share results" in stripped or re.search(
|
|
148
|
+
r"Found \d+ share\(s\):", stripped
|
|
149
|
+
):
|
|
146
150
|
save_current_item()
|
|
147
|
-
current_section =
|
|
151
|
+
current_section = "shares"
|
|
148
152
|
continue
|
|
149
|
-
elif
|
|
153
|
+
elif "After merging group results" in stripped:
|
|
150
154
|
save_current_item()
|
|
151
|
-
current_section =
|
|
155
|
+
current_section = "groups"
|
|
152
156
|
continue
|
|
153
157
|
|
|
154
158
|
# Extract workgroup/domain
|
|
155
|
-
wg_match = re.search(r
|
|
159
|
+
wg_match = re.search(r"Got domain/workgroup name:\s*(\S+)", stripped)
|
|
156
160
|
if wg_match:
|
|
157
|
-
result[
|
|
161
|
+
result["workgroup"] = wg_match.group(1).strip()
|
|
158
162
|
|
|
159
163
|
# Extract target
|
|
160
|
-
target_match = re.search(r
|
|
164
|
+
target_match = re.search(r"Target\s+\.+\s+(\S+)", stripped)
|
|
161
165
|
if target_match:
|
|
162
|
-
result[
|
|
166
|
+
result["target"] = target_match.group(1).strip()
|
|
163
167
|
|
|
164
168
|
# Parse user entries in users section
|
|
165
|
-
if current_section ==
|
|
169
|
+
if current_section == "users":
|
|
166
170
|
# Start of new user entry: '1000':
|
|
167
171
|
rid_match = re.match(r"^'(\d+)':\s*$", stripped)
|
|
168
172
|
if rid_match:
|
|
@@ -172,14 +176,14 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
|
|
|
172
176
|
|
|
173
177
|
# Username field
|
|
174
178
|
if in_item:
|
|
175
|
-
username_match = re.match(r
|
|
179
|
+
username_match = re.match(r"^\s+username:\s*(.+)$", stripped)
|
|
176
180
|
if username_match:
|
|
177
181
|
username = username_match.group(1).strip().strip("'\"")
|
|
178
|
-
if username and username !=
|
|
179
|
-
current_item[
|
|
182
|
+
if username and username != "(null)":
|
|
183
|
+
current_item["username"] = username
|
|
180
184
|
|
|
181
185
|
# Parse share entries in shares section
|
|
182
|
-
elif current_section ==
|
|
186
|
+
elif current_section == "shares":
|
|
183
187
|
# Start of new share entry: 'print$': or ADMIN$:
|
|
184
188
|
share_key_match = re.match(r"^'([^']+)':\s*$", stripped)
|
|
185
189
|
if not share_key_match:
|
|
@@ -187,26 +191,26 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
|
|
|
187
191
|
share_key_match = re.match(r"^([A-Za-z0-9_$]+):\s*$", stripped)
|
|
188
192
|
if share_key_match:
|
|
189
193
|
save_current_item()
|
|
190
|
-
current_item = {
|
|
194
|
+
current_item = {"name": share_key_match.group(1)}
|
|
191
195
|
in_item = True
|
|
192
196
|
continue
|
|
193
197
|
|
|
194
198
|
# Share properties
|
|
195
199
|
if in_item:
|
|
196
|
-
name_match = re.match(r
|
|
200
|
+
name_match = re.match(r"^\s+name:\s*(.+)$", stripped)
|
|
197
201
|
if name_match:
|
|
198
|
-
current_item[
|
|
202
|
+
current_item["name"] = name_match.group(1).strip()
|
|
199
203
|
|
|
200
|
-
type_match = re.match(r
|
|
204
|
+
type_match = re.match(r"^\s+type:\s*(.+)$", stripped)
|
|
201
205
|
if type_match:
|
|
202
|
-
current_item[
|
|
206
|
+
current_item["type"] = type_match.group(1).strip()
|
|
203
207
|
|
|
204
|
-
comment_match = re.match(r
|
|
208
|
+
comment_match = re.match(r"^\s+comment:\s*(.*)$", stripped)
|
|
205
209
|
if comment_match:
|
|
206
|
-
current_item[
|
|
210
|
+
current_item["comment"] = comment_match.group(1).strip()
|
|
207
211
|
|
|
208
212
|
# Parse group entries
|
|
209
|
-
elif current_section ==
|
|
213
|
+
elif current_section == "groups":
|
|
210
214
|
rid_match = re.match(r"^'(\d+)':\s*$", stripped)
|
|
211
215
|
if rid_match:
|
|
212
216
|
save_current_item()
|
|
@@ -214,11 +218,11 @@ def _parse_enum4linux_ng_output(output: str, target: str = "") -> Dict[str, Any]
|
|
|
214
218
|
continue
|
|
215
219
|
|
|
216
220
|
if in_item:
|
|
217
|
-
groupname_match = re.match(r
|
|
221
|
+
groupname_match = re.match(r"^\s+groupname:\s*(.+)$", stripped)
|
|
218
222
|
if groupname_match:
|
|
219
223
|
groupname = groupname_match.group(1).strip().strip("'\"")
|
|
220
224
|
if groupname:
|
|
221
|
-
current_item[
|
|
225
|
+
current_item["groupname"] = groupname
|
|
222
226
|
|
|
223
227
|
# Don't forget the last item being parsed
|
|
224
228
|
save_current_item()
|
|
@@ -231,94 +235,100 @@ def _parse_enum4linux_classic_output(output: str, target: str = "") -> Dict[str,
|
|
|
231
235
|
Parse original enum4linux table-based output format.
|
|
232
236
|
"""
|
|
233
237
|
result = {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
"target": target,
|
|
239
|
+
"workgroup": None,
|
|
240
|
+
"domain_sid": None,
|
|
241
|
+
"shares": [],
|
|
242
|
+
"users": [],
|
|
243
|
+
"groups": [],
|
|
240
244
|
}
|
|
241
245
|
|
|
242
|
-
lines = output.split(
|
|
246
|
+
lines = output.split("\n")
|
|
243
247
|
current_section = None
|
|
244
248
|
in_share_table = False
|
|
245
249
|
share_table_started = False
|
|
246
250
|
|
|
247
251
|
for i, line in enumerate(lines):
|
|
248
252
|
# Remove ANSI color codes
|
|
249
|
-
line = re.sub(r
|
|
253
|
+
line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
|
250
254
|
line = line.strip()
|
|
251
255
|
|
|
252
256
|
# Extract target
|
|
253
|
-
if line.startswith(
|
|
254
|
-
target_match = re.search(r
|
|
257
|
+
if line.startswith("Target ..........."):
|
|
258
|
+
target_match = re.search(r"Target\s+\.+\s+(\S+)", line)
|
|
255
259
|
if target_match:
|
|
256
|
-
result[
|
|
260
|
+
result["target"] = target_match.group(1)
|
|
257
261
|
|
|
258
262
|
# Extract workgroup/domain
|
|
259
|
-
elif
|
|
260
|
-
wg_match = re.search(r
|
|
263
|
+
elif "[+] Got domain/workgroup name:" in line:
|
|
264
|
+
wg_match = re.search(r"Got domain/workgroup name:\s+(\S+)", line)
|
|
261
265
|
if wg_match:
|
|
262
|
-
result[
|
|
266
|
+
result["workgroup"] = wg_match.group(1)
|
|
263
267
|
|
|
264
268
|
# Extract domain SID
|
|
265
|
-
elif line.startswith(
|
|
266
|
-
sid_match = re.search(r
|
|
269
|
+
elif line.startswith("Domain Sid:"):
|
|
270
|
+
sid_match = re.search(r"Domain Sid:\s+(.+)", line)
|
|
267
271
|
if sid_match:
|
|
268
272
|
sid = sid_match.group(1).strip()
|
|
269
|
-
if sid !=
|
|
270
|
-
result[
|
|
273
|
+
if sid != "(NULL SID)":
|
|
274
|
+
result["domain_sid"] = sid
|
|
271
275
|
|
|
272
276
|
# Detect share enumeration section
|
|
273
|
-
elif
|
|
274
|
-
current_section =
|
|
277
|
+
elif "Share Enumeration on" in line:
|
|
278
|
+
current_section = "shares"
|
|
275
279
|
in_share_table = False
|
|
276
280
|
share_table_started = False
|
|
277
281
|
|
|
278
282
|
# Parse share table header
|
|
279
|
-
elif current_section ==
|
|
283
|
+
elif current_section == "shares" and "Sharename" in line and "Type" in line:
|
|
280
284
|
in_share_table = True
|
|
281
285
|
share_table_started = False
|
|
282
286
|
continue
|
|
283
287
|
|
|
284
288
|
# Parse share separator line
|
|
285
|
-
elif current_section ==
|
|
289
|
+
elif current_section == "shares" and line.startswith("---"):
|
|
286
290
|
if in_share_table:
|
|
287
291
|
share_table_started = True
|
|
288
292
|
continue
|
|
289
293
|
|
|
290
294
|
# Parse share lines
|
|
291
|
-
elif current_section ==
|
|
295
|
+
elif current_section == "shares" and in_share_table and share_table_started:
|
|
292
296
|
# Check if we've left the table
|
|
293
|
-
if
|
|
297
|
+
if (
|
|
298
|
+
not line
|
|
299
|
+
or line.startswith("Reconnecting")
|
|
300
|
+
or line.startswith("Server")
|
|
301
|
+
or line.startswith("Workgroup")
|
|
302
|
+
or line.startswith("[")
|
|
303
|
+
):
|
|
294
304
|
in_share_table = False
|
|
295
305
|
continue
|
|
296
306
|
|
|
297
307
|
# Parse share line: "sharename Type Comment"
|
|
298
308
|
share = _parse_share_line(line)
|
|
299
309
|
if share:
|
|
300
|
-
result[
|
|
310
|
+
result["shares"].append(share)
|
|
301
311
|
|
|
302
312
|
# Parse share mapping results
|
|
303
|
-
elif current_section ==
|
|
313
|
+
elif current_section == "shares" and line.startswith("//"):
|
|
304
314
|
mapping_info = _parse_share_mapping(line)
|
|
305
315
|
if mapping_info:
|
|
306
316
|
# Find and update the matching share
|
|
307
|
-
for share in result[
|
|
308
|
-
if mapping_info[
|
|
317
|
+
for share in result["shares"]:
|
|
318
|
+
if mapping_info["name"] in line:
|
|
309
319
|
share.update(mapping_info)
|
|
310
320
|
break
|
|
311
321
|
|
|
312
322
|
# Parse users section
|
|
313
|
-
elif
|
|
314
|
-
current_section =
|
|
323
|
+
elif "Users on" in line or "user(s) returned" in line:
|
|
324
|
+
current_section = "users"
|
|
315
325
|
|
|
316
326
|
# Parse groups section
|
|
317
|
-
elif
|
|
318
|
-
current_section =
|
|
327
|
+
elif "Groups on" in line or "group(s) returned" in line:
|
|
328
|
+
current_section = "groups"
|
|
319
329
|
|
|
320
330
|
# Parse user lines from RID cycling output (Local User or Domain User)
|
|
321
|
-
elif current_section ==
|
|
331
|
+
elif current_section == "users" and line and not line.startswith("="):
|
|
322
332
|
# Format variations:
|
|
323
333
|
# "S-1-5-21-...-RID DOMAIN\username (Local User)"
|
|
324
334
|
# "S-1-5-21-...-RID DOMAIN\\username (Local User)"
|
|
@@ -326,34 +336,50 @@ def _parse_enum4linux_classic_output(output: str, target: str = "") -> Dict[str,
|
|
|
326
336
|
# "[+] DOMAIN\username" - alternate prefix
|
|
327
337
|
|
|
328
338
|
# Try full SID format first (flexible escaping)
|
|
329
|
-
user_match = re.search(
|
|
339
|
+
user_match = re.search(
|
|
340
|
+
r"S-1-5-21-[\d-]+\s+\S+[\\]+(\S+)\s+\((Local|Domain)\s*User\)",
|
|
341
|
+
line,
|
|
342
|
+
re.IGNORECASE,
|
|
343
|
+
)
|
|
330
344
|
if user_match:
|
|
331
345
|
username = user_match.group(1)
|
|
332
|
-
if username and username not in result[
|
|
333
|
-
result[
|
|
346
|
+
if username and username not in result["users"]:
|
|
347
|
+
result["users"].append(username)
|
|
334
348
|
else:
|
|
335
349
|
# Try simpler DOMAIN\username format
|
|
336
|
-
user_match = re.search(
|
|
350
|
+
user_match = re.search(
|
|
351
|
+
r"[\[\+\]\s]*\S+[\\]+(\S+)\s+\((Local|Domain)\s*User\)",
|
|
352
|
+
line,
|
|
353
|
+
re.IGNORECASE,
|
|
354
|
+
)
|
|
337
355
|
if user_match:
|
|
338
356
|
username = user_match.group(1)
|
|
339
|
-
if username and username not in result[
|
|
340
|
-
result[
|
|
357
|
+
if username and username not in result["users"]:
|
|
358
|
+
result["users"].append(username)
|
|
341
359
|
|
|
342
360
|
# Also parse group lines from RID cycling (Domain Group, Local Group)
|
|
343
|
-
elif current_section ==
|
|
361
|
+
elif current_section == "groups" and line and not line.startswith("="):
|
|
344
362
|
# Format variations similar to users
|
|
345
|
-
group_match = re.search(
|
|
363
|
+
group_match = re.search(
|
|
364
|
+
r"S-1-5-21-[\d-]+\s+\S+[\\]+(\S+)\s+\((Domain|Local)\s*Group\)",
|
|
365
|
+
line,
|
|
366
|
+
re.IGNORECASE,
|
|
367
|
+
)
|
|
346
368
|
if group_match:
|
|
347
369
|
groupname = group_match.group(1)
|
|
348
|
-
if groupname and groupname not in result[
|
|
349
|
-
result[
|
|
370
|
+
if groupname and groupname not in result["groups"]:
|
|
371
|
+
result["groups"].append(groupname)
|
|
350
372
|
else:
|
|
351
373
|
# Try simpler format
|
|
352
|
-
group_match = re.search(
|
|
374
|
+
group_match = re.search(
|
|
375
|
+
r"[\[\+\]\s]*\S+[\\]+(\S+)\s+\((Domain|Local)\s*Group\)",
|
|
376
|
+
line,
|
|
377
|
+
re.IGNORECASE,
|
|
378
|
+
)
|
|
353
379
|
if group_match:
|
|
354
380
|
groupname = group_match.group(1)
|
|
355
|
-
if groupname and groupname not in result[
|
|
356
|
-
result[
|
|
381
|
+
if groupname and groupname not in result["groups"]:
|
|
382
|
+
result["groups"].append(groupname)
|
|
357
383
|
|
|
358
384
|
return result
|
|
359
385
|
|
|
@@ -373,50 +399,52 @@ def _parse_share_line(line: str) -> Dict[str, Any]:
|
|
|
373
399
|
# Try multiple parsing strategies for different formats
|
|
374
400
|
|
|
375
401
|
# Strategy 1: Split on 2+ whitespace (most common)
|
|
376
|
-
parts = re.split(r
|
|
402
|
+
parts = re.split(r"\s{2,}", line)
|
|
377
403
|
if len(parts) >= 2:
|
|
378
404
|
share_name = parts[0].strip()
|
|
379
405
|
share_type = parts[1].strip()
|
|
380
|
-
comment = parts[2].strip() if len(parts) > 2 else
|
|
406
|
+
comment = parts[2].strip() if len(parts) > 2 else ""
|
|
381
407
|
|
|
382
408
|
# Validate share type is a known type
|
|
383
|
-
if share_type.upper() in [
|
|
409
|
+
if share_type.upper() in ["DISK", "IPC", "PRINT", "PRINTER", "COMM", "DEVICE"]:
|
|
384
410
|
return {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
411
|
+
"name": share_name,
|
|
412
|
+
"type": share_type,
|
|
413
|
+
"comment": comment,
|
|
414
|
+
"mapping": None,
|
|
415
|
+
"listing": None,
|
|
416
|
+
"writing": None,
|
|
391
417
|
}
|
|
392
418
|
|
|
393
419
|
# Strategy 2: Tab-separated
|
|
394
|
-
parts = line.split(
|
|
420
|
+
parts = line.split("\t")
|
|
395
421
|
if len(parts) >= 2:
|
|
396
422
|
share_name = parts[0].strip()
|
|
397
423
|
share_type = parts[1].strip()
|
|
398
|
-
comment = parts[2].strip() if len(parts) > 2 else
|
|
424
|
+
comment = parts[2].strip() if len(parts) > 2 else ""
|
|
399
425
|
|
|
400
|
-
if share_type.upper() in [
|
|
426
|
+
if share_type.upper() in ["DISK", "IPC", "PRINT", "PRINTER", "COMM", "DEVICE"]:
|
|
401
427
|
return {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
428
|
+
"name": share_name,
|
|
429
|
+
"type": share_type,
|
|
430
|
+
"comment": comment,
|
|
431
|
+
"mapping": None,
|
|
432
|
+
"listing": None,
|
|
433
|
+
"writing": None,
|
|
408
434
|
}
|
|
409
435
|
|
|
410
436
|
# Strategy 3: Regex for flexible whitespace (single space minimum)
|
|
411
|
-
match = re.match(
|
|
437
|
+
match = re.match(
|
|
438
|
+
r"^(\S+)\s+(Disk|IPC|Print|Printer|Comm|Device)\s*(.*)?$", line, re.IGNORECASE
|
|
439
|
+
)
|
|
412
440
|
if match:
|
|
413
441
|
return {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
442
|
+
"name": match.group(1),
|
|
443
|
+
"type": match.group(2),
|
|
444
|
+
"comment": match.group(3).strip() if match.group(3) else "",
|
|
445
|
+
"mapping": None,
|
|
446
|
+
"listing": None,
|
|
447
|
+
"writing": None,
|
|
420
448
|
}
|
|
421
449
|
|
|
422
450
|
return None
|
|
@@ -431,22 +459,22 @@ def _parse_share_mapping(line: str) -> Dict[str, Any]:
|
|
|
431
459
|
"""
|
|
432
460
|
try:
|
|
433
461
|
# Extract share name from path
|
|
434
|
-
share_match = re.search(r
|
|
462
|
+
share_match = re.search(r"//[^/]+/(\S+)", line)
|
|
435
463
|
if not share_match:
|
|
436
464
|
return None
|
|
437
465
|
|
|
438
466
|
share_name = share_match.group(1)
|
|
439
467
|
|
|
440
468
|
# Extract mapping status
|
|
441
|
-
mapping_match = re.search(r
|
|
442
|
-
listing_match = re.search(r
|
|
443
|
-
writing_match = re.search(r
|
|
469
|
+
mapping_match = re.search(r"Mapping:\s*(\S+)", line)
|
|
470
|
+
listing_match = re.search(r"Listing:\s*(\S+)", line)
|
|
471
|
+
writing_match = re.search(r"Writing:\s*(\S+)", line)
|
|
444
472
|
|
|
445
473
|
return {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
474
|
+
"name": share_name,
|
|
475
|
+
"mapping": mapping_match.group(1) if mapping_match else None,
|
|
476
|
+
"listing": listing_match.group(1) if listing_match else None,
|
|
477
|
+
"writing": writing_match.group(1) if writing_match else None,
|
|
450
478
|
}
|
|
451
479
|
except Exception:
|
|
452
480
|
return None
|
|
@@ -459,18 +487,20 @@ def get_smb_stats(parsed: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
459
487
|
Returns:
|
|
460
488
|
Dict with counts and summary info
|
|
461
489
|
"""
|
|
462
|
-
accessible_shares = sum(
|
|
463
|
-
|
|
490
|
+
accessible_shares = sum(
|
|
491
|
+
1 for s in parsed.get("shares", []) if s.get("mapping") == "OK"
|
|
492
|
+
)
|
|
464
493
|
|
|
465
|
-
writable_shares = sum(
|
|
466
|
-
|
|
494
|
+
writable_shares = sum(
|
|
495
|
+
1 for s in parsed.get("shares", []) if s.get("writing") == "OK"
|
|
496
|
+
)
|
|
467
497
|
|
|
468
498
|
return {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
499
|
+
"total_shares": len(parsed.get("shares", [])),
|
|
500
|
+
"accessible_shares": accessible_shares,
|
|
501
|
+
"writable_shares": writable_shares,
|
|
502
|
+
"workgroup": parsed.get("workgroup"),
|
|
503
|
+
"has_domain_sid": parsed.get("domain_sid") is not None,
|
|
474
504
|
}
|
|
475
505
|
|
|
476
506
|
|
|
@@ -480,15 +510,15 @@ def categorize_share(share: Dict[str, Any]) -> str:
|
|
|
480
510
|
|
|
481
511
|
Returns: 'open', 'readable', 'restricted', 'denied'
|
|
482
512
|
"""
|
|
483
|
-
mapping = share.get(
|
|
484
|
-
listing = share.get(
|
|
485
|
-
writing = share.get(
|
|
486
|
-
|
|
487
|
-
if writing ==
|
|
488
|
-
return
|
|
489
|
-
elif listing ==
|
|
490
|
-
return
|
|
491
|
-
elif mapping ==
|
|
492
|
-
return
|
|
513
|
+
mapping = share.get("mapping", "N/A")
|
|
514
|
+
listing = share.get("listing", "N/A")
|
|
515
|
+
writing = share.get("writing", "N/A")
|
|
516
|
+
|
|
517
|
+
if writing == "OK":
|
|
518
|
+
return "open" # Writable = high risk
|
|
519
|
+
elif listing == "OK":
|
|
520
|
+
return "readable" # Readable = medium risk
|
|
521
|
+
elif mapping == "OK":
|
|
522
|
+
return "restricted" # Accessible but limited
|
|
493
523
|
else:
|
|
494
|
-
return
|
|
524
|
+
return "denied" # Not accessible
|
souleyez/parsers/ffuf_parser.py
CHANGED
|
@@ -10,7 +10,7 @@ def parse_ffuf(log_path: str, target: str) -> Dict[str, Any]:
|
|
|
10
10
|
"""Parse ffuf JSON output."""
|
|
11
11
|
|
|
12
12
|
try:
|
|
13
|
-
with open(log_path,
|
|
13
|
+
with open(log_path, "r", encoding="utf-8") as f:
|
|
14
14
|
# ffuf writes JSON on first line, then appends text output
|
|
15
15
|
# Read only the first line to get the JSON
|
|
16
16
|
first_line = f.readline()
|
|
@@ -20,35 +20,39 @@ def parse_ffuf(log_path: str, target: str) -> Dict[str, Any]:
|
|
|
20
20
|
raise ValueError("Empty log file")
|
|
21
21
|
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
|
|
22
22
|
return {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
"tool": "ffuf",
|
|
24
|
+
"target": target,
|
|
25
|
+
"error": str(e),
|
|
26
|
+
"results_found": 0,
|
|
27
|
+
"paths": [],
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
results = []
|
|
31
31
|
|
|
32
|
-
for result in data.get(
|
|
33
|
-
results.append(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
for result in data.get("results", []):
|
|
33
|
+
results.append(
|
|
34
|
+
{
|
|
35
|
+
"url": result.get("url"),
|
|
36
|
+
"status_code": result.get("status"), # Normalized to match gobuster
|
|
37
|
+
"size": result.get("length"), # Normalized to match gobuster
|
|
38
|
+
"redirect": result.get(
|
|
39
|
+
"redirectlocation", ""
|
|
40
|
+
), # Normalized to match gobuster
|
|
41
|
+
"words": result.get("words"),
|
|
42
|
+
"lines": result.get("lines"),
|
|
43
|
+
"content_type": result.get("content-type", ""),
|
|
44
|
+
"input": result.get("input", {}),
|
|
45
|
+
}
|
|
46
|
+
)
|
|
43
47
|
|
|
44
|
-
config = data.get(
|
|
48
|
+
config = data.get("config", {})
|
|
45
49
|
|
|
46
50
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
"tool": "ffuf",
|
|
52
|
+
"target": target,
|
|
53
|
+
"results_found": len(results),
|
|
54
|
+
"wordlist": config.get("wordlist"),
|
|
55
|
+
"method": config.get("method", "GET"),
|
|
56
|
+
"paths": results,
|
|
57
|
+
"config": config,
|
|
54
58
|
}
|