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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +9526 -2879
  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 +563 -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 +408 -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 +371 -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 +854 -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 +173 -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 +223 -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 +22827 -10678
  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-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -9,516 +9,473 @@ from typing import Dict, List
9
9
 
10
10
  class ChartGenerator:
11
11
  """Generate Chart.js chart configurations."""
12
-
12
+
13
13
  def __init__(self):
14
14
  self.colors = {
15
- 'critical': '#dc3545',
16
- 'high': '#fd7e14',
17
- 'medium': '#ffc107',
18
- 'low': '#28a745',
19
- 'info': '#17a2b8'
15
+ "critical": "#dc3545",
16
+ "high": "#fd7e14",
17
+ "medium": "#ffc107",
18
+ "low": "#28a745",
19
+ "info": "#17a2b8",
20
20
  }
21
-
21
+
22
22
  def severity_distribution_chart(self, findings_by_severity: Dict) -> str:
23
23
  """Generate pie chart showing finding distribution by severity."""
24
24
  data = {
25
- 'Critical': len(findings_by_severity.get('critical', [])),
26
- 'High': len(findings_by_severity.get('high', [])),
27
- 'Medium': len(findings_by_severity.get('medium', [])),
28
- 'Low': len(findings_by_severity.get('low', [])),
29
- 'Info': len(findings_by_severity.get('info', []))
25
+ "Critical": len(findings_by_severity.get("critical", [])),
26
+ "High": len(findings_by_severity.get("high", [])),
27
+ "Medium": len(findings_by_severity.get("medium", [])),
28
+ "Low": len(findings_by_severity.get("low", [])),
29
+ "Info": len(findings_by_severity.get("info", [])),
30
30
  }
31
-
31
+
32
32
  # Filter out zero values
33
33
  filtered_data = {k: v for k, v in data.items() if v > 0}
34
-
34
+
35
35
  if not filtered_data:
36
36
  return ""
37
-
37
+
38
38
  labels = list(filtered_data.keys())
39
39
  values = list(filtered_data.values())
40
40
  colors = [self.colors[k.lower()] for k in labels]
