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.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +22827 -10678
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {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()