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.
- 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
|
@@ -9,261 +9,266 @@ from datetime import datetime
|
|
|
9
9
|
|
|
10
10
|
class AttackChainAnalyzer:
|
|
11
11
|
"""Analyze and visualize attack chains from evidence."""
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
def __init__(self):
|
|
14
14
|
self.phase_colors = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
"reconnaissance": "#17a2b8",
|
|
16
|
+
"enumeration": "#28a745",
|
|
17
|
+
"exploitation": "#ffc107",
|
|
18
|
+
"post_exploitation": "#dc3545",
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
def build_attack_chain(
|
|
22
|
-
|
|
20
|
+
|
|
21
|
+
def build_attack_chain(
|
|
22
|
+
self, evidence: Dict, findings: List[Dict], credentials: List[Dict]
|
|
23
|
+
) -> Dict:
|
|
23
24
|
"""
|
|
24
25
|
Build attack chain from evidence timeline.
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
Returns dict with nodes and edges for graph visualization.
|
|
27
28
|
"""
|
|
28
29
|
nodes = []
|
|
29
30
|
edges = []
|
|
30
31
|
node_id = 0
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
# Track hosts and services
|
|
33
34
|
hosts_seen = set()
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
# Phase 1: Reconnaissance nodes
|
|
36
|
-
recon_items = evidence.get(
|
|
37
|
+
recon_items = evidence.get("reconnaissance", [])
|
|
37
38
|
if recon_items:
|
|
38
39
|
recon_node = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
"id": f"node{node_id}",
|
|
41
|
+
"label": f"Reconnaissance\\n{len(recon_items)} items",
|
|
42
|
+
"type": "reconnaissance",
|
|
43
|
+
"count": len(recon_items),
|
|
43
44
|
}
|
|
44
45
|
nodes.append(recon_node)
|
|
45
|
-
recon_id = recon_node[
|
|
46
|
+
recon_id = recon_node["id"]
|
|
46
47
|
node_id += 1
|
|
47
48
|
else:
|
|
48
49
|
recon_id = None
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
# Phase 2: Enumeration nodes (per host)
|
|
51
|
-
enum_items = evidence.get(
|
|
52
|
+
enum_items = evidence.get("enumeration", [])
|
|
52
53
|
enum_nodes = {}
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
for item in enum_items:
|
|
55
|
-
host = item.get(
|
|
56
|
+
host = item.get("host", "unknown")
|
|
56
57
|
hosts_seen.add(host)
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
if host not in enum_nodes:
|
|
59
60
|
enum_node = {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
"id": f"node{node_id}",
|
|
62
|
+
"label": f"Enumeration\\n{host}",
|
|
63
|
+
"type": "enumeration",
|
|
64
|
+
"host": host,
|
|
64
65
|
}
|
|
65
66
|
nodes.append(enum_node)
|
|
66
|
-
enum_nodes[host] = enum_node[
|
|
67
|
+
enum_nodes[host] = enum_node["id"]
|
|
67
68
|
node_id += 1
|
|
68
|
-
|
|
69
|
+
|
|
69
70
|
# Link from reconnaissance
|
|
70
71
|
if recon_id:
|
|
71
|
-
edges.append(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
76
|
-
|
|
72
|
+
edges.append(
|
|
73
|
+
{"from": recon_id, "to": enum_node["id"], "label": "discovered"}
|
|
74
|
+
)
|
|
75
|
+
|
|
77
76
|
# Phase 3: Exploitation nodes
|
|
78
|
-
exploit_items = evidence.get(
|
|
77
|
+
exploit_items = evidence.get("exploitation", [])
|
|
79
78
|
exploit_nodes = {}
|
|
80
|
-
|
|
79
|
+
|
|
81
80
|
for item in exploit_items:
|
|
82
|
-
host = item.get(
|
|
83
|
-
service = item.get(
|
|
81
|
+
host = item.get("host", "unknown")
|
|
82
|
+
service = item.get("service", "service")
|
|
84
83
|
hosts_seen.add(host)
|
|
85
|
-
|
|
84
|
+
|
|
86
85
|
key = f"{host}:{service}"
|
|
87
86
|
if key not in exploit_nodes:
|
|
88
87
|
exploit_node = {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
"id": f"node{node_id}",
|
|
89
|
+
"label": f"Exploit\\n{host}\\n{service}",
|
|
90
|
+
"type": "exploitation",
|
|
91
|
+
"host": host,
|
|
92
|
+
"service": service,
|
|
94
93
|
}
|
|
95
94
|
nodes.append(exploit_node)
|
|
96
|
-
exploit_nodes[key] = exploit_node[
|
|
95
|
+
exploit_nodes[key] = exploit_node["id"]
|
|
97
96
|
node_id += 1
|
|
98
|
-
|
|
97
|
+
|
|
99
98
|
# Link from enumeration
|
|
100
99
|
if host in enum_nodes:
|
|
101
|
-
edges.append(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
edges.append(
|
|
101
|
+
{
|
|
102
|
+
"from": enum_nodes[host],
|
|
103
|
+
"to": exploit_node["id"],
|
|
104
|
+
"label": "exploited",
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
107
108
|
# Add credential nodes
|
|
108
109
|
cred_nodes = {}
|
|
109
110
|
for cred in credentials:
|
|
110
|
-
host = cred.get(
|
|
111
|
-
service = cred.get(
|
|
112
|
-
username = cred.get(
|
|
111
|
+
host = cred.get("host", "unknown")
|
|
112
|
+
service = cred.get("service", "service")
|
|
113
|
+
username = cred.get("username", "user")
|
|
113
114
|
hosts_seen.add(host)
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
key = f"{host}:{service}:{username}"
|
|
116
117
|
if key not in cred_nodes:
|
|
117
118
|
cred_node = {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
"id": f"node{node_id}",
|
|
120
|
+
"label": f"Credential\\n{username}@{host}",
|
|
121
|
+
"type": "credential",
|
|
122
|
+
"host": host,
|
|
122
123
|
}
|
|
123
124
|
nodes.append(cred_node)
|
|
124
|
-
cred_nodes[key] = cred_node[
|
|
125
|
+
cred_nodes[key] = cred_node["id"]
|
|
125
126
|
node_id += 1
|
|
126
|
-
|
|
127
|
+
|
|
127
128
|
# Link from exploitation
|
|
128
129
|
exploit_key = f"{host}:{service}"
|
|
129
130
|
if exploit_key in exploit_nodes:
|
|
130
|
-
edges.append(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
edges.append(
|
|
132
|
+
{
|
|
133
|
+
"from": exploit_nodes[exploit_key],
|
|
134
|
+
"to": cred_node["id"],
|
|
135
|
+
"label": "obtained",
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
136
139
|
# Phase 4: Post-exploitation nodes
|
|
137
|
-
post_items = evidence.get(
|
|
140
|
+
post_items = evidence.get("post_exploitation", [])
|
|
138
141
|
if post_items:
|
|
139
142
|
# Group by host
|
|
140
143
|
post_by_host = {}
|
|
141
144
|
for item in post_items:
|
|
142
|
-
host = item.get(
|
|
145
|
+
host = item.get("host", "unknown")
|
|
143
146
|
hosts_seen.add(host)
|
|
144
147
|
if host not in post_by_host:
|
|
145
148
|
post_by_host[host] = []
|
|
146
149
|
post_by_host[host].append(item)
|
|
147
|
-
|
|
150
|
+
|
|
148
151
|
for host, items in post_by_host.items():
|
|
149
152
|
post_node = {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
"id": f"node{node_id}",
|
|
154
|
+
"label": f"Post-Exploit\\n{host}\\n{len(items)} items",
|
|
155
|
+
"type": "post_exploitation",
|
|
156
|
+
"host": host,
|
|
154
157
|
}
|
|
155
158
|
nodes.append(post_node)
|
|
156
159
|
node_id += 1
|
|
157
|
-
|
|
160
|
+
|
|
158
161
|
# Link from credentials or exploits
|
|
159
162
|
linked = False
|
|
160
163
|
for cred_key, cred_id in cred_nodes.items():
|
|
161
164
|
if host in cred_key:
|
|
162
|
-
edges.append(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
'label': 'access'
|
|
166
|
-
})
|
|
165
|
+
edges.append(
|
|
166
|
+
{"from": cred_id, "to": post_node["id"], "label": "access"}
|
|
167
|
+
)
|
|
167
168
|
linked = True
|
|
168
169
|
break
|
|
169
|
-
|
|
170
|
+
|
|
170
171
|
if not linked:
|
|
171
172
|
for exploit_key, exploit_id in exploit_nodes.items():
|
|
172
173
|
if host in exploit_key:
|
|
173
|
-
edges.append(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
edges.append(
|
|
175
|
+
{
|
|
176
|
+
"from": exploit_id,
|
|
177
|
+
"to": post_node["id"],
|
|
178
|
+
"label": "access",
|
|
179
|
+
}
|
|
180
|
+
)
|
|
178
181
|
break
|
|
179
|
-
|
|
182
|
+
|
|
180
183
|
return {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
184
|
+
"nodes": nodes,
|
|
185
|
+
"edges": edges,
|
|
186
|
+
"hosts": list(hosts_seen),
|
|
187
|
+
"phases": {
|
|
188
|
+
"reconnaissance": len(recon_items),
|
|
189
|
+
"enumeration": len(enum_items),
|
|
190
|
+
"exploitation": len(exploit_items),
|
|
191
|
+
"post_exploitation": len(post_items),
|
|
192
|
+
},
|
|
190
193
|
}
|
|
191
|
-
|
|
194
|
+
|
|
192
195
|
def generate_mermaid_diagram(self, chain: Dict) -> str:
|
|
193
196
|
"""Generate Mermaid.js flowchart from attack chain."""
|
|
194
|
-
if not chain[
|
|
197
|
+
if not chain["nodes"]:
|
|
195
198
|
return ""
|
|
196
|
-
|
|
199
|
+
|
|
197
200
|
mermaid = "graph TD\n"
|
|
198
|
-
|
|
201
|
+
|
|
199
202
|
# Define node styles
|
|
200
203
|
mermaid += " classDef recon fill:#17a2b8,stroke:#0c5460,color:#fff\n"
|
|
201
204
|
mermaid += " classDef enum fill:#28a745,stroke:#155724,color:#fff\n"
|
|
202
205
|
mermaid += " classDef exploit fill:#ffc107,stroke:#856404,color:#000\n"
|
|
203
206
|
mermaid += " classDef post fill:#dc3545,stroke:#721c24,color:#fff\n"
|
|
204
207
|
mermaid += " classDef cred fill:#6f42c1,stroke:#3d1f66,color:#fff\n\n"
|
|
205
|
-
|
|
208
|
+
|
|
206
209
|
# Add nodes
|
|
207
|
-
for node in chain[
|
|
208
|
-
node_id = node[
|
|
209
|
-
label = node[
|
|
210
|
-
node_type = node[
|
|
211
|
-
|
|
210
|
+
for node in chain["nodes"]:
|
|
211
|
+
node_id = node["id"]
|
|
212
|
+
label = node["label"].replace("\n", "<br/>")
|
|
213
|
+
node_type = node["type"]
|
|
214
|
+
|
|
212
215
|
# Shape based on type
|
|
213
|
-
if node_type ==
|
|
216
|
+
if node_type == "reconnaissance":
|
|
214
217
|
mermaid += f" {node_id}[{label}]:::recon\n"
|
|
215
|
-
elif node_type ==
|
|
218
|
+
elif node_type == "enumeration":
|
|
216
219
|
mermaid += f" {node_id}[{label}]:::enum\n"
|
|
217
|
-
elif node_type ==
|
|
220
|
+
elif node_type == "exploitation":
|
|
218
221
|
mermaid += f" {node_id}[{label}]:::exploit\n"
|
|
219
|
-
elif node_type ==
|
|
222
|
+
elif node_type == "post_exploitation":
|
|
220
223
|
mermaid += f" {node_id}[{label}]:::post\n"
|
|
221
|
-
elif node_type ==
|
|
224
|
+
elif node_type == "credential":
|
|
222
225
|
mermaid += f" {node_id}[{label}]:::cred\n"
|
|
223
|
-
|
|
226
|
+
|
|
224
227
|
# Add edges
|
|
225
228
|
mermaid += "\n"
|
|
226
|
-
for edge in chain[
|
|
227
|
-
label = edge.get(
|
|
229
|
+
for edge in chain["edges"]:
|
|
230
|
+
label = edge.get("label", "")
|
|
228
231
|
mermaid += f" {edge['from']} -->|{label}| {edge['to']}\n"
|
|
229
|
-
|
|
232
|
+
|
|
230
233
|
return mermaid
|
|
231
|
-
|
|
234
|
+
|
|
232
235
|
def get_attack_summary(self, chain: Dict) -> Dict:
|
|
233
236
|
"""Generate summary statistics for attack chain."""
|
|
234
237
|
return {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
"total_nodes": len(chain["nodes"]),
|
|
239
|
+
"total_edges": len(chain["edges"]),
|
|
240
|
+
"hosts_compromised": len(chain["hosts"]),
|
|
241
|
+
"phases_active": sum(1 for count in chain["phases"].values() if count > 0),
|
|
242
|
+
"longest_path": self._calculate_longest_path(chain),
|
|
243
|
+
"critical_nodes": self._identify_critical_nodes(chain),
|
|
241
244
|
}
|
|
242
|
-
|
|
245
|
+
|
|
243
246
|
def _calculate_longest_path(self, chain: Dict) -> int:
|
|
244
247
|
"""Calculate longest path in attack graph (simplified)."""
|
|
245
|
-
if not chain[
|
|
246
|
-
return len(chain[
|
|
247
|
-
|
|
248
|
+
if not chain["edges"]:
|
|
249
|
+
return len(chain["nodes"])
|
|
250
|
+
|
|
248
251
|
# Build adjacency list
|
|
249
252
|
adj = {}
|
|
250
|
-
for edge in chain[
|
|
251
|
-
if edge[
|
|
252
|
-
adj[edge[
|
|
253
|
-
adj[edge[
|
|
254
|
-
|
|
253
|
+
for edge in chain["edges"]:
|
|
254
|
+
if edge["from"] not in adj:
|
|
255
|
+
adj[edge["from"]] = []
|
|
256
|
+
adj[edge["from"]].append(edge["to"])
|
|
257
|
+
|
|
255
258
|
# Find nodes with no incoming edges (starting points)
|
|
256
|
-
has_incoming = set(edge[
|
|
257
|
-
start_nodes = [
|
|
258
|
-
|
|
259
|
+
has_incoming = set(edge["to"] for edge in chain["edges"])
|
|
260
|
+
start_nodes = [
|
|
261
|
+
node["id"] for node in chain["nodes"] if node["id"] not in has_incoming
|
|
262
|
+
]
|
|
263
|
+
|
|
259
264
|
if not start_nodes:
|
|
260
265
|
return 1
|
|
261
|
-
|
|
266
|
+
|
|
262
267
|
# DFS to find longest path
|
|
263
268
|
def dfs(node, visited):
|
|
264
269
|
if node not in adj:
|
|
265
270
|
return 1
|
|
266
|
-
|
|
271
|
+
|
|
267
272
|
max_depth = 1
|
|
268
273
|
for neighbor in adj[node]:
|
|
269
274
|
if neighbor not in visited:
|
|
@@ -271,26 +276,31 @@ class AttackChainAnalyzer:
|
|
|
271
276
|
depth = 1 + dfs(neighbor, visited)
|
|
272
277
|
max_depth = max(max_depth, depth)
|
|
273
278
|
visited.remove(neighbor)
|
|
274
|
-
|
|
279
|
+
|
|
275
280
|
return max_depth
|
|
276
|
-
|
|
281
|
+
|
|
277
282
|
longest = max(dfs(start, {start}) for start in start_nodes)
|
|
278
283
|
return longest
|
|
279
|
-
|
|
284
|
+
|
|
280
285
|
def _identify_critical_nodes(self, chain: Dict) -> List[str]:
|
|
281
286
|
"""Identify critical nodes (high connectivity)."""
|
|
282
287
|
# Count edges per node
|
|
283
288
|
node_degree = {}
|
|
284
|
-
for edge in chain[
|
|
285
|
-
node_degree[edge[
|
|
286
|
-
node_degree[edge[
|
|
289
|
+
for edge in chain["edges"]:
|
|
290
|
+
node_degree[edge["from"]] = node_degree.get(edge["from"], 0) + 1
|
|
291
|
+
node_degree[edge["to"]] = node_degree.get(edge["to"], 0) + 1
|
|
287
292
|
|
|
288
293
|
# Critical if degree > 2
|
|
289
294
|
critical = [node_id for node_id, degree in node_degree.items() if degree > 2]
|
|
290
295
|
return critical
|
|
291
296
|
|
|
292
|
-
def build_host_centric_chain(
|
|
293
|
-
|
|
297
|
+
def build_host_centric_chain(
|
|
298
|
+
self,
|
|
299
|
+
evidence: Dict,
|
|
300
|
+
findings: List[Dict],
|
|
301
|
+
credentials: List[Dict],
|
|
302
|
+
attack_surface: Dict = None,
|
|
303
|
+
) -> Dict:
|
|
294
304
|
"""
|
|
295
305
|
Build host-centric attack chain showing per-host attack journey
|
|
296
306
|
and lateral movement between hosts.
|
|
@@ -318,23 +328,23 @@ class AttackChainAnalyzer:
|
|
|
318
328
|
|
|
319
329
|
# Build host info from attack_surface (primary source)
|
|
320
330
|
host_info = {}
|
|
321
|
-
if attack_surface and attack_surface.get(
|
|
322
|
-
for h in attack_surface[
|
|
323
|
-
host_ip = h.get(
|
|
331
|
+
if attack_surface and attack_surface.get("hosts"):
|
|
332
|
+
for h in attack_surface["hosts"]:
|
|
333
|
+
host_ip = h.get("host")
|
|
324
334
|
if host_ip:
|
|
325
335
|
host_info[host_ip] = {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
"hostname": h.get("hostname"),
|
|
337
|
+
"score": h.get("score", 0),
|
|
338
|
+
"services": h.get("services", []),
|
|
339
|
+
"findings_count": h.get("findings", 0),
|
|
340
|
+
"critical_findings": h.get("critical_findings", 0),
|
|
341
|
+
"open_ports": h.get("open_ports", 0),
|
|
332
342
|
}
|
|
333
343
|
|
|
334
344
|
# Collect findings by host
|
|
335
345
|
findings_by_host = {}
|
|
336
346
|
for f in findings:
|
|
337
|
-
host = f.get(
|
|
347
|
+
host = f.get("ip_address")
|
|
338
348
|
if host:
|
|
339
349
|
if host not in findings_by_host:
|
|
340
350
|
findings_by_host[host] = []
|
|
@@ -343,7 +353,7 @@ class AttackChainAnalyzer:
|
|
|
343
353
|
# Collect credentials by host
|
|
344
354
|
creds_by_host = {}
|
|
345
355
|
for c in credentials:
|
|
346
|
-
host = c.get(
|
|
356
|
+
host = c.get("ip_address") or c.get("host")
|
|
347
357
|
if host:
|
|
348
358
|
if host not in creds_by_host:
|
|
349
359
|
creds_by_host[host] = []
|
|
@@ -351,65 +361,94 @@ class AttackChainAnalyzer:
|
|
|
351
361
|
|
|
352
362
|
# Collect evidence by host and phase
|
|
353
363
|
evidence_by_host = {}
|
|
354
|
-
for phase in [
|
|
364
|
+
for phase in [
|
|
365
|
+
"reconnaissance",
|
|
366
|
+
"enumeration",
|
|
367
|
+
"exploitation",
|
|
368
|
+
"post_exploitation",
|
|
369
|
+
]:
|
|
355
370
|
for item in evidence.get(phase, []):
|
|
356
|
-
host = item.get(
|
|
371
|
+
host = item.get("host") or item.get("ip_address")
|
|
357
372
|
if host:
|
|
358
373
|
if host not in evidence_by_host:
|
|
359
|
-
evidence_by_host[host] = {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
374
|
+
evidence_by_host[host] = {
|
|
375
|
+
"recon": [],
|
|
376
|
+
"enum": [],
|
|
377
|
+
"exploit": [],
|
|
378
|
+
"post": [],
|
|
379
|
+
}
|
|
380
|
+
if phase == "reconnaissance":
|
|
381
|
+
evidence_by_host[host]["recon"].append(item)
|
|
382
|
+
elif phase == "enumeration":
|
|
383
|
+
evidence_by_host[host]["enum"].append(item)
|
|
384
|
+
elif phase == "exploitation":
|
|
385
|
+
evidence_by_host[host]["exploit"].append(item)
|
|
366
386
|
else:
|
|
367
|
-
evidence_by_host[host][
|
|
387
|
+
evidence_by_host[host]["post"].append(item)
|
|
368
388
|
|
|
369
389
|
# Get all hosts (union of all sources)
|
|
370
|
-
all_hosts =
|
|
390
|
+
all_hosts = (
|
|
391
|
+
set(host_info.keys())
|
|
392
|
+
| set(findings_by_host.keys())
|
|
393
|
+
| set(creds_by_host.keys())
|
|
394
|
+
| set(evidence_by_host.keys())
|
|
395
|
+
)
|
|
371
396
|
|
|
372
397
|
# Build chain for each host
|
|
373
398
|
for host in all_hosts:
|
|
374
399
|
info = host_info.get(host, {})
|
|
375
400
|
host_findings = findings_by_host.get(host, [])
|
|
376
401
|
host_creds = creds_by_host.get(host, [])
|
|
377
|
-
host_evidence = evidence_by_host.get(
|
|
402
|
+
host_evidence = evidence_by_host.get(
|
|
403
|
+
host, {"recon": [], "enum": [], "exploit": [], "post": []}
|
|
404
|
+
)
|
|
378
405
|
|
|
379
406
|
# Get services from attack_surface
|
|
380
|
-
services = info.get(
|
|
407
|
+
services = info.get("services", [])
|
|
381
408
|
service_count = len(services) if isinstance(services, list) else services
|
|
382
409
|
|
|
383
410
|
# Categorize findings
|
|
384
|
-
critical_findings = [
|
|
385
|
-
|
|
411
|
+
critical_findings = [
|
|
412
|
+
f for f in host_findings if f.get("severity") == "critical"
|
|
413
|
+
]
|
|
414
|
+
high_findings = [f for f in host_findings if f.get("severity") == "high"]
|
|
386
415
|
|
|
387
416
|
# Determine what phases to show (SMART INFERENCE)
|
|
388
|
-
has_any_data = bool(
|
|
389
|
-
|
|
417
|
+
has_any_data = bool(
|
|
418
|
+
host_findings or host_creds or services or any(host_evidence.values())
|
|
419
|
+
)
|
|
420
|
+
has_services = service_count > 0 or host_evidence["enum"]
|
|
390
421
|
has_vulns = bool(critical_findings or high_findings)
|
|
391
422
|
has_exploitation = bool(
|
|
392
|
-
host_creds
|
|
393
|
-
host_evidence[
|
|
394
|
-
any(
|
|
395
|
-
|
|
423
|
+
host_creds
|
|
424
|
+
or host_evidence["exploit"]
|
|
425
|
+
or any(
|
|
426
|
+
f.get("title", "").lower().find("exploit") >= 0
|
|
427
|
+
for f in host_findings
|
|
428
|
+
)
|
|
429
|
+
or any(
|
|
430
|
+
f.get("finding_type") in ["exploitation", "data_breach"]
|
|
431
|
+
for f in host_findings
|
|
432
|
+
)
|
|
396
433
|
)
|
|
397
434
|
has_creds = bool(host_creds)
|
|
398
|
-
has_post = bool(host_evidence[
|
|
435
|
+
has_post = bool(host_evidence["post"])
|
|
399
436
|
|
|
400
437
|
# Check for exploited services in attack_surface
|
|
401
438
|
exploited_services = []
|
|
402
439
|
if isinstance(services, list):
|
|
403
|
-
exploited_services = [
|
|
440
|
+
exploited_services = [
|
|
441
|
+
s for s in services if s.get("status") == "exploited"
|
|
442
|
+
]
|
|
404
443
|
if exploited_services:
|
|
405
444
|
has_exploitation = True
|
|
406
445
|
|
|
407
446
|
host_data = {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
447
|
+
"host": host,
|
|
448
|
+
"hostname": info.get("hostname"),
|
|
449
|
+
"nodes": [],
|
|
450
|
+
"internal_edges": [],
|
|
451
|
+
"score": info.get("score", 0),
|
|
413
452
|
}
|
|
414
453
|
|
|
415
454
|
prev_node_id = None
|
|
@@ -417,49 +456,57 @@ class AttackChainAnalyzer:
|
|
|
417
456
|
# Phase 1: Discovery (INFERRED if we have any data about this host)
|
|
418
457
|
if has_any_data:
|
|
419
458
|
node_id = next_node_id()
|
|
420
|
-
recon_count = len(host_evidence[
|
|
459
|
+
recon_count = len(host_evidence["recon"])
|
|
421
460
|
if recon_count > 0:
|
|
422
|
-
detail = f
|
|
423
|
-
elif info.get(
|
|
461
|
+
detail = f"{recon_count} scans"
|
|
462
|
+
elif info.get("open_ports"):
|
|
424
463
|
detail = f"{info['open_ports']} ports found"
|
|
425
464
|
else:
|
|
426
|
-
detail =
|
|
427
|
-
host_data[
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
465
|
+
detail = "Host identified"
|
|
466
|
+
host_data["nodes"].append(
|
|
467
|
+
{
|
|
468
|
+
"id": node_id,
|
|
469
|
+
"label": "Discovery",
|
|
470
|
+
"detail": detail,
|
|
471
|
+
"type": "discovery",
|
|
472
|
+
"phase": 1,
|
|
473
|
+
}
|
|
474
|
+
)
|
|
434
475
|
prev_node_id = node_id
|
|
435
476
|
|
|
436
477
|
# Phase 2: Enumeration (INFERRED if we have services or findings)
|
|
437
478
|
if has_services or host_findings:
|
|
438
479
|
node_id = next_node_id()
|
|
439
|
-
enum_count = len(host_evidence[
|
|
480
|
+
enum_count = len(host_evidence["enum"])
|
|
440
481
|
if enum_count > 0:
|
|
441
482
|
# Get service names from evidence
|
|
442
483
|
svc_names = set()
|
|
443
|
-
for item in host_evidence[
|
|
444
|
-
svc = item.get(
|
|
484
|
+
for item in host_evidence["enum"]:
|
|
485
|
+
svc = item.get("service") or item.get("tool", "")
|
|
445
486
|
if svc:
|
|
446
487
|
svc_names.add(svc)
|
|
447
|
-
detail =
|
|
488
|
+
detail = (
|
|
489
|
+
", ".join(list(svc_names)[:3])
|
|
490
|
+
if svc_names
|
|
491
|
+
else f"{enum_count} items"
|
|
492
|
+
)
|
|
448
493
|
elif service_count > 0:
|
|
449
|
-
detail = f
|
|
494
|
+
detail = f"{service_count} services"
|
|
450
495
|
else:
|
|
451
|
-
detail =
|
|
452
|
-
host_data[
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
496
|
+
detail = "Services scanned"
|
|
497
|
+
host_data["nodes"].append(
|
|
498
|
+
{
|
|
499
|
+
"id": node_id,
|
|
500
|
+
"label": "Enumeration",
|
|
501
|
+
"detail": detail,
|
|
502
|
+
"type": "enumeration",
|
|
503
|
+
"phase": 2,
|
|
504
|
+
}
|
|
505
|
+
)
|
|
459
506
|
if prev_node_id:
|
|
460
|
-
host_data[
|
|
461
|
-
|
|
462
|
-
|
|
507
|
+
host_data["internal_edges"].append(
|
|
508
|
+
{"from": prev_node_id, "to": node_id, "label": "scanned"}
|
|
509
|
+
)
|
|
463
510
|
prev_node_id = node_id
|
|
464
511
|
|
|
465
512
|
# Phase 3: Vulnerabilities (if we have critical/high findings)
|
|
@@ -467,139 +514,155 @@ class AttackChainAnalyzer:
|
|
|
467
514
|
node_id = next_node_id()
|
|
468
515
|
# Get top vulnerability title
|
|
469
516
|
top_vuln = (critical_findings + high_findings)[0]
|
|
470
|
-
top_title = top_vuln.get(
|
|
517
|
+
top_title = top_vuln.get("title", "Vulnerability")[:25]
|
|
471
518
|
vuln_detail = []
|
|
472
519
|
if critical_findings:
|
|
473
|
-
vuln_detail.append(f
|
|
520
|
+
vuln_detail.append(f"{len(critical_findings)} critical")
|
|
474
521
|
if high_findings:
|
|
475
|
-
vuln_detail.append(f
|
|
476
|
-
host_data[
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
522
|
+
vuln_detail.append(f"{len(high_findings)} high")
|
|
523
|
+
host_data["nodes"].append(
|
|
524
|
+
{
|
|
525
|
+
"id": node_id,
|
|
526
|
+
"label": top_title,
|
|
527
|
+
"detail": ", ".join(vuln_detail),
|
|
528
|
+
"type": "vulnerability",
|
|
529
|
+
"phase": 3,
|
|
530
|
+
}
|
|
531
|
+
)
|
|
483
532
|
if prev_node_id:
|
|
484
|
-
host_data[
|
|
485
|
-
|
|
486
|
-
|
|
533
|
+
host_data["internal_edges"].append(
|
|
534
|
+
{"from": prev_node_id, "to": node_id, "label": "found"}
|
|
535
|
+
)
|
|
487
536
|
prev_node_id = node_id
|
|
488
537
|
|
|
489
538
|
# Phase 4: Exploitation (if we have creds, exploit evidence, or exploited services)
|
|
490
539
|
if has_exploitation:
|
|
491
540
|
node_id = next_node_id()
|
|
492
|
-
exploit_count = len(host_evidence[
|
|
541
|
+
exploit_count = len(host_evidence["exploit"])
|
|
493
542
|
if exploited_services:
|
|
494
|
-
svc_names = [
|
|
495
|
-
|
|
543
|
+
svc_names = [
|
|
544
|
+
s.get("service", "service") for s in exploited_services[:2]
|
|
545
|
+
]
|
|
546
|
+
detail = ", ".join(svc_names)
|
|
496
547
|
elif exploit_count > 0:
|
|
497
|
-
detail = f
|
|
548
|
+
detail = f"{exploit_count} exploits"
|
|
498
549
|
elif has_creds:
|
|
499
|
-
detail =
|
|
550
|
+
detail = "Access gained"
|
|
500
551
|
else:
|
|
501
|
-
detail =
|
|
502
|
-
host_data[
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
552
|
+
detail = "Exploited"
|
|
553
|
+
host_data["nodes"].append(
|
|
554
|
+
{
|
|
555
|
+
"id": node_id,
|
|
556
|
+
"label": "Exploited",
|
|
557
|
+
"detail": detail,
|
|
558
|
+
"type": "exploitation",
|
|
559
|
+
"phase": 4,
|
|
560
|
+
}
|
|
561
|
+
)
|
|
509
562
|
if prev_node_id:
|
|
510
|
-
host_data[
|
|
511
|
-
|
|
512
|
-
|
|
563
|
+
host_data["internal_edges"].append(
|
|
564
|
+
{"from": prev_node_id, "to": node_id, "label": "exploited"}
|
|
565
|
+
)
|
|
513
566
|
prev_node_id = node_id
|
|
514
567
|
|
|
515
568
|
# Phase 5: Credentials (if we have creds)
|
|
516
569
|
if has_creds:
|
|
517
570
|
node_id = next_node_id()
|
|
518
|
-
usernames = set(c.get(
|
|
519
|
-
host_data[
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
571
|
+
usernames = set(c.get("username", "user") for c in host_creds)
|
|
572
|
+
host_data["nodes"].append(
|
|
573
|
+
{
|
|
574
|
+
"id": node_id,
|
|
575
|
+
"label": "Credentials",
|
|
576
|
+
"detail": ", ".join(list(usernames)[:3]),
|
|
577
|
+
"type": "credential",
|
|
578
|
+
"phase": 5,
|
|
579
|
+
"creds": host_creds,
|
|
580
|
+
}
|
|
581
|
+
)
|
|
527
582
|
if prev_node_id:
|
|
528
|
-
host_data[
|
|
529
|
-
|
|
530
|
-
|
|
583
|
+
host_data["internal_edges"].append(
|
|
584
|
+
{"from": prev_node_id, "to": node_id, "label": "dumped"}
|
|
585
|
+
)
|
|
531
586
|
prev_node_id = node_id
|
|
532
587
|
|
|
533
588
|
# Phase 6: Post-Exploitation (if we have post evidence)
|
|
534
589
|
if has_post:
|
|
535
590
|
node_id = next_node_id()
|
|
536
|
-
host_data[
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
591
|
+
host_data["nodes"].append(
|
|
592
|
+
{
|
|
593
|
+
"id": node_id,
|
|
594
|
+
"label": "Post-Exploit",
|
|
595
|
+
"detail": f"{len(host_evidence['post'])} actions",
|
|
596
|
+
"type": "post_exploitation",
|
|
597
|
+
"phase": 6,
|
|
598
|
+
}
|
|
599
|
+
)
|
|
543
600
|
if prev_node_id:
|
|
544
|
-
host_data[
|
|
545
|
-
|
|
546
|
-
|
|
601
|
+
host_data["internal_edges"].append(
|
|
602
|
+
{"from": prev_node_id, "to": node_id, "label": "accessed"}
|
|
603
|
+
)
|
|
547
604
|
|
|
548
605
|
# Only add host if it has nodes
|
|
549
|
-
if host_data[
|
|
606
|
+
if host_data["nodes"]:
|
|
550
607
|
host_chains[host] = host_data
|
|
551
608
|
|
|
552
609
|
# Detect lateral movement (credentials from host A used on host B)
|
|
553
610
|
for source_host, source_data in host_chains.items():
|
|
554
|
-
for node in source_data[
|
|
555
|
-
if node[
|
|
611
|
+
for node in source_data["nodes"]:
|
|
612
|
+
if node["type"] == "credential" and node.get("creds"):
|
|
556
613
|
for target_host, target_data in host_chains.items():
|
|
557
614
|
if target_host == source_host:
|
|
558
615
|
continue
|
|
559
616
|
# If target was exploited and source has creds, potential lateral
|
|
560
|
-
target_exploited = any(
|
|
561
|
-
|
|
617
|
+
target_exploited = any(
|
|
618
|
+
n["type"] in ["exploitation", "post_exploitation"]
|
|
619
|
+
for n in target_data["nodes"]
|
|
620
|
+
)
|
|
562
621
|
if target_exploited:
|
|
563
622
|
# Find target's first exploitation node
|
|
564
|
-
for target_node in target_data[
|
|
565
|
-
if target_node[
|
|
566
|
-
cred_username = node[
|
|
567
|
-
lateral_edges.append(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
623
|
+
for target_node in target_data["nodes"]:
|
|
624
|
+
if target_node["type"] == "exploitation":
|
|
625
|
+
cred_username = node["creds"][0].get("username", "")
|
|
626
|
+
lateral_edges.append(
|
|
627
|
+
{
|
|
628
|
+
"from_host": source_host,
|
|
629
|
+
"from_node": node["id"],
|
|
630
|
+
"to_host": target_host,
|
|
631
|
+
"to_node": target_node["id"],
|
|
632
|
+
"label": "lateral",
|
|
633
|
+
"credential": cred_username,
|
|
634
|
+
}
|
|
635
|
+
)
|
|
575
636
|
break
|
|
576
637
|
break # Only one lateral edge per source-target pair
|
|
577
638
|
|
|
578
639
|
# Sort hosts by score (highest first)
|
|
579
640
|
sorted_hosts = sorted(
|
|
580
|
-
host_chains.values(),
|
|
581
|
-
key=lambda x: x.get('score', 0),
|
|
582
|
-
reverse=True
|
|
641
|
+
host_chains.values(), key=lambda x: x.get("score", 0), reverse=True
|
|
583
642
|
)
|
|
584
643
|
|
|
585
644
|
# Calculate summary
|
|
586
|
-
total_nodes = sum(len(h[
|
|
587
|
-
total_internal_edges = sum(len(h[
|
|
645
|
+
total_nodes = sum(len(h["nodes"]) for h in sorted_hosts)
|
|
646
|
+
total_internal_edges = sum(len(h["internal_edges"]) for h in sorted_hosts)
|
|
588
647
|
hosts_with_exploitation = sum(
|
|
589
|
-
1
|
|
590
|
-
|
|
648
|
+
1
|
|
649
|
+
for h in sorted_hosts
|
|
650
|
+
if any(
|
|
651
|
+
n["type"] in ["exploitation", "credential", "post_exploitation"]
|
|
652
|
+
for n in h["nodes"]
|
|
653
|
+
)
|
|
591
654
|
)
|
|
592
655
|
|
|
593
656
|
return {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
657
|
+
"hosts": sorted_hosts,
|
|
658
|
+
"lateral_edges": lateral_edges,
|
|
659
|
+
"summary": {
|
|
660
|
+
"total_hosts": len(sorted_hosts),
|
|
661
|
+
"hosts_exploited": hosts_with_exploitation,
|
|
662
|
+
"total_nodes": total_nodes,
|
|
663
|
+
"total_internal_edges": total_internal_edges,
|
|
664
|
+
"lateral_movements": len(lateral_edges),
|
|
665
|
+
},
|
|
603
666
|
}
|
|
604
667
|
|
|
605
668
|
def _sanitize_mermaid_label(self, text: str) -> str:
|
|
@@ -609,20 +672,20 @@ class AttackChainAnalyzer:
|
|
|
609
672
|
# Replace characters that break Mermaid syntax
|
|
610
673
|
text = str(text)
|
|
611
674
|
text = text.replace('"', "'") # Double quotes to single
|
|
612
|
-
text = text.replace(
|
|
613
|
-
text = text.replace(
|
|
614
|
-
text = text.replace(
|
|
615
|
-
text = text.replace(
|
|
616
|
-
text = text.replace(
|
|
617
|
-
text = text.replace(
|
|
618
|
-
text = text.replace(
|
|
619
|
-
text = text.replace(
|
|
620
|
-
text = text.replace(
|
|
621
|
-
text = text.replace(
|
|
622
|
-
text = text.replace(
|
|
675
|
+
text = text.replace("[", "(") # Brackets to parens
|
|
676
|
+
text = text.replace("]", ")")
|
|
677
|
+
text = text.replace("{", "(")
|
|
678
|
+
text = text.replace("}", ")")
|
|
679
|
+
text = text.replace("<", "") # Remove angle brackets (except our <br/>)
|
|
680
|
+
text = text.replace(">", "")
|
|
681
|
+
text = text.replace("|", "-") # Pipe breaks edge labels
|
|
682
|
+
text = text.replace("#", "") # Hash can cause issues
|
|
683
|
+
text = text.replace("&", "and")
|
|
684
|
+
text = text.replace("\n", " ")
|
|
685
|
+
text = text.replace("\r", "")
|
|
623
686
|
# Limit length
|
|
624
687
|
if len(text) > 40:
|
|
625
|
-
text = text[:37] +
|
|
688
|
+
text = text[:37] + "..."
|
|
626
689
|
return text
|
|
627
690
|
|
|
628
691
|
def generate_host_centric_mermaid(self, chain: Dict) -> str:
|
|
@@ -630,8 +693,8 @@ class AttackChainAnalyzer:
|
|
|
630
693
|
Generate Mermaid.js diagram with subgraphs per host.
|
|
631
694
|
Shows internal attack progression and lateral movement.
|
|
632
695
|
"""
|
|
633
|
-
hosts = chain.get(
|
|
634
|
-
lateral_edges = chain.get(
|
|
696
|
+
hosts = chain.get("hosts", [])
|
|
697
|
+
lateral_edges = chain.get("lateral_edges", [])
|
|
635
698
|
|
|
636
699
|
if not hosts:
|
|
637
700
|
return ""
|
|
@@ -648,18 +711,20 @@ class AttackChainAnalyzer:
|
|
|
648
711
|
mermaid += " classDef vulnerability fill:#ffc107,stroke:#856404,color:#000\n"
|
|
649
712
|
mermaid += " classDef exploitation fill:#fd7e14,stroke:#c45d00,color:#fff\n"
|
|
650
713
|
mermaid += " classDef credential fill:#6f42c1,stroke:#3d1f66,color:#fff\n"
|
|
651
|
-
mermaid +=
|
|
714
|
+
mermaid += (
|
|
715
|
+
" classDef post_exploitation fill:#dc3545,stroke:#721c24,color:#fff\n\n"
|
|
716
|
+
)
|
|
652
717
|
|
|
653
718
|
# Generate subgraph for each host
|
|
654
719
|
for idx, host_data in enumerate(display_hosts):
|
|
655
|
-
host_ip = host_data[
|
|
656
|
-
hostname = host_data.get(
|
|
720
|
+
host_ip = host_data["host"]
|
|
721
|
+
hostname = host_data.get("hostname", "")
|
|
657
722
|
|
|
658
723
|
# Sanitize host IP for Mermaid ID
|
|
659
|
-
safe_host = host_ip.replace(
|
|
724
|
+
safe_host = host_ip.replace(".", "_").replace("-", "_").replace(":", "_")
|
|
660
725
|
# Ensure ID starts with letter
|
|
661
726
|
if safe_host[0].isdigit():
|
|
662
|
-
safe_host =
|
|
727
|
+
safe_host = "h" + safe_host
|
|
663
728
|
|
|
664
729
|
# Sanitize subgraph title
|
|
665
730
|
subgraph_title = self._sanitize_mermaid_label(host_ip)
|
|
@@ -671,11 +736,11 @@ class AttackChainAnalyzer:
|
|
|
671
736
|
mermaid += f" direction TB\n"
|
|
672
737
|
|
|
673
738
|
# Add nodes for this host
|
|
674
|
-
for node in host_data[
|
|
675
|
-
node_id = node[
|
|
676
|
-
label = self._sanitize_mermaid_label(node[
|
|
677
|
-
detail = self._sanitize_mermaid_label(node.get(
|
|
678
|
-
node_type = node[
|
|
739
|
+
for node in host_data["nodes"]:
|
|
740
|
+
node_id = node["id"]
|
|
741
|
+
label = self._sanitize_mermaid_label(node["label"])
|
|
742
|
+
detail = self._sanitize_mermaid_label(node.get("detail", ""))
|
|
743
|
+
node_type = node["type"]
|
|
679
744
|
|
|
680
745
|
# Format label - use line break for detail
|
|
681
746
|
if detail:
|
|
@@ -686,26 +751,26 @@ class AttackChainAnalyzer:
|
|
|
686
751
|
mermaid += f' {node_id}["{full_label}"]:::{node_type}\n'
|
|
687
752
|
|
|
688
753
|
# Add internal edges
|
|
689
|
-
for edge in host_data[
|
|
690
|
-
edge_label = self._sanitize_mermaid_label(edge.get(
|
|
754
|
+
for edge in host_data["internal_edges"]:
|
|
755
|
+
edge_label = self._sanitize_mermaid_label(edge.get("label", ""))
|
|
691
756
|
mermaid += f" {edge['from']} -->|{edge_label}| {edge['to']}\n"
|
|
692
757
|
|
|
693
758
|
mermaid += " end\n\n"
|
|
694
759
|
|
|
695
760
|
# Add lateral movement edges between subgraphs (only for displayed hosts)
|
|
696
|
-
displayed_host_ips = {h[
|
|
761
|
+
displayed_host_ips = {h["host"] for h in display_hosts}
|
|
697
762
|
if lateral_edges:
|
|
698
763
|
mermaid += " %% Lateral movement\n"
|
|
699
764
|
for edge in lateral_edges:
|
|
700
765
|
# Only show lateral edges between displayed hosts
|
|
701
|
-
if edge[
|
|
766
|
+
if edge["from_host"] not in displayed_host_ips:
|
|
702
767
|
continue
|
|
703
|
-
if edge[
|
|
768
|
+
if edge["to_host"] not in displayed_host_ips:
|
|
704
769
|
continue
|
|
705
770
|
|
|
706
|
-
from_node = edge[
|
|
707
|
-
to_node = edge[
|
|
708
|
-
cred = self._sanitize_mermaid_label(edge.get(
|
|
771
|
+
from_node = edge["from_node"]
|
|
772
|
+
to_node = edge["to_node"]
|
|
773
|
+
cred = self._sanitize_mermaid_label(edge.get("credential", ""))
|
|
709
774
|
label = f"lateral: {cred}" if cred else "lateral"
|
|
710
775
|
|
|
711
776
|
mermaid += f" {from_node} -.->|{label}| {to_node}\n"
|
|
@@ -714,40 +779,40 @@ class AttackChainAnalyzer:
|
|
|
714
779
|
|
|
715
780
|
def get_host_centric_summary(self, chain: Dict) -> Dict:
|
|
716
781
|
"""Generate summary statistics for host-centric attack chain."""
|
|
717
|
-
summary = chain.get(
|
|
718
|
-
hosts = chain.get(
|
|
719
|
-
lateral_edges = chain.get(
|
|
782
|
+
summary = chain.get("summary", {})
|
|
783
|
+
hosts = chain.get("hosts", [])
|
|
784
|
+
lateral_edges = chain.get("lateral_edges", [])
|
|
720
785
|
|
|
721
786
|
# Find the host with deepest attack progression
|
|
722
787
|
max_depth = 0
|
|
723
788
|
deepest_host = None
|
|
724
789
|
for host_data in hosts:
|
|
725
|
-
depth = len(host_data[
|
|
790
|
+
depth = len(host_data["nodes"])
|
|
726
791
|
if depth > max_depth:
|
|
727
792
|
max_depth = depth
|
|
728
|
-
deepest_host = host_data[
|
|
793
|
+
deepest_host = host_data["host"]
|
|
729
794
|
|
|
730
795
|
# Count hosts at each phase
|
|
731
796
|
phase_counts = {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
797
|
+
"discovery": 0,
|
|
798
|
+
"enumeration": 0,
|
|
799
|
+
"vulnerability": 0,
|
|
800
|
+
"exploitation": 0,
|
|
801
|
+
"credential": 0,
|
|
802
|
+
"post_exploitation": 0,
|
|
738
803
|
}
|
|
739
804
|
for host_data in hosts:
|
|
740
|
-
for node in host_data[
|
|
741
|
-
node_type = node[
|
|
805
|
+
for node in host_data["nodes"]:
|
|
806
|
+
node_type = node["type"]
|
|
742
807
|
if node_type in phase_counts:
|
|
743
808
|
phase_counts[node_type] += 1
|
|
744
809
|
|
|
745
810
|
return {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
811
|
+
"total_hosts": summary.get("total_hosts", len(hosts)),
|
|
812
|
+
"hosts_exploited": summary.get("hosts_exploited", 0),
|
|
813
|
+
"total_nodes": summary.get("total_nodes", 0),
|
|
814
|
+
"lateral_movements": len(lateral_edges),
|
|
815
|
+
"deepest_attack": max_depth,
|
|
816
|
+
"deepest_host": deepest_host,
|
|
817
|
+
"phase_counts": phase_counts,
|
|
753
818
|
}
|