souleyez 2.43.28__py3-none-any.whl → 2.43.32__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 (356) 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 +9592 -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 +1238 -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 +2198 -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 +288 -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/whois_handler.py +277 -0
  126. souleyez/handlers/wpscan_handler.py +554 -0
  127. souleyez/history.py +32 -16
  128. souleyez/importers/msf_importer.py +106 -75
  129. souleyez/importers/smart_importer.py +208 -147
  130. souleyez/integrations/siem/__init__.py +10 -10
  131. souleyez/integrations/siem/base.py +17 -18
  132. souleyez/integrations/siem/elastic.py +108 -122
  133. souleyez/integrations/siem/factory.py +207 -80
  134. souleyez/integrations/siem/googlesecops.py +146 -154
  135. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  136. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  137. souleyez/integrations/siem/sentinel.py +107 -109
  138. souleyez/integrations/siem/splunk.py +246 -212
  139. souleyez/integrations/siem/wazuh.py +65 -71
  140. souleyez/integrations/wazuh/__init__.py +5 -5
  141. souleyez/integrations/wazuh/client.py +70 -93
  142. souleyez/integrations/wazuh/config.py +85 -57
  143. souleyez/integrations/wazuh/host_mapper.py +28 -36
  144. souleyez/integrations/wazuh/sync.py +78 -68
  145. souleyez/intelligence/__init__.py +4 -5
  146. souleyez/intelligence/correlation_analyzer.py +309 -295
  147. souleyez/intelligence/exploit_knowledge.py +661 -623
  148. souleyez/intelligence/exploit_suggestions.py +159 -139
  149. souleyez/intelligence/gap_analyzer.py +132 -97
  150. souleyez/intelligence/gap_detector.py +251 -214
  151. souleyez/intelligence/sensitive_tables.py +266 -129
  152. souleyez/intelligence/service_parser.py +137 -123
  153. souleyez/intelligence/surface_analyzer.py +407 -268
  154. souleyez/intelligence/target_parser.py +159 -162
  155. souleyez/licensing/__init__.py +6 -6
  156. souleyez/licensing/validator.py +17 -19
  157. souleyez/log_config.py +79 -54
  158. souleyez/main.py +1505 -687
  159. souleyez/migrations/fix_job_counter.py +16 -14
  160. souleyez/parsers/bloodhound_parser.py +41 -39
  161. souleyez/parsers/crackmapexec_parser.py +178 -111
  162. souleyez/parsers/dalfox_parser.py +72 -77
  163. souleyez/parsers/dnsrecon_parser.py +103 -91
  164. souleyez/parsers/enum4linux_parser.py +183 -153
  165. souleyez/parsers/ffuf_parser.py +29 -25
  166. souleyez/parsers/gobuster_parser.py +301 -41
  167. souleyez/parsers/hashcat_parser.py +324 -79
  168. souleyez/parsers/http_fingerprint_parser.py +350 -103
  169. souleyez/parsers/hydra_parser.py +131 -111
  170. souleyez/parsers/impacket_parser.py +231 -178
  171. souleyez/parsers/john_parser.py +98 -86
  172. souleyez/parsers/katana_parser.py +316 -0
  173. souleyez/parsers/msf_parser.py +943 -498
  174. souleyez/parsers/nikto_parser.py +346 -65
  175. souleyez/parsers/nmap_parser.py +262 -174
  176. souleyez/parsers/nuclei_parser.py +40 -44
  177. souleyez/parsers/responder_parser.py +26 -26
  178. souleyez/parsers/searchsploit_parser.py +74 -74
  179. souleyez/parsers/service_explorer_parser.py +279 -0
  180. souleyez/parsers/smbmap_parser.py +180 -124
  181. souleyez/parsers/sqlmap_parser.py +434 -308
  182. souleyez/parsers/theharvester_parser.py +75 -57
  183. souleyez/parsers/whois_parser.py +135 -94
  184. souleyez/parsers/wpscan_parser.py +278 -190
  185. souleyez/plugins/afp.py +44 -36
  186. souleyez/plugins/afp_brute.py +114 -46
  187. souleyez/plugins/ard.py +48 -37
  188. souleyez/plugins/bloodhound.py +95 -61
  189. souleyez/plugins/certipy.py +303 -0
  190. souleyez/plugins/crackmapexec.py +186 -85
  191. souleyez/plugins/dalfox.py +120 -59
  192. souleyez/plugins/dns_hijack.py +146 -41
  193. souleyez/plugins/dnsrecon.py +97 -61
  194. souleyez/plugins/enum4linux.py +91 -66
  195. souleyez/plugins/evil_winrm.py +291 -0
  196. souleyez/plugins/ffuf.py +166 -90
  197. souleyez/plugins/firmware_extract.py +133 -29
  198. souleyez/plugins/gobuster.py +387 -190
  199. souleyez/plugins/gpp_extract.py +393 -0
  200. souleyez/plugins/hashcat.py +100 -73
  201. souleyez/plugins/http_fingerprint.py +854 -267
  202. souleyez/plugins/hydra.py +566 -200
  203. souleyez/plugins/impacket_getnpusers.py +117 -69
  204. souleyez/plugins/impacket_psexec.py +84 -64
  205. souleyez/plugins/impacket_secretsdump.py +103 -69
  206. souleyez/plugins/impacket_smbclient.py +89 -75
  207. souleyez/plugins/john.py +86 -69
  208. souleyez/plugins/katana.py +313 -0
  209. souleyez/plugins/kerbrute.py +237 -0
  210. souleyez/plugins/lfi_extract.py +541 -0
  211. souleyez/plugins/macos_ssh.py +117 -48
  212. souleyez/plugins/mdns.py +35 -30
  213. souleyez/plugins/msf_auxiliary.py +253 -130
  214. souleyez/plugins/msf_exploit.py +239 -161
  215. souleyez/plugins/nikto.py +134 -78
  216. souleyez/plugins/nmap.py +275 -91
  217. souleyez/plugins/nuclei.py +180 -89
  218. souleyez/plugins/nxc.py +285 -0
  219. souleyez/plugins/plugin_base.py +35 -36
  220. souleyez/plugins/plugin_template.py +13 -5
  221. souleyez/plugins/rdp_sec_check.py +130 -0
  222. souleyez/plugins/responder.py +112 -71
  223. souleyez/plugins/router_http_brute.py +76 -65
  224. souleyez/plugins/router_ssh_brute.py +118 -41
  225. souleyez/plugins/router_telnet_brute.py +124 -42
  226. souleyez/plugins/routersploit.py +91 -59
  227. souleyez/plugins/routersploit_exploit.py +77 -55
  228. souleyez/plugins/searchsploit.py +91 -77
  229. souleyez/plugins/service_explorer.py +1160 -0
  230. souleyez/plugins/smbmap.py +122 -72
  231. souleyez/plugins/smbpasswd.py +215 -0
  232. souleyez/plugins/sqlmap.py +301 -113
  233. souleyez/plugins/theharvester.py +127 -75
  234. souleyez/plugins/tr069.py +79 -57
  235. souleyez/plugins/upnp.py +65 -47
  236. souleyez/plugins/upnp_abuse.py +73 -55
  237. souleyez/plugins/vnc_access.py +129 -42
  238. souleyez/plugins/vnc_brute.py +109 -38
  239. souleyez/plugins/whois.py +77 -58
  240. souleyez/plugins/wpscan.py +173 -69
  241. souleyez/reporting/__init__.py +2 -1
  242. souleyez/reporting/attack_chain.py +411 -346
  243. souleyez/reporting/charts.py +436 -501
  244. souleyez/reporting/compliance_mappings.py +334 -201
  245. souleyez/reporting/detection_report.py +126 -125
  246. souleyez/reporting/formatters.py +828 -591
  247. souleyez/reporting/generator.py +386 -302
  248. souleyez/reporting/metrics.py +72 -75
  249. souleyez/scanner.py +35 -29
  250. souleyez/security/__init__.py +37 -11
  251. souleyez/security/scope_validator.py +175 -106
  252. souleyez/security/validation.py +223 -149
  253. souleyez/security.py +22 -6
  254. souleyez/storage/credentials.py +247 -186
  255. souleyez/storage/crypto.py +296 -129
  256. souleyez/storage/database.py +73 -50
  257. souleyez/storage/db.py +58 -36
  258. souleyez/storage/deliverable_evidence.py +177 -128
  259. souleyez/storage/deliverable_exporter.py +282 -246
  260. souleyez/storage/deliverable_templates.py +134 -116
  261. souleyez/storage/deliverables.py +135 -130
  262. souleyez/storage/engagements.py +109 -56
  263. souleyez/storage/evidence.py +181 -152
  264. souleyez/storage/execution_log.py +31 -17
  265. souleyez/storage/exploit_attempts.py +93 -57
  266. souleyez/storage/exploits.py +67 -36
  267. souleyez/storage/findings.py +48 -61
  268. souleyez/storage/hosts.py +176 -144
  269. souleyez/storage/migrate_to_engagements.py +43 -19
  270. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  271. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  272. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  273. souleyez/storage/migrations/_005_screenshots.py +13 -5
  274. souleyez/storage/migrations/_006_deliverables.py +13 -5
  275. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  276. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  277. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  278. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  279. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  280. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  281. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  282. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  283. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  284. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  285. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  286. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  287. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  288. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  289. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  290. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  291. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  292. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  293. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  294. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  295. souleyez/storage/migrations/__init__.py +26 -26
  296. souleyez/storage/migrations/migration_manager.py +19 -19
  297. souleyez/storage/msf_sessions.py +100 -65
  298. souleyez/storage/osint.py +17 -24
  299. souleyez/storage/recommendation_engine.py +269 -235
  300. souleyez/storage/screenshots.py +33 -32
  301. souleyez/storage/smb_shares.py +136 -92
  302. souleyez/storage/sqlmap_data.py +183 -128
  303. souleyez/storage/team_collaboration.py +135 -141
  304. souleyez/storage/timeline_tracker.py +122 -94
  305. souleyez/storage/wazuh_vulns.py +64 -66
  306. souleyez/storage/web_paths.py +33 -37
  307. souleyez/testing/credential_tester.py +221 -205
  308. souleyez/ui/__init__.py +1 -1
  309. souleyez/ui/ai_quotes.py +12 -12
  310. souleyez/ui/attack_surface.py +2439 -1516
  311. souleyez/ui/chain_rules_view.py +914 -382
  312. souleyez/ui/correlation_view.py +312 -230
  313. souleyez/ui/dashboard.py +2382 -1130
  314. souleyez/ui/deliverables_view.py +148 -62
  315. souleyez/ui/design_system.py +13 -13
  316. souleyez/ui/errors.py +49 -49
  317. souleyez/ui/evidence_linking_view.py +284 -179
  318. souleyez/ui/evidence_vault.py +393 -285
  319. souleyez/ui/exploit_suggestions_view.py +555 -349
  320. souleyez/ui/export_view.py +100 -66
  321. souleyez/ui/gap_analysis_view.py +315 -171
  322. souleyez/ui/help_system.py +105 -97
  323. souleyez/ui/intelligence_view.py +436 -293
  324. souleyez/ui/interactive.py +23142 -10430
  325. souleyez/ui/interactive_selector.py +75 -68
  326. souleyez/ui/log_formatter.py +47 -39
  327. souleyez/ui/menu_components.py +22 -13
  328. souleyez/ui/msf_auxiliary_menu.py +184 -133
  329. souleyez/ui/pending_chains_view.py +336 -172
  330. souleyez/ui/progress_indicators.py +5 -3
  331. souleyez/ui/recommendations_view.py +195 -137
  332. souleyez/ui/rule_builder.py +343 -225
  333. souleyez/ui/setup_wizard.py +678 -284
  334. souleyez/ui/shortcuts.py +217 -165
  335. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  336. souleyez/ui/splunk_vulns_view.py +139 -86
  337. souleyez/ui/team_dashboard.py +498 -335
  338. souleyez/ui/template_selector.py +196 -105
  339. souleyez/ui/terminal.py +6 -6
  340. souleyez/ui/timeline_view.py +198 -127
  341. souleyez/ui/tool_setup.py +264 -164
  342. souleyez/ui/tutorial.py +202 -72
  343. souleyez/ui/tutorial_state.py +40 -40
  344. souleyez/ui/wazuh_vulns_view.py +235 -141
  345. souleyez/ui/wordlist_browser.py +260 -107
  346. souleyez/ui.py +464 -312
  347. souleyez/utils/tool_checker.py +427 -367
  348. souleyez/utils.py +33 -29
  349. souleyez/wordlists.py +134 -167
  350. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/METADATA +1 -1
  351. souleyez-2.43.32.dist-info/RECORD +441 -0
  352. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/WHEEL +1 -1
  353. souleyez-2.43.28.dist-info/RECORD +0 -379
  354. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/entry_points.txt +0 -0
  355. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/licenses/LICENSE +0 -0
  356. {souleyez-2.43.28.dist-info → souleyez-2.43.32.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,815 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ffuf handler.
4
+
5
+ Consolidates parsing and display logic for ffuf fuzzing jobs.
6
+ """
7
+ import logging
8
+ import os
9
+ import re
10
+ from typing import Any, Dict, List, Optional
11
+ from urllib.parse import urlparse
12
+
13
+ import click
14
+
15
+ from souleyez.engine.job_status import (
16
+ STATUS_DONE,
17
+ STATUS_ERROR,
18
+ STATUS_NO_RESULTS,
19
+ STATUS_WARNING,
20
+ )
21
+ from souleyez.handlers.base import BaseToolHandler
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class FfufHandler(BaseToolHandler):
27
+ """Handler for ffuf fuzzing jobs."""
28
+
29
+ tool_name = "ffuf"
30
+ display_name = "Ffuf"
31
+
32
+ # All handlers enabled
33
+ has_error_handler = True
34
+ has_warning_handler = True
35
+ has_no_results_handler = True
36
+ has_done_handler = True
37
+
38
+ # Security concern patterns (shared with gobuster)
39
+ SECURITY_CONCERN_PATTERNS = {
40
+ "lfi_php_wrapper": {
41
+ "patterns": [r"php://filter", r"php://input", r"data://", r"expect://"],
42
+ "label": "LFI via PHP wrapper (SOURCE CODE EXPOSURE)",
43
+ "severity": "critical",
44
+ },
45
+ "lfi_etc_passwd": {
46
+ "patterns": [r"/etc/passwd", r"/etc/shadow", r"/etc/group"],
47
+ "label": "LFI - System file readable",
48
+ "severity": "critical",
49
+ },
50
+ "lfi_traversal": {
51
+ "patterns": [
52
+ r"\.\./.*passwd",
53
+ r"\.\./.*shadow",
54
+ r"\.\./.*config",
55
+ r"\.\.[\\/]",
56
+ ],
57
+ "label": "LFI via directory traversal",
58
+ "severity": "critical",
59
+ },
60
+ "lfi_log_files": {
61
+ "patterns": [
62
+ r"/var/log/",
63
+ r"/var/mail/",
64
+ r"/proc/self/",
65
+ r"access\.log",
66
+ r"error\.log",
67
+ r"auth\.log",
68
+ ],
69
+ "label": "LFI - Log/proc file readable",
70
+ "severity": "high",
71
+ },
72
+ "lfi_ssh_keys": {
73
+ "patterns": [r"\.ssh/id_rsa", r"\.ssh/authorized_keys", r"\.bash_history"],
74
+ "label": "LFI - SSH keys/history exposed",
75
+ "severity": "critical",
76
+ },
77
+ "database_files": {
78
+ "patterns": [
79
+ r"\.sql$",
80
+ r"\.db$",
81
+ r"\.mdb$",
82
+ r"\.sqlite",
83
+ r"/db\.",
84
+ r"/database\.",
85
+ r"\.bak\.sql",
86
+ r"database\.yml",
87
+ ],
88
+ "label": "Database file exposed",
89
+ "severity": "high",
90
+ },
91
+ "backup_files": {
92
+ "patterns": [
93
+ r"\.bak$",
94
+ r"\.old$",
95
+ r"\.backup$",
96
+ r"\.orig$",
97
+ r"\.save$",
98
+ r"\.swp$",
99
+ r"~$",
100
+ r"\.zip$",
101
+ r"\.tar",
102
+ r"\.gz$",
103
+ r"\.rar$",
104
+ ],
105
+ "label": "Backup/archive file",
106
+ "severity": "high",
107
+ },
108
+ "config_files": {
109
+ "patterns": [
110
+ r"web\.config",
111
+ r"\.htaccess",
112
+ r"\.htpasswd",
113
+ r"\.env$",
114
+ r"config\.php",
115
+ r"config\.inc",
116
+ r"settings\.py",
117
+ r"\.ini$",
118
+ r"\.conf$",
119
+ r"\.cfg$",
120
+ r"wp-config\.php",
121
+ ],
122
+ "label": "Configuration file exposed",
123
+ "severity": "high",
124
+ },
125
+ "source_files": {
126
+ "patterns": [
127
+ r"\.git(/|$)",
128
+ r"\.svn(/|$)",
129
+ r"\.DS_Store",
130
+ r"\.vscode(/|$)",
131
+ r"\.idea(/|$)",
132
+ r"Thumbs\.db",
133
+ r"\.log$",
134
+ r"debug\.",
135
+ r"test\.php",
136
+ r"phpinfo",
137
+ ],
138
+ "label": "Development/debug file",
139
+ "severity": "medium",
140
+ },
141
+ "legacy_dirs": {
142
+ "patterns": [
143
+ r"_vti_",
144
+ r"/cgi-bin(/|$)",
145
+ r"/cgi(/|$)",
146
+ r"/fcgi(/|$)",
147
+ r"/admin(/|$)",
148
+ r"/administrator(/|$)",
149
+ r"/phpmyadmin(/|$)",
150
+ r"/pma(/|$)",
151
+ r"/myadmin(/|$)",
152
+ ],
153
+ "label": "Legacy/admin directory",
154
+ "severity": "medium",
155
+ },
156
+ "sensitive_endpoints": {
157
+ "patterns": [
158
+ r"/upload(/|$)",
159
+ r"/uploads(/|$)",
160
+ r"/file(/|$)",
161
+ r"/files(/|$)",
162
+ r"/tmp(/|$)",
163
+ r"/temp(/|$)",
164
+ r"/private(/|$)",
165
+ r"/internal(/|$)",
166
+ r"/api(/|$)",
167
+ r"/bank(/|$)",
168
+ ],
169
+ "label": "Potentially sensitive directory",
170
+ "severity": "low",
171
+ },
172
+ }
173
+
174
+ def parse_job(
175
+ self,
176
+ engagement_id: int,
177
+ log_path: str,
178
+ job: Dict[str, Any],
179
+ host_manager: Optional[Any] = None,
180
+ findings_manager: Optional[Any] = None,
181
+ credentials_manager: Optional[Any] = None,
182
+ ) -> Dict[str, Any]:
183
+ """
184
+ Parse ffuf job results.
185
+
186
+ Extracts discovered paths and stores them in the database.
187
+ """
188
+ try:
189
+ from souleyez.parsers.ffuf_parser import parse_ffuf
190
+ from souleyez.engine.result_handler import detect_tool_error
191
+
192
+ # Import managers if not provided
193
+ if host_manager is None:
194
+ from souleyez.storage.hosts import HostManager
195
+
196
+ host_manager = HostManager()
197
+ if findings_manager is None:
198
+ from souleyez.storage.findings import FindingsManager
199
+
200
+ findings_manager = FindingsManager()
201
+
202
+ target = job.get("target", "")
203
+ parsed = parse_ffuf(log_path, target)
204
+
205
+ if "error" in parsed:
206
+ return parsed
207
+
208
+ # Extract base target for host tracking
209
+ parsed_url = urlparse(target.replace("FUZZ", ""))
210
+ target_host = parsed_url.hostname or target
211
+
212
+ host_id = None
213
+ if target_host:
214
+ result = host_manager.add_or_update_host(
215
+ engagement_id, {"ip": target_host, "status": "up"}
216
+ )
217
+ if isinstance(result, dict):
218
+ host_id = result.get("id")
219
+ else:
220
+ host_id = result
221
+
222
+ # Store web paths
223
+ paths_added = 0
224
+ created_findings = []
225
+
226
+ if host_id and parsed.get("paths"):
227
+ try:
228
+ from souleyez.storage.web_paths import WebPathsManager
229
+
230
+ wpm = WebPathsManager()
231
+ paths_added = wpm.bulk_add_web_paths(host_id, parsed["paths"])
232
+ except Exception as e:
233
+ logger.warning(f"Failed to store web paths: {e}")
234
+
235
+ # Create findings for sensitive paths
236
+ created_findings = self._create_findings_for_sensitive_paths(
237
+ engagement_id, host_id, parsed["paths"], findings_manager
238
+ )
239
+
240
+ # Auto-extract credentials from PHP filter LFI results
241
+ if credentials_manager is None:
242
+ from souleyez.storage.credentials import CredentialsManager
243
+
244
+ credentials_manager = CredentialsManager()
245
+
246
+ extracted_creds = self._extract_lfi_credentials(
247
+ engagement_id,
248
+ host_id,
249
+ parsed["paths"],
250
+ credentials_manager,
251
+ findings_manager,
252
+ )
253
+ if extracted_creds:
254
+ logger.info(
255
+ f"LFI auto-extraction: Found {len(extracted_creds)} credential(s)"
256
+ )
257
+
258
+ # Check for ffuf errors
259
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
260
+ log_content = f.read()
261
+ ffuf_error = detect_tool_error(log_content, "ffuf")
262
+
263
+ # Determine status
264
+ if ffuf_error:
265
+ status = STATUS_ERROR
266
+ elif parsed.get("results_found", 0) > 0:
267
+ status = STATUS_DONE
268
+ else:
269
+ status = STATUS_NO_RESULTS
270
+
271
+ # Build summary for job queue display
272
+ summary_parts = []
273
+ results_found = parsed.get("results_found", 0)
274
+ if results_found > 0:
275
+ summary_parts.append(f"{results_found} result(s)")
276
+ if len(created_findings) > 0:
277
+ summary_parts.append(f"{len(created_findings)} finding(s)")
278
+ summary = " | ".join(summary_parts) if summary_parts else "No results"
279
+
280
+ return {
281
+ "tool": "ffuf",
282
+ "status": status,
283
+ "summary": summary,
284
+ "target": target,
285
+ "results_found": parsed.get("results_found", 0),
286
+ "paths_added": paths_added,
287
+ "findings_added": len(created_findings),
288
+ "method": parsed.get("method"),
289
+ "parameters_found": parsed.get("paths", []),
290
+ }
291
+
292
+ except Exception as e:
293
+ logger.error(f"Error parsing ffuf job: {e}")
294
+ return {"error": str(e)}
295
+
296
+ def _create_findings_for_sensitive_paths(
297
+ self, engagement_id: int, host_id: int, paths: List[Dict], findings_manager: Any
298
+ ) -> List[Dict]:
299
+ """Create findings for sensitive/interesting paths discovered."""
300
+ created_findings = []
301
+
302
+ for path_entry in paths:
303
+ url = path_entry.get("url", "").lower()
304
+ for concern_type, concern_info in self.SECURITY_CONCERN_PATTERNS.items():
305
+ for pattern in concern_info["patterns"]:
306
+ if re.search(pattern, url, re.IGNORECASE):
307
+ try:
308
+ findings_manager.add_finding(
309
+ engagement_id=engagement_id,
310
+ host_id=host_id,
311
+ title=f"{concern_info['label']}: {url.split('/')[-1]}",
312
+ finding_type="web_path",
313
+ severity=concern_info["severity"],
314
+ description=f"Ffuf discovered a potentially sensitive path: {path_entry.get('url', '')}\n"
315
+ f"Status code: {path_entry.get('status_code', 'unknown')}\n"
316
+ f"Category: {concern_info['label']}",
317
+ tool="ffuf",
318
+ )
319
+ created_findings.append(
320
+ {
321
+ "url": path_entry.get("url"),
322
+ "type": concern_type,
323
+ "severity": concern_info["severity"],
324
+ }
325
+ )
326
+ except Exception as e:
327
+ logger.warning(f"Failed to create finding: {e}")
328
+ break
329
+
330
+ return created_findings
331
+
332
+ def _extract_lfi_credentials(
333
+ self,
334
+ engagement_id: int,
335
+ host_id: int,
336
+ paths: List[Dict],
337
+ credentials_manager: Any,
338
+ findings_manager: Any,
339
+ ) -> List[Dict]:
340
+ """
341
+ Auto-extract credentials from successful PHP filter LFI results.
342
+
343
+ When php://filter/convert.base64-encode URLs are found, fetch them,
344
+ decode the base64, and parse for credentials.
345
+ """
346
+ import base64
347
+ import requests
348
+
349
+ extracted_creds = []
350
+
351
+ # Find PHP filter URLs (config files are most valuable)
352
+ php_filter_urls = []
353
+ for path_entry in paths:
354
+ url = path_entry.get("url", "")
355
+ if "php://filter" in url and "base64-encode" in url:
356
+ # Prioritize config files
357
+ if any(
358
+ kw in url.lower()
359
+ for kw in ["config", "database", "settings", "db", "connect"]
360
+ ):
361
+ php_filter_urls.insert(0, url) # High priority
362
+ else:
363
+ php_filter_urls.append(url)
364
+
365
+ if not php_filter_urls:
366
+ return []
367
+
368
+ logger.info(
369
+ f"LFI auto-extraction: Found {len(php_filter_urls)} PHP filter URL(s)"
370
+ )
371
+
372
+ # Credential patterns to search for in decoded PHP
373
+ cred_patterns = [
374
+ # PHP variable assignments
375
+ (r'\$(?:password|passwd|pass|pwd)\s*=\s*["\']([^"\']+)["\']', "password"),
376
+ (r'\$(?:username|user|usr|login)\s*=\s*["\']([^"\']+)["\']', "username"),
377
+ (r'\$(?:database|db|dbname|db_name)\s*=\s*["\']([^"\']+)["\']', "database"),
378
+ (r'\$(?:server|host|hostname|db_host)\s*=\s*["\']([^"\']+)["\']', "host"),
379
+ # Array-style configs
380
+ (r'["\']password["\']\s*=>\s*["\']([^"\']+)["\']', "password"),
381
+ (r'["\']username["\']\s*=>\s*["\']([^"\']+)["\']', "username"),
382
+ (r'["\']database["\']\s*=>\s*["\']([^"\']+)["\']', "database"),
383
+ (r'["\']host["\']\s*=>\s*["\']([^"\']+)["\']', "host"),
384
+ # Define constants
385
+ (
386
+ r"define\s*\(\s*['\"]DB_PASSWORD['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)",
387
+ "password",
388
+ ),
389
+ (
390
+ r"define\s*\(\s*['\"]DB_USER['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)",
391
+ "username",
392
+ ),
393
+ (
394
+ r"define\s*\(\s*['\"]DB_NAME['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)",
395
+ "database",
396
+ ),
397
+ (r"define\s*\(\s*['\"]DB_HOST['\"]\s*,\s*['\"]([^'\"]+)['\"]\s*\)", "host"),
398
+ ]
399
+
400
+ # Try to fetch and parse each URL (limit to prevent abuse)
401
+ max_fetch = 5
402
+ for url in php_filter_urls[:max_fetch]:
403
+ try:
404
+ logger.info(f" Fetching: {url}")
405
+ resp = requests.get(
406
+ url, timeout=10, verify=False
407
+ ) # nosec B501 - pentesting tool
408
+
409
+ if resp.status_code != 200:
410
+ continue
411
+
412
+ # Extract base64 from response (usually in HTML body)
413
+ html = resp.text
414
+
415
+ # Look for base64 content - usually a long string of alphanumeric + /+=
416
+ base64_pattern = r"([A-Za-z0-9+/]{50,}={0,2})"
417
+ matches = re.findall(base64_pattern, html)
418
+
419
+ for b64_match in matches:
420
+ try:
421
+ decoded = base64.b64decode(b64_match).decode(
422
+ "utf-8", errors="ignore"
423
+ )
424
+
425
+ # Skip if it doesn't look like PHP
426
+ if "<?php" not in decoded and "$" not in decoded:
427
+ continue
428
+
429
+ logger.info(f" Decoded PHP source ({len(decoded)} bytes)")
430
+
431
+ # Extract credentials
432
+ creds = {"source": url, "source_file": "config.php"}
433
+ for pattern, field in cred_patterns:
434
+ match = re.search(pattern, decoded, re.IGNORECASE)
435
+ if match:
436
+ creds[field] = match.group(1)
437
+
438
+ # If we found at least username and password, store it
439
+ if creds.get("username") and creds.get("password"):
440
+ logger.info(
441
+ f" Found credentials: {creds.get('username')}:***"
442
+ )
443
+
444
+ # Store in credentials manager
445
+ try:
446
+ credentials_manager.add_credential(
447
+ engagement_id=engagement_id,
448
+ host_id=host_id,
449
+ username=creds.get("username"),
450
+ password=creds.get("password"),
451
+ credential_type="database",
452
+ service=creds.get("database", "mysql"),
453
+ source="LFI auto-extraction",
454
+ notes=f"Extracted from {url}\nDatabase: {creds.get('database', 'unknown')}\nHost: {creds.get('host', 'localhost')}",
455
+ )
456
+ extracted_creds.append(creds)
457
+ except Exception as e:
458
+ logger.warning(f"Failed to store credential: {e}")
459
+
460
+ # Also create a critical finding
461
+ try:
462
+ findings_manager.add_finding(
463
+ engagement_id=engagement_id,
464
+ host_id=host_id,
465
+ title=f"LFI Credential Extraction: {creds.get('username')}@{creds.get('database', 'database')}",
466
+ finding_type="credential",
467
+ severity="critical",
468
+ description=f"Credentials automatically extracted via LFI:\n\n"
469
+ f"Username: {creds.get('username')}\n"
470
+ f"Database: {creds.get('database', 'unknown')}\n"
471
+ f"Host: {creds.get('host', 'localhost')}\n\n"
472
+ f"Source URL: {url}\n\n"
473
+ f"This indicates a critical LFI vulnerability that exposes database credentials.",
474
+ tool="ffuf",
475
+ )
476
+ except Exception as e:
477
+ logger.warning(f"Failed to create finding: {e}")
478
+
479
+ break # Found creds in this URL, move to next
480
+
481
+ except Exception as e:
482
+ logger.debug(f"Failed to decode base64: {e}")
483
+ continue
484
+
485
+ except requests.RequestException as e:
486
+ logger.warning(f"Failed to fetch {url}: {e}")
487
+ continue
488
+ except Exception as e:
489
+ logger.warning(f"Error processing {url}: {e}")
490
+ continue
491
+
492
+ return extracted_creds
493
+
494
+ def _identify_security_concerns(self, paths: List[Dict]) -> List[Dict]:
495
+ """Identify security concerns in discovered paths."""
496
+ concerns = []
497
+
498
+ for path_entry in paths:
499
+ url = path_entry.get("url", "").lower()
500
+ for concern_type, concern_info in self.SECURITY_CONCERN_PATTERNS.items():
501
+ for pattern in concern_info["patterns"]:
502
+ if re.search(pattern, url, re.IGNORECASE):
503
+ concerns.append(
504
+ {
505
+ "url": path_entry.get("url", ""),
506
+ "type": concern_type,
507
+ "label": concern_info["label"],
508
+ "severity": concern_info["severity"],
509
+ "status_code": path_entry.get("status_code", "unknown"),
510
+ }
511
+ )
512
+ break
513
+
514
+ return concerns
515
+
516
+ def display_done(
517
+ self,
518
+ job: Dict[str, Any],
519
+ log_path: str,
520
+ show_all: bool = False,
521
+ show_passwords: bool = False,
522
+ ) -> None:
523
+ """Display successful ffuf scan results."""
524
+ try:
525
+ from souleyez.parsers.ffuf_parser import parse_ffuf
526
+
527
+ if not log_path or not os.path.exists(log_path):
528
+ return
529
+
530
+ parsed = parse_ffuf(log_path, job.get("target", ""))
531
+ paths = parsed.get("paths", [])
532
+
533
+ if not paths:
534
+ self.display_no_results(job, log_path)
535
+ return
536
+
537
+ # Identify security concerns
538
+ security_concerns = self._identify_security_concerns(paths)
539
+
540
+ # Display security concerns if found
541
+ if security_concerns:
542
+ click.echo(click.style("=" * 70, fg="red"))
543
+ click.echo(click.style("SECURITY CONCERNS", bold=True, fg="red"))
544
+ click.echo(click.style("=" * 70, fg="red"))
545
+ click.echo()
546
+
547
+ critical_concerns = [
548
+ c for c in security_concerns if c["severity"] == "critical"
549
+ ]
550
+ high_concerns = [
551
+ c for c in security_concerns if c["severity"] == "high"
552
+ ]
553
+ medium_concerns = [
554
+ c for c in security_concerns if c["severity"] == "medium"
555
+ ]
556
+ low_concerns = [c for c in security_concerns if c["severity"] == "low"]
557
+
558
+ if critical_concerns:
559
+ click.echo(
560
+ click.style(
561
+ "[CRITICAL] LFI VULNERABILITY CONFIRMED:",
562
+ fg="red",
563
+ bold=True,
564
+ blink=True,
565
+ )
566
+ )
567
+ by_label = {}
568
+ for c in critical_concerns:
569
+ if c["label"] not in by_label:
570
+ by_label[c["label"]] = []
571
+ by_label[c["label"]].append(c["url"])
572
+ for label, urls in by_label.items():
573
+ click.echo(click.style(f" - {label}:", fg="red"))
574
+ for url in urls[:5]:
575
+ click.echo(f" {url}")
576
+ if len(urls) > 5:
577
+ click.echo(f" ... and {len(urls) - 5} more")
578
+ click.echo()
579
+
580
+ if high_concerns:
581
+ click.echo(
582
+ click.style("[HIGH] Critical findings:", fg="red", bold=True)
583
+ )
584
+ by_label = {}
585
+ for c in high_concerns:
586
+ if c["label"] not in by_label:
587
+ by_label[c["label"]] = []
588
+ by_label[c["label"]].append(c["url"])
589
+ for label, urls in by_label.items():
590
+ click.echo(click.style(f" - {label}:", fg="red"))
591
+ for url in urls[:5]:
592
+ click.echo(f" {url}")
593
+ if len(urls) > 5:
594
+ click.echo(f" ... and {len(urls) - 5} more")
595
+ click.echo()
596
+
597
+ if medium_concerns:
598
+ click.echo(
599
+ click.style(
600
+ "[MEDIUM] Notable findings:", fg="yellow", bold=True
601
+ )
602
+ )
603
+ by_label = {}
604
+ for c in medium_concerns:
605
+ if c["label"] not in by_label:
606
+ by_label[c["label"]] = []
607
+ by_label[c["label"]].append(c["url"])
608
+ for label, urls in by_label.items():
609
+ click.echo(click.style(f" - {label}:", fg="yellow"))
610
+ for url in urls[:5]:
611
+ click.echo(f" {url}")
612
+ if len(urls) > 5:
613
+ click.echo(f" ... and {len(urls) - 5} more")
614
+ click.echo()
615
+
616
+ if low_concerns:
617
+ click.echo(
618
+ click.style("[LOW] Worth investigating:", fg="cyan", bold=True)
619
+ )
620
+ by_label = {}
621
+ for c in low_concerns:
622
+ if c["label"] not in by_label:
623
+ by_label[c["label"]] = []
624
+ by_label[c["label"]].append(c["url"])
625
+ for label, urls in by_label.items():
626
+ click.echo(f" - {label}: {len(urls)} path(s)")
627
+ click.echo()
628
+
629
+ # Display discovered paths
630
+ click.echo(click.style("=" * 70, fg="cyan"))
631
+ click.echo(click.style("FFUF DISCOVERED PATHS", bold=True, fg="cyan"))
632
+ click.echo(click.style("=" * 70, fg="cyan"))
633
+ click.echo()
634
+ click.echo(f"Total found: {len(paths)}")
635
+ if parsed.get("method"):
636
+ click.echo(f"Method: {parsed.get('method')}")
637
+ click.echo()
638
+
639
+ # Group by status code
640
+ status_groups = {}
641
+ for path in paths:
642
+ status = path.get("status_code", "unknown")
643
+ if status not in status_groups:
644
+ status_groups[status] = []
645
+ status_groups[status].append(path)
646
+
647
+ # Display by status code
648
+ for status in sorted(status_groups.keys()):
649
+ status_color = (
650
+ "green"
651
+ if status == 200
652
+ else "cyan" if status in [301, 302] else "yellow"
653
+ )
654
+ click.echo(
655
+ click.style(
656
+ f"[{status}] ({len(status_groups[status])} paths)",
657
+ bold=True,
658
+ fg=status_color,
659
+ )
660
+ )
661
+
662
+ paths_to_show = (
663
+ status_groups[status] if show_all else status_groups[status][:10]
664
+ )
665
+
666
+ for path in paths_to_show:
667
+ url = path.get("url", "")
668
+ size = path.get("size", "")
669
+ redirect = path.get("redirect", "")
670
+
671
+ if redirect:
672
+ click.echo(f" {url} -> {redirect}")
673
+ elif size:
674
+ click.echo(f" {url} ({size} bytes)")
675
+ else:
676
+ click.echo(f" {url}")
677
+
678
+ if not show_all and len(status_groups[status]) > 10:
679
+ click.echo(f" ... and {len(status_groups[status]) - 10} more")
680
+ click.echo()
681
+
682
+ click.echo(click.style("=" * 70, fg="cyan"))
683
+ click.echo()
684
+
685
+ except Exception as e:
686
+ logger.debug(f"Error in display_done: {e}")
687
+
688
+ def display_warning(
689
+ self,
690
+ job: Dict[str, Any],
691
+ log_path: str,
692
+ log_content: Optional[str] = None,
693
+ ) -> None:
694
+ """Display warning status for ffuf scan."""
695
+ click.echo(click.style("=" * 70, fg="yellow"))
696
+ click.echo(click.style("[WARNING] FFUF SCAN", bold=True, fg="yellow"))
697
+ click.echo(click.style("=" * 70, fg="yellow"))
698
+ click.echo()
699
+ click.echo(" Scan completed with warnings. Check raw logs for details.")
700
+ click.echo(" Press [r] to view raw logs.")
701
+ click.echo()
702
+ click.echo(click.style("=" * 70, fg="yellow"))
703
+ click.echo()
704
+
705
+ def display_error(
706
+ self,
707
+ job: Dict[str, Any],
708
+ log_path: str,
709
+ log_content: Optional[str] = None,
710
+ ) -> None:
711
+ """Display error status for ffuf scan."""
712
+ # Read log if not provided
713
+ if log_content is None and log_path and os.path.exists(log_path):
714
+ try:
715
+ with open(log_path, "r", encoding="utf-8", errors="replace") as f:
716
+ log_content = f.read()
717
+ except Exception:
718
+ log_content = ""
719
+
720
+ click.echo(click.style("=" * 70, fg="red"))
721
+ click.echo(click.style("[ERROR] FFUF SCAN FAILED", bold=True, fg="red"))
722
+ click.echo(click.style("=" * 70, fg="red"))
723
+ click.echo()
724
+
725
+ # Check if it was a timeout
726
+ if log_content and (
727
+ "timed out" in log_content.lower() or "Command timed out" in log_content
728
+ ):
729
+ click.echo(" Scan reached timeout before completing.")
730
+ click.echo()
731
+ click.echo(click.style(" Possible causes:", fg="bright_black"))
732
+ click.echo(
733
+ click.style(" - Target is rate limiting requests", fg="bright_black")
734
+ )
735
+ click.echo(
736
+ click.style(
737
+ " - Wordlist too large for timeout window", fg="bright_black"
738
+ )
739
+ )
740
+ click.echo(click.style(" - Network latency issues", fg="bright_black"))
741
+ click.echo()
742
+ click.echo(click.style(" Suggestions:", fg="bright_black"))
743
+ click.echo(click.style(" - Try smaller wordlist", fg="bright_black"))
744
+ click.echo(
745
+ click.style(
746
+ " - Increase -p (delay) between requests", fg="bright_black"
747
+ )
748
+ )
749
+ click.echo(click.style(" - Reduce -t (threads)", fg="bright_black"))
750
+ else:
751
+ error_msg = None
752
+ if log_content and "ERROR:" in log_content:
753
+ match = re.search(r"ERROR:\s*(.+?)(?:\n|$)", log_content)
754
+ if match:
755
+ error_msg = match.group(1).strip()
756
+
757
+ if error_msg:
758
+ click.echo(f" Error: {error_msg}")
759
+ else:
760
+ click.echo(" Scan failed - see raw logs for details.")
761
+ click.echo(" Press [r] to view raw logs.")
762
+
763
+ click.echo()
764
+ click.echo(click.style("=" * 70, fg="red"))
765
+ click.echo()
766
+
767
+ def display_no_results(
768
+ self,
769
+ job: Dict[str, Any],
770
+ log_path: str,
771
+ ) -> None:
772
+ """Display no_results status for ffuf scan."""
773
+ click.echo(click.style("=" * 70, fg="cyan"))
774
+ click.echo(click.style("FFUF SCAN RESULTS", bold=True, fg="cyan"))
775
+ click.echo(click.style("=" * 70, fg="cyan"))
776
+ click.echo()
777
+ click.echo(" No paths discovered.")
778
+ click.echo()
779
+
780
+ # Try to get config info from parsed results
781
+ if log_path and os.path.exists(log_path):
782
+ try:
783
+ from souleyez.parsers.ffuf_parser import parse_ffuf
784
+
785
+ parsed = parse_ffuf(log_path, job.get("target", ""))
786
+ if parsed.get("wordlist"):
787
+ click.echo(f" Wordlist: {os.path.basename(parsed['wordlist'])}")
788
+ if parsed.get("method"):
789
+ click.echo(f" Method: {parsed['method']}")
790
+ except Exception:
791
+ pass
792
+
793
+ click.echo()
794
+ click.echo(click.style(" This could mean:", fg="bright_black"))
795
+ click.echo(
796
+ click.style(
797
+ " - Target has good security (no exposed paths)", fg="bright_black"
798
+ )
799
+ )
800
+ click.echo(
801
+ click.style(" - Try a different/larger wordlist", fg="bright_black")
802
+ )
803
+ click.echo(
804
+ click.style(
805
+ " - Check filter settings (-fc, -fs, -fw)", fg="bright_black"
806
+ )
807
+ )
808
+ click.echo(
809
+ click.style(
810
+ " - Target may be blocking automated requests", fg="bright_black"
811
+ )
812
+ )
813
+ click.echo()
814
+ click.echo(click.style("=" * 70, fg="cyan"))
815
+ click.echo()