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,563 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hydra handler.
|
|
4
|
+
|
|
5
|
+
Consolidates parsing and display logic for Hydra brute-force jobs.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from souleyez.engine.job_status import (
|
|
15
|
+
STATUS_DONE,
|
|
16
|
+
STATUS_ERROR,
|
|
17
|
+
STATUS_NO_RESULTS,
|
|
18
|
+
STATUS_WARNING,
|
|
19
|
+
)
|
|
20
|
+
from souleyez.handlers.base import BaseToolHandler
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HydraHandler(BaseToolHandler):
|
|
26
|
+
"""Handler for Hydra brute-force attack jobs."""
|
|
27
|
+
|
|
28
|
+
tool_name = "hydra"
|
|
29
|
+
display_name = "Hydra"
|
|
30
|
+
|
|
31
|
+
# All handlers enabled
|
|
32
|
+
has_error_handler = True
|
|
33
|
+
has_warning_handler = True
|
|
34
|
+
has_no_results_handler = True
|
|
35
|
+
has_done_handler = True
|
|
36
|
+
|
|
37
|
+
# Connection failure patterns
|
|
38
|
+
CONNECTION_FAILURE_PATTERNS = [
|
|
39
|
+
(r"connection refused", "Connection refused"),
|
|
40
|
+
(r"timed?\s*out|timeout", "Connection timed out"),
|
|
41
|
+
(r"could not connect|can not connect", "Could not connect"),
|
|
42
|
+
(r"no route to host", "No route to host"),
|
|
43
|
+
(r"network is unreachable", "Network unreachable"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def parse_job(
|
|
47
|
+
self,
|
|
48
|
+
engagement_id: int,
|
|
49
|
+
log_path: str,
|
|
50
|
+
job: Dict[str, Any],
|
|
51
|
+
host_manager: Optional[Any] = None,
|
|
52
|
+
findings_manager: Optional[Any] = None,
|
|
53
|
+
credentials_manager: Optional[Any] = None,
|
|
54
|
+
) -> Dict[str, Any]:
|
|
55
|
+
"""
|
|
56
|
+
Parse Hydra job results.
|
|
57
|
+
|
|
58
|
+
Extracts credentials and usernames from the output.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
from souleyez.parsers.hydra_parser import parse_hydra_output
|
|
62
|
+
from souleyez.engine.result_handler import detect_tool_error
|
|
63
|
+
|
|
64
|
+
# Import managers if not provided
|
|
65
|
+
if host_manager is None:
|
|
66
|
+
from souleyez.storage.hosts import HostManager
|
|
67
|
+
|
|
68
|
+
host_manager = HostManager()
|
|
69
|
+
if findings_manager is None:
|
|
70
|
+
from souleyez.storage.findings import FindingsManager
|
|
71
|
+
|
|
72
|
+
findings_manager = FindingsManager()
|
|
73
|
+
if credentials_manager is None:
|
|
74
|
+
from souleyez.storage.credentials import CredentialsManager
|
|
75
|
+
|
|
76
|
+
credentials_manager = CredentialsManager()
|
|
77
|
+
|
|
78
|
+
# Read the log file
|
|
79
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
80
|
+
log_content = f.read()
|
|
81
|
+
|
|
82
|
+
# Parse Hydra output
|
|
83
|
+
target = job.get("target", "")
|
|
84
|
+
parsed = parse_hydra_output(log_content, target)
|
|
85
|
+
|
|
86
|
+
creds_added = 0
|
|
87
|
+
usernames_added = 0
|
|
88
|
+
hosts_affected = set()
|
|
89
|
+
|
|
90
|
+
# Get target host for username-only entries
|
|
91
|
+
target_host = parsed.get("target_host", target)
|
|
92
|
+
# Extract IP from URL if needed
|
|
93
|
+
if "://" in str(target_host):
|
|
94
|
+
from urllib.parse import urlparse
|
|
95
|
+
|
|
96
|
+
parsed_url = urlparse(target_host)
|
|
97
|
+
target_host = parsed_url.hostname or target_host
|
|
98
|
+
|
|
99
|
+
for cred in parsed.get("credentials", []):
|
|
100
|
+
# Get actual host from credential
|
|
101
|
+
actual_host = cred.get("host", target_host)
|
|
102
|
+
|
|
103
|
+
# Skip if still contains multi-target string
|
|
104
|
+
if not actual_host or " " in str(actual_host):
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
hosts_affected.add(actual_host)
|
|
108
|
+
|
|
109
|
+
# Get or create host for this specific IP
|
|
110
|
+
host = host_manager.get_host_by_ip(engagement_id, actual_host)
|
|
111
|
+
if not host:
|
|
112
|
+
host = host_manager.add_or_update_host(
|
|
113
|
+
engagement_id, {"ip": actual_host, "status": "up"}
|
|
114
|
+
)
|
|
115
|
+
host_id = host["id"]
|
|
116
|
+
|
|
117
|
+
# Add credential
|
|
118
|
+
credentials_manager.add_credential(
|
|
119
|
+
engagement_id=engagement_id,
|
|
120
|
+
host_id=host_id,
|
|
121
|
+
username=cred["username"],
|
|
122
|
+
password=cred["password"],
|
|
123
|
+
service=cred.get("service", parsed.get("service", "unknown")),
|
|
124
|
+
port=cred.get("port", parsed.get("port")),
|
|
125
|
+
credential_type="password",
|
|
126
|
+
tool="hydra",
|
|
127
|
+
status="valid",
|
|
128
|
+
)
|
|
129
|
+
creds_added += 1
|
|
130
|
+
|
|
131
|
+
# Handle username-only enumeration results
|
|
132
|
+
for username in parsed.get("usernames", []):
|
|
133
|
+
actual_host = target_host
|
|
134
|
+
if not actual_host or " " in str(actual_host):
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
hosts_affected.add(actual_host)
|
|
138
|
+
|
|
139
|
+
# Get or create host
|
|
140
|
+
host = host_manager.get_host_by_ip(engagement_id, actual_host)
|
|
141
|
+
if not host:
|
|
142
|
+
host = host_manager.add_or_update_host(
|
|
143
|
+
engagement_id, {"ip": actual_host, "status": "up"}
|
|
144
|
+
)
|
|
145
|
+
host_id = host["id"]
|
|
146
|
+
|
|
147
|
+
# Add username-only credential
|
|
148
|
+
credentials_manager.add_credential(
|
|
149
|
+
engagement_id=engagement_id,
|
|
150
|
+
host_id=host_id,
|
|
151
|
+
username=username,
|
|
152
|
+
password="",
|
|
153
|
+
service=parsed.get("service", "http-post-form"),
|
|
154
|
+
port=parsed.get("port"),
|
|
155
|
+
credential_type="username",
|
|
156
|
+
tool="hydra",
|
|
157
|
+
status="username_valid",
|
|
158
|
+
)
|
|
159
|
+
usernames_added += 1
|
|
160
|
+
|
|
161
|
+
# Create findings
|
|
162
|
+
findings_added = 0
|
|
163
|
+
|
|
164
|
+
# Finding for valid credentials (high severity)
|
|
165
|
+
if parsed.get("credentials"):
|
|
166
|
+
cred_list = parsed["credentials"]
|
|
167
|
+
usernames_str = ", ".join([c["username"] for c in cred_list])
|
|
168
|
+
service = parsed.get("service", "unknown")
|
|
169
|
+
port = parsed.get("port", "")
|
|
170
|
+
|
|
171
|
+
first_host = list(hosts_affected)[0] if hosts_affected else target_host
|
|
172
|
+
host = host_manager.get_host_by_ip(engagement_id, first_host)
|
|
173
|
+
finding_host_id = host["id"] if host else None
|
|
174
|
+
|
|
175
|
+
findings_manager.add_finding(
|
|
176
|
+
engagement_id=engagement_id,
|
|
177
|
+
title=f"Valid Credentials Found - {service.upper()}",
|
|
178
|
+
finding_type="credential",
|
|
179
|
+
severity="high",
|
|
180
|
+
description=f"Hydra brute-force attack discovered {len(cred_list)} valid credential(s) on {service}:{port}.\n\n"
|
|
181
|
+
f"Affected usernames: {usernames_str}\n\n"
|
|
182
|
+
f"These credentials allow direct access to the service.",
|
|
183
|
+
host_id=finding_host_id,
|
|
184
|
+
tool="hydra",
|
|
185
|
+
)
|
|
186
|
+
findings_added += 1
|
|
187
|
+
|
|
188
|
+
# Finding for username enumeration (medium severity)
|
|
189
|
+
if parsed.get("usernames"):
|
|
190
|
+
username_list = parsed["usernames"]
|
|
191
|
+
usernames_str = ", ".join(username_list)
|
|
192
|
+
service = parsed.get("service", "http-post-form")
|
|
193
|
+
port = parsed.get("port", 80)
|
|
194
|
+
|
|
195
|
+
first_host = list(hosts_affected)[0] if hosts_affected else target_host
|
|
196
|
+
host = host_manager.get_host_by_ip(engagement_id, first_host)
|
|
197
|
+
finding_host_id = host["id"] if host else None
|
|
198
|
+
|
|
199
|
+
findings_manager.add_finding(
|
|
200
|
+
engagement_id=engagement_id,
|
|
201
|
+
title="Username Enumeration - Valid Usernames Discovered",
|
|
202
|
+
finding_type="enumeration",
|
|
203
|
+
severity="medium",
|
|
204
|
+
description=f"Username enumeration via {service}:{port} revealed {len(username_list)} valid username(s).\n\n"
|
|
205
|
+
f"Valid usernames: {usernames_str}\n\n"
|
|
206
|
+
f"The application differentiates between valid and invalid usernames in error messages, "
|
|
207
|
+
f"allowing attackers to enumerate valid accounts.",
|
|
208
|
+
host_id=finding_host_id,
|
|
209
|
+
tool="hydra",
|
|
210
|
+
)
|
|
211
|
+
findings_added += 1
|
|
212
|
+
|
|
213
|
+
# Check for hydra errors
|
|
214
|
+
hydra_error = detect_tool_error(log_content, "hydra")
|
|
215
|
+
summary = None
|
|
216
|
+
|
|
217
|
+
# Determine status based on results
|
|
218
|
+
if hydra_error:
|
|
219
|
+
hydra_error_lower = hydra_error.lower()
|
|
220
|
+
if "connection refused" in hydra_error_lower:
|
|
221
|
+
status = STATUS_WARNING
|
|
222
|
+
summary = "Target unreachable (connection refused)"
|
|
223
|
+
elif "timed out" in hydra_error_lower or "timeout" in hydra_error_lower:
|
|
224
|
+
status = STATUS_WARNING
|
|
225
|
+
summary = "Target unreachable (connection timed out)"
|
|
226
|
+
elif (
|
|
227
|
+
"could not connect" in hydra_error_lower
|
|
228
|
+
or "can not connect" in hydra_error_lower
|
|
229
|
+
):
|
|
230
|
+
status = STATUS_WARNING
|
|
231
|
+
summary = "Target unreachable (could not connect)"
|
|
232
|
+
elif "no route to host" in hydra_error_lower:
|
|
233
|
+
status = STATUS_WARNING
|
|
234
|
+
summary = "Target unreachable (no route to host)"
|
|
235
|
+
elif "network is unreachable" in hydra_error_lower:
|
|
236
|
+
status = STATUS_WARNING
|
|
237
|
+
summary = "Target unreachable (network unreachable)"
|
|
238
|
+
else:
|
|
239
|
+
status = STATUS_ERROR
|
|
240
|
+
summary = f"Error: {hydra_error}"
|
|
241
|
+
elif len(parsed.get("credentials", [])) > 0:
|
|
242
|
+
status = STATUS_DONE
|
|
243
|
+
cred_count = len(parsed.get("credentials", []))
|
|
244
|
+
summary = f"Found {cred_count} valid credential{'s' if cred_count != 1 else ''}"
|
|
245
|
+
elif len(parsed.get("usernames", [])) > 0:
|
|
246
|
+
status = STATUS_DONE
|
|
247
|
+
user_count = len(parsed.get("usernames", []))
|
|
248
|
+
summary = (
|
|
249
|
+
f"Found {user_count} valid username{'s' if user_count != 1 else ''}"
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
status = STATUS_NO_RESULTS
|
|
253
|
+
summary = "No valid credentials found (target responded)"
|
|
254
|
+
|
|
255
|
+
result = {
|
|
256
|
+
"tool": "hydra",
|
|
257
|
+
"status": status,
|
|
258
|
+
"target": target,
|
|
259
|
+
"hosts_affected": list(hosts_affected),
|
|
260
|
+
"service": parsed.get("service"),
|
|
261
|
+
"port": parsed.get("port"),
|
|
262
|
+
"credentials_found": len(parsed.get("credentials", [])),
|
|
263
|
+
"credentials_added": creds_added,
|
|
264
|
+
"usernames_found": len(parsed.get("usernames", [])),
|
|
265
|
+
"usernames": parsed.get("usernames", []),
|
|
266
|
+
"usernames_added": usernames_added,
|
|
267
|
+
"findings_added": findings_added,
|
|
268
|
+
"attempts": parsed.get("attempts", 0),
|
|
269
|
+
}
|
|
270
|
+
if summary:
|
|
271
|
+
result["summary"] = summary
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Error parsing hydra job: {e}")
|
|
276
|
+
return {"error": str(e)}
|
|
277
|
+
|
|
278
|
+
def display_done(
|
|
279
|
+
self,
|
|
280
|
+
job: Dict[str, Any],
|
|
281
|
+
log_path: str,
|
|
282
|
+
show_all: bool = False,
|
|
283
|
+
show_passwords: bool = False,
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Display successful Hydra attack results."""
|
|
286
|
+
try:
|
|
287
|
+
from souleyez.parsers.hydra_parser import parse_hydra_output
|
|
288
|
+
|
|
289
|
+
if not log_path or not os.path.exists(log_path):
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
293
|
+
log_content = f.read()
|
|
294
|
+
parsed = parse_hydra_output(log_content, job.get("target", ""))
|
|
295
|
+
|
|
296
|
+
credentials = parsed.get("credentials", [])
|
|
297
|
+
usernames = parsed.get("usernames", [])
|
|
298
|
+
|
|
299
|
+
if credentials:
|
|
300
|
+
click.echo(click.style("=" * 70, fg="cyan"))
|
|
301
|
+
click.echo(click.style("HYDRA CREDENTIALS FOUND", bold=True, fg="cyan"))
|
|
302
|
+
click.echo(click.style("=" * 70, fg="cyan"))
|
|
303
|
+
click.echo()
|
|
304
|
+
|
|
305
|
+
# Summary info
|
|
306
|
+
click.echo(
|
|
307
|
+
click.style(
|
|
308
|
+
f"Target: {parsed.get('target_host', 'unknown')}", bold=True
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
click.echo(
|
|
312
|
+
f"Service: {parsed.get('service', 'unknown')} (port {parsed.get('port', 'unknown')})"
|
|
313
|
+
)
|
|
314
|
+
click.echo(
|
|
315
|
+
click.style(
|
|
316
|
+
f"\n{len(credentials)} Valid Credential(s) Found",
|
|
317
|
+
fg="green",
|
|
318
|
+
bold=True,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
click.echo()
|
|
322
|
+
|
|
323
|
+
# Display credentials
|
|
324
|
+
for i, cred in enumerate(credentials, 1):
|
|
325
|
+
host = cred.get("host", parsed.get("target_host", "unknown"))
|
|
326
|
+
service = cred.get("service", parsed.get("service", "unknown"))
|
|
327
|
+
port = cred.get("port", parsed.get("port", "unknown"))
|
|
328
|
+
|
|
329
|
+
click.echo(
|
|
330
|
+
click.style(
|
|
331
|
+
f"[{i}] {host}:{port} ({service})", bold=True, fg="green"
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
click.echo(
|
|
335
|
+
f" Username: {click.style(cred['username'], fg='yellow')}"
|
|
336
|
+
)
|
|
337
|
+
if show_passwords:
|
|
338
|
+
click.echo(
|
|
339
|
+
f" Password: {click.style(cred['password'], fg='yellow')}"
|
|
340
|
+
)
|
|
341
|
+
else:
|
|
342
|
+
click.echo(
|
|
343
|
+
f" Password: {click.style('********', fg='red', dim=True)}"
|
|
344
|
+
)
|
|
345
|
+
click.echo()
|
|
346
|
+
|
|
347
|
+
if not show_passwords:
|
|
348
|
+
click.echo(
|
|
349
|
+
click.style(
|
|
350
|
+
"Passwords are hidden. Use [p] to reveal.",
|
|
351
|
+
fg="yellow",
|
|
352
|
+
dim=True,
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
click.echo()
|
|
356
|
+
|
|
357
|
+
# Check if credentials were saved
|
|
358
|
+
engagement_id = job.get("engagement_id")
|
|
359
|
+
if engagement_id:
|
|
360
|
+
try:
|
|
361
|
+
from souleyez.storage.credentials import CredentialsManager
|
|
362
|
+
|
|
363
|
+
cm = CredentialsManager()
|
|
364
|
+
all_creds = cm.list_credentials(engagement_id)
|
|
365
|
+
hydra_creds = [c for c in all_creds if c.get("tool") == "hydra"]
|
|
366
|
+
if hydra_creds:
|
|
367
|
+
click.echo(
|
|
368
|
+
click.style(
|
|
369
|
+
f"Credentials saved to database ({len(hydra_creds)} total from Hydra)",
|
|
370
|
+
fg="green",
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
click.echo()
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
click.echo(click.style("=" * 70, fg="cyan"))
|
|
378
|
+
click.echo()
|
|
379
|
+
|
|
380
|
+
elif usernames:
|
|
381
|
+
# Username-only enumeration results
|
|
382
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
383
|
+
click.echo(
|
|
384
|
+
click.style("HYDRA VALID USERNAMES FOUND", bold=True, fg="yellow")
|
|
385
|
+
)
|
|
386
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
387
|
+
click.echo()
|
|
388
|
+
|
|
389
|
+
click.echo(
|
|
390
|
+
click.style(
|
|
391
|
+
f"Target: {parsed.get('target_host', 'unknown')}", bold=True
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
click.echo(
|
|
395
|
+
f"Service: {parsed.get('service', 'unknown')} (port {parsed.get('port', 'unknown')})"
|
|
396
|
+
)
|
|
397
|
+
click.echo(
|
|
398
|
+
click.style(
|
|
399
|
+
f"\n{len(usernames)} Valid Username(s) Found (password unknown)",
|
|
400
|
+
fg="yellow",
|
|
401
|
+
bold=True,
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
click.echo()
|
|
405
|
+
|
|
406
|
+
for i, username in enumerate(usernames, 1):
|
|
407
|
+
click.echo(
|
|
408
|
+
click.style(
|
|
409
|
+
f"[{i}] Username: {username}", bold=True, fg="yellow"
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
click.echo()
|
|
413
|
+
|
|
414
|
+
click.echo(
|
|
415
|
+
click.style(
|
|
416
|
+
"Note: These usernames exist but passwords were not cracked.",
|
|
417
|
+
fg="white",
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
click.echo(
|
|
421
|
+
click.style(
|
|
422
|
+
"Consider running targeted password attacks on these accounts.",
|
|
423
|
+
fg="white",
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
click.echo()
|
|
427
|
+
|
|
428
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
429
|
+
click.echo()
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
logger.debug(f"Error in display_done: {e}")
|
|
433
|
+
|
|
434
|
+
def display_warning(
|
|
435
|
+
self,
|
|
436
|
+
job: Dict[str, Any],
|
|
437
|
+
log_path: str,
|
|
438
|
+
log_content: Optional[str] = None,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Display warning status for Hydra attack."""
|
|
441
|
+
parse_result = job.get("parse_result", {})
|
|
442
|
+
summary = "Hydra attack encountered issues"
|
|
443
|
+
if isinstance(parse_result, dict):
|
|
444
|
+
summary = parse_result.get("summary", "Hydra attack encountered issues")
|
|
445
|
+
|
|
446
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
447
|
+
click.echo(click.style("[WARNING] HYDRA ATTACK", bold=True, fg="yellow"))
|
|
448
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
449
|
+
click.echo()
|
|
450
|
+
click.echo(f" {summary}")
|
|
451
|
+
click.echo()
|
|
452
|
+
click.echo(click.style(" Common causes:", fg="bright_black"))
|
|
453
|
+
click.echo(
|
|
454
|
+
click.style(
|
|
455
|
+
" - Target unreachable (firewall, network issue)", fg="bright_black"
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
click.echo(
|
|
459
|
+
click.style(" - Service not running on expected port", fg="bright_black")
|
|
460
|
+
)
|
|
461
|
+
click.echo(
|
|
462
|
+
click.style(
|
|
463
|
+
" - Too many connections - try reducing threads", fg="bright_black"
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
click.echo()
|
|
467
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
468
|
+
click.echo()
|
|
469
|
+
|
|
470
|
+
def display_error(
|
|
471
|
+
self,
|
|
472
|
+
job: Dict[str, Any],
|
|
473
|
+
log_path: str,
|
|
474
|
+
log_content: Optional[str] = None,
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Display error status for Hydra attack."""
|
|
477
|
+
# Read log if not provided
|
|
478
|
+
if log_content is None and log_path and os.path.exists(log_path):
|
|
479
|
+
try:
|
|
480
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
481
|
+
log_content = f.read()
|
|
482
|
+
except Exception:
|
|
483
|
+
log_content = ""
|
|
484
|
+
|
|
485
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
486
|
+
click.echo(click.style("[ERROR] HYDRA ATTACK FAILED", bold=True, fg="red"))
|
|
487
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
488
|
+
click.echo()
|
|
489
|
+
|
|
490
|
+
# Check for common hydra errors
|
|
491
|
+
error_msg = None
|
|
492
|
+
if log_content:
|
|
493
|
+
if "Connection refused" in log_content:
|
|
494
|
+
error_msg = "Connection refused - service may be down or port closed"
|
|
495
|
+
elif "could not connect" in log_content.lower():
|
|
496
|
+
error_msg = "Could not connect to target service"
|
|
497
|
+
elif "timed out" in log_content.lower() or "timeout" in log_content.lower():
|
|
498
|
+
error_msg = "Connection timed out - target may be slow or filtering"
|
|
499
|
+
elif "too many connections" in log_content.lower():
|
|
500
|
+
error_msg = "Too many connections - try reducing threads with -t"
|
|
501
|
+
elif "target does not support" in log_content.lower():
|
|
502
|
+
error_msg = "Target does not support the specified protocol"
|
|
503
|
+
elif "ERROR" in log_content:
|
|
504
|
+
match = re.search(r"\[ERROR\]\s*(.+?)(?:\n|$)", log_content)
|
|
505
|
+
if match:
|
|
506
|
+
error_msg = match.group(1).strip()[:100]
|
|
507
|
+
|
|
508
|
+
if error_msg:
|
|
509
|
+
click.echo(f" {error_msg}")
|
|
510
|
+
else:
|
|
511
|
+
click.echo(" Attack failed - see raw logs for details (press 'r')")
|
|
512
|
+
|
|
513
|
+
click.echo()
|
|
514
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
515
|
+
click.echo()
|
|
516
|
+
|
|
517
|
+
def display_no_results(
|
|
518
|
+
self,
|
|
519
|
+
job: Dict[str, Any],
|
|
520
|
+
log_path: str,
|
|
521
|
+
) -> None:
|
|
522
|
+
"""Display no_results status for Hydra attack."""
|
|
523
|
+
# Try to get target info from log
|
|
524
|
+
target = job.get("target", "unknown")
|
|
525
|
+
service = "unknown"
|
|
526
|
+
port = "unknown"
|
|
527
|
+
|
|
528
|
+
if log_path and os.path.exists(log_path):
|
|
529
|
+
try:
|
|
530
|
+
from souleyez.parsers.hydra_parser import parse_hydra_output
|
|
531
|
+
|
|
532
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
533
|
+
log_content = f.read()
|
|
534
|
+
parsed = parse_hydra_output(log_content, target)
|
|
535
|
+
target = parsed.get("target_host") or target
|
|
536
|
+
service = parsed.get("service", "unknown")
|
|
537
|
+
port = parsed.get("port", "unknown")
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
click.echo(click.style("=" * 70, fg="cyan"))
|
|
542
|
+
click.echo(click.style("HYDRA PASSWORD ATTACK", bold=True, fg="cyan"))
|
|
543
|
+
click.echo(click.style("=" * 70, fg="cyan"))
|
|
544
|
+
click.echo()
|
|
545
|
+
|
|
546
|
+
click.echo(click.style(f"Target: {target}", bold=True))
|
|
547
|
+
if service != "unknown" or port != "unknown":
|
|
548
|
+
click.echo(f"Service: {service} (port {port})")
|
|
549
|
+
click.echo()
|
|
550
|
+
|
|
551
|
+
click.echo(
|
|
552
|
+
click.style("Result: No valid credentials found", fg="yellow", bold=True)
|
|
553
|
+
)
|
|
554
|
+
click.echo()
|
|
555
|
+
click.echo(" The password attack completed without finding valid credentials.")
|
|
556
|
+
click.echo()
|
|
557
|
+
click.echo(click.style("Tips:", dim=True))
|
|
558
|
+
click.echo(" - Try a larger wordlist")
|
|
559
|
+
click.echo(" - Verify the service is accessible")
|
|
560
|
+
click.echo(" - Check if account lockout is enabled")
|
|
561
|
+
click.echo()
|
|
562
|
+
click.echo(click.style("=" * 70, fg="cyan"))
|
|
563
|
+
click.echo()
|