souleyez 2.43.29__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 +22827 -10678
- 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-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
souleyez.plugins.service_explorer - Unified service exploration tool
|
|
4
|
+
|
|
5
|
+
Connects to and explores various services after access is discovered:
|
|
6
|
+
- FTP (anonymous or authenticated)
|
|
7
|
+
- SFTP (SSH file transfer)
|
|
8
|
+
- SMB (Windows shares)
|
|
9
|
+
- NFS (Network file system)
|
|
10
|
+
- TFTP (Trivial FTP)
|
|
11
|
+
- Redis (NoSQL database)
|
|
12
|
+
- MongoDB (NoSQL database)
|
|
13
|
+
|
|
14
|
+
This tool is designed to auto-chain from discovery tools when access is found.
|
|
15
|
+
"""
|
|
16
|
+
import ftplib # nosec B402 - intentional for pentesting FTP services
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import socket
|
|
20
|
+
import tempfile
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
24
|
+
from urllib.parse import urlparse
|
|
25
|
+
|
|
26
|
+
from .plugin_base import PluginBase
|
|
27
|
+
|
|
28
|
+
HELP = {
|
|
29
|
+
"name": "Service Explorer - Unified File/Data Browser",
|
|
30
|
+
"description": (
|
|
31
|
+
"Connects to and explores services after access is discovered.\n\n"
|
|
32
|
+
"Supported protocols:\n"
|
|
33
|
+
" - ftp:// - FTP file browsing (anonymous or authenticated)\n"
|
|
34
|
+
" - sftp:// - SFTP/SSH file browsing\n"
|
|
35
|
+
" - smb:// - SMB/Windows share browsing\n"
|
|
36
|
+
" - nfs:// - NFS mount browsing\n"
|
|
37
|
+
" - tftp:// - TFTP file retrieval\n"
|
|
38
|
+
" - redis:// - Redis database exploration\n"
|
|
39
|
+
" - mongo:// - MongoDB database exploration\n\n"
|
|
40
|
+
"Auto-chains from findings like 'FTP anonymous access' or 'SMB null session'.\n"
|
|
41
|
+
),
|
|
42
|
+
"usage": "souleyez jobs enqueue service_explorer <protocol://target>",
|
|
43
|
+
"examples": [
|
|
44
|
+
"souleyez jobs enqueue service_explorer ftp://anonymous@192.168.1.100",
|
|
45
|
+
"souleyez jobs enqueue service_explorer ftp://user:pass@192.168.1.100",
|
|
46
|
+
"souleyez jobs enqueue service_explorer smb://192.168.1.100/share",
|
|
47
|
+
"souleyez jobs enqueue service_explorer redis://192.168.1.100:6379",
|
|
48
|
+
],
|
|
49
|
+
"flags": [
|
|
50
|
+
["--depth <n>", "Maximum directory depth to explore (default: 3)"],
|
|
51
|
+
["--download", "Download interesting files to evidence folder"],
|
|
52
|
+
["--download-all", "Download ALL files (use with caution)"],
|
|
53
|
+
["--timeout <sec>", "Connection timeout (default: 10)"],
|
|
54
|
+
],
|
|
55
|
+
"presets": [
|
|
56
|
+
{
|
|
57
|
+
"name": "FTP Anonymous",
|
|
58
|
+
"args": ["ftp://anonymous@{target}"],
|
|
59
|
+
"desc": "Browse FTP with anonymous login",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "FTP Anon+Download",
|
|
63
|
+
"args": ["ftp://anonymous@{target}", "--download"],
|
|
64
|
+
"desc": "FTP anonymous + download interesting files",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "FTP with Creds",
|
|
68
|
+
"args": ["ftp://{target}"],
|
|
69
|
+
"desc": "Browse FTP (will prompt for creds)",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "Redis",
|
|
73
|
+
"args": ["redis://{target}:6379"],
|
|
74
|
+
"desc": "Explore Redis database",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "MongoDB",
|
|
78
|
+
"args": ["mongodb://{target}:27017"],
|
|
79
|
+
"desc": "Explore MongoDB database",
|
|
80
|
+
},
|
|
81
|
+
{"name": "NFS", "args": ["nfs://{target}"], "desc": "Browse NFS exports"},
|
|
82
|
+
{
|
|
83
|
+
"name": "SFTP",
|
|
84
|
+
"args": ["sftp://{target}"],
|
|
85
|
+
"desc": "Browse via SFTP (needs creds)",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
"help_sections": [
|
|
89
|
+
{
|
|
90
|
+
"title": "URL Format & Protocols",
|
|
91
|
+
"color": "cyan",
|
|
92
|
+
"content": [
|
|
93
|
+
{
|
|
94
|
+
"title": "Format",
|
|
95
|
+
"desc": "protocol://[user:pass@]host[:port][/path]",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"title": "Examples",
|
|
99
|
+
"desc": "Common URL formats for each protocol",
|
|
100
|
+
"tips": [
|
|
101
|
+
"FTP: ftp://anonymous@192.168.1.100",
|
|
102
|
+
"FTP w/creds: ftp://admin:password@192.168.1.100",
|
|
103
|
+
"SFTP: sftp://user:pass@192.168.1.100",
|
|
104
|
+
"NFS: nfs://192.168.1.100/export",
|
|
105
|
+
"Redis: redis://192.168.1.100:6379",
|
|
106
|
+
"MongoDB: mongodb://192.168.1.100:27017",
|
|
107
|
+
"TFTP: tftp://192.168.1.100/filename",
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"title": "Auto-Chain Triggers",
|
|
114
|
+
"color": "green",
|
|
115
|
+
"content": [
|
|
116
|
+
{
|
|
117
|
+
"title": "How It Works",
|
|
118
|
+
"desc": "Service Explorer auto-runs when access is discovered by other tools",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"title": "Triggers",
|
|
122
|
+
"desc": "These findings trigger Service Explorer",
|
|
123
|
+
"tips": [
|
|
124
|
+
"FTP Anonymous: MSF finds anonymous FTP -> auto explores files",
|
|
125
|
+
"Redis No-Auth: Nmap finds redis -> auto explores database",
|
|
126
|
+
"NFS Exports: MSF finds NFS exports -> auto explores shares",
|
|
127
|
+
"MongoDB No-Auth: Nmap finds mongodb -> auto explores database",
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"title": "What Gets Flagged",
|
|
134
|
+
"color": "yellow",
|
|
135
|
+
"content": [
|
|
136
|
+
{
|
|
137
|
+
"title": "Interesting Files",
|
|
138
|
+
"desc": "These file patterns are auto-flagged for review",
|
|
139
|
+
"tips": [
|
|
140
|
+
"Credentials: *.conf, *.ini, *.env, passwd, shadow, *.pem, *.key",
|
|
141
|
+
"Database: *.sql, *.db, *.sqlite, *.mdb",
|
|
142
|
+
"Backups: *.bak, *.backup, *.tar.gz, *.zip",
|
|
143
|
+
"CTF Flags: flag*, proof*, root.txt, user.txt",
|
|
144
|
+
"Source Code: *.php, *.asp, *.jsp",
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Interesting file patterns to flag/download
|
|
153
|
+
INTERESTING_PATTERNS = [
|
|
154
|
+
# Credentials & configs
|
|
155
|
+
"*.conf",
|
|
156
|
+
"*.config",
|
|
157
|
+
"*.cfg",
|
|
158
|
+
"*.ini",
|
|
159
|
+
"*.env",
|
|
160
|
+
"*.htpasswd",
|
|
161
|
+
"*.htaccess",
|
|
162
|
+
"passwd",
|
|
163
|
+
"shadow",
|
|
164
|
+
"*.pem",
|
|
165
|
+
"*.key",
|
|
166
|
+
"id_rsa",
|
|
167
|
+
"id_dsa",
|
|
168
|
+
"id_ecdsa",
|
|
169
|
+
"id_ed25519",
|
|
170
|
+
"authorized_keys",
|
|
171
|
+
"*.pfx",
|
|
172
|
+
"*.p12",
|
|
173
|
+
"credentials*",
|
|
174
|
+
"secrets*",
|
|
175
|
+
"password*",
|
|
176
|
+
# Database
|
|
177
|
+
"*.sql",
|
|
178
|
+
"*.db",
|
|
179
|
+
"*.sqlite",
|
|
180
|
+
"*.mdb",
|
|
181
|
+
# Backups
|
|
182
|
+
"*.bak",
|
|
183
|
+
"*.backup",
|
|
184
|
+
"*.old",
|
|
185
|
+
"*.orig",
|
|
186
|
+
"*.save",
|
|
187
|
+
"*.tar",
|
|
188
|
+
"*.tar.gz",
|
|
189
|
+
"*.tgz",
|
|
190
|
+
"*.zip",
|
|
191
|
+
"*.rar",
|
|
192
|
+
"*.7z",
|
|
193
|
+
# Source code
|
|
194
|
+
"*.php",
|
|
195
|
+
"*.asp",
|
|
196
|
+
"*.aspx",
|
|
197
|
+
"*.jsp",
|
|
198
|
+
# CTF flags
|
|
199
|
+
"flag*",
|
|
200
|
+
"*flag*",
|
|
201
|
+
"FLAG*",
|
|
202
|
+
"proof*",
|
|
203
|
+
"root.txt",
|
|
204
|
+
"user.txt",
|
|
205
|
+
"local.txt",
|
|
206
|
+
# Interesting docs
|
|
207
|
+
"readme*",
|
|
208
|
+
"README*",
|
|
209
|
+
"todo*",
|
|
210
|
+
"TODO*",
|
|
211
|
+
"notes*",
|
|
212
|
+
"NOTES*",
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ProtocolHandler(ABC):
|
|
217
|
+
"""Base class for protocol-specific handlers."""
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
target: str,
|
|
222
|
+
username: str = None,
|
|
223
|
+
password: str = None,
|
|
224
|
+
port: int = None,
|
|
225
|
+
timeout: int = 10,
|
|
226
|
+
):
|
|
227
|
+
self.target = target
|
|
228
|
+
self.username = username
|
|
229
|
+
self.password = password
|
|
230
|
+
self.port = port
|
|
231
|
+
self.timeout = timeout
|
|
232
|
+
self.files_found: List[Dict[str, Any]] = []
|
|
233
|
+
self.interesting_files: List[Dict[str, Any]] = []
|
|
234
|
+
self.errors: List[str] = []
|
|
235
|
+
self.downloaded: List[str] = []
|
|
236
|
+
|
|
237
|
+
@abstractmethod
|
|
238
|
+
def connect(self) -> bool:
|
|
239
|
+
"""Connect to the service. Returns True on success."""
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
@abstractmethod
|
|
243
|
+
def list_directory(self, path: str = "/") -> List[Dict[str, Any]]:
|
|
244
|
+
"""List contents of a directory. Returns list of file info dicts."""
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
@abstractmethod
|
|
248
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
249
|
+
"""Download a file. Returns True on success."""
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
@abstractmethod
|
|
253
|
+
def disconnect(self):
|
|
254
|
+
"""Close the connection."""
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
def is_interesting(self, filename: str) -> bool:
|
|
258
|
+
"""Check if a filename matches interesting patterns."""
|
|
259
|
+
import fnmatch
|
|
260
|
+
|
|
261
|
+
filename_lower = filename.lower()
|
|
262
|
+
for pattern in INTERESTING_PATTERNS:
|
|
263
|
+
if fnmatch.fnmatch(filename_lower, pattern.lower()):
|
|
264
|
+
return True
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
def explore(
|
|
268
|
+
self, path: str = "/", depth: int = 3, current_depth: int = 0
|
|
269
|
+
) -> List[Dict[str, Any]]:
|
|
270
|
+
"""Recursively explore directories up to max depth."""
|
|
271
|
+
if current_depth >= depth:
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
results = []
|
|
275
|
+
try:
|
|
276
|
+
items = self.list_directory(path)
|
|
277
|
+
for item in items:
|
|
278
|
+
item["path"] = path
|
|
279
|
+
item["full_path"] = os.path.join(path, item["name"]).replace("\\", "/")
|
|
280
|
+
results.append(item)
|
|
281
|
+
|
|
282
|
+
# Flag interesting files
|
|
283
|
+
if self.is_interesting(item["name"]):
|
|
284
|
+
item["interesting"] = True
|
|
285
|
+
self.interesting_files.append(item)
|
|
286
|
+
|
|
287
|
+
# Recurse into directories
|
|
288
|
+
if item.get("type") == "directory":
|
|
289
|
+
subpath = item["full_path"]
|
|
290
|
+
results.extend(self.explore(subpath, depth, current_depth + 1))
|
|
291
|
+
except Exception as e:
|
|
292
|
+
self.errors.append(f"Error exploring {path}: {e}")
|
|
293
|
+
|
|
294
|
+
return results
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class FTPHandler(ProtocolHandler):
|
|
298
|
+
"""FTP protocol handler using ftplib."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, *args, **kwargs):
|
|
301
|
+
super().__init__(*args, **kwargs)
|
|
302
|
+
self.ftp: Optional[ftplib.FTP] = None
|
|
303
|
+
self.port = self.port or 21
|
|
304
|
+
|
|
305
|
+
def connect(self) -> bool:
|
|
306
|
+
try:
|
|
307
|
+
self.ftp = ftplib.FTP() # nosec B321 - intentional for pentesting
|
|
308
|
+
self.ftp.connect(self.target, self.port, timeout=self.timeout)
|
|
309
|
+
|
|
310
|
+
# Login
|
|
311
|
+
username = self.username or "anonymous"
|
|
312
|
+
password = self.password or "anonymous@"
|
|
313
|
+
self.ftp.login(username, password)
|
|
314
|
+
return True
|
|
315
|
+
except Exception as e:
|
|
316
|
+
self.errors.append(f"FTP connection failed: {e}")
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
def list_directory(self, path: str = "/") -> List[Dict[str, Any]]:
|
|
320
|
+
if not self.ftp:
|
|
321
|
+
return []
|
|
322
|
+
|
|
323
|
+
items = []
|
|
324
|
+
try:
|
|
325
|
+
self.ftp.cwd(path)
|
|
326
|
+
|
|
327
|
+
# Try MLSD first (more detailed)
|
|
328
|
+
try:
|
|
329
|
+
for name, facts in self.ftp.mlsd():
|
|
330
|
+
if name in (".", ".."):
|
|
331
|
+
continue
|
|
332
|
+
item = {
|
|
333
|
+
"name": name,
|
|
334
|
+
"type": "directory" if facts.get("type") == "dir" else "file",
|
|
335
|
+
"size": int(facts.get("size", 0)),
|
|
336
|
+
}
|
|
337
|
+
items.append(item)
|
|
338
|
+
except (ftplib.error_perm, AttributeError):
|
|
339
|
+
# Fall back to NLST
|
|
340
|
+
names = self.ftp.nlst()
|
|
341
|
+
for name in names:
|
|
342
|
+
if name in (".", ".."):
|
|
343
|
+
continue
|
|
344
|
+
# Try to determine if it's a directory
|
|
345
|
+
is_dir = False
|
|
346
|
+
try:
|
|
347
|
+
self.ftp.cwd(name)
|
|
348
|
+
self.ftp.cwd("..")
|
|
349
|
+
is_dir = True
|
|
350
|
+
except ftplib.error_perm:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
items.append(
|
|
354
|
+
{
|
|
355
|
+
"name": name,
|
|
356
|
+
"type": "directory" if is_dir else "file",
|
|
357
|
+
"size": 0,
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
self.errors.append(f"FTP list error at {path}: {e}")
|
|
362
|
+
|
|
363
|
+
return items
|
|
364
|
+
|
|
365
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
366
|
+
if not self.ftp:
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
# Navigate to directory
|
|
371
|
+
dirname = os.path.dirname(remote_path)
|
|
372
|
+
filename = os.path.basename(remote_path)
|
|
373
|
+
|
|
374
|
+
if dirname:
|
|
375
|
+
self.ftp.cwd(dirname)
|
|
376
|
+
|
|
377
|
+
# Download
|
|
378
|
+
with open(local_path, "wb") as f:
|
|
379
|
+
self.ftp.retrbinary(f"RETR {filename}", f.write)
|
|
380
|
+
|
|
381
|
+
self.downloaded.append(remote_path)
|
|
382
|
+
return True
|
|
383
|
+
except Exception as e:
|
|
384
|
+
self.errors.append(f"FTP download error {remote_path}: {e}")
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
def disconnect(self):
|
|
388
|
+
if self.ftp:
|
|
389
|
+
try:
|
|
390
|
+
self.ftp.quit()
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
self.ftp = None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class TFTPHandler(ProtocolHandler):
|
|
397
|
+
"""TFTP protocol handler - attempts to retrieve common files."""
|
|
398
|
+
|
|
399
|
+
# Common files found on TFTP servers (routers, network devices)
|
|
400
|
+
COMMON_TFTP_FILES = [
|
|
401
|
+
"running-config",
|
|
402
|
+
"startup-config",
|
|
403
|
+
"config.txt",
|
|
404
|
+
"config",
|
|
405
|
+
"backup.cfg",
|
|
406
|
+
"router.cfg",
|
|
407
|
+
"switch.cfg",
|
|
408
|
+
"etc/passwd",
|
|
409
|
+
"etc/shadow",
|
|
410
|
+
"boot.ini",
|
|
411
|
+
"win.ini",
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
def __init__(self, *args, **kwargs):
|
|
415
|
+
super().__init__(*args, **kwargs)
|
|
416
|
+
self.port = self.port or 69
|
|
417
|
+
|
|
418
|
+
def connect(self) -> bool:
|
|
419
|
+
# TFTP is connectionless, just verify target is reachable
|
|
420
|
+
try:
|
|
421
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
422
|
+
sock.settimeout(self.timeout)
|
|
423
|
+
sock.close()
|
|
424
|
+
return True
|
|
425
|
+
except Exception as e:
|
|
426
|
+
self.errors.append(f"TFTP target unreachable: {e}")
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
def list_directory(self, path: str = "/") -> List[Dict[str, Any]]:
|
|
430
|
+
# TFTP doesn't support directory listing
|
|
431
|
+
# Return list of common files to try
|
|
432
|
+
return [
|
|
433
|
+
{"name": f, "type": "file", "size": 0, "note": "common TFTP file"}
|
|
434
|
+
for f in self.COMMON_TFTP_FILES
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
438
|
+
"""Download file via TFTP using system tftp client."""
|
|
439
|
+
try:
|
|
440
|
+
import subprocess
|
|
441
|
+
|
|
442
|
+
result = subprocess.run(
|
|
443
|
+
["tftp", self.target, "-c", "get", remote_path, local_path],
|
|
444
|
+
capture_output=True,
|
|
445
|
+
text=True,
|
|
446
|
+
timeout=self.timeout,
|
|
447
|
+
)
|
|
448
|
+
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
|
|
449
|
+
self.downloaded.append(remote_path)
|
|
450
|
+
return True
|
|
451
|
+
return False
|
|
452
|
+
except FileNotFoundError:
|
|
453
|
+
# tftp client not installed, try atftp
|
|
454
|
+
try:
|
|
455
|
+
import subprocess
|
|
456
|
+
|
|
457
|
+
result = subprocess.run(
|
|
458
|
+
["atftp", "-g", "-r", remote_path, "-l", local_path, self.target],
|
|
459
|
+
capture_output=True,
|
|
460
|
+
text=True,
|
|
461
|
+
timeout=self.timeout,
|
|
462
|
+
)
|
|
463
|
+
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
|
|
464
|
+
self.downloaded.append(remote_path)
|
|
465
|
+
return True
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
self.errors.append("No TFTP client available (tftp/atftp)")
|
|
469
|
+
return False
|
|
470
|
+
except Exception as e:
|
|
471
|
+
self.errors.append(f"TFTP download error {remote_path}: {e}")
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
def disconnect(self):
|
|
475
|
+
pass # TFTP is connectionless
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class RedisHandler(ProtocolHandler):
|
|
479
|
+
"""Redis protocol handler for exploring Redis databases."""
|
|
480
|
+
|
|
481
|
+
def __init__(self, *args, **kwargs):
|
|
482
|
+
super().__init__(*args, **kwargs)
|
|
483
|
+
self.port = self.port or 6379
|
|
484
|
+
self.client = None
|
|
485
|
+
|
|
486
|
+
def connect(self) -> bool:
|
|
487
|
+
try:
|
|
488
|
+
import redis
|
|
489
|
+
|
|
490
|
+
self.client = redis.Redis(
|
|
491
|
+
host=self.target,
|
|
492
|
+
port=self.port,
|
|
493
|
+
password=self.password,
|
|
494
|
+
socket_timeout=self.timeout,
|
|
495
|
+
decode_responses=True,
|
|
496
|
+
)
|
|
497
|
+
# Test connection
|
|
498
|
+
self.client.ping()
|
|
499
|
+
return True
|
|
500
|
+
except ImportError:
|
|
501
|
+
self.errors.append("redis-py not installed (pip install redis)")
|
|
502
|
+
return False
|
|
503
|
+
except Exception as e:
|
|
504
|
+
self.errors.append(f"Redis connection failed: {e}")
|
|
505
|
+
return False
|
|
506
|
+
|
|
507
|
+
def list_directory(self, path: str = "/") -> List[Dict[str, Any]]:
|
|
508
|
+
"""List Redis keys (path is used as pattern)."""
|
|
509
|
+
if not self.client:
|
|
510
|
+
return []
|
|
511
|
+
|
|
512
|
+
items = []
|
|
513
|
+
try:
|
|
514
|
+
pattern = "*" if path == "/" else f"{path}*"
|
|
515
|
+
keys = self.client.keys(pattern)[:1000] # Limit to 1000 keys
|
|
516
|
+
|
|
517
|
+
for key in keys:
|
|
518
|
+
try:
|
|
519
|
+
key_type = self.client.type(key)
|
|
520
|
+
size = 0
|
|
521
|
+
if key_type == "string":
|
|
522
|
+
size = self.client.strlen(key)
|
|
523
|
+
elif key_type in ("list", "set", "zset"):
|
|
524
|
+
size = (
|
|
525
|
+
self.client.llen(key)
|
|
526
|
+
if key_type == "list"
|
|
527
|
+
else self.client.scard(key)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
items.append(
|
|
531
|
+
{
|
|
532
|
+
"name": key,
|
|
533
|
+
"type": key_type,
|
|
534
|
+
"size": size,
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
except Exception:
|
|
538
|
+
items.append({"name": key, "type": "unknown", "size": 0})
|
|
539
|
+
|
|
540
|
+
# Also get server info
|
|
541
|
+
try:
|
|
542
|
+
info = self.client.info()
|
|
543
|
+
items.append(
|
|
544
|
+
{
|
|
545
|
+
"name": "__SERVER_INFO__",
|
|
546
|
+
"type": "info",
|
|
547
|
+
"size": 0,
|
|
548
|
+
"data": {
|
|
549
|
+
"redis_version": info.get("redis_version"),
|
|
550
|
+
"os": info.get("os"),
|
|
551
|
+
"used_memory_human": info.get("used_memory_human"),
|
|
552
|
+
"connected_clients": info.get("connected_clients"),
|
|
553
|
+
"total_keys": sum(
|
|
554
|
+
info.get(f"db{i}", {}).get("keys", 0) for i in range(16)
|
|
555
|
+
),
|
|
556
|
+
},
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
except Exception:
|
|
560
|
+
pass
|
|
561
|
+
|
|
562
|
+
except Exception as e:
|
|
563
|
+
self.errors.append(f"Redis key listing error: {e}")
|
|
564
|
+
|
|
565
|
+
return items
|
|
566
|
+
|
|
567
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
568
|
+
"""Dump a Redis key's value to a file."""
|
|
569
|
+
if not self.client:
|
|
570
|
+
return False
|
|
571
|
+
|
|
572
|
+
try:
|
|
573
|
+
key = remote_path
|
|
574
|
+
key_type = self.client.type(key)
|
|
575
|
+
|
|
576
|
+
if key_type == "string":
|
|
577
|
+
value = self.client.get(key)
|
|
578
|
+
elif key_type == "list":
|
|
579
|
+
value = self.client.lrange(key, 0, -1)
|
|
580
|
+
elif key_type == "set":
|
|
581
|
+
value = list(self.client.smembers(key))
|
|
582
|
+
elif key_type == "zset":
|
|
583
|
+
value = self.client.zrange(key, 0, -1, withscores=True)
|
|
584
|
+
elif key_type == "hash":
|
|
585
|
+
value = self.client.hgetall(key)
|
|
586
|
+
else:
|
|
587
|
+
value = f"Unknown type: {key_type}"
|
|
588
|
+
|
|
589
|
+
with open(local_path, "w") as f:
|
|
590
|
+
if isinstance(value, (list, dict)):
|
|
591
|
+
json.dump(value, f, indent=2, default=str)
|
|
592
|
+
else:
|
|
593
|
+
f.write(str(value))
|
|
594
|
+
|
|
595
|
+
self.downloaded.append(key)
|
|
596
|
+
return True
|
|
597
|
+
except Exception as e:
|
|
598
|
+
self.errors.append(f"Redis dump error {remote_path}: {e}")
|
|
599
|
+
return False
|
|
600
|
+
|
|
601
|
+
def disconnect(self):
|
|
602
|
+
if self.client:
|
|
603
|
+
try:
|
|
604
|
+
self.client.close()
|
|
605
|
+
except Exception:
|
|
606
|
+
pass
|
|
607
|
+
self.client = None
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class SFTPHandler(ProtocolHandler):
|
|
611
|
+
"""SFTP protocol handler using paramiko."""
|
|
612
|
+
|
|
613
|
+
def __init__(self, *args, **kwargs):
|
|
614
|
+
super().__init__(*args, **kwargs)
|
|
615
|
+
self.port = self.port or 22
|
|
616
|
+
self.sftp = None
|
|
617
|
+
self.transport = None
|
|
618
|
+
|
|
619
|
+
def connect(self) -> bool:
|
|
620
|
+
try:
|
|
621
|
+
import paramiko
|
|
622
|
+
|
|
623
|
+
self.transport = paramiko.Transport((self.target, self.port))
|
|
624
|
+
self.transport.connect(
|
|
625
|
+
username=self.username or "anonymous", password=self.password or ""
|
|
626
|
+
)
|
|
627
|
+
self.sftp = paramiko.SFTPClient.from_transport(self.transport)
|
|
628
|
+
return True
|
|
629
|
+
except ImportError:
|
|
630
|
+
self.errors.append("paramiko not installed (pip install paramiko)")
|
|
631
|
+
return False
|
|
632
|
+
except Exception as e:
|
|
633
|
+
self.errors.append(f"SFTP connection failed: {e}")
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
def list_directory(self, path: str = "/") -> List[Dict[str, Any]]:
|
|
637
|
+
if not self.sftp:
|
|
638
|
+
return []
|
|
639
|
+
|
|
640
|
+
items = []
|
|
641
|
+
try:
|
|
642
|
+
for entry in self.sftp.listdir_attr(path):
|
|
643
|
+
import stat
|
|
644
|
+
|
|
645
|
+
is_dir = stat.S_ISDIR(entry.st_mode)
|
|
646
|
+
items.append(
|
|
647
|
+
{
|
|
648
|
+
"name": entry.filename,
|
|
649
|
+
"type": "directory" if is_dir else "file",
|
|
650
|
+
"size": entry.st_size,
|
|
651
|
+
"mtime": entry.st_mtime,
|
|
652
|
+
}
|
|
653
|
+
)
|
|
654
|
+
except Exception as e:
|
|
655
|
+
self.errors.append(f"SFTP list error at {path}: {e}")
|
|
656
|
+
|
|
657
|
+
return items
|
|
658
|
+
|
|
659
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
660
|
+
if not self.sftp:
|
|
661
|
+
return False
|
|
662
|
+
|
|
663
|
+
try:
|
|
664
|
+
self.sftp.get(remote_path, local_path)
|
|
665
|
+
self.downloaded.append(remote_path)
|
|
666
|
+
return True
|
|
667
|
+
except Exception as e:
|
|
668
|
+
self.errors.append(f"SFTP download error {remote_path}: {e}")
|
|
669
|
+
return False
|
|
670
|
+
|
|
671
|
+
def disconnect(self):
|
|
672
|
+
if self.sftp:
|
|
673
|
+
try:
|
|
674
|
+
self.sftp.close()
|
|
675
|
+
except Exception:
|
|
676
|
+
pass
|
|
677
|
+
self.sftp = None
|
|
678
|
+
if self.transport:
|
|
679
|
+
try:
|
|
680
|
+
self.transport.close()
|
|
681
|
+
except Exception:
|
|
682
|
+
pass
|
|
683
|
+
self.transport = None
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
class NFSHandler(ProtocolHandler):
|
|
687
|
+
"""NFS protocol handler using showmount and mount commands."""
|
|
688
|
+
|
|
689
|
+
def __init__(self, *args, **kwargs):
|
|
690
|
+
super().__init__(*args, **kwargs)
|
|
691
|
+
self.port = self.port or 2049
|
|
692
|
+
self.mount_point = None
|
|
693
|
+
self.export_path = None
|
|
694
|
+
|
|
695
|
+
def connect(self) -> bool:
|
|
696
|
+
"""Check NFS exports available on target."""
|
|
697
|
+
try:
|
|
698
|
+
import subprocess
|
|
699
|
+
|
|
700
|
+
result = subprocess.run(
|
|
701
|
+
["showmount", "-e", self.target],
|
|
702
|
+
capture_output=True,
|
|
703
|
+
text=True,
|
|
704
|
+
timeout=self.timeout,
|
|
705
|
+
)
|
|
706
|
+
if result.returncode != 0:
|
|
707
|
+
self.errors.append(f"showmount failed: {result.stderr}")
|
|
708
|
+
return False
|
|
709
|
+
|
|
710
|
+
# Parse exports
|
|
711
|
+
lines = result.stdout.strip().split("\n")[1:] # Skip header
|
|
712
|
+
if not lines:
|
|
713
|
+
self.errors.append("No NFS exports found")
|
|
714
|
+
return False
|
|
715
|
+
|
|
716
|
+
# Use first export or specified path
|
|
717
|
+
self.export_path = (
|
|
718
|
+
self.password or lines[0].split()[0]
|
|
719
|
+
) # Reuse password field for export path
|
|
720
|
+
return True
|
|
721
|
+
except FileNotFoundError:
|
|
722
|
+
self.errors.append("showmount not installed (apt install nfs-common)")
|
|
723
|
+
return False
|
|
724
|
+
except Exception as e:
|
|
725
|
+
self.errors.append(f"NFS connection failed: {e}")
|
|
726
|
+
return False
|
|
727
|
+
|
|
728
|
+
def list_directory(self, path: str = "/") -> List[Dict[str, Any]]:
|
|
729
|
+
"""List NFS export contents by mounting temporarily."""
|
|
730
|
+
import subprocess
|
|
731
|
+
import tempfile
|
|
732
|
+
|
|
733
|
+
items = []
|
|
734
|
+
|
|
735
|
+
# Create temporary mount point
|
|
736
|
+
self.mount_point = tempfile.mkdtemp(prefix="souleyez_nfs_")
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
# Mount the export
|
|
740
|
+
mount_target = f"{self.target}:{self.export_path}"
|
|
741
|
+
result = subprocess.run(
|
|
742
|
+
[
|
|
743
|
+
"mount",
|
|
744
|
+
"-t",
|
|
745
|
+
"nfs",
|
|
746
|
+
"-o",
|
|
747
|
+
"ro,nolock",
|
|
748
|
+
mount_target,
|
|
749
|
+
self.mount_point,
|
|
750
|
+
],
|
|
751
|
+
capture_output=True,
|
|
752
|
+
text=True,
|
|
753
|
+
timeout=30,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
if result.returncode != 0:
|
|
757
|
+
self.errors.append(f"NFS mount failed: {result.stderr}")
|
|
758
|
+
return items
|
|
759
|
+
|
|
760
|
+
# List files
|
|
761
|
+
full_path = os.path.join(self.mount_point, path.lstrip("/"))
|
|
762
|
+
if os.path.isdir(full_path):
|
|
763
|
+
for entry in os.listdir(full_path):
|
|
764
|
+
entry_path = os.path.join(full_path, entry)
|
|
765
|
+
try:
|
|
766
|
+
stat_info = os.stat(entry_path)
|
|
767
|
+
items.append(
|
|
768
|
+
{
|
|
769
|
+
"name": entry,
|
|
770
|
+
"type": (
|
|
771
|
+
"directory" if os.path.isdir(entry_path) else "file"
|
|
772
|
+
),
|
|
773
|
+
"size": stat_info.st_size,
|
|
774
|
+
"mtime": stat_info.st_mtime,
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
except Exception:
|
|
778
|
+
items.append({"name": entry, "type": "unknown", "size": 0})
|
|
779
|
+
|
|
780
|
+
except Exception as e:
|
|
781
|
+
self.errors.append(f"NFS list error: {e}")
|
|
782
|
+
finally:
|
|
783
|
+
# Unmount
|
|
784
|
+
try:
|
|
785
|
+
subprocess.run(
|
|
786
|
+
["umount", self.mount_point], capture_output=True, timeout=10
|
|
787
|
+
)
|
|
788
|
+
except Exception:
|
|
789
|
+
pass
|
|
790
|
+
|
|
791
|
+
return items
|
|
792
|
+
|
|
793
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
794
|
+
"""Download file from NFS share."""
|
|
795
|
+
import subprocess
|
|
796
|
+
import tempfile
|
|
797
|
+
import shutil
|
|
798
|
+
|
|
799
|
+
if not self.mount_point:
|
|
800
|
+
self.mount_point = tempfile.mkdtemp(prefix="souleyez_nfs_")
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
# Mount if not already mounted
|
|
804
|
+
mount_target = f"{self.target}:{self.export_path}"
|
|
805
|
+
subprocess.run(
|
|
806
|
+
[
|
|
807
|
+
"mount",
|
|
808
|
+
"-t",
|
|
809
|
+
"nfs",
|
|
810
|
+
"-o",
|
|
811
|
+
"ro,nolock",
|
|
812
|
+
mount_target,
|
|
813
|
+
self.mount_point,
|
|
814
|
+
],
|
|
815
|
+
capture_output=True,
|
|
816
|
+
timeout=30,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Copy file
|
|
820
|
+
src_path = os.path.join(self.mount_point, remote_path.lstrip("/"))
|
|
821
|
+
if os.path.exists(src_path):
|
|
822
|
+
shutil.copy2(src_path, local_path)
|
|
823
|
+
self.downloaded.append(remote_path)
|
|
824
|
+
return True
|
|
825
|
+
else:
|
|
826
|
+
self.errors.append(f"File not found: {remote_path}")
|
|
827
|
+
return False
|
|
828
|
+
|
|
829
|
+
except Exception as e:
|
|
830
|
+
self.errors.append(f"NFS download error {remote_path}: {e}")
|
|
831
|
+
return False
|
|
832
|
+
finally:
|
|
833
|
+
try:
|
|
834
|
+
subprocess.run(
|
|
835
|
+
["umount", self.mount_point], capture_output=True, timeout=10
|
|
836
|
+
)
|
|
837
|
+
except Exception:
|
|
838
|
+
pass
|
|
839
|
+
|
|
840
|
+
def disconnect(self):
|
|
841
|
+
"""Cleanup mount point."""
|
|
842
|
+
import subprocess
|
|
843
|
+
|
|
844
|
+
if self.mount_point:
|
|
845
|
+
try:
|
|
846
|
+
subprocess.run(
|
|
847
|
+
["umount", self.mount_point], capture_output=True, timeout=10
|
|
848
|
+
)
|
|
849
|
+
os.rmdir(self.mount_point)
|
|
850
|
+
except Exception:
|
|
851
|
+
pass
|
|
852
|
+
self.mount_point = None
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
class MongoDBHandler(ProtocolHandler):
|
|
856
|
+
"""MongoDB protocol handler using pymongo."""
|
|
857
|
+
|
|
858
|
+
def __init__(self, *args, **kwargs):
|
|
859
|
+
super().__init__(*args, **kwargs)
|
|
860
|
+
self.port = self.port or 27017
|
|
861
|
+
self.client = None
|
|
862
|
+
|
|
863
|
+
def connect(self) -> bool:
|
|
864
|
+
try:
|
|
865
|
+
from pymongo import MongoClient
|
|
866
|
+
from pymongo.errors import ServerSelectionTimeoutError
|
|
867
|
+
|
|
868
|
+
# Build connection URI
|
|
869
|
+
if self.username and self.password:
|
|
870
|
+
uri = f"mongodb://{self.username}:{self.password}@{self.target}:{self.port}"
|
|
871
|
+
else:
|
|
872
|
+
uri = f"mongodb://{self.target}:{self.port}"
|
|
873
|
+
|
|
874
|
+
self.client = MongoClient(uri, serverSelectionTimeoutMS=self.timeout * 1000)
|
|
875
|
+
# Test connection
|
|
876
|
+
self.client.admin.command("ping")
|
|
877
|
+
return True
|
|
878
|
+
except ImportError:
|
|
879
|
+
self.errors.append("pymongo not installed (pip install pymongo)")
|
|
880
|
+
return False
|
|
881
|
+
except Exception as e:
|
|
882
|
+
self.errors.append(f"MongoDB connection failed: {e}")
|
|
883
|
+
return False
|
|
884
|
+
|
|
885
|
+
def list_directory(self, path: str = "/") -> List[Dict[str, Any]]:
|
|
886
|
+
"""List MongoDB databases and collections."""
|
|
887
|
+
if not self.client:
|
|
888
|
+
return []
|
|
889
|
+
|
|
890
|
+
items = []
|
|
891
|
+
try:
|
|
892
|
+
if path == "/" or path == "":
|
|
893
|
+
# List databases
|
|
894
|
+
for db_name in self.client.list_database_names():
|
|
895
|
+
db = self.client[db_name]
|
|
896
|
+
items.append(
|
|
897
|
+
{
|
|
898
|
+
"name": db_name,
|
|
899
|
+
"type": "database",
|
|
900
|
+
"size": 0,
|
|
901
|
+
"collections": len(db.list_collection_names()),
|
|
902
|
+
}
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
# Also get server info
|
|
906
|
+
try:
|
|
907
|
+
info = self.client.admin.command("serverStatus")
|
|
908
|
+
items.append(
|
|
909
|
+
{
|
|
910
|
+
"name": "__SERVER_INFO__",
|
|
911
|
+
"type": "info",
|
|
912
|
+
"size": 0,
|
|
913
|
+
"data": {
|
|
914
|
+
"version": info.get("version"),
|
|
915
|
+
"uptime": info.get("uptime"),
|
|
916
|
+
"connections": info.get("connections", {}).get(
|
|
917
|
+
"current"
|
|
918
|
+
),
|
|
919
|
+
},
|
|
920
|
+
}
|
|
921
|
+
)
|
|
922
|
+
except Exception:
|
|
923
|
+
pass
|
|
924
|
+
else:
|
|
925
|
+
# List collections in database
|
|
926
|
+
db_name = path.strip("/").split("/")[0]
|
|
927
|
+
db = self.client[db_name]
|
|
928
|
+
for coll_name in db.list_collection_names():
|
|
929
|
+
coll = db[coll_name]
|
|
930
|
+
items.append(
|
|
931
|
+
{
|
|
932
|
+
"name": coll_name,
|
|
933
|
+
"type": "collection",
|
|
934
|
+
"size": coll.estimated_document_count(),
|
|
935
|
+
}
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
except Exception as e:
|
|
939
|
+
self.errors.append(f"MongoDB list error: {e}")
|
|
940
|
+
|
|
941
|
+
return items
|
|
942
|
+
|
|
943
|
+
def download_file(self, remote_path: str, local_path: str) -> bool:
|
|
944
|
+
"""Dump MongoDB collection to JSON file."""
|
|
945
|
+
if not self.client:
|
|
946
|
+
return False
|
|
947
|
+
|
|
948
|
+
try:
|
|
949
|
+
parts = remote_path.strip("/").split("/")
|
|
950
|
+
if len(parts) < 2:
|
|
951
|
+
self.errors.append("Path must be /database/collection")
|
|
952
|
+
return False
|
|
953
|
+
|
|
954
|
+
db_name, coll_name = parts[0], parts[1]
|
|
955
|
+
db = self.client[db_name]
|
|
956
|
+
coll = db[coll_name]
|
|
957
|
+
|
|
958
|
+
# Export collection (limit to 10000 docs)
|
|
959
|
+
docs = list(coll.find().limit(10000))
|
|
960
|
+
|
|
961
|
+
with open(local_path, "w") as f:
|
|
962
|
+
json.dump(docs, f, indent=2, default=str)
|
|
963
|
+
|
|
964
|
+
self.downloaded.append(remote_path)
|
|
965
|
+
return True
|
|
966
|
+
except Exception as e:
|
|
967
|
+
self.errors.append(f"MongoDB dump error {remote_path}: {e}")
|
|
968
|
+
return False
|
|
969
|
+
|
|
970
|
+
def disconnect(self):
|
|
971
|
+
if self.client:
|
|
972
|
+
try:
|
|
973
|
+
self.client.close()
|
|
974
|
+
except Exception:
|
|
975
|
+
pass
|
|
976
|
+
self.client = None
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
class ServiceExplorerPlugin(PluginBase):
|
|
980
|
+
"""Unified service explorer plugin."""
|
|
981
|
+
|
|
982
|
+
name = "Service Explorer"
|
|
983
|
+
tool = "service_explorer"
|
|
984
|
+
category = "discovery_collection"
|
|
985
|
+
HELP = HELP
|
|
986
|
+
|
|
987
|
+
# Protocol handler mapping
|
|
988
|
+
PROTOCOLS = {
|
|
989
|
+
"ftp": FTPHandler,
|
|
990
|
+
"tftp": TFTPHandler,
|
|
991
|
+
"redis": RedisHandler,
|
|
992
|
+
"sftp": SFTPHandler,
|
|
993
|
+
"nfs": NFSHandler,
|
|
994
|
+
"mongo": MongoDBHandler,
|
|
995
|
+
"mongodb": MongoDBHandler, # Alias
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
def build_command(
|
|
999
|
+
self, target: str, args: List[str] = None, label: str = "", log_path: str = None
|
|
1000
|
+
):
|
|
1001
|
+
"""Service Explorer runs in Python, not via external command."""
|
|
1002
|
+
return None
|
|
1003
|
+
|
|
1004
|
+
def run(
|
|
1005
|
+
self, target: str, args: List[str] = None, label: str = "", log_path: str = None
|
|
1006
|
+
) -> int:
|
|
1007
|
+
"""Execute service exploration."""
|
|
1008
|
+
args = args or []
|
|
1009
|
+
|
|
1010
|
+
# Check if any arg is a URL (for preset support)
|
|
1011
|
+
# Presets can pass URLs like ftp://anonymous@{target} in args
|
|
1012
|
+
for arg in args:
|
|
1013
|
+
if "://" in arg:
|
|
1014
|
+
target = arg
|
|
1015
|
+
args = [a for a in args if a != arg]
|
|
1016
|
+
break
|
|
1017
|
+
|
|
1018
|
+
# Parse options
|
|
1019
|
+
depth = 3
|
|
1020
|
+
download = False
|
|
1021
|
+
download_all = False
|
|
1022
|
+
timeout = 10
|
|
1023
|
+
|
|
1024
|
+
i = 0
|
|
1025
|
+
while i < len(args):
|
|
1026
|
+
if args[i] == "--depth" and i + 1 < len(args):
|
|
1027
|
+
depth = int(args[i + 1])
|
|
1028
|
+
i += 2
|
|
1029
|
+
elif args[i] == "--download":
|
|
1030
|
+
download = True
|
|
1031
|
+
i += 1
|
|
1032
|
+
elif args[i] == "--download-all":
|
|
1033
|
+
download_all = True
|
|
1034
|
+
download = True
|
|
1035
|
+
i += 1
|
|
1036
|
+
elif args[i] == "--timeout" and i + 1 < len(args):
|
|
1037
|
+
timeout = int(args[i + 1])
|
|
1038
|
+
i += 2
|
|
1039
|
+
else:
|
|
1040
|
+
i += 1
|
|
1041
|
+
|
|
1042
|
+
# Parse target URL
|
|
1043
|
+
parsed = self._parse_target(target)
|
|
1044
|
+
if not parsed:
|
|
1045
|
+
self._write_log(log_path, {"error": f"Invalid target format: {target}"})
|
|
1046
|
+
return 1
|
|
1047
|
+
|
|
1048
|
+
protocol, host, port, username, password, path = parsed
|
|
1049
|
+
|
|
1050
|
+
# Get handler
|
|
1051
|
+
handler_class = self.PROTOCOLS.get(protocol)
|
|
1052
|
+
if not handler_class:
|
|
1053
|
+
self._write_log(log_path, {"error": f"Unsupported protocol: {protocol}"})
|
|
1054
|
+
return 1
|
|
1055
|
+
|
|
1056
|
+
# Create handler and connect
|
|
1057
|
+
handler = handler_class(
|
|
1058
|
+
target=host,
|
|
1059
|
+
username=username,
|
|
1060
|
+
password=password,
|
|
1061
|
+
port=port,
|
|
1062
|
+
timeout=timeout,
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
if not handler.connect():
|
|
1066
|
+
self._write_log(
|
|
1067
|
+
log_path, {"error": "Connection failed", "errors": handler.errors}
|
|
1068
|
+
)
|
|
1069
|
+
return 1
|
|
1070
|
+
|
|
1071
|
+
# Explore
|
|
1072
|
+
try:
|
|
1073
|
+
files = handler.explore(path or "/", depth=depth)
|
|
1074
|
+
handler.files_found = files
|
|
1075
|
+
|
|
1076
|
+
# Download interesting files if requested
|
|
1077
|
+
if download:
|
|
1078
|
+
evidence_dir = self._get_evidence_dir(host, protocol)
|
|
1079
|
+
files_to_download = (
|
|
1080
|
+
handler.interesting_files if not download_all else files
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
for item in files_to_download:
|
|
1084
|
+
if item.get("type") == "file":
|
|
1085
|
+
remote_path = item.get("full_path", item["name"])
|
|
1086
|
+
local_name = (
|
|
1087
|
+
remote_path.replace("/", "_").replace("\\", "_").lstrip("_")
|
|
1088
|
+
)
|
|
1089
|
+
local_path = os.path.join(evidence_dir, local_name)
|
|
1090
|
+
|
|
1091
|
+
handler.download_file(remote_path, local_path)
|
|
1092
|
+
|
|
1093
|
+
# Write results
|
|
1094
|
+
results = {
|
|
1095
|
+
"protocol": protocol,
|
|
1096
|
+
"target": host,
|
|
1097
|
+
"port": port,
|
|
1098
|
+
"username": username or "anonymous",
|
|
1099
|
+
"files_found": len(files),
|
|
1100
|
+
"interesting_files": len(handler.interesting_files),
|
|
1101
|
+
"downloaded": len(handler.downloaded),
|
|
1102
|
+
"files": files,
|
|
1103
|
+
"interesting": handler.interesting_files,
|
|
1104
|
+
"downloaded_files": handler.downloaded,
|
|
1105
|
+
"errors": handler.errors,
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
self._write_log(log_path, results)
|
|
1109
|
+
|
|
1110
|
+
finally:
|
|
1111
|
+
handler.disconnect()
|
|
1112
|
+
|
|
1113
|
+
# Return success if we found anything
|
|
1114
|
+
if handler.files_found or handler.interesting_files:
|
|
1115
|
+
return 0
|
|
1116
|
+
elif handler.errors:
|
|
1117
|
+
return 1
|
|
1118
|
+
else:
|
|
1119
|
+
return 0
|
|
1120
|
+
|
|
1121
|
+
def _parse_target(
|
|
1122
|
+
self, target: str
|
|
1123
|
+
) -> Optional[Tuple[str, str, int, str, str, str]]:
|
|
1124
|
+
"""Parse target URL into components."""
|
|
1125
|
+
# Handle simple format: protocol://host
|
|
1126
|
+
if "://" not in target:
|
|
1127
|
+
# Assume it's just a host, try to detect protocol from args
|
|
1128
|
+
return None
|
|
1129
|
+
|
|
1130
|
+
try:
|
|
1131
|
+
parsed = urlparse(target)
|
|
1132
|
+
protocol = parsed.scheme.lower()
|
|
1133
|
+
host = parsed.hostname
|
|
1134
|
+
port = parsed.port
|
|
1135
|
+
username = parsed.username
|
|
1136
|
+
password = parsed.password
|
|
1137
|
+
path = parsed.path or "/"
|
|
1138
|
+
|
|
1139
|
+
if not host:
|
|
1140
|
+
return None
|
|
1141
|
+
|
|
1142
|
+
return (protocol, host, port, username, password, path)
|
|
1143
|
+
except Exception:
|
|
1144
|
+
return None
|
|
1145
|
+
|
|
1146
|
+
def _get_evidence_dir(self, host: str, protocol: str) -> str:
|
|
1147
|
+
"""Get or create evidence directory for downloads."""
|
|
1148
|
+
base_dir = Path.home() / ".souleyez" / "evidence" / host / protocol
|
|
1149
|
+
base_dir.mkdir(parents=True, exist_ok=True)
|
|
1150
|
+
return str(base_dir)
|
|
1151
|
+
|
|
1152
|
+
def _write_log(self, log_path: str, data: dict):
|
|
1153
|
+
"""Write results to log file."""
|
|
1154
|
+
if log_path:
|
|
1155
|
+
with open(log_path, "w") as f:
|
|
1156
|
+
json.dump(data, f, indent=2, default=str)
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
# Export the plugin (loader expects lowercase 'plugin')
|
|
1160
|
+
plugin = ServiceExplorerPlugin()
|