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