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
@@ -34,12 +34,19 @@ class ReportGenerator:
34
34
  """Lazy load AI report service."""
35
35
  if self._ai_service is None:
36
36
  from souleyez.ai.report_service import AIReportService
37
+
37
38
  self._ai_service = AIReportService()
38
39
  return self._ai_service
39
40
 
40
- def generate_report(self, engagement_id: int, format: str = 'markdown',
41
- output_path: Optional[str] = None, report_type: str = 'technical',
42
- ai_enhanced: bool = False, compare_to: Optional[int] = None) -> str:
41
+ def generate_report(
42
+ self,
43
+ engagement_id: int,
44
+ format: str = "markdown",
45
+ output_path: Optional[str] = None,
46
+ report_type: str = "technical",
47
+ ai_enhanced: bool = False,
48
+ compare_to: Optional[int] = None,
49
+ ) -> str:
43
50
  """
44
51
  Generate comprehensive pentest report.
45
52
 
@@ -59,46 +66,48 @@ class ReportGenerator:
59
66
  self._check_ai_permission()
60
67
 
61
68
  # Validate report type
62
- valid_types = ['executive', 'technical', 'summary', 'detection']
69
+ valid_types = ["executive", "technical", "summary", "detection"]
63
70
  if report_type not in valid_types:
64
- raise ValueError(f"Invalid report_type '{report_type}'. Must be one of: {', '.join(valid_types)}")
71
+ raise ValueError(
72
+ f"Invalid report_type '{report_type}'. Must be one of: {', '.join(valid_types)}"
73
+ )
65
74
 
66
75
  # Detection reports use a different generation path
67
- if report_type == 'detection':
68
- return self._generate_detection_report(
69
- engagement_id, format, output_path
70
- )
76
+ if report_type == "detection":
77
+ return self._generate_detection_report(engagement_id, format, output_path)
71
78
 
72
79
  # Gather all data
73
80
  data = self._gather_report_data(engagement_id)
74
- data['report_type'] = report_type
75
- data['ai_enhanced'] = ai_enhanced
81
+ data["report_type"] = report_type
82
+ data["ai_enhanced"] = ai_enhanced
76
83
 
77
84
  # Gather comparison data if requested
78
85
  if compare_to:
79
- data['comparison'] = self._gather_comparison_data(compare_to)
86
+ data["comparison"] = self._gather_comparison_data(compare_to)
80
87
  else:
81
- data['comparison'] = None
88
+ data["comparison"] = None
82
89
 
83
90
  # Generate AI content if enabled
84
91
  if ai_enhanced:
85
92
  if self.ai_service.is_available():
86
93
  logger.info("AI enhancement enabled - generating AI content")
87
- data['ai_content'] = self._generate_ai_content(engagement_id, data)
94
+ data["ai_content"] = self._generate_ai_content(engagement_id, data)
88
95
  else:
89
- logger.warning("AI enhancement requested but provider not available - falling back to standard report")
90
- data['ai_content'] = None
96
+ logger.warning(
97
+ "AI enhancement requested but provider not available - falling back to standard report"
98
+ )
99
+ data["ai_content"] = None
91
100
  else:
92
- data['ai_content'] = None
101
+ data["ai_content"] = None
93
102
 
94
103
  # Generate report based on format
95
- if format == 'markdown':
104
+ if format == "markdown":
96
105
  report_content = self._generate_markdown(data)
97
- ext = '.md'
98
- elif format == 'html':
106
+ ext = ".md"
107
+ elif format == "html":
99
108
  report_content = self._generate_html(data)
100
- ext = '.html'
101
- elif format == 'pdf':
109
+ ext = ".html"
110
+ elif format == "pdf":
102
111
  # Generate HTML first, then convert to PDF
103
112
  html_content = self._generate_html(data)
104
113
  return self._convert_to_pdf(html_content, data, output_path)
@@ -107,10 +116,10 @@ class ReportGenerator:
107
116
 
108
117
  # Write report to file
109
118
  if not output_path:
110
- output_path = self._default_output_path(data['engagement']['name'], ext)
119
+ output_path = self._default_output_path(data["engagement"]["name"], ext)
111
120
 
112
121
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
113
- with open(output_path, 'w', encoding='utf-8') as f:
122
+ with open(output_path, "w", encoding="utf-8") as f:
114
123
  f.write(report_content)
115
124
 
116
125
  return output_path
@@ -125,58 +134,66 @@ class ReportGenerator:
125
134
 
126
135
  # Organize findings by severity
127
136
  findings_by_severity = {
128
- 'critical': [],
129
- 'high': [],
130
- 'medium': [],
131
- 'low': [],
132
- 'info': []
137
+ "critical": [],
138
+ "high": [],
139
+ "medium": [],
140
+ "low": [],
141
+ "info": [],
133
142
  }
134
143
  for finding in findings:
135
- severity = finding.get('severity', 'info')
144
+ severity = finding.get("severity", "info")
136
145
  findings_by_severity[severity].append(finding)
137
146
 
138
147
  # Evidence counts
139
148
  evidence_counts = {
140
- 'reconnaissance': len(evidence.get('reconnaissance', [])),
141
- 'enumeration': len(evidence.get('enumeration', [])),
142
- 'exploitation': len(evidence.get('exploitation', [])),
143
- 'post_exploitation': len(evidence.get('post_exploitation', []))
149
+ "reconnaissance": len(evidence.get("reconnaissance", [])),
150
+ "enumeration": len(evidence.get("enumeration", [])),
151
+ "exploitation": len(evidence.get("exploitation", [])),
152
+ "post_exploitation": len(evidence.get("post_exploitation", [])),
144
153
  }
145
-
154
+
146
155
  # Collect unique tools used from findings and evidence
147
156
  tools_used = set()
148
-
157
+
149
158
  # Tools from findings
150
159
  for finding in findings:
151
- tool = finding.get('tool')
152
- if tool and tool.strip() and tool.lower() not in ['unknown', 'none', 'n/a']:
160
+ tool = finding.get("tool")
161
+ if tool and tool.strip() and tool.lower() not in ["unknown", "none", "n/a"]:
153
162
  tools_used.add(tool)
154
-
163
+
155
164
  # Tools from evidence (all phases)
156
165
  for phase_evidence in evidence.values():
157
166
  if isinstance(phase_evidence, list):
158
167
  for item in phase_evidence:
159
168
  if isinstance(item, dict):
160
- tool = item.get('tool')
161
- if tool and tool.strip() and tool.lower() not in ['unknown', 'none', 'n/a']:
169
+ tool = item.get("tool")
170
+ if (
171
+ tool
172
+ and tool.strip()
173
+ and tool.lower() not in ["unknown", "none", "n/a"]
174
+ ):
162
175
  tools_used.add(tool)
163
-
176
+
164
177
  # Tools from credentials
165
178
  for cred in credentials:
166
- source = cred.get('source')
167
- if source and source.strip() and source.lower() not in ['unknown', 'none', 'n/a']:
179
+ source = cred.get("source")
180
+ if (
181
+ source
182
+ and source.strip()
183
+ and source.lower() not in ["unknown", "none", "n/a"]
184
+ ):
168
185
  tools_used.add(source)
169
186
 
170
187
  return {
171
- 'engagement': engagement,
172
- 'attack_surface': attack_surface,
173
- 'findings': findings,
174
- 'findings_by_severity': findings_by_severity,
175
- 'credentials': credentials,
176
- 'evidence': evidence,
177
- 'evidence_counts': evidence_counts,
178
- 'tools_used': sorted(list(tools_used)), # Sorted list of unique tools
179
- 'generated_at': datetime.now()
188
+ "engagement": engagement,
189
+ "attack_surface": attack_surface,
190
+ "findings": findings,
191
+ "findings_by_severity": findings_by_severity,
192
+ "credentials": credentials,
193
+ "evidence": evidence,
194
+ "evidence_counts": evidence_counts,
195
+ "tools_used": sorted(list(tools_used)), # Sorted list of unique tools
196
+ "generated_at": datetime.now(),
180
197
  }
181
198
 
182
199
  def _gather_comparison_data(self, previous_engagement_id: int) -> Optional[Dict]:
@@ -193,7 +210,9 @@ class ReportGenerator:
193
210
  try:
194
211
  previous_engagement = self.em.get_by_id(previous_engagement_id)
195
212
  if not previous_engagement:
196
- logger.warning(f"Previous engagement {previous_engagement_id} not found")
213
+ logger.warning(
214
+ f"Previous engagement {previous_engagement_id} not found"
215
+ )
197
216
  return None
198
217
 
199
218
  # Gather previous engagement data
@@ -204,10 +223,10 @@ class ReportGenerator:
204
223
  previous_metrics = metrics_calc.get_dashboard_metrics(previous_data)
205
224
 
206
225
  return {
207
- 'engagement': previous_engagement,
208
- 'metrics': previous_metrics,
209
- 'findings_by_severity': previous_data['findings_by_severity'],
210
- 'data': previous_data
226
+ "engagement": previous_engagement,
227
+ "metrics": previous_metrics,
228
+ "findings_by_severity": previous_data["findings_by_severity"],
229
+ "data": previous_data,
211
230
  }
212
231
  except Exception as e:
213
232
  logger.warning(f"Failed to gather comparison data: {e}")
@@ -221,66 +240,80 @@ class ReportGenerator:
221
240
  sections = []
222
241
 
223
242
  # Title page
224
- sections.append(formatter.title_page(data['engagement'], data['generated_at']))
243
+ sections.append(formatter.title_page(data["engagement"], data["generated_at"]))
225
244
 
226
245
  # Table of contents
227
246
  sections.append(formatter.table_of_contents())
228
247
 
229
248
  # Get report type and AI content
230
- report_type = data.get('report_type', 'technical')
231
- ai_content = data.get('ai_content')
249
+ report_type = data.get("report_type", "technical")
250
+ ai_content = data.get("ai_content")
232
251
 
233
252
  # Executive Summary (AI-enhanced if available)
234
- if ai_content and ai_content.get('executive_summary'):
235
- sections.append(formatter.ai_executive_summary(
236
- ai_content['executive_summary'],
237
- ai_content.get('provider', 'AI')
238
- ))
253
+ if ai_content and ai_content.get("executive_summary"):
254
+ sections.append(
255
+ formatter.ai_executive_summary(
256
+ ai_content["executive_summary"], ai_content.get("provider", "AI")
257
+ )
258
+ )
239
259
  else:
240
- sections.append(formatter.executive_summary(
241
- data['engagement'],
242
- data['findings_by_severity'],
243
- data['attack_surface']['overview'],
244
- report_type
245
- ))
260
+ sections.append(
261
+ formatter.executive_summary(
262
+ data["engagement"],
263
+ data["findings_by_severity"],
264
+ data["attack_surface"]["overview"],
265
+ report_type,
266
+ )
267
+ )
246
268
 
247
269
  # Engagement Overview
248
- sections.append(formatter.engagement_overview(data['engagement'], data['tools_used'], report_type))
270
+ sections.append(
271
+ formatter.engagement_overview(
272
+ data["engagement"], data["tools_used"], report_type
273
+ )
274
+ )
249
275
 
250
276
  # Note: Attack Surface section removed - now covered by Intelligence Hub
251
277
 
252
278
  # Findings Summary
253
- sections.append(formatter.findings_summary(data['findings_by_severity']))
254
-
279
+ sections.append(formatter.findings_summary(data["findings_by_severity"]))
280
+
255
281
  # Key Findings Summary (Top Critical/High) - for quick scanning
256
- sections.append(formatter.key_findings_summary(data['findings_by_severity']))
282
+ sections.append(formatter.key_findings_summary(data["findings_by_severity"]))
257
283
 
258
284
  # Detailed Findings
259
- sections.append(formatter.detailed_findings(data['findings_by_severity'], report_type))
285
+ sections.append(
286
+ formatter.detailed_findings(data["findings_by_severity"], report_type)
287
+ )
260
288
 
261
289
  # Note: Evidence section removed - evidence is now displayed with each finding card
262
290
 
263
291
  # Recommendations (AI-enhanced if available)
264
- if ai_content and ai_content.get('remediation_plan'):
265
- sections.append(formatter.ai_remediation_plan(
266
- ai_content['remediation_plan'],
267
- ai_content.get('provider', 'AI')
268
- ))
292
+ if ai_content and ai_content.get("remediation_plan"):
293
+ sections.append(
294
+ formatter.ai_remediation_plan(
295
+ ai_content["remediation_plan"], ai_content.get("provider", "AI")
296
+ )
297
+ )
269
298
  else:
270
- sections.append(formatter.recommendations(
271
- data['findings_by_severity'],
272
- data['attack_surface']['recommendations']
273
- ))
299
+ sections.append(
300
+ formatter.recommendations(
301
+ data["findings_by_severity"],
302
+ data["attack_surface"]["recommendations"],
303
+ )
304
+ )
274
305
 
275
306
  # Appendix with Methodology (moved here for cleaner report flow)
276
- sections.append(formatter.appendix(
277
- data['attack_surface']['hosts'],
278
- data['credentials'],
279
- include_methodology=True
280
- ))
307
+ sections.append(
308
+ formatter.appendix(
309
+ data["attack_surface"]["hosts"],
310
+ data["credentials"],
311
+ include_methodology=True,
312
+ )
313
+ )
281
314
 
282
315
  # Footer
283
- sections.append(formatter.footer(data['generated_at']))
316
+ sections.append(formatter.footer(data["generated_at"]))
284
317
 
285
318
  return "\n\n".join(sections)
286
319
 
@@ -301,100 +334,105 @@ class ReportGenerator:
301
334
  chain_analyzer = AttackChainAnalyzer()
302
335
 
303
336
  # Get report type
304
- report_type = data.get('report_type', 'technical')
337
+ report_type = data.get("report_type", "technical")
305
338
 
306
339
  # Calculate metrics, charts, and compliance
307
340
  metrics = metrics_calc.get_dashboard_metrics(data)
308
341
  charts = chart_gen.generate_all_charts(data)
309
-
342
+
310
343
  # Get all findings as flat list for compliance mapping
311
344
  all_findings = []
312
- for severity_findings in data['findings_by_severity'].values():
345
+ for severity_findings in data["findings_by_severity"].values():
313
346
  all_findings.extend(severity_findings)
314
347
  compliance_data = compliance_mapper.get_compliance_coverage(all_findings)
315
-
348
+
316
349
  # Build attack chain (legacy)
317
350
  attack_chain = chain_analyzer.build_attack_chain(
318
- data.get('evidence', {}),
319
- all_findings,
320
- data.get('credentials', [])
351
+ data.get("evidence", {}), all_findings, data.get("credentials", [])
321
352
  )
322
353
  attack_summary = chain_analyzer.get_attack_summary(attack_chain)
323
354
 
324
355
  # Build host-centric attack chain (new visualization)
325
356
  host_centric_chain = chain_analyzer.build_host_centric_chain(
326
- data.get('evidence', {}),
357
+ data.get("evidence", {}),
327
358
  all_findings,
328
- data.get('credentials', []),
329
- data.get('attack_surface')
359
+ data.get("credentials", []),
360
+ data.get("attack_surface"),
330
361
  )
331
362
 
332
363
  sections = []
333
- ai_content = data.get('ai_content')
364
+ ai_content = data.get("ai_content")
334
365
 
335
366
  # Title page (all types)
336
- sections.append(md_formatter.title_page(data['engagement'], data['generated_at']))
367
+ sections.append(
368
+ md_formatter.title_page(data["engagement"], data["generated_at"])
369
+ )
337
370
 
338
371
  # Executive One-Pager (first page summary - all report types)
339
- sections.append(formatter.executive_one_pager(
340
- metrics,
341
- data['findings_by_severity'],
342
- data['engagement']
343
- ))
372
+ sections.append(
373
+ formatter.executive_one_pager(
374
+ metrics, data["findings_by_severity"], data["engagement"]
375
+ )
376
+ )
344
377
 
345
378
  # Compare to Previous (if comparison data provided)
346
- comparison = data.get('comparison')
379
+ comparison = data.get("comparison")
347
380
  if comparison:
348
- sections.append(formatter.compare_to_previous(
349
- metrics,
350
- comparison['metrics'],
351
- data['engagement'],
352
- comparison['engagement']
353
- ))
381
+ sections.append(
382
+ formatter.compare_to_previous(
383
+ metrics,
384
+ comparison["metrics"],
385
+ data["engagement"],
386
+ comparison["engagement"],
387
+ )
388
+ )
354
389
 
355
390
  # Table of contents (technical only)
356
- if report_type in ['technical', 'executive']:
391
+ if report_type in ["technical", "executive"]:
357
392
  sections.append(md_formatter.table_of_contents())
358
393
 
359
394
  # Executive Summary (AI-enhanced if available)
360
- if ai_content and ai_content.get('executive_summary'):
361
- sections.append(formatter.ai_executive_summary(
362
- ai_content['executive_summary'],
363
- ai_content.get('provider', 'AI')
364
- ))
395
+ if ai_content and ai_content.get("executive_summary"):
396
+ sections.append(
397
+ formatter.ai_executive_summary(
398
+ ai_content["executive_summary"], ai_content.get("provider", "AI")
399
+ )
400
+ )
365
401
  else:
366
- sections.append(md_formatter.executive_summary(
367
- data['engagement'],
368
- data['findings_by_severity'],
369
- data['attack_surface']['overview'],
370
- report_type
371
- ))
402
+ sections.append(
403
+ md_formatter.executive_summary(
404
+ data["engagement"],
405
+ data["findings_by_severity"],
406
+ data["attack_surface"]["overview"],
407
+ report_type,
408
+ )
409
+ )
372
410
 
373
411
  # Intelligence Hub (all report types - near the top for visibility)
374
- sections.append(formatter.intelligence_hub_section(data['attack_surface']))
412
+ sections.append(formatter.intelligence_hub_section(data["attack_surface"]))
375
413
 
376
414
  # Risk Quadrant (visual risk matrix - all report types)
377
- sections.append(formatter.risk_quadrant(data['findings_by_severity']))
415
+ sections.append(formatter.risk_quadrant(data["findings_by_severity"]))
378
416
 
379
417
  # Remediation Timeline (visual timeline - all report types)
380
418
  sections.append(formatter.remediation_timeline(metrics))
381
419
 
382
420
  # Executive Dashboard (executive and summary)
383
- if report_type in ['executive', 'summary']:
421
+ if report_type in ["executive", "summary"]:
384
422
  sections.append(formatter.executive_dashboard(metrics))
385
423
 
386
424
  # Charts Section
387
- if report_type == 'executive':
425
+ if report_type == "executive":
388
426
  # Executive: Key charts only (severity, exploitation rate)
389
427
  exec_charts = {
390
- 'severity_distribution': charts.get('severity_distribution'),
391
- 'exploitation_progress': charts.get('exploitation_progress')
428
+ "severity_distribution": charts.get("severity_distribution"),
429
+ "exploitation_progress": charts.get("exploitation_progress"),
392
430
  }
393
431
  sections.append(formatter.charts_section(exec_charts))
394
- elif report_type == 'summary':
432
+ elif report_type == "summary":
395
433
  # Summary: Basic chart only
396
434
  summary_charts = {
397
- 'severity_distribution': charts.get('severity_distribution')
435
+ "severity_distribution": charts.get("severity_distribution")
398
436
  }
399
437
  sections.append(formatter.charts_section(summary_charts))
400
438
  else:
@@ -402,95 +440,115 @@ class ReportGenerator:
402
440
  sections.append(formatter.charts_section(charts))
403
441
 
404
442
  # Engagement Overview (technical only)
405
- if report_type == 'technical':
406
- sections.append(md_formatter.engagement_overview(data['engagement'], data['tools_used'], report_type))
443
+ if report_type == "technical":
444
+ sections.append(
445
+ md_formatter.engagement_overview(
446
+ data["engagement"], data["tools_used"], report_type
447
+ )
448
+ )
407
449
  # Note: Attack Surface section removed - now covered by Intelligence Hub
408
-
450
+
409
451
  # Findings Summary (all types)
410
- sections.append(md_formatter.findings_summary(data['findings_by_severity']))
411
-
452
+ sections.append(md_formatter.findings_summary(data["findings_by_severity"]))
453
+
412
454
  # Key Findings Summary (Top Critical/High) - for quick scanning
413
- sections.append(md_formatter.key_findings_summary(data['findings_by_severity']))
414
-
455
+ sections.append(md_formatter.key_findings_summary(data["findings_by_severity"]))
456
+
415
457
  # Compliance Mapping (executive and technical)
416
- if report_type in ['executive', 'technical']:
417
- sections.append(formatter.compliance_section(all_findings, compliance_data, report_type))
458
+ if report_type in ["executive", "technical"]:
459
+ sections.append(
460
+ formatter.compliance_section(all_findings, compliance_data, report_type)
461
+ )
418
462
 
419
463
  # Detailed Findings
420
- if report_type == 'executive':
464
+ if report_type == "executive":
421
465
  # Executive: Top 5 critical/high only
422
- top_findings = self._filter_top_findings(data['findings_by_severity'], limit=5)
466
+ top_findings = self._filter_top_findings(
467
+ data["findings_by_severity"], limit=5
468
+ )
423
469
  sections.append(formatter.detailed_findings_collapsible(top_findings))
424
- elif report_type == 'summary':
470
+ elif report_type == "summary":
425
471
  # Summary: Top 3 critical/high only
426
- top_findings = self._filter_top_findings(data['findings_by_severity'], limit=3)
472
+ top_findings = self._filter_top_findings(
473
+ data["findings_by_severity"], limit=3
474
+ )
427
475
  sections.append(formatter.detailed_findings_collapsible(top_findings))
428
476
  else:
429
477
  # Technical: All findings
430
- sections.append(formatter.detailed_findings_collapsible(data['findings_by_severity']))
431
-
478
+ sections.append(
479
+ formatter.detailed_findings_collapsible(data["findings_by_severity"])
480
+ )
481
+
432
482
  # Attack Chain Visualization (technical only)
433
- if report_type == 'technical':
434
- sections.append(md_formatter.attack_chain_section(
435
- attack_chain, attack_summary, host_centric_chain
436
- ))
483
+ if report_type == "technical":
484
+ sections.append(
485
+ md_formatter.attack_chain_section(
486
+ attack_chain, attack_summary, host_centric_chain
487
+ )
488
+ )
437
489
 
438
490
  # Note: Evidence section removed - evidence is now displayed with each finding card
439
491
 
440
492
  # Recommendations (AI-enhanced if available)
441
- if ai_content and ai_content.get('remediation_plan'):
442
- sections.append(formatter.ai_remediation_plan(
443
- ai_content['remediation_plan'],
444
- ai_content.get('provider', 'AI')
445
- ))
446
- elif report_type == 'executive':
493
+ if ai_content and ai_content.get("remediation_plan"):
494
+ sections.append(
495
+ formatter.ai_remediation_plan(
496
+ ai_content["remediation_plan"], ai_content.get("provider", "AI")
497
+ )
498
+ )
499
+ elif report_type == "executive":
447
500
  # Executive: Business-focused recommendations
448
- sections.append(self._generate_executive_recommendations(
449
- data['findings_by_severity'],
450
- metrics
451
- ))
501
+ sections.append(
502
+ self._generate_executive_recommendations(
503
+ data["findings_by_severity"], metrics
504
+ )
505
+ )
452
506
  else:
453
507
  # Technical/Summary: Standard recommendations
454
- sections.append(md_formatter.recommendations(
455
- data['findings_by_severity'],
456
- data['attack_surface']['recommendations']
457
- ))
458
-
508
+ sections.append(
509
+ md_formatter.recommendations(
510
+ data["findings_by_severity"],
511
+ data["attack_surface"]["recommendations"],
512
+ )
513
+ )
514
+
459
515
  # Appendix with Methodology (technical only)
460
516
  # Note: Methodology moved to appendix for cleaner report flow
461
- if report_type == 'technical':
462
- sections.append(md_formatter.appendix(
463
- data['attack_surface']['hosts'],
464
- data['credentials'],
465
- include_methodology=True
466
- ))
467
-
517
+ if report_type == "technical":
518
+ sections.append(
519
+ md_formatter.appendix(
520
+ data["attack_surface"]["hosts"],
521
+ data["credentials"],
522
+ include_methodology=True,
523
+ )
524
+ )
525
+
468
526
  # Footer (all types)
469
- sections.append(md_formatter.footer(data['generated_at']))
527
+ sections.append(md_formatter.footer(data["generated_at"]))
470
528
 
471
529
  # Join all sections
472
530
  markdown_content = "\n\n".join(sections)
473
531
 
474
532
  # Convert markdown to HTML with extensions
475
533
  html_content = markdown.markdown(
476
- markdown_content,
477
- extensions=['tables', 'nl2br', 'sane_lists']
534
+ markdown_content, extensions=["tables", "nl2br", "sane_lists"]
478
535
  )
479
536
 
480
537
  # Wrap in HTML structure
481
538
  html_parts = [
482
- formatter.html_header(data['engagement']['name']),
539
+ formatter.html_header(data["engagement"]["name"]),
483
540
  html_content,
484
- formatter.html_footer()
541
+ formatter.html_footer(),
485
542
  ]
486
543
 
487
544
  return "\n".join(html_parts)
488
545
 
489
- def _convert_to_pdf(self, html_content: str, data: Dict,
490
- output_path: Optional[str] = None) -> str:
546
+ def _convert_to_pdf(
547
+ self, html_content: str, data: Dict, output_path: Optional[str] = None
548
+ ) -> str:
491
549
  """Convert HTML to PDF using WeasyPrint (primary) or wkhtmltopdf (fallback)."""
492
550
  if not output_path:
493
- output_path = self._default_output_path(data['engagement']['name'], '.pdf')
551
+ output_path = self._default_output_path(data["engagement"]["name"], ".pdf")
494
552
 
495
553
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
496
554
 
@@ -502,7 +560,8 @@ class ReportGenerator:
502
560
  font_config = FontConfiguration()
503
561
 
504
562
  # Additional CSS for PDF rendering
505
- pdf_css = CSS(string='''
563
+ pdf_css = CSS(
564
+ string="""
506
565
  @page {
507
566
  size: Letter;
508
567
  margin: 20mm 15mm;
@@ -514,10 +573,14 @@ class ReportGenerator:
514
573
  .chart-container, .finding-card, details {
515
574
  page-break-inside: avoid;
516
575
  }
517
- ''', font_config=font_config)
576
+ """,
577
+ font_config=font_config,
578
+ )
518
579
 
519
580
  html_doc = HTML(string=html_content)
520
- html_doc.write_pdf(output_path, stylesheets=[pdf_css], font_config=font_config)
581
+ html_doc.write_pdf(
582
+ output_path, stylesheets=[pdf_css], font_config=font_config
583
+ )
521
584
 
522
585
  logger.info(f"PDF generated with WeasyPrint: {output_path}")
523
586
  return output_path
@@ -531,23 +594,36 @@ class ReportGenerator:
531
594
 
532
595
  # Fallback to wkhtmltopdf
533
596
  import tempfile
534
- with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
597
+
598
+ with tempfile.NamedTemporaryFile(
599
+ mode="w", suffix=".html", delete=False, encoding="utf-8"
600
+ ) as f:
535
601
  f.write(html_content)
536
602
  html_path = f.name
537
603
 
538
604
  try:
539
605
  import subprocess
540
- result = subprocess.run([
541
- 'wkhtmltopdf',
542
- '--enable-local-file-access',
543
- '--page-size', 'Letter',
544
- '--margin-top', '20mm',
545
- '--margin-bottom', '20mm',
546
- '--margin-left', '15mm',
547
- '--margin-right', '15mm',
548
- html_path,
549
- output_path
550
- ], check=True, capture_output=True)
606
+
607
+ result = subprocess.run(
608
+ [
609
+ "wkhtmltopdf",
610
+ "--enable-local-file-access",
611
+ "--page-size",
612
+ "Letter",
613
+ "--margin-top",
614
+ "20mm",
615
+ "--margin-bottom",
616
+ "20mm",
617
+ "--margin-left",
618
+ "15mm",
619
+ "--margin-right",
620
+ "15mm",
621
+ html_path,
622
+ output_path,
623
+ ],
624
+ check=True,
625
+ capture_output=True,
626
+ )
551
627
 
552
628
  return output_path
553
629
 
@@ -583,8 +659,8 @@ class ReportGenerator:
583
659
  project_root = os.getcwd()
584
660
 
585
661
  # Check if we're in souleyez directory structure
586
- if 'souleyez' in project_root and os.path.exists(
587
- os.path.join(project_root, 'setup.py')
662
+ if "souleyez" in project_root and os.path.exists(
663
+ os.path.join(project_root, "setup.py")
588
664
  ):
589
665
  output_dir = os.path.join(project_root, "reports")
590
666
  else:
@@ -594,36 +670,30 @@ class ReportGenerator:
594
670
  os.makedirs(output_dir, exist_ok=True)
595
671
 
596
672
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
597
- safe_name = engagement_name.replace(' ', '_').replace('/', '_')
673
+ safe_name = engagement_name.replace(" ", "_").replace("/", "_")
598
674
  filename = f"{safe_name}{suffix}_report_{timestamp}{ext}"
599
675
 
600
676
  return os.path.join(output_dir, filename)
601
-
677
+
602
678
  def _filter_top_findings(self, findings: Dict, limit: int = 5) -> Dict:
603
679
  """Filter to top N critical/high findings for executive/summary reports."""
604
- filtered = {
605
- 'critical': [],
606
- 'high': [],
607
- 'medium': [],
608
- 'low': [],
609
- 'info': []
610
- }
611
-
680
+ filtered = {"critical": [], "high": [], "medium": [], "low": [], "info": []}
681
+
612
682
  # Get critical and high findings
613
- critical = findings.get('critical', [])
614
- high = findings.get('high', [])
615
-
683
+ critical = findings.get("critical", [])
684
+ high = findings.get("high", [])
685
+
616
686
  # Combine and take top N
617
687
  top_findings = (critical + high)[:limit]
618
-
688
+
619
689
  # Split back into severity categories
620
690
  for finding in top_findings:
621
- severity = finding.get('severity', 'info').lower()
691
+ severity = finding.get("severity", "info").lower()
622
692
  if severity in filtered:
623
693
  filtered[severity].append(finding)
624
-
694
+
625
695
  return filtered
626
-
696
+
627
697
  def _generate_executive_recommendations(self, findings: Dict, metrics: Dict) -> str:
628
698
  """Generate business-focused recommendations for executive report."""
629
699
  section = """## EXECUTIVE RECOMMENDATIONS
@@ -632,37 +702,39 @@ class ReportGenerator:
632
702
 
633
703
  """
634
704
  # Critical findings
635
- critical_count = len(findings.get('critical', []))
705
+ critical_count = len(findings.get("critical", []))
636
706
  if critical_count > 0:
637
707
  section += f"**{critical_count} Critical vulnerabilities require immediate remediation:**\n\n"
638
- for idx, finding in enumerate(findings['critical'][:3], 1):
708
+ for idx, finding in enumerate(findings["critical"][:3], 1):
639
709
  section += f"{idx}. {finding['title']}\n"
640
710
  section += f" - **Business Impact:** High risk of data breach or system compromise\n"
641
711
  section += f" - **Recommended Action:** Emergency patch or system isolation\n\n"
642
712
  else:
643
713
  section += "✓ No critical vulnerabilities identified.\n\n"
644
-
714
+
645
715
  section += """### Short-Term (1-2 Weeks)
646
716
 
647
717
  **Remediation Timeline:**
648
718
  """
649
- timeline = metrics.get('remediation_timeline', {})
719
+ timeline = metrics.get("remediation_timeline", {})
650
720
  section += f"- Estimated effort: {timeline.get('total_days', 0)} business days ({timeline.get('weeks', 0)} weeks)\n"
651
721
  section += f"- Critical issues: {timeline.get('critical', 0)} days\n"
652
722
  section += f"- High priority: {timeline.get('high', 0)} days\n\n"
653
-
723
+
654
724
  section += """### Compliance & Risk Posture
655
725
 
656
726
  """
657
727
  section += f"**Overall Risk Score:** {metrics.get('risk_score', 0)}/100 ({metrics.get('risk_level', 'UNKNOWN')})\n\n"
658
-
659
- if metrics.get('risk_score', 0) >= 75:
728
+
729
+ if metrics.get("risk_score", 0) >= 75:
660
730
  section += "⚠️ **Action Required:** Critical risk level requires board notification and immediate action plan.\n\n"
661
- elif metrics.get('risk_score', 0) >= 50:
731
+ elif metrics.get("risk_score", 0) >= 50:
662
732
  section += "⚠️ **Action Required:** High risk level requires executive review and remediation plan.\n\n"
663
733
  else:
664
- section += "✓ Risk level is manageable with standard remediation timeline.\n\n"
665
-
734
+ section += (
735
+ "✓ Risk level is manageable with standard remediation timeline.\n\n"
736
+ )
737
+
666
738
  section += """### Budget Considerations
667
739
 
668
740
  **Estimated Costs:**
@@ -714,58 +786,66 @@ class ReportGenerator:
714
786
  """Generate all AI content for report."""
715
787
 
716
788
  ai_content = {
717
- 'executive_summary': None,
718
- 'remediation_plan': None,
719
- 'risk_rating': None,
720
- 'enhanced_findings': {},
721
- 'provider': None,
722
- 'errors': []
789
+ "executive_summary": None,
790
+ "remediation_plan": None,
791
+ "risk_rating": None,
792
+ "enhanced_findings": {},
793
+ "provider": None,
794
+ "errors": [],
723
795
  }
724
796
 
725
797
  try:
726
- ai_content['provider'] = self.ai_service.provider.provider_type.value
798
+ ai_content["provider"] = self.ai_service.provider.provider_type.value
727
799
  except Exception:
728
800
  pass
729
801
 
730
802
  # Generate executive summary
731
803
  try:
732
- ai_content['executive_summary'] = self.ai_service.generate_executive_summary(engagement_id)
733
- if ai_content['executive_summary']:
804
+ ai_content["executive_summary"] = (
805
+ self.ai_service.generate_executive_summary(engagement_id)
806
+ )
807
+ if ai_content["executive_summary"]:
734
808
  logger.info("AI executive summary generated")
735
809
  except Exception as e:
736
- ai_content['errors'].append(f'Executive summary: {e}')
810
+ ai_content["errors"].append(f"Executive summary: {e}")
737
811
  logger.warning(f"AI executive summary failed: {e}")
738
812
 
739
813
  # Generate remediation plan
740
814
  try:
741
- ai_content['remediation_plan'] = self.ai_service.generate_remediation_plan(engagement_id)
742
- if ai_content['remediation_plan']:
815
+ ai_content["remediation_plan"] = self.ai_service.generate_remediation_plan(
816
+ engagement_id
817
+ )
818
+ if ai_content["remediation_plan"]:
743
819
  logger.info("AI remediation plan generated")
744
820
  except Exception as e:
745
- ai_content['errors'].append(f'Remediation plan: {e}')
821
+ ai_content["errors"].append(f"Remediation plan: {e}")
746
822
  logger.warning(f"AI remediation plan failed: {e}")
747
823
 
748
824
  # Generate risk rating
749
825
  try:
750
- ai_content['risk_rating'] = self.ai_service.generate_risk_rating(engagement_id)
826
+ ai_content["risk_rating"] = self.ai_service.generate_risk_rating(
827
+ engagement_id
828
+ )
751
829
  except Exception as e:
752
- ai_content['errors'].append(f'Risk rating: {e}')
830
+ ai_content["errors"].append(f"Risk rating: {e}")
753
831
 
754
832
  # Enhance top critical/high findings (limit to 10 for cost control)
755
833
  try:
756
834
  priority_findings = (
757
- data['findings_by_severity'].get('critical', [])[:5] +
758
- data['findings_by_severity'].get('high', [])[:5]
835
+ data["findings_by_severity"].get("critical", [])[:5]
836
+ + data["findings_by_severity"].get("high", [])[:5]
759
837
  )
760
838
  for finding in priority_findings:
761
839
  try:
762
840
  enhanced = self.ai_service.enhance_finding(finding)
763
841
  if enhanced:
764
- ai_content['enhanced_findings'][finding['id']] = enhanced
842
+ ai_content["enhanced_findings"][finding["id"]] = enhanced
765
843
  except Exception as e:
766
- logger.warning(f"Failed to enhance finding {finding.get('id')}: {e}")
844
+ logger.warning(
845
+ f"Failed to enhance finding {finding.get('id')}: {e}"
846
+ )
767
847
  except Exception as e:
768
- ai_content['errors'].append(f'Finding enhancement: {e}')
848
+ ai_content["errors"].append(f"Finding enhancement: {e}")
769
849
 
770
850
  return ai_content
771
851
 
@@ -774,10 +854,7 @@ class ReportGenerator:
774
854
  # =========================================================================
775
855
 
776
856
  def _generate_detection_report(
777
- self,
778
- engagement_id: int,
779
- format: str,
780
- output_path: Optional[str] = None
857
+ self, engagement_id: int, format: str, output_path: Optional[str] = None
781
858
  ) -> str:
782
859
  """
783
860
  Generate detection coverage report.
@@ -800,7 +877,7 @@ class ReportGenerator:
800
877
 
801
878
  # Check if Wazuh is configured
802
879
  config = WazuhConfig.get_config(engagement_id)
803
- if not config or not config.get('enabled'):
880
+ if not config or not config.get("enabled"):
804
881
  raise ValueError(
805
882
  "Detection reports require Wazuh integration. "
806
883
  "Configure with 'souleyez wazuh config' first."
@@ -815,28 +892,26 @@ class ReportGenerator:
815
892
  charts = chart_gen.generate_detection_charts(data)
816
893
 
817
894
  # Generate report based on format
818
- if format == 'html':
895
+ if format == "html":
819
896
  report_content = self._generate_detection_html(data, charts)
820
- ext = '.html'
821
- elif format == 'pdf':
897
+ ext = ".html"
898
+ elif format == "pdf":
822
899
  html_content = self._generate_detection_html(data, charts)
823
900
  return self._convert_detection_to_pdf(html_content, data, output_path)
824
- elif format == 'markdown':
901
+ elif format == "markdown":
825
902
  report_content = self._generate_detection_markdown(data)
826
- ext = '.md'
903
+ ext = ".md"
827
904
  else:
828
905
  raise ValueError(f"Unsupported format for detection report: {format}")
829
906
 
830
907
  # Write report to file
831
908
  if not output_path:
832
909
  output_path = self._default_output_path(
833
- data.engagement.get('name', 'detection'),
834
- ext,
835
- suffix='_detection'
910
+ data.engagement.get("name", "detection"), ext, suffix="_detection"
836
911
  )
837
912
 
838
913
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
839
- with open(output_path, 'w', encoding='utf-8') as f:
914
+ with open(output_path, "w", encoding="utf-8") as f:
840
915
  f.write(report_content)
841
916
 
842
917
  logger.info(f"Detection report generated: {output_path}")
@@ -850,9 +925,9 @@ class ReportGenerator:
850
925
  sections = []
851
926
 
852
927
  # Title page
853
- sections.append(formatter.detection_title_page(
854
- data.engagement, data.generated_at
855
- ))
928
+ sections.append(
929
+ formatter.detection_title_page(data.engagement, data.generated_at)
930
+ )
856
931
 
857
932
  # Executive summary
858
933
  sections.append(formatter.detection_executive_summary(data))
@@ -888,12 +963,14 @@ class ReportGenerator:
888
963
  sections.append(formatter.per_host_detection_section(data))
889
964
 
890
965
  # Footer
891
- sections.append(f"""
966
+ sections.append(
967
+ f"""
892
968
  ---
893
969
 
894
970
  *Detection Coverage Report generated by SoulEyez*
895
971
  *{data.generated_at.strftime('%B %d, %Y at %H:%M:%S')}*
896
- """)
972
+ """
973
+ )
897
974
 
898
975
  return "\n\n".join(sections)
899
976
 
@@ -904,15 +981,17 @@ class ReportGenerator:
904
981
 
905
982
  # Helper to convert markdown with table support
906
983
  def md_to_html(md_text: str) -> str:
907
- return markdown.markdown(md_text, extensions=['tables', 'fenced_code'])
984
+ return markdown.markdown(md_text, extensions=["tables", "fenced_code"])
908
985
 
909
986
  formatter = HTMLFormatter()
910
987
  sections = []
911
988
 
912
989
  # HTML header with detection-specific styles
913
- sections.append(formatter.detection_report_header(
914
- f"Detection Coverage - {data.engagement.get('name', 'Report')}"
915
- ))
990
+ sections.append(
991
+ formatter.detection_report_header(
992
+ f"Detection Coverage - {data.engagement.get('name', 'Report')}"
993
+ )
994
+ )
916
995
  sections.append('<div class="container">')
917
996
 
918
997
  # Title
@@ -926,7 +1005,8 @@ class ReportGenerator:
926
1005
 
927
1006
  # Stats cards
928
1007
  summary = data.summary
929
- sections.append(f"""
1008
+ sections.append(
1009
+ f"""
930
1010
  <div class="detection-stat-grid">
931
1011
  <div class="detection-stat-card coverage">
932
1012
  <div class="detection-stat-value">{summary.coverage_percent}%</div>
@@ -945,32 +1025,37 @@ class ReportGenerator:
945
1025
  <div class="detection-stat-label">Not Detected</div>
946
1026
  </div>
947
1027
  </div>
948
- """)
1028
+ """
1029
+ )
949
1030
 
950
1031
  # Coverage overview
951
1032
  overview_md = formatter.detection_coverage_overview(data)
952
1033
  sections.append(md_to_html(overview_md))
953
1034
 
954
1035
  # Charts
955
- if charts.get('detection_coverage'):
956
- sections.append(f"""
1036
+ if charts.get("detection_coverage"):
1037
+ sections.append(
1038
+ f"""
957
1039
  <div class="chart-container" style="max-width: 400px; margin: 20px auto;">
958
1040
  <canvas id="detectionCoverageChart"></canvas>
959
1041
  </div>
960
1042
  <script>
961
1043
  new Chart(document.getElementById('detectionCoverageChart'), {charts['detection_coverage']});
962
1044
  </script>
963
- """)
1045
+ """
1046
+ )
964
1047
 
965
- if charts.get('detection_by_tactic'):
966
- sections.append(f"""
1048
+ if charts.get("detection_by_tactic"):
1049
+ sections.append(
1050
+ f"""
967
1051
  <div class="chart-container" style="max-width: 800px; margin: 20px auto;">
968
1052
  <canvas id="detectionTacticChart"></canvas>
969
1053
  </div>
970
1054
  <script>
971
1055
  new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by_tactic']});
972
1056
  </script>
973
- """)
1057
+ """
1058
+ )
974
1059
 
975
1060
  # MITRE ATT&CK Heatmap (HTML version)
976
1061
  sections.append(formatter.mitre_heatmap_html(data))
@@ -993,12 +1078,14 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
993
1078
 
994
1079
  # Detection gaps (with warning styling)
995
1080
  if data.gaps:
996
- sections.append("""
1081
+ sections.append(
1082
+ """
997
1083
  <div class="gap-warning">
998
1084
  <h4>Detection Gaps Identified</h4>
999
1085
  <p>The following attacks were NOT detected by the SIEM. These represent potential blindspots that should be addressed.</p>
1000
1086
  </div>
1001
- """)
1087
+ """
1088
+ )
1002
1089
  gaps_md = formatter.detection_gaps_section(data)
1003
1090
  sections.append(md_to_html(gaps_md))
1004
1091
 
@@ -1015,34 +1102,33 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
1015
1102
  sections.append(md_to_html(host_md))
1016
1103
 
1017
1104
  # Footer
1018
- sections.append(f"""
1105
+ sections.append(
1106
+ f"""
1019
1107
  <hr>
1020
1108
  <p style="text-align: center; color: #6c757d;">
1021
1109
  <em>Detection Coverage Report generated by SoulEyez</em><br>
1022
1110
  {data.generated_at.strftime('%B %d, %Y at %H:%M:%S')}
1023
1111
  </p>
1024
- """)
1112
+ """
1113
+ )
1025
1114
 
1026
1115
  # HTML footer with Chart.js
1027
- sections.append("""
1116
+ sections.append(
1117
+ """
1028
1118
  <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
1029
- """)
1119
+ """
1120
+ )
1030
1121
  sections.append(formatter.html_footer())
1031
1122
 
1032
1123
  return "\n".join(sections)
1033
1124
 
1034
1125
  def _convert_detection_to_pdf(
1035
- self,
1036
- html_content: str,
1037
- data,
1038
- output_path: Optional[str] = None
1126
+ self, html_content: str, data, output_path: Optional[str] = None
1039
1127
  ) -> str:
1040
1128
  """Convert detection HTML report to PDF."""
1041
1129
  if not output_path:
1042
1130
  output_path = self._default_output_path(
1043
- data.engagement.get('name', 'detection'),
1044
- '.pdf',
1045
- suffix='_detection'
1131
+ data.engagement.get("name", "detection"), ".pdf", suffix="_detection"
1046
1132
  )
1047
1133
 
1048
1134
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
@@ -1053,12 +1139,12 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
1053
1139
 
1054
1140
  # Inline all external resources for PDF
1055
1141
  html_content = html_content.replace(
1056
- 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js',
1057
- ''
1142
+ "https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js", ""
1058
1143
  )
1059
1144
 
1060
1145
  html = HTML(string=html_content)
1061
- css = CSS(string="""
1146
+ css = CSS(
1147
+ string="""
1062
1148
  @page {
1063
1149
  size: letter;
1064
1150
  margin: 20mm 15mm;
@@ -1075,7 +1161,8 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
1075
1161
  .mitre-heatmap {
1076
1162
  page-break-inside: avoid;
1077
1163
  }
1078
- """)
1164
+ """
1165
+ )
1079
1166
  html.write_pdf(output_path, stylesheets=[css])
1080
1167
  logger.info(f"PDF generated with WeasyPrint: {output_path}")
1081
1168
  return output_path
@@ -1087,16 +1174,13 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
1087
1174
  import subprocess
1088
1175
  import tempfile
1089
1176
 
1090
- with tempfile.NamedTemporaryFile(
1091
- mode='w', suffix='.html', delete=False
1092
- ) as tmp:
1177
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as tmp:
1093
1178
  tmp.write(html_content)
1094
1179
  tmp_path = tmp.name
1095
1180
 
1096
1181
  try:
1097
1182
  subprocess.run(
1098
- ['wkhtmltopdf', '--quiet', tmp_path, output_path],
1099
- check=True
1183
+ ["wkhtmltopdf", "--quiet", tmp_path, output_path], check=True
1100
1184
  )
1101
1185
  logger.info(f"PDF generated with wkhtmltopdf: {output_path}")
1102
1186
  return output_path