41
-
41
+
42
42
  config = {
43
- 'type': 'doughnut',
44
- 'data': {
45
- 'labels': labels,
46
- 'datasets': [{
47
- 'data': values,
48
- 'backgroundColor': colors,
49
- 'borderWidth': 2,
50
- 'borderColor': '#fff'
51
- }]
43
+ "type": "doughnut",
44
+ "data": {
45
+ "labels": labels,
46
+ "datasets": [
47
+ {
48
+ "data": values,
49
+ "backgroundColor": colors,
50
+ "borderWidth": 2,
51
+ "borderColor": "#fff",
52
+ }
53
+ ],
52
54
  },
53
- 'options': {
54
- 'responsive': True,
55
- 'maintainAspectRatio': True,
56
- 'plugins': {
57
- 'legend': {
58
- 'position': 'right',
59
- 'labels': {
60
- 'font': {
61
- 'size': 14
62
- }
63
- }
55
+ "options": {
56
+ "responsive": True,
57
+ "maintainAspectRatio": True,
58
+ "plugins": {
59
+ "legend": {"position": "right", "labels": {"font": {"size": 14}}},
60
+ "title": {
61
+ "display": True,
62
+ "text": "Findings by Severity",
63
+ "font": {"size": 16, "weight": "bold"},
64
64
  },
65
- 'title': {
66
- 'display': True,
67
- 'text': 'Findings by Severity',
68
- 'font': {
69
- 'size': 16,
70
- 'weight': 'bold'
71
- }
72
- }
73
- }
74
- }
65
+ },
66
+ },
75
67
  }
76
-
68
+
77
69
  return json.dumps(config)
78
-
70
+
79
71
  def host_impact_chart(self, attack_surface: Dict) -> str:
80
72
  """Generate bar chart showing findings per host."""
81
- hosts = attack_surface.get('hosts', [])
82
-
73
+ hosts = attack_surface.get("hosts", [])
74
+
83
75
  # Sort by findings count (descending) and take top 10
84
- sorted_hosts = sorted(hosts, key=lambda h: h.get('findings', 0), reverse=True)[:10]
85
-
76
+ sorted_hosts = sorted(hosts, key=lambda h: h.get("findings", 0), reverse=True)[
77
+ :10
78
+ ]
79
+
86
80
  if not sorted_hosts:
87
81
  return ""
88
-
82
+
89
83
  labels = []
90
84
  critical_data = []
91
85
  high_data = []
92
86
  other_data = []
93
-
87
+
94
88
  for host in sorted_hosts:
95
89
  # Use hostname or IP
96
- label = host.get('hostname') or host.get('host', 'Unknown')
90
+ label = host.get("hostname") or host.get("host", "Unknown")
97
91
  labels.append(label)
98
-
99
- critical_data.append(host.get('critical_findings', 0))
100
- high_findings = host.get('findings', 0) - host.get('critical_findings', 0)
92
+
93
+ critical_data.append(host.get("critical_findings", 0))
94
+ high_findings = host.get("findings", 0) - host.get("critical_findings", 0)
101
95
  # Assume 30% of non-critical are high, rest are medium/low (simplified)
102
96
  high_count = int(high_findings * 0.3)
103
97
  other_count = high_findings - high_count
104
-
98
+
105
99
  high_data.append(high_count)
106
100
  other_data.append(other_count)
107
-
101
+
108
102
  config = {
109
- 'type': 'bar',
110
- 'data': {
111
- 'labels': labels,
112
- 'datasets': [
103
+ "type": "bar",
104
+ "data": {
105
+ "labels": labels,
106
+ "datasets": [
113
107
  {
114
- 'label': 'Critical',
115
- 'data': critical_data,
116
- 'backgroundColor': self.colors['critical'],
117
- 'stack': 'stack0'
108
+ "label": "Critical",
109
+ "data": critical_data,
110
+ "backgroundColor": self.colors["critical"],
111
+ "stack": "stack0",
118
112
  },
119
113
  {
120
- 'label': 'High',
121
- 'data': high_data,
122
- 'backgroundColor': self.colors['high'],
123
- 'stack': 'stack0'
114
+ "label": "High",
115
+ "data": high_data,
116
+ "backgroundColor": self.colors["high"],
117
+ "stack": "stack0",
124
118
  },
125
119
  {
126
- 'label': 'Medium/Low',
127
- 'data': other_data,
128
- 'backgroundColor': self.colors['medium'],
129
- 'stack': 'stack0'
130
- }
131
- ]
120
+ "label": "Medium/Low",
121
+ "data": other_data,
122
+ "backgroundColor": self.colors["medium"],
123
+ "stack": "stack0",
124
+ },
125
+ ],
132
126
  },
133
- 'options': {
134
- 'responsive': True,
135
- 'maintainAspectRatio': True,
136
- 'indexAxis': 'y',
137
- 'plugins': {
138
- 'legend': {
139
- 'display': True,
140
- 'position': 'top'
127
+ "options": {
128
+ "responsive": True,
129
+ "maintainAspectRatio": True,
130
+ "indexAxis": "y",
131
+ "plugins": {
132
+ "legend": {"display": True, "position": "top"},
133
+ "title": {
134
+ "display": True,
135
+ "text": "Top 10 Hosts by Finding Count",
136
+ "font": {"size": 16, "weight": "bold"},
141
137
  },
142
- 'title': {
143
- 'display': True,
144
- 'text': 'Top 10 Hosts by Finding Count',
145
- 'font': {
146
- 'size': 16,
147
- 'weight': 'bold'
148
- }
149
- }
150
138
  },
151
- 'scales': {
152
- 'x': {
153
- 'stacked': True,
154
- 'title': {
155
- 'display': True,
156
- 'text': 'Number of Findings'
157
- }
139
+ "scales": {
140
+ "x": {
141
+ "stacked": True,
142
+ "title": {"display": True, "text": "Number of Findings"},
158
143
  },
159
- 'y': {
160
- 'stacked': True
161
- }
162
- }
163
- }
144
+ "y": {"stacked": True},
145
+ },
146
+ },
164
147
  }
165
-
148
+
166
149
  return json.dumps(config)
167
-
150
+
168
151
  def exploitation_progress_chart(self, attack_surface: Dict) -> str:
169
152
  """Generate gauge/progress chart for exploitation rate."""
170
- overview = attack_surface.get('overview', {})
171
- total_services = overview.get('total_services', 0)
172
- exploited = overview.get('exploited_services', 0)
173
-
153
+ overview = attack_surface.get("overview", {})
154
+ total_services = overview.get("total_services", 0)
155
+ exploited = overview.get("exploited_services", 0)
156
+
174
157
  if total_services == 0:
175
158
  return ""
176
-
159
+
177
160
  exploitation_rate = round((exploited / total_services) * 100, 1)
178
161
  remaining = 100 - exploitation_rate
179
-
162
+
180
163
  # Determine color based on rate
181
164
  if exploitation_rate >= 50:
182
- color = self.colors['critical']
165
+ color = self.colors["critical"]
183
166
  elif exploitation_rate >= 25:
184
- color = self.colors['high']
167
+ color = self.colors["high"]
185
168
  elif exploitation_rate >= 10:
186
- color = self.colors['medium']
169
+ color = self.colors["medium"]
187
170
  else:
188
- color = self.colors['low']
189
-
171
+ color = self.colors["low"]
172
+
190
173
  config = {
191
- 'type': 'doughnut',
192
- 'data': {
193
- 'labels': ['Exploited', 'Not Exploited'],
194
- 'datasets': [{
195
- 'data': [exploitation_rate, remaining],
196
- 'backgroundColor': [color, '#e9ecef'],
197
- 'borderWidth': 0,
198
- 'circumference': 180,
199
- 'rotation': 270
200
- }]
174
+ "type": "doughnut",
175
+ "data": {
176
+ "labels": ["Exploited", "Not Exploited"],
177
+ "datasets": [
178
+ {
179
+ "data": [exploitation_rate, remaining],
180
+ "backgroundColor": [color, "#e9ecef"],
181
+ "borderWidth": 0,
182
+ "circumference": 180,
183
+ "rotation": 270,
184
+ }
185
+ ],
201
186
  },
202
- 'options': {
203
- 'responsive': True,
204
- 'maintainAspectRatio': True,
205
- 'plugins': {
206
- 'legend': {
207
- 'display': False
187
+ "options": {
188
+ "responsive": True,
189
+ "maintainAspectRatio": True,
190
+ "plugins": {
191
+ "legend": {"display": False},
192
+ "title": {
193
+ "display": True,
194
+ "text": f"Exploitation Rate: {exploitation_rate}%",
195
+ "font": {"size": 16, "weight": "bold"},
208
196
  },
209
- 'title': {
210
- 'display': True,
211
- 'text': f'Exploitation Rate: {exploitation_rate}%',
212
- 'font': {
213
- 'size': 16,
214
- 'weight': 'bold'
215
- }
216
- },
217
- 'tooltip': {
218
- 'enabled': True
219
- }
220
- }
221
- }
197
+ "tooltip": {"enabled": True},
198
+ },
199
+ },
222
200
  }
223
-
201
+
224
202
  return json.dumps(config)
225
-
203
+
226
204
  def timeline_chart(self, evidence: Dict) -> str:
227
205
  """Generate timeline chart showing findings discovered over time."""
228
206
  from datetime import datetime
229
-
207
+
230
208
  # Group evidence by date
231
209
  timeline_data = {}
232
-
210
+
233
211
  for phase, items in evidence.items():
234
212
  if isinstance(items, list):
235
213
  for item in items:
236
- timestamp = item.get('timestamp', '')
214
+ timestamp = item.get("timestamp", "")
237
215
  if timestamp:
238
216
  # Extract date (YYYY-MM-DD)
239
217
  date = timestamp[:10] if len(timestamp) >= 10 else timestamp
240
218
  if date not in timeline_data:
241
219
  timeline_data[date] = 0
242
220
  timeline_data[date] += 1
243
-
221
+
244
222
  if not timeline_data:
245
223
  return ""
246
-
224
+
247
225
  # Sort by date
248
226
  sorted_dates = sorted(timeline_data.keys())
249
227
  counts = [timeline_data[date] for date in sorted_dates]
250
-
228
+
251
229
  config = {
252
- 'type': 'line',
253
- 'data': {
254
- 'labels': sorted_dates,
255
- 'datasets': [{
256
- 'label': 'Evidence Items Collected',
257
- 'data': counts,
258
- 'borderColor': self.colors['info'],
259
- 'backgroundColor': 'rgba(23, 162, 184, 0.1)',
260
- 'fill': True,
261
- 'tension': 0.3,
262
- 'pointRadius': 5,
263
- 'pointHoverRadius': 7
264
- }]
230
+ "type": "line",
231
+ "data": {
232
+ "labels": sorted_dates,
233
+ "datasets": [
234
+ {
235
+ "label": "Evidence Items Collected",
236
+ "data": counts,
237
+ "borderColor": self.colors["info"],
238
+ "backgroundColor": "rgba(23, 162, 184, 0.1)",
239
+ "fill": True,
240
+ "tension": 0.3,
241
+ "pointRadius": 5,
242
+ "pointHoverRadius": 7,
243
+ }
244
+ ],
265
245
  },
266
- 'options': {
267
- 'responsive': True,
268
- 'maintainAspectRatio': True,
269
- 'plugins': {
270
- 'legend': {
271
- 'display': True,
272
- 'position': 'top'
246
+ "options": {
247
+ "responsive": True,
248
+ "maintainAspectRatio": True,
249
+ "plugins": {
250
+ "legend": {"display": True, "position": "top"},
251
+ "title": {
252
+ "display": True,
253
+ "text": "Evidence Collection Timeline",
254
+ "font": {"size": 16, "weight": "bold"},
273
255
  },
274
- 'title': {
275
- 'display': True,
276
- 'text': 'Evidence Collection Timeline',
277
- 'font': {
278
- 'size': 16,
279
- 'weight': 'bold'
280
- }
281
- }
282
256
  },
283
- 'scales': {
284
- 'y': {
285
- 'beginAtZero': True,
286
- 'title': {
287
- 'display': True,
288
- 'text': 'Items Collected'
289
- }
257
+ "scales": {
258
+ "y": {
259
+ "beginAtZero": True,
260
+ "title": {"display": True, "text": "Items Collected"},
290
261
  },
291
- 'x': {
292
- 'title': {
293
- 'display': True,
294
- 'text': 'Date'
295
- }
296
- }
297
- }
298
- }
262
+ "x": {"title": {"display": True, "text": "Date"}},
263
+ },
264
+ },
299
265
  }
300
-
266
+
301
267
  return json.dumps(config)
302
-
268
+
303
269
  def evidence_by_phase_chart(self, evidence_counts: Dict) -> str:
304
270
  """Generate stacked bar chart for evidence by phase."""
305
271
  if not evidence_counts:
306
272
  return ""
307
-
308
- phases = ['Reconnaissance', 'Enumeration', 'Exploitation', 'Post-Exploitation']
273
+
274
+ phases = ["Reconnaissance", "Enumeration", "Exploitation", "Post-Exploitation"]
309
275
  counts = [
310
- evidence_counts.get('reconnaissance', 0),
311
- evidence_counts.get('enumeration', 0),
312
- evidence_counts.get('exploitation', 0),
313
- evidence_counts.get('post_exploitation', 0)
276
+ evidence_counts.get("reconnaissance", 0),
277
+ evidence_counts.get("enumeration", 0),
278
+ evidence_counts.get("exploitation", 0),
279
+ evidence_counts.get("post_exploitation", 0),
314
280
  ]
315
-
281
+
316
282
  if sum(counts) == 0:
317
283
  return ""
318
-
284
+
319
285
  config = {
320
- 'type': 'bar',
321
- 'data': {
322
- 'labels': phases,
323
- 'datasets': [{
324
- 'label': 'Evidence Count',
325
- 'data': counts,
326
- 'backgroundColor': [
327
- self.colors['info'],
328
- self.colors['low'],
329
- self.colors['medium'],
330
- self.colors['high']
331
- ],
332
- 'borderWidth': 1
333
- }]
286
+ "type": "bar",
287
+ "data": {
288
+ "labels": phases,
289
+ "datasets": [
290
+ {
291
+ "label": "Evidence Count",
292
+ "data": counts,
293
+ "backgroundColor": [
294
+ self.colors["info"],
295
+ self.colors["low"],
296
+ self.colors["medium"],
297
+ self.colors["high"],
298
+ ],
299
+ "borderWidth": 1,
300
+ }
301
+ ],
334
302
  },
335
- 'options': {
336
- 'responsive': True,
337
- 'maintainAspectRatio': True,
338
- 'plugins': {
339
- 'legend': {
340
- 'display': False
303
+ "options": {
304
+ "responsive": True,
305
+ "maintainAspectRatio": True,
306
+ "plugins": {
307
+ "legend": {"display": False},
308
+ "title": {
309
+ "display": True,
310
+ "text": "Evidence by Testing Phase",
311
+ "font": {"size": 16, "weight": "bold"},
341
312
  },
342
- 'title': {
343
- 'display': True,
344
- 'text': 'Evidence by Testing Phase',
345
- 'font': {
346
- 'size': 16,
347
- 'weight': 'bold'
348
- }
349
- }
350
313
  },
351
- 'scales': {
352
- 'y': {
353
- 'beginAtZero': True,
354
- 'title': {
355
- 'display': True,
356
- 'text': 'Evidence Items'
357
- }
314
+ "scales": {
315
+ "y": {
316
+ "beginAtZero": True,
317
+ "title": {"display": True, "text": "Evidence Items"},
358
318
  }
359
- }
360
- }
319
+ },
320
+ },
361
321
  }
362
-
322
+
363
323
  return json.dumps(config)
364
-
324
+
365
325
  def service_exposure_chart(self, attack_surface: Dict) -> str:
366
326
  """Generate chart showing service exposure distribution."""
367
- hosts = attack_surface.get('hosts', [])
368
-
327
+ hosts = attack_surface.get("hosts", [])
328
+
369
329
  if not hosts:
370
330
  return ""
371
-
331
+
372
332
  # Count services across all hosts
373
333
  service_counts = {}
374
334
  for host in hosts:
375
- services_data = host.get('services', [])
335
+ services_data = host.get("services", [])
376
336
  if isinstance(services_data, list):
377
337
  for service in services_data:
378
- service_name = service.get('service', 'unknown')
379
- service_counts[service_name] = service_counts.get(service_name, 0) + 1
380
-
338
+ service_name = service.get("service", "unknown")
339
+ service_counts[service_name] = (
340
+ service_counts.get(service_name, 0) + 1
341
+ )
342
+
381
343
  if not service_counts:
382
344
  return ""
383
-
345
+
384
346
  # Take top 10 services
385
- sorted_services = sorted(service_counts.items(), key=lambda x: x[1], reverse=True)[:10]
347
+ sorted_services = sorted(
348
+ service_counts.items(), key=lambda x: x[1], reverse=True
349
+ )[:10]
386
350
  labels = [s[0] for s in sorted_services]
387
351
  counts = [s[1] for s in sorted_services]
388
-
352
+
389
353
  config = {
390
- 'type': 'bar',
391
- 'data': {
392
- 'labels': labels,
393
- 'datasets': [{
394
- 'label': 'Occurrences',
395
- 'data': counts,
396
- 'backgroundColor': self.colors['info'],
397
- 'borderWidth': 1
398
- }]
354
+ "type": "bar",
355
+ "data": {
356
+ "labels": labels,
357
+ "datasets": [
358
+ {
359
+ "label": "Occurrences",
360
+ "data": counts,
361
+ "backgroundColor": self.colors["info"],
362
+ "borderWidth": 1,
363
+ }
364
+ ],
399
365
  },
400
- 'options': {
401
- 'responsive': True,
402
- 'maintainAspectRatio': True,
403
- 'indexAxis': 'y',
404
- 'plugins': {
405
- 'legend': {
406
- 'display': False
366
+ "options": {
367
+ "responsive": True,
368
+ "maintainAspectRatio": True,
369
+ "indexAxis": "y",
370
+ "plugins": {
371
+ "legend": {"display": False},
372
+ "title": {
373
+ "display": True,
374
+ "text": "Top 10 Exposed Services",
375
+ "font": {"size": 16, "weight": "bold"},
407
376
  },
408
- 'title': {
409
- 'display': True,
410
- 'text': 'Top 10 Exposed Services',
411
- 'font': {
412
- 'size': 16,
413
- 'weight': 'bold'
414
- }
415
- }
416
377
  },
417
- 'scales': {
418
- 'x': {
419
- 'beginAtZero': True,
420
- 'title': {
421
- 'display': True,
422
- 'text': 'Number of Instances'
423
- }
378
+ "scales": {
379
+ "x": {
380
+ "beginAtZero": True,
381
+ "title": {"display": True, "text": "Number of Instances"},
424
382
  }
425
- }
426
- }
383
+ },
384
+ },
427
385
  }
428
-
386
+
429
387
  return json.dumps(config)
430
-
388
+
431
389
  def credentials_by_service_chart(self, credentials: List[Dict]) -> str:
432
390
  """Generate chart showing credentials found by service type."""
433
391
  if not credentials:
434
392
  return ""
435
-
393
+
436
394
  service_counts = {}
437
395
  for cred in credentials:
438
- service = cred.get('service', 'unknown')
396
+ service = cred.get("service", "unknown")
439
397
  service_counts[service] = service_counts.get(service, 0) + 1
440
-
398
+
441
399
  if not service_counts:
442
400
  return ""
443
-
401
+
444
402
  labels = list(service_counts.keys())
445
403
  counts = list(service_counts.values())
446
-
404
+
447
405
  config = {
448
- 'type': 'pie',
449
- 'data': {
450
- 'labels': labels,
451
- 'datasets': [{
452
- 'data': counts,
453
- 'backgroundColor': [
454
- self.colors['critical'],
455
- self.colors['high'],
456
- self.colors['medium'],
457
- self.colors['low'],
458
- self.colors['info'],
459
- '#6c757d',
460
- '#17a2b8',
461
- '#28a745'
462
- ],
463
- 'borderWidth': 2,
464
- 'borderColor': '#fff'
465
- }]
406
+ "type": "pie",
407
+ "data": {
408
+ "labels": labels,
409
+ "datasets": [
410
+ {
411
+ "data": counts,
412
+ "backgroundColor": [
413
+ self.colors["critical"],
414
+ self.colors["high"],
415
+ self.colors["medium"],
416
+ self.colors["low"],
417
+ self.colors["info"],
418
+ "#6c757d",
419
+ "#17a2b8",
420
+ "#28a745",
421
+ ],
422
+ "borderWidth": 2,
423
+ "borderColor": "#fff",
424
+ }
425
+ ],
466
426
  },
467
- 'options': {
468
- 'responsive': True,
469
- 'maintainAspectRatio': True,
470
- 'plugins': {
471
- 'legend': {
472
- 'position': 'right'
427
+ "options": {
428
+ "responsive": True,
429
+ "maintainAspectRatio": True,
430
+ "plugins": {
431
+ "legend": {"position": "right"},
432
+ "title": {
433
+ "display": True,
434
+ "text": "Credentials by Service",
435
+ "font": {"size": 16, "weight": "bold"},
473
436
  },
474
- 'title': {
475
- 'display': True,
476
- 'text': 'Credentials by Service',
477
- 'font': {
478
- 'size': 16,
479
- 'weight': 'bold'
480
- }
481
- }
482
- }
483
- }
437
+ },
438
+ },
484
439
  }
485
-
440
+
486
441
  return json.dumps(config)
487
-
442
+
488
443
  def generate_all_charts(self, data: Dict) -> Dict:
489
444
  """Generate all chart configurations."""
490
445
  charts = {}
491
-
446
+
492
447
  # Phase 1 charts
493
- severity_chart = self.severity_distribution_chart(data['findings_by_severity'])
448
+ severity_chart = self.severity_distribution_chart(data["findings_by_severity"])
494
449
  if severity_chart:
495
- charts['severity_distribution'] = severity_chart
496
-
497
- host_chart = self.host_impact_chart(data['attack_surface'])
450
+ charts["severity_distribution"] = severity_chart
451
+
452
+ host_chart = self.host_impact_chart(data["attack_surface"])
498
453
  if host_chart:
499
- charts['host_impact'] = host_chart
500
-
501
- exploitation_chart = self.exploitation_progress_chart(data['attack_surface'])
454
+ charts["host_impact"] = host_chart
455
+
456
+ exploitation_chart = self.exploitation_progress_chart(data["attack_surface"])
502
457
  if exploitation_chart:
503
- charts['exploitation_progress'] = exploitation_chart
504
-
458
+ charts["exploitation_progress"] = exploitation_chart
459
+
505
460
  # Phase 2 charts
506
- timeline = self.timeline_chart(data.get('evidence', {}))
461
+ timeline = self.timeline_chart(data.get("evidence", {}))
507
462
  if timeline:
508
- charts['timeline'] = timeline
509
-
510
- evidence_phase = self.evidence_by_phase_chart(data.get('evidence_counts', {}))
463
+ charts["timeline"] = timeline
464
+
465
+ evidence_phase = self.evidence_by_phase_chart(data.get("evidence_counts", {}))
511
466
  if evidence_phase:
512
- charts['evidence_by_phase'] = evidence_phase
513
-
514
- service_exposure = self.service_exposure_chart(data['attack_surface'])
467
+ charts["evidence_by_phase"] = evidence_phase
468
+
469
+ service_exposure = self.service_exposure_chart(data["attack_surface"])
515
470
  if service_exposure:
516
- charts['service_exposure'] = service_exposure
517
-
518
- credentials_chart = self.credentials_by_service_chart(data.get('credentials', []))
471
+ charts["service_exposure"] = service_exposure
472
+
473
+ credentials_chart = self.credentials_by_service_chart(
474
+ data.get("credentials", [])
475
+ )
519
476
  if credentials_chart:
520
- charts['credentials_by_service'] = credentials_chart
521
-
477
+ charts["credentials_by_service"] = credentials_chart
478
+
522
479
  return charts
523
480
 
524
481
  # =========================================================================
@@ -536,22 +493,22 @@ class ChartGenerator:
536
493
  Chart.js JSON config string
537
494
  """
538
495
  # Handle both object and dict
539
- if hasattr(summary, 'detected'):
496
+ if hasattr(summary, "detected"):
540
497
  detected = summary.detected
541
498
  not_detected = summary.not_detected
542
- partial = getattr(summary, 'partial', 0)
543
- offline = getattr(summary, 'offline', 0)
499
+ partial = getattr(summary, "partial", 0)
500
+ offline = getattr(summary, "offline", 0)
544
501
  else:
545
- detected = summary.get('detected', 0)
546
- not_detected = summary.get('not_detected', 0)
547
- partial = summary.get('partial', 0)
548
- offline = summary.get('offline', 0)
502
+ detected = summary.get("detected", 0)
503
+ not_detected = summary.get("not_detected", 0)
504
+ partial = summary.get("partial", 0)
505
+ offline = summary.get("offline", 0)
549
506
 
550
507
  data = {
551
- 'Detected': detected,
552
- 'Not Detected': not_detected,
553
- 'Partial': partial,
554
- 'Offline': offline,
508
+ "Detected": detected,
509
+ "Not Detected": not_detected,
510
+ "Partial": partial,
511
+ "Offline": offline,
555
512
  }
556
513
 
557
514
  # Filter out zero values
@@ -565,44 +522,38 @@ class ChartGenerator:
565
522
 
566
523
  # Detection-specific colors
567
524
  color_map = {
568
- 'Detected': '#28a745', # Green
569
- 'Not Detected': '#dc3545', # Red
570
- 'Partial': '#ffc107', # Yellow
571
- 'Offline': '#6c757d', # Gray
525
+ "Detected": "#28a745", # Green
526
+ "Not Detected": "#dc3545", # Red
527
+ "Partial": "#ffc107", # Yellow
528
+ "Offline": "#6c757d", # Gray
572
529
  }
573
- colors = [color_map.get(k, '#17a2b8') for k in labels]
530
+ colors = [color_map.get(k, "#17a2b8") for k in labels]
574
531
 
575
532
  config = {
576
- 'type': 'doughnut',
577
- 'data': {
578
- 'labels': labels,
579
- 'datasets': [{
580
- 'data': values,
581
- 'backgroundColor': colors,
582
- 'borderWidth': 2,
583
- 'borderColor': '#fff'
584
- }]
533
+ "type": "doughnut",
534
+ "data": {
535
+ "labels": labels,
536
+ "datasets": [
537
+ {
538
+ "data": values,
539
+ "backgroundColor": colors,
540
+ "borderWidth": 2,
541
+ "borderColor": "#fff",
542
+ }
543
+ ],
585
544
  },
586
- 'options': {
587
- 'responsive': True,
588
- 'maintainAspectRatio': True,
589
- 'plugins': {
590
- 'legend': {
591
- 'position': 'right',
592
- 'labels': {
593
- 'font': {'size': 14}
594
- }
545
+ "options": {
546
+ "responsive": True,
547
+ "maintainAspectRatio": True,
548
+ "plugins": {
549
+ "legend": {"position": "right", "labels": {"font": {"size": 14}}},
550
+ "title": {
551
+ "display": True,
552
+ "text": "Detection Coverage",
553
+ "font": {"size": 16, "weight": "bold"},
595
554
  },
596
- 'title': {
597
- 'display': True,
598
- 'text': 'Detection Coverage',
599
- 'font': {
600
- 'size': 16,
601
- 'weight': 'bold'
602
- }
603
- }
604
- }
605
- }
555
+ },
556
+ },
606
557
  }
607
558
 
608
559
  return json.dumps(config)
@@ -624,7 +575,7 @@ class ChartGenerator:
624
575
  tested_tactics = [
625
576
  (tid, tactic)
626
577
  for tid, tactic in tactic_summary.items()
627
- if hasattr(tactic, 'techniques_tested') and tactic.techniques_tested > 0
578
+ if hasattr(tactic, "techniques_tested") and tactic.techniques_tested > 0
628
579
  ]
629
580
 
630
581
  if not tested_tactics:
@@ -636,64 +587,57 @@ class ChartGenerator:
636
587
  not_detected_data = []
637
588
 
638
589
  for tid, tactic in tested_tactics:
639
- labels.append(tactic.tactic_name if hasattr(tactic, 'tactic_name') else tid)
590
+ labels.append(tactic.tactic_name if hasattr(tactic, "tactic_name") else tid)
640
591
  detected_data.append(
641
- tactic.techniques_detected if hasattr(tactic, 'techniques_detected') else 0
592
+ tactic.techniques_detected
593
+ if hasattr(tactic, "techniques_detected")
594
+ else 0
642
595
  )
643
596
  not_detected_data.append(
644
- tactic.techniques_not_detected if hasattr(tactic, 'techniques_not_detected') else 0
597
+ tactic.techniques_not_detected
598
+ if hasattr(tactic, "techniques_not_detected")
599
+ else 0
645
600
  )
646
601
 
647
602
  config = {
648
- 'type': 'bar',
649
- 'data': {
650
- 'labels': labels,
651
- 'datasets': [
603
+ "type": "bar",
604
+ "data": {
605
+ "labels": labels,
606
+ "datasets": [
652
607
  {
653
- 'label': 'Detected',
654
- 'data': detected_data,
655
- 'backgroundColor': '#28a745',
656
- 'stack': 'stack0'
608
+ "label": "Detected",
609
+ "data": detected_data,
610
+ "backgroundColor": "#28a745",
611
+ "stack": "stack0",
657
612
  },
658
613
  {
659
- 'label': 'Not Detected',
660
- 'data': not_detected_data,
661
- 'backgroundColor': '#dc3545',
662
- 'stack': 'stack0'
663
- }
664
- ]
614
+ "label": "Not Detected",
615
+ "data": not_detected_data,
616
+ "backgroundColor": "#dc3545",
617
+ "stack": "stack0",
618
+ },
619
+ ],
665
620
  },
666
- 'options': {
667
- 'responsive': True,
668
- 'maintainAspectRatio': True,
669
- 'indexAxis': 'y',
670
- 'plugins': {
671
- 'legend': {
672
- 'display': True,
673
- 'position': 'top'
621
+ "options": {
622
+ "responsive": True,
623
+ "maintainAspectRatio": True,
624
+ "indexAxis": "y",
625
+ "plugins": {
626
+ "legend": {"display": True, "position": "top"},
627
+ "title": {
628
+ "display": True,
629
+ "text": "Detection Coverage by MITRE ATT&CK Tactic",
630
+ "font": {"size": 16, "weight": "bold"},
674
631
  },
675
- 'title': {
676
- 'display': True,
677
- 'text': 'Detection Coverage by MITRE ATT&CK Tactic',
678
- 'font': {
679
- 'size': 16,
680
- 'weight': 'bold'
681
- }
682
- }
683
632
  },
684
- 'scales': {
685
- 'x': {
686
- 'stacked': True,
687
- 'title': {
688
- 'display': True,
689
- 'text': 'Techniques'
690
- }
633
+ "scales": {
634
+ "x": {
635
+ "stacked": True,
636
+ "title": {"display": True, "text": "Techniques"},
691
637
  },
692
- 'y': {
693
- 'stacked': True
694
- }
695
- }
696
- }
638
+ "y": {"stacked": True},
639
+ },
640
+ },
697
641
  }
698
642
 
699
643
  return json.dumps(config)
@@ -717,84 +661,75 @@ class ChartGenerator:
717
661
  categories = {}
718
662
  for result in detection_results:
719
663
  # Get attack type
720
- attack_type = getattr(result, 'attack_type', None)
664
+ attack_type = getattr(result, "attack_type", None)
721
665
  if not attack_type and isinstance(result, dict):
722
- attack_type = result.get('attack_type')
666
+ attack_type = result.get("attack_type")
723
667
  if not attack_type:
724
668
  continue
725
669
 
726
670
  # Get status
727
- status = getattr(result, 'status', None)
671
+ status = getattr(result, "status", None)
728
672
  if not status and isinstance(result, dict):
729
- status = result.get('status')
673
+ status = result.get("status")
730
674
 
731
675
  # Get category from signature
732
676
  sig = get_signature(attack_type)
733
- category = sig.get('category', 'unknown')
677
+ category = sig.get("category", "unknown")
734
678
 
735
679
  if category not in categories:
736
- categories[category] = {'detected': 0, 'not_detected': 0}
680
+ categories[category] = {"detected": 0, "not_detected": 0}
737
681
 
738
- if status == 'detected':
739
- categories[category]['detected'] += 1
740
- elif status == 'not_detected':
741
- categories[category]['not_detected'] += 1
682
+ if status == "detected":
683
+ categories[category]["detected"] += 1
684
+ elif status == "not_detected":
685
+ categories[category]["not_detected"] += 1
742
686
 
743
687
  if not categories:
744
688
  return ""
745
689
 
746
690
  # Sort and build chart data
747
691
  labels = list(categories.keys())
748
- detected_data = [categories[c]['detected'] for c in labels]
749
- not_detected_data = [categories[c]['not_detected'] for c in labels]
692
+ detected_data = [categories[c]["detected"] for c in labels]
693
+ not_detected_data = [categories[c]["not_detected"] for c in labels]
750
694
 
751
695
  # Capitalize labels
752
- labels = [label.replace('_', ' ').title() for label in labels]
696
+ labels = [label.replace("_", " ").title() for label in labels]
753
697
 
754
698
  config = {
755
- 'type': 'bar',
756
- 'data': {
757
- 'labels': labels,
758
- 'datasets': [
699
+ "type": "bar",
700
+ "data": {
701
+ "labels": labels,
702
+ "datasets": [
759
703
  {
760
- 'label': 'Detected',
761
- 'data': detected_data,
762
- 'backgroundColor': '#28a745'
704
+ "label": "Detected",
705
+ "data": detected_data,
706
+ "backgroundColor": "#28a745",
763
707
  },
764
708
  {
765
- 'label': 'Not Detected',
766
- 'data': not_detected_data,
767
- 'backgroundColor': '#dc3545'
768
- }
769
- ]
709
+ "label": "Not Detected",
710
+ "data": not_detected_data,
711
+ "backgroundColor": "#dc3545",
712
+ },
713
+ ],
770
714
  },
771
- 'options': {
772
- 'responsive': True,
773
- 'maintainAspectRatio': True,
774
- 'plugins': {
775
- 'legend': {
776
- 'display': True,
777
- 'position': 'top'
715
+ "options": {
716
+ "responsive": True,
717
+ "maintainAspectRatio": True,
718
+ "plugins": {
719
+ "legend": {"display": True, "position": "top"},
720
+ "title": {
721
+ "display": True,
722
+ "text": "Detection by Attack Category",
723
+ "font": {"size": 16, "weight": "bold"},
778
724
  },
779
- 'title': {
780
- 'display': True,
781
- 'text': 'Detection by Attack Category',
782
- 'font': {
783
- 'size': 16,
784
- 'weight': 'bold'
785
- }
786
- }
787
725
  },
788
- 'scales': {
789
- 'y': {
790
- 'beginAtZero': True,
791
- 'title': {
792
- 'display': True,
793
- 'text': 'Attack Count'
794
- }
726
+ "scales": {
727
+ "y": {
728
+ "beginAtZero": True,
729
+ "title": {"display": True, "text": "Attack Count"},
795
730
  }
796
- }
797
- }
731
+ },
732
+ },
798
733
  }
799
734
 
800
735
  return json.dumps(config)
@@ -812,21 +747,21 @@ class ChartGenerator:
812
747
  charts = {}
813
748
 
814
749
  # Coverage pie chart
815
- if hasattr(data, 'summary'):
750
+ if hasattr(data, "summary"):
816
751
  coverage_chart = self.detection_coverage_pie_chart(data.summary)
817
752
  if coverage_chart:
818
- charts['detection_coverage'] = coverage_chart
753
+ charts["detection_coverage"] = coverage_chart
819
754
 
820
755
  # Tactic bar chart
821
- if hasattr(data, 'tactic_summary'):
756
+ if hasattr(data, "tactic_summary"):
822
757
  tactic_chart = self.detection_by_tactic_chart(data.tactic_summary)
823
758
  if tactic_chart:
824
- charts['detection_by_tactic'] = tactic_chart
759
+ charts["detection_by_tactic"] = tactic_chart
825
760
 
826
761
  # Category chart
827
- if hasattr(data, 'detection_results'):
762
+ if hasattr(data, "detection_results"):
828
763
  category_chart = self.detection_by_category_chart(data.detection_results)
829
764
  if category_chart:
830
- charts['detection_by_category'] = category_chart
765
+ charts["detection_by_category"] = category_chart
831
766
 
832
767
  return charts