souleyez 2.43.26__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.
Potentially problematic release.
This version of souleyez might be problematic. Click here for more details.
- 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 +23434 -10286
- 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.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Handler for BloodHound - Active Directory attack path mapping.
|
|
4
|
+
Parses bloodhound-python collection results.
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
from souleyez.handlers.base import BaseToolHandler
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
STATUS_DONE = "done"
|
|
18
|
+
STATUS_ERROR = "error"
|
|
19
|
+
STATUS_WARNING = "warning"
|
|
20
|
+
STATUS_NO_RESULTS = "no_results"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BloodhoundHandler(BaseToolHandler):
|
|
24
|
+
"""Handler for BloodHound AD collection."""
|
|
25
|
+
|
|
26
|
+
tool_name = "bloodhound"
|
|
27
|
+
display_name = "BloodHound"
|
|
28
|
+
|
|
29
|
+
has_error_handler = True
|
|
30
|
+
has_warning_handler = True
|
|
31
|
+
has_no_results_handler = True
|
|
32
|
+
has_done_handler = True
|
|
33
|
+
|
|
34
|
+
def parse_job(
|
|
35
|
+
self,
|
|
36
|
+
engagement_id: int,
|
|
37
|
+
log_path: str,
|
|
38
|
+
job: Dict[str, Any],
|
|
39
|
+
host_manager: Optional[Any] = None,
|
|
40
|
+
findings_manager: Optional[Any] = None,
|
|
41
|
+
credentials_manager: Optional[Any] = None,
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
"""Parse bloodhound-python results."""
|
|
44
|
+
try:
|
|
45
|
+
target = job.get("target", "")
|
|
46
|
+
|
|
47
|
+
if not log_path or not os.path.exists(log_path):
|
|
48
|
+
return {
|
|
49
|
+
"tool": "bloodhound",
|
|
50
|
+
"status": STATUS_ERROR,
|
|
51
|
+
"error": "Log file not found",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
55
|
+
log_content = f.read()
|
|
56
|
+
|
|
57
|
+
# Check for errors
|
|
58
|
+
if "ERROR" in log_content and "bloodhound-python not found" in log_content:
|
|
59
|
+
return {
|
|
60
|
+
"tool": "bloodhound",
|
|
61
|
+
"status": STATUS_ERROR,
|
|
62
|
+
"error": "bloodhound-python not installed",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if "ERROR: Missing required arguments" in log_content:
|
|
66
|
+
return {
|
|
67
|
+
"tool": "bloodhound",
|
|
68
|
+
"status": STATUS_ERROR,
|
|
69
|
+
"error": "Missing credentials",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Check for authentication errors
|
|
73
|
+
auth_errors = [
|
|
74
|
+
"Authentication failed",
|
|
75
|
+
"Invalid credentials",
|
|
76
|
+
"Logon failure",
|
|
77
|
+
"KDC_ERR_PREAUTH_FAILED",
|
|
78
|
+
"KDC_ERR_C_PRINCIPAL_UNKNOWN",
|
|
79
|
+
]
|
|
80
|
+
for err in auth_errors:
|
|
81
|
+
if err.lower() in log_content.lower():
|
|
82
|
+
return {
|
|
83
|
+
"tool": "bloodhound",
|
|
84
|
+
"status": STATUS_ERROR,
|
|
85
|
+
"error": f"Authentication failed: {err}",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Parse collection statistics
|
|
89
|
+
stats = {
|
|
90
|
+
"users": 0,
|
|
91
|
+
"groups": 0,
|
|
92
|
+
"computers": 0,
|
|
93
|
+
"domains": 0,
|
|
94
|
+
"gpos": 0,
|
|
95
|
+
"ous": 0,
|
|
96
|
+
"containers": 0,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Pattern: "Done in 00m 05s" or object counts
|
|
100
|
+
patterns = [
|
|
101
|
+
(r"(\d+)\s+user", "users"),
|
|
102
|
+
(r"(\d+)\s+group", "groups"),
|
|
103
|
+
(r"(\d+)\s+computer", "computers"),
|
|
104
|
+
(r"(\d+)\s+domain", "domains"),
|
|
105
|
+
(r"(\d+)\s+gpo", "gpos"),
|
|
106
|
+
(r"(\d+)\s+ou", "ous"),
|
|
107
|
+
(r"(\d+)\s+container", "containers"),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for pattern, key in patterns:
|
|
111
|
+
match = re.search(pattern, log_content, re.IGNORECASE)
|
|
112
|
+
if match:
|
|
113
|
+
stats[key] = int(match.group(1))
|
|
114
|
+
|
|
115
|
+
# Check for output files
|
|
116
|
+
output_path = ""
|
|
117
|
+
output_match = re.search(r"Output saved to:\s*(.+)", log_content)
|
|
118
|
+
if output_match:
|
|
119
|
+
output_path = output_match.group(1).strip()
|
|
120
|
+
|
|
121
|
+
# Check for success indicators
|
|
122
|
+
success = (
|
|
123
|
+
"Data collection complete" in log_content
|
|
124
|
+
or "Done in" in log_content
|
|
125
|
+
or any(v > 0 for v in stats.values())
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if success:
|
|
129
|
+
status = STATUS_DONE
|
|
130
|
+
total_objects = sum(stats.values())
|
|
131
|
+
if total_objects == 0:
|
|
132
|
+
# Collected but no stats parsed - still success
|
|
133
|
+
status = STATUS_DONE
|
|
134
|
+
elif "timeout" in log_content.lower():
|
|
135
|
+
status = STATUS_ERROR
|
|
136
|
+
else:
|
|
137
|
+
status = STATUS_NO_RESULTS
|
|
138
|
+
|
|
139
|
+
result = {
|
|
140
|
+
"tool": "bloodhound",
|
|
141
|
+
"status": status,
|
|
142
|
+
"target": target,
|
|
143
|
+
"stats": stats,
|
|
144
|
+
"total_objects": sum(stats.values()),
|
|
145
|
+
"output_path": output_path,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if sum(stats.values()) > 0:
|
|
149
|
+
logger.info(f"bloodhound: Collected {sum(stats.values())} AD objects")
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"Error parsing bloodhound job: {e}")
|
|
155
|
+
return {"tool": "bloodhound", "status": STATUS_ERROR, "error": str(e)}
|
|
156
|
+
|
|
157
|
+
def display_done(
|
|
158
|
+
self,
|
|
159
|
+
job: Dict[str, Any],
|
|
160
|
+
log_path: str,
|
|
161
|
+
show_all: bool = False,
|
|
162
|
+
show_passwords: bool = False,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Display successful bloodhound results."""
|
|
165
|
+
click.echo()
|
|
166
|
+
click.echo(click.style("=" * 70, fg="green"))
|
|
167
|
+
click.echo(click.style("BLOODHOUND AD COLLECTION", fg="green", bold=True))
|
|
168
|
+
click.echo(click.style("=" * 70, fg="green"))
|
|
169
|
+
click.echo()
|
|
170
|
+
|
|
171
|
+
parse_result = job.get("parse_result", {})
|
|
172
|
+
stats = parse_result.get("stats", {})
|
|
173
|
+
output_path = parse_result.get("output_path", "")
|
|
174
|
+
total = parse_result.get("total_objects", 0)
|
|
175
|
+
|
|
176
|
+
if total > 0:
|
|
177
|
+
click.echo(click.style(" Objects Collected:", bold=True))
|
|
178
|
+
for key, value in stats.items():
|
|
179
|
+
if value > 0:
|
|
180
|
+
click.echo(f" {key.capitalize()}: {value}")
|
|
181
|
+
click.echo()
|
|
182
|
+
|
|
183
|
+
if output_path:
|
|
184
|
+
click.echo(f" Output: {output_path}")
|
|
185
|
+
click.echo()
|
|
186
|
+
|
|
187
|
+
click.echo(click.style(" Next Steps:", fg="cyan"))
|
|
188
|
+
click.echo(" 1. Start BloodHound GUI: bloodhound")
|
|
189
|
+
click.echo(" 2. Import the ZIP file(s)")
|
|
190
|
+
click.echo(" 3. Run query: 'Shortest Path to Domain Admins'")
|
|
191
|
+
click.echo()
|
|
192
|
+
|
|
193
|
+
def display_error(
|
|
194
|
+
self,
|
|
195
|
+
job: Dict[str, Any],
|
|
196
|
+
log_path: str,
|
|
197
|
+
show_all: bool = False,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Display bloodhound error."""
|
|
200
|
+
click.echo()
|
|
201
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
202
|
+
click.echo(click.style("BLOODHOUND COLLECTION FAILED", fg="red", bold=True))
|
|
203
|
+
click.echo(click.style("=" * 70, fg="red"))
|
|
204
|
+
click.echo()
|
|
205
|
+
|
|
206
|
+
error = job.get("parse_result", {}).get("error") or job.get("error")
|
|
207
|
+
if error:
|
|
208
|
+
click.echo(f" Error: {error}")
|
|
209
|
+
else:
|
|
210
|
+
click.echo(" Check log for details")
|
|
211
|
+
click.echo()
|
|
212
|
+
|
|
213
|
+
def display_warning(
|
|
214
|
+
self,
|
|
215
|
+
job: Dict[str, Any],
|
|
216
|
+
log_path: str,
|
|
217
|
+
show_all: bool = False,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Display bloodhound warning."""
|
|
220
|
+
self.display_done(job, log_path, show_all, False)
|
|
221
|
+
|
|
222
|
+
def display_no_results(
|
|
223
|
+
self,
|
|
224
|
+
job: Dict[str, Any],
|
|
225
|
+
log_path: str,
|
|
226
|
+
show_all: bool = False,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Display bloodhound no results."""
|
|
229
|
+
click.echo()
|
|
230
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
231
|
+
click.echo(click.style("BLOODHOUND NO DATA COLLECTED", fg="yellow", bold=True))
|
|
232
|
+
click.echo(click.style("=" * 70, fg="yellow"))
|
|
233
|
+
click.echo()
|
|
234
|
+
click.echo(" No AD objects were collected.")
|
|
235
|
+
click.echo(" Possible causes:")
|
|
236
|
+
click.echo(" - Invalid credentials")
|
|
237
|
+
click.echo(" - Network connectivity issues")
|
|
238
|
+
click.echo(" - Domain controller not reachable")
|
|
239
|
+
click.echo()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Register handler
|
|
243
|
+
handler = BloodhoundHandler()
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Handler for Certipy - Active Directory Certificate Services (ADCS) enumeration.
|
|
4
|
+
Parses vulnerable certificate templates (ESC1-ESC8).
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from souleyez.handlers.base import BaseToolHandler
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
STATUS_DONE = "done"
|
|
16
|
+
STATUS_ERROR = "error"
|
|
17
|
+
STATUS_WARNING = "warning"
|
|
18
|
+
STATUS_NO_RESULTS = "no_results"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CertipyHandler(BaseToolHandler):
|
|
22
|
+
"""Handler for Certipy ADCS enumeration."""
|
|
23
|
+
|
|
24
|
+
tool_name = "certipy"
|
|
25
|
+
display_name = "Certipy"
|
|
26
|
+
|
|
27
|
+
has_error_handler = True
|
|
28
|
+
has_warning_handler = True
|
|
29
|
+
has_no_results_handler = True
|
|
30
|
+
has_done_handler = True
|
|
31
|
+
|
|
32
|
+
# ESC vulnerability patterns
|
|
33
|
+
ESC_PATTERNS = {
|
|
34
|
+
"ESC1": r"ESC1\s*:.*Enrollee Supplies Subject",
|
|
35
|
+
"ESC2": r"ESC2\s*:.*Any Purpose",
|
|
36
|
+
"ESC3": r"ESC3\s*:.*Certificate Request Agent",
|
|
37
|
+
"ESC4": r"ESC4\s*:.*Vulnerable Access Control",
|
|
38
|
+
"ESC5": r"ESC5\s*:.*PKI Object Access Control",
|
|
39
|
+
"ESC6": r"ESC6\s*:.*EDITF_ATTRIBUTESUBJECTALTNAME2",
|
|
40
|
+
"ESC7": r"ESC7\s*:.*CA Access Control",
|
|
41
|
+
"ESC8": r"ESC8\s*:.*NTLM Relay.*AD CS",
|
|
42
|
+
"ESC9": r"ESC9\s*:.*No Security Extension",
|
|
43
|
+
"ESC10": r"ESC10\s*:.*Weak Certificate Mappings",
|
|
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
|
+
"""Parse certipy results."""
|
|
56
|
+
try:
|
|
57
|
+
target = job.get("target", "")
|
|
58
|
+
if not log_path or not os.path.exists(log_path):
|
|
59
|
+
return {
|
|
60
|
+
"tool": "certipy",
|
|
61
|
+
"status": STATUS_ERROR,
|
|
62
|
+
"error": "Log file not found",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as f:
|
|
66
|
+
log_content = f.read()
|
|
67
|
+
|
|
68
|
+
# Strip ANSI codes
|
|
69
|
+
log_content = re.sub(r"\x1b\[[0-9;]*m", "", log_content)
|
|
70
|
+
|
|
71
|
+
vulnerabilities = []
|
|
72
|
+
templates = []
|
|
73
|
+
cas = [] # Certificate Authorities
|
|
74
|
+
domain = ""
|
|
75
|
+
|
|
76
|
+
# Parse domain
|
|
77
|
+
domain_match = re.search(r"Domain\s*:\s*(\S+)", log_content, re.IGNORECASE)
|
|
78
|
+
if domain_match:
|
|
79
|
+
domain = domain_match.group(1)
|
|
80
|
+
|
|
81
|
+
# Track counts from summary output (certipy v5.x stdout format)
|
|
82
|
+
template_count = 0
|
|
83
|
+
ca_count = 0
|
|
84
|
+
enabled_count = 0
|
|
85
|
+
|
|
86
|
+
# Parse counts from stdout summary lines
|
|
87
|
+
template_count_match = re.search(
|
|
88
|
+
r"Found (\d+) certificate templates", log_content
|
|
89
|
+
)
|
|
90
|
+
if template_count_match:
|
|
91
|
+
template_count = int(template_count_match.group(1))
|
|
92
|
+
|
|
93
|
+
ca_count_match = re.search(r"Found (\d+) certificate authorit", log_content)
|
|
94
|
+
if ca_count_match:
|
|
95
|
+
ca_count = int(ca_count_match.group(1))
|
|
96
|
+
|
|
97
|
+
enabled_count_match = re.search(
|
|
98
|
+
r"Found (\d+) enabled certificate templates", log_content
|
|
99
|
+
)
|
|
100
|
+
if enabled_count_match:
|
|
101
|
+
enabled_count = int(enabled_count_match.group(1))
|
|
102
|
+
|
|
103
|
+
# Parse Certificate Authorities (from detailed output if available)
|
|
104
|
+
ca_pattern = r"CA Name\s*:\s*(.+?)(?:\n|$)"
|
|
105
|
+
for match in re.finditer(ca_pattern, log_content):
|
|
106
|
+
ca_name = match.group(1).strip()
|
|
107
|
+
if ca_name and ca_name not in cas:
|
|
108
|
+
cas.append(ca_name)
|
|
109
|
+
|
|
110
|
+
# Also extract CA name from "CA configuration for 'name'" pattern
|
|
111
|
+
ca_config_match = re.search(r"CA configuration for '([^']+)'", log_content)
|
|
112
|
+
if ca_config_match:
|
|
113
|
+
ca_name = ca_config_match.group(1)
|
|
114
|
+
if ca_name and ca_name not in cas:
|
|
115
|
+
cas.append(ca_name)
|
|
116
|
+
|
|
117
|
+
# Parse vulnerable templates (from detailed output if available)
|
|
118
|
+
template_pattern = r"Template Name\s*:\s*(.+?)(?:\n|$)"
|
|
119
|
+
for match in re.finditer(template_pattern, log_content):
|
|
120
|
+
template_name = match.group(1).strip()
|
|
121
|
+
if template_name and template_name not in templates:
|
|
122
|
+
templates.append(template_name)
|
|
123
|
+
|
|
124
|
+
# Check for ESC vulnerabilities
|
|
125
|
+
for esc_name, pattern in self.ESC_PATTERNS.items():
|
|
126
|
+
if re.search(pattern, log_content, re.IGNORECASE):
|
|
127
|
+
# Extract the template name associated with this vulnerability
|
|
128
|
+
# Look for template name before the ESC finding
|
|
129
|
+
vuln = {
|
|
130
|
+
"type": esc_name,
|
|
131
|
+
"severity": (
|
|
132
|
+
"high"
|
|
133
|
+
if esc_name in ["ESC1", "ESC4", "ESC7", "ESC8"]
|
|
134
|
+
else "medium"
|
|
135
|
+
),
|
|
136
|
+
"description": self._get_esc_description(esc_name),
|
|
137
|
+
}
|
|
138
|
+
vulnerabilities.append(vuln)
|
|
139
|
+
|
|
140
|
+
# Store as finding
|
|
141
|
+
if findings_manager and host_manager:
|
|
142
|
+
try:
|
|
143
|
+
host = host_manager.get_host_by_ip(engagement_id, target)
|
|
144
|
+
if host:
|
|
145
|
+
findings_manager.add_finding(
|
|
146
|
+
host_id=host["id"],
|
|
147
|
+
title=f"ADCS {esc_name} Vulnerability",
|
|
148
|
+
severity=vuln["severity"],
|
|
149
|
+
description=vuln["description"],
|
|
150
|
+
tool="certipy",
|
|
151
|
+
port=0,
|
|
152
|
+
service="adcs",
|
|
153
|
+
)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.debug(f"Could not store finding: {e}")
|
|
156
|
+
|
|
157
|
+
# Check for "[!] Vulnerabilities" section
|
|
158
|
+
vuln_section = re.search(
|
|
159
|
+
r"\[\!\]\s*Vulnerabilities", log_content, re.IGNORECASE
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Check for real errors (not just timeout warnings)
|
|
163
|
+
has_real_error = False
|
|
164
|
+
if "error" in log_content.lower() or "failed" in log_content.lower():
|
|
165
|
+
# Exclude known non-fatal warnings
|
|
166
|
+
non_fatal_patterns = [
|
|
167
|
+
"error checking web enrollment: timed out",
|
|
168
|
+
"error checking web enrollment",
|
|
169
|
+
"timed out",
|
|
170
|
+
]
|
|
171
|
+
# Check if there are errors OTHER than non-fatal ones
|
|
172
|
+
error_lines = [
|
|
173
|
+
line
|
|
174
|
+
for line in log_content.lower().split("\n")
|
|
175
|
+
if "error" in line or "failed" in line
|
|
176
|
+
]
|
|
177
|
+
for line in error_lines:
|
|
178
|
+
is_non_fatal = any(
|
|
179
|
+
pattern in line for pattern in non_fatal_patterns
|
|
180
|
+
)
|
|
181
|
+
if not is_non_fatal:
|
|
182
|
+
has_real_error = True
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
# Determine status - use counts if detailed lists are empty
|
|
186
|
+
has_results = templates or cas or template_count > 0 or ca_count > 0
|
|
187
|
+
|
|
188
|
+
if vulnerabilities:
|
|
189
|
+
status = (
|
|
190
|
+
STATUS_WARNING
|
|
191
|
+
if any(v["severity"] == "high" for v in vulnerabilities)
|
|
192
|
+
else STATUS_DONE
|
|
193
|
+
)
|
|
194
|
+
elif has_results:
|
|
195
|
+
status = STATUS_DONE
|
|
196
|
+
elif has_real_error:
|
|
197
|
+
status = STATUS_ERROR
|
|
198
|
+
else:
|
|
199
|
+
status = STATUS_NO_RESULTS
|
|
200
|
+
|
|
201
|
+
result = {
|
|
202
|
+
"tool": "certipy",
|
|
203
|
+
"status": status,
|
|
204
|
+
"target": target,
|
|
205
|
+
"domain": domain,
|
|
206
|
+
"certificate_authorities": cas,
|
|
207
|
+
"templates": templates,
|
|
208
|
+
"template_count": template_count,
|
|
209
|
+
"ca_count": ca_count,
|
|
210
|
+
"enabled_template_count": enabled_count,
|
|
211
|
+
"vulnerabilities": vulnerabilities,
|
|
212
|
+
"findings": vulnerabilities, # For chaining compatibility
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if vulnerabilities:
|
|
216
|
+
logger.warning(
|
|
217
|
+
f"certipy: Found {len(vulnerabilities)} ADCS vulnerability(ies)!"
|
|
218
|
+
)
|
|
219
|
+
if template_count > 0 or templates:
|
|
220
|
+
count = template_count if template_count > 0 else len(templates)
|
|
221
|
+
logger.info(f"certipy: Found {count} certificate template(s)")
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Error parsing certipy job: {e}")
|
|
227
|
+
return {"tool": "certipy", "status": STATUS_ERROR, "error": str(e)}
|
|
228
|
+
|
|
229
|
+
def _get_esc_description(self, esc_type: str) -> str:
|
|
230
|
+
"""Get description for ESC vulnerability type."""
|
|
231
|
+
descriptions = {
|
|
232
|
+
"ESC1": "Template allows requestor to specify Subject Alternative Name (SAN). Attacker can request cert as any user.",
|
|
233
|
+
"ESC2": "Template allows Any Purpose or no EKU. Can be used for any authentication.",
|
|
234
|
+
"ESC3": "Template allows Certificate Request Agent enrollment. Can enroll on behalf of others.",
|
|
235
|
+
"ESC4": "Template has vulnerable access control. Low-privileged users can modify template.",
|
|
236
|
+
"ESC5": "PKI object has vulnerable access control. Can modify CA or template objects.",
|
|
237
|
+
"ESC6": "CA has EDITF_ATTRIBUTESUBJECTALTNAME2 enabled. Requestor can specify SAN in any request.",
|
|
238
|
+
"ESC7": "CA has vulnerable access control. Low-privileged users can manage CA.",
|
|
239
|
+
"ESC8": "CA Web Enrollment or CEP/CES endpoints vulnerable to NTLM relay.",
|
|
240
|
+
"ESC9": "Certificate has no security extension (szOID_NTDS_CA_SECURITY_EXT).",
|
|
241
|
+
"ESC10": "Weak certificate mapping allows impersonation.",
|
|
242
|
+
}
|
|
243
|
+
return descriptions.get(esc_type, f"{esc_type} vulnerability detected")
|
|
244
|
+
|
|
245
|
+
def display_done(
|
|
246
|
+
self,
|
|
247
|
+
job: Dict[str, Any],
|
|
248
|
+
log_path: str,
|
|
249
|
+
show_all: bool = False,
|
|
250
|
+
show_passwords: bool = False,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Display successful certipy results."""
|
|
253
|
+
import click
|
|
254
|
+
|
|
255
|
+
parse_result = job.get("parse_result", {})
|
|
256
|
+
|
|
257
|
+
domain = parse_result.get("domain", "")
|
|
258
|
+
cas = parse_result.get("certificate_authorities", [])
|
|
259
|
+
templates = parse_result.get("templates", [])
|
|
260
|
+
vulnerabilities = parse_result.get("vulnerabilities", [])
|
|
261
|
+
template_count = parse_result.get("template_count", 0)
|
|
262
|
+
ca_count = parse_result.get("ca_count", 0)
|
|
263
|
+
enabled_count = parse_result.get("enabled_template_count", 0)
|
|
264
|
+
|
|
265
|
+
if domain:
|
|
266
|
+
click.echo(f" Domain: {domain}")
|
|
267
|
+
|
|
268
|
+
# Show CA info
|
|
269
|
+
if cas:
|
|
270
|
+
click.secho(
|
|
271
|
+
f"\n Certificate Authorities ({len(cas)}):", fg="cyan", bold=True
|
|
272
|
+
)
|
|
273
|
+
for ca in cas:
|
|
274
|
+
click.echo(f" - {ca}")
|
|
275
|
+
elif ca_count > 0:
|
|
276
|
+
click.secho(
|
|
277
|
+
f"\n Certificate Authorities: {ca_count}", fg="cyan", bold=True
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Show template summary
|
|
281
|
+
if template_count > 0 or enabled_count > 0:
|
|
282
|
+
click.echo(
|
|
283
|
+
f"\n Templates: {template_count} total, {enabled_count} enabled"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if vulnerabilities:
|
|
287
|
+
click.secho(
|
|
288
|
+
f"\n ADCS Vulnerabilities ({len(vulnerabilities)}):",
|
|
289
|
+
fg="red",
|
|
290
|
+
bold=True,
|
|
291
|
+
)
|
|
292
|
+
for vuln in vulnerabilities:
|
|
293
|
+
esc_type = vuln.get("type", "")
|
|
294
|
+
severity = vuln.get("severity", "medium")
|
|
295
|
+
desc = vuln.get("description", "")
|
|
296
|
+
|
|
297
|
+
color = "red" if severity == "high" else "yellow"
|
|
298
|
+
click.secho(f" [{esc_type}] {desc}", fg=color)
|
|
299
|
+
elif template_count > 0:
|
|
300
|
+
click.secho("\n No vulnerable templates found (ESC1-ESC10)", fg="green")
|
|
301
|
+
|
|
302
|
+
if templates and show_all:
|
|
303
|
+
click.secho(f"\n Certificate Templates ({len(templates)}):", fg="cyan")
|
|
304
|
+
for template in templates[:10]: # Limit display
|
|
305
|
+
click.echo(f" - {template}")
|
|
306
|
+
if len(templates) > 10:
|
|
307
|
+
click.echo(f" ... and {len(templates) - 10} more")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Register handler
|
|
311
|
+
handler = CertipyHandler()
|