souleyez 2.43.29__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9564 -2881
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +564 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +409 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +417 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +913 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +219 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +237 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23034 -10679
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
  353. souleyez-3.0.0.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-3.0.0.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
- 'reconnaissance': '#17a2b8',
16
- 'enumeration': '#28a745',
17
- 'exploitation': '#ffc107',
18
- 'post_exploitation': '#dc3545'
15
+ "reconnaissance": "#17a2b8",
16
+ "enumeration": "#28a745",
17
+ "exploitation": "#ffc107",
18
+ "post_exploitation": "#dc3545",
19
19
  }
20
-
21
- def build_attack_chain(self, evidence: Dict, findings: List[Dict],
22
- credentials: List[Dict]) -> Dict:
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('reconnaissance', [])
37
+ recon_items = evidence.get("reconnaissance", [])
37
38
  if recon_items:
38
39
  recon_node = {
39
- 'id': f'node{node_id}',
40
- 'label': f'Reconnaissance\\n{len(recon_items)} items',
41
- 'type': 'reconnaissance',
42
- 'count': len(recon_items)
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['id']
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('enumeration', [])
52
+ enum_items = evidence.get("enumeration", [])
52
53
  enum_nodes = {}
53
-
54
+
54
55
  for item in enum_items:
55
- host = item.get('host', 'unknown')
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
- 'id': f'node{node_id}',
61
- 'label': f'Enumeration\\n{host}',
62
- 'type': 'enumeration',
63
- 'host': host
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['id']
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
- 'from': recon_id,
73
- 'to': enum_node['id'],
74
- 'label': 'discovered'
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('exploitation', [])
77
+ exploit_items = evidence.get("exploitation", [])
79
78
  exploit_nodes = {}
80
-
79
+
81
80
  for item in exploit_items:
82
- host = item.get('host', 'unknown')
83
- service = item.get('service', 'service')
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
- 'id': f'node{node_id}',
90
- 'label': f'Exploit\\n{host}\\n{service}',
91
- 'type': 'exploitation',
92
- 'host': host,
93
- 'service': service
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['id']
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
- 'from': enum_nodes[host],
103
- 'to': exploit_node['id'],
104
- 'label': 'exploited'
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('host', 'unknown')
111
- service = cred.get('service', 'service')
112
- username = cred.get('username', 'user')
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
- 'id': f'node{node_id}',
119
- 'label': f'Credential\\n{username}@{host}',
120
- 'type': 'credential',
121
- 'host': host
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['id']
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
- 'from': exploit_nodes[exploit_key],
132
- 'to': cred_node['id'],
133
- 'label': 'obtained'
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('post_exploitation', [])
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('host', 'unknown')
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
- 'id': f'node{node_id}',
151
- 'label': f'Post-Exploit\\n{host}\\n{len(items)} items',
152
- 'type': 'post_exploitation',
153
- 'host': host
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
- 'from': cred_id,
164
- 'to': post_node['id'],
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
- 'from': exploit_id,
175
- 'to': post_node['id'],
176
- 'label': 'access'
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
- 'nodes': nodes,
182
- 'edges': edges,
183
- 'hosts': list(hosts_seen),
184
- 'phases': {
185
- 'reconnaissance': len(recon_items),
186
- 'enumeration': len(enum_items),
187
- 'exploitation': len(exploit_items),
188
- 'post_exploitation': len(post_items)
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['nodes']:
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['nodes']:
208
- node_id = node['id']
209
- label = node['label'].replace('\n', '<br/>')
210
- node_type = node['type']
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 == 'reconnaissance':
216
+ if node_type == "reconnaissance":
214
217
  mermaid += f" {node_id}[{label}]:::recon\n"
215
- elif node_type == 'enumeration':
218
+ elif node_type == "enumeration":
216
219
  mermaid += f" {node_id}[{label}]:::enum\n"
217
- elif node_type == 'exploitation':
220
+ elif node_type == "exploitation":
218
221
  mermaid += f" {node_id}[{label}]:::exploit\n"
219
- elif node_type == 'post_exploitation':
222
+ elif node_type == "post_exploitation":
220
223
  mermaid += f" {node_id}[{label}]:::post\n"
221
- elif node_type == 'credential':
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['edges']:
227
- label = edge.get('label', '')
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
- 'total_nodes': len(chain['nodes']),
236
- 'total_edges': len(chain['edges']),
237
- 'hosts_compromised': len(chain['hosts']),
238
- 'phases_active': sum(1 for count in chain['phases'].values() if count > 0),
239
- 'longest_path': self._calculate_longest_path(chain),
240
- 'critical_nodes': self._identify_critical_nodes(chain)
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['edges']:
246
- return len(chain['nodes'])
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['edges']:
251
- if edge['from'] not in adj:
252
- adj[edge['from']] = []
253
- adj[edge['from']].append(edge['to'])
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['to'] for edge in chain['edges'])
257
- start_nodes = [node['id'] for node in chain['nodes'] if node['id'] not in has_incoming]
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['edges']:
285
- node_degree[edge['from']] = node_degree.get(edge['from'], 0) + 1
286
- node_degree[edge['to']] = node_degree.get(edge['to'], 0) + 1
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(self, evidence: Dict, findings: List[Dict],
293
- credentials: List[Dict], attack_surface: Dict = None) -> Dict:
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('hosts'):
322
- for h in attack_surface['hosts']:
323
- host_ip = h.get('host')
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
- 'hostname': h.get('hostname'),
327
- 'score': h.get('score', 0),
328
- 'services': h.get('services', []),
329
- 'findings_count': h.get('findings', 0),
330
- 'critical_findings': h.get('critical_findings', 0),
331
- 'open_ports': h.get('open_ports', 0)
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('ip_address')
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('ip_address') or c.get('host')
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 ['reconnaissance', 'enumeration', 'exploitation', 'post_exploitation']:
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('host') or item.get('ip_address')
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] = {'recon': [], 'enum': [], 'exploit': [], 'post': []}
360
- if phase == 'reconnaissance':
361
- evidence_by_host[host]['recon'].append(item)
362
- elif phase == 'enumeration':
363
- evidence_by_host[host]['enum'].append(item)
364
- elif phase == 'exploitation':
365
- evidence_by_host[host]['exploit'].append(item)
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]['post'].append(item)
387
+ evidence_by_host[host]["post"].append(item)
368
388
 
369
389
  # Get all hosts (union of all sources)
370
- all_hosts = set(host_info.keys()) | set(findings_by_host.keys()) | set(creds_by_host.keys()) | set(evidence_by_host.keys())
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(host, {'recon': [], 'enum': [], 'exploit': [], 'post': []})
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('services', [])
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 = [f for f in host_findings if f.get('severity') == 'critical']
385
- high_findings = [f for f in host_findings if f.get('severity') == 'high']
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(host_findings or host_creds or services or any(host_evidence.values()))
389
- has_services = service_count > 0 or host_evidence['enum']
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 or
393
- host_evidence['exploit'] or
394
- any(f.get('title', '').lower().find('exploit') >= 0 for f in host_findings) or
395
- any(f.get('finding_type') in ['exploitation', 'data_breach'] for f in host_findings)
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['post'])
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 = [s for s in services if s.get('status') == 'exploited']
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
- 'host': host,
409
- 'hostname': info.get('hostname'),
410
- 'nodes': [],
411
- 'internal_edges': [],
412
- 'score': info.get('score', 0)
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['recon'])
459
+ recon_count = len(host_evidence["recon"])
421
460
  if recon_count > 0:
422
- detail = f'{recon_count} scans'
423
- elif info.get('open_ports'):
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 = 'Host identified'
427
- host_data['nodes'].append({
428
- 'id': node_id,
429
- 'label': 'Discovery',
430
- 'detail': detail,
431
- 'type': 'discovery',
432
- 'phase': 1
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['enum'])
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['enum']:
444
- svc = item.get('service') or item.get('tool', '')
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 = ', '.join(list(svc_names)[:3]) if svc_names else f'{enum_count} items'
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'{service_count} services'
494
+ detail = f"{service_count} services"
450
495
  else:
451
- detail = 'Services scanned'
452
- host_data['nodes'].append({
453
- 'id': node_id,
454
- 'label': 'Enumeration',
455
- 'detail': detail,
456
- 'type': 'enumeration',
457
- 'phase': 2
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['internal_edges'].append({
461
- 'from': prev_node_id, 'to': node_id, 'label': 'scanned'
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('title', 'Vulnerability')[:25]
517
+ top_title = top_vuln.get("title", "Vulnerability")[:25]
471
518
  vuln_detail = []
472
519
  if critical_findings:
473
- vuln_detail.append(f'{len(critical_findings)} critical')
520
+ vuln_detail.append(f"{len(critical_findings)} critical")
474
521
  if high_findings:
475
- vuln_detail.append(f'{len(high_findings)} high')
476
- host_data['nodes'].append({
477
- 'id': node_id,
478
- 'label': top_title,
479
- 'detail': ', '.join(vuln_detail),
480
- 'type': 'vulnerability',
481
- 'phase': 3
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['internal_edges'].append({
485
- 'from': prev_node_id, 'to': node_id, 'label': 'found'
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['exploit'])
541
+ exploit_count = len(host_evidence["exploit"])
493
542
  if exploited_services:
494
- svc_names = [s.get('service', 'service') for s in exploited_services[:2]]
495
- detail = ', '.join(svc_names)
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'{exploit_count} exploits'
548
+ detail = f"{exploit_count} exploits"
498
549
  elif has_creds:
499
- detail = 'Access gained'
550
+ detail = "Access gained"
500
551
  else:
501
- detail = 'Exploited'
502
- host_data['nodes'].append({
503
- 'id': node_id,
504
- 'label': 'Exploited',
505
- 'detail': detail,
506
- 'type': 'exploitation',
507
- 'phase': 4
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['internal_edges'].append({
511
- 'from': prev_node_id, 'to': node_id, 'label': 'exploited'
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('username', 'user') for c in host_creds)
519
- host_data['nodes'].append({
520
- 'id': node_id,
521
- 'label': 'Credentials',
522
- 'detail': ', '.join(list(usernames)[:3]),
523
- 'type': 'credential',
524
- 'phase': 5,
525
- 'creds': host_creds
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['internal_edges'].append({
529
- 'from': prev_node_id, 'to': node_id, 'label': 'dumped'
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['nodes'].append({
537
- 'id': node_id,
538
- 'label': 'Post-Exploit',
539
- 'detail': f"{len(host_evidence['post'])} actions",
540
- 'type': 'post_exploitation',
541
- 'phase': 6
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['internal_edges'].append({
545
- 'from': prev_node_id, 'to': node_id, 'label': 'accessed'
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['nodes']:
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['nodes']:
555
- if node['type'] == 'credential' and node.get('creds'):
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(n['type'] in ['exploitation', 'post_exploitation']
561
- for n in target_data['nodes'])
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['nodes']:
565
- if target_node['type'] == 'exploitation':
566
- cred_username = node['creds'][0].get('username', '')
567
- lateral_edges.append({
568
- 'from_host': source_host,
569
- 'from_node': node['id'],
570
- 'to_host': target_host,
571
- 'to_node': target_node['id'],
572
- 'label': 'lateral',
573
- 'credential': cred_username
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['nodes']) for h in sorted_hosts)
587
- total_internal_edges = sum(len(h['internal_edges']) for h in sorted_hosts)
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 for h in sorted_hosts
590
- if any(n['type'] in ['exploitation', 'credential', 'post_exploitation'] for n in h['nodes'])
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
- 'hosts': sorted_hosts,
595
- 'lateral_edges': lateral_edges,
596
- 'summary': {
597
- 'total_hosts': len(sorted_hosts),
598
- 'hosts_exploited': hosts_with_exploitation,
599
- 'total_nodes': total_nodes,
600
- 'total_internal_edges': total_internal_edges,
601
- 'lateral_movements': len(lateral_edges)
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('[', '(') # Brackets to parens
613
- text = text.replace(']', ')')
614
- text = text.replace('{', '(')
615
- text = text.replace('}', ')')
616
- text = text.replace('<', '') # Remove angle brackets (except our <br/>)
617
- text = text.replace('>', '')
618
- text = text.replace('|', '-') # Pipe breaks edge labels
619
- text = text.replace('#', '') # Hash can cause issues
620
- text = text.replace('&', 'and')
621
- text = text.replace('\n', ' ')
622
- text = text.replace('\r', '')
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('hosts', [])
634
- lateral_edges = chain.get('lateral_edges', [])
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 += " classDef post_exploitation fill:#dc3545,stroke:#721c24,color:#fff\n\n"
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['host']
656
- hostname = host_data.get('hostname', '')
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('.', '_').replace('-', '_').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 = 'h' + 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['nodes']:
675
- node_id = node['id']
676
- label = self._sanitize_mermaid_label(node['label'])
677
- detail = self._sanitize_mermaid_label(node.get('detail', ''))
678
- node_type = node['type']
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['internal_edges']:
690
- edge_label = self._sanitize_mermaid_label(edge.get('label', ''))
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['host'] for h in display_hosts}
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['from_host'] not in displayed_host_ips:
766
+ if edge["from_host"] not in displayed_host_ips:
702
767
  continue
703
- if edge['to_host'] not in displayed_host_ips:
768
+ if edge["to_host"] not in displayed_host_ips:
704
769
  continue
705
770
 
706
- from_node = edge['from_node']
707
- to_node = edge['to_node']
708
- cred = self._sanitize_mermaid_label(edge.get('credential', ''))
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('summary', {})
718
- hosts = chain.get('hosts', [])
719
- lateral_edges = chain.get('lateral_edges', [])
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['nodes'])
790
+ depth = len(host_data["nodes"])
726
791
  if depth > max_depth:
727
792
  max_depth = depth
728
- deepest_host = host_data['host']
793
+ deepest_host = host_data["host"]
729
794
 
730
795
  # Count hosts at each phase
731
796
  phase_counts = {
732
- 'discovery': 0,
733
- 'enumeration': 0,
734
- 'vulnerability': 0,
735
- 'exploitation': 0,
736
- 'credential': 0,
737
- 'post_exploitation': 0
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['nodes']:
741
- node_type = node['type']
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
- 'total_hosts': summary.get('total_hosts', len(hosts)),
747
- 'hosts_exploited': summary.get('hosts_exploited', 0),
748
- 'total_nodes': summary.get('total_nodes', 0),
749
- 'lateral_movements': len(lateral_edges),
750
- 'deepest_attack': max_depth,
751
- 'deepest_host': deepest_host,
752
- 'phase_counts': phase_counts
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
  }