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
@@ -35,21 +35,27 @@ class MarkdownFormatter:
35
35
 
36
36
  ---"""
37
37
 
38
- def executive_summary(self, engagement: Dict, findings: Dict, overview: Dict, report_type: str = 'technical') -> str:
38
+ def executive_summary(
39
+ self,
40
+ engagement: Dict,
41
+ findings: Dict,
42
+ overview: Dict,
43
+ report_type: str = "technical",
44
+ ) -> str:
39
45
  """
40
46
  Generate executive summary with business impact context.
41
-
47
+
42
48
  Args:
43
49
  engagement: Engagement details
44
50
  findings: Findings grouped by severity
45
51
  overview: Attack surface overview
46
52
  report_type: 'executive', 'technical', or 'summary'
47
53
  """
48
- critical_count = len(findings['critical'])
49
- high_count = len(findings['high'])
50
- medium_count = len(findings['medium'])
51
- total_hosts = overview['total_hosts']
52
- exploited = overview['exploited_services']
54
+ critical_count = len(findings["critical"])
55
+ high_count = len(findings["high"])
56
+ medium_count = len(findings["medium"])
57
+ total_hosts = overview["total_hosts"]
58
+ exploited = overview["exploited_services"]
53
59
 
54
60
  # Determine risk level
55
61
  if critical_count > 0:
@@ -87,86 +93,94 @@ The penetration test identified **{critical_count} critical** and **{high_count}
87
93
  summary += " Immediate remediation is recommended for critical findings to prevent potential compromise."
88
94
 
89
95
  # Business Impact Section (for executive reports)
90
- if report_type == 'executive' and (critical_count > 0 or high_count > 0):
96
+ if report_type == "executive" and (critical_count > 0 or high_count > 0):
91
97
  summary += "\n\n### BUSINESS IMPACT\n\n"
92
-
98
+
93
99
  # Analyze top findings for business impact
94
- top_findings = findings['critical'][:3] if critical_count > 0 else findings['high'][:3]
100
+ top_findings = (
101
+ findings["critical"][:3] if critical_count > 0 else findings["high"][:3]
102
+ )
95
103
  impacts = self._calculate_business_impacts(top_findings)
96
-
104
+
97
105
  # Overall business risks
98
106
  summary += "**Key Business Risks:**\n\n"
99
-
100
- if any('data breach' in str(f.get('title', '')).lower() or
101
- 'sql injection' in str(f.get('title', '')).lower()
102
- for f in findings['critical']):
107
+
108
+ if any(
109
+ "data breach" in str(f.get("title", "")).lower()
110
+ or "sql injection" in str(f.get("title", "")).lower()
111
+ for f in findings["critical"]
112
+ ):
103
113
  summary += "- **Data Breach Risk:** Vulnerabilities could expose sensitive customer data, leading to regulatory fines (GDPR up to €20M/4% revenue) and reputational damage\n"
104
-
114
+
105
115
  if critical_count > 0:
106
116
  summary += f"- **System Compromise:** {critical_count} critical vulnerabilit{'y' if critical_count == 1 else 'ies'} could allow attackers full system access\n"
107
-
117
+
108
118
  if high_count >= 3:
109
119
  summary += f"- **Security Posture:** {high_count} high-severity issues indicate systematic security gaps requiring attention\n"
110
-
120
+
111
121
  summary += "\n**Compliance Impact:**\n\n"
112
-
122
+
113
123
  # Estimate compliance implications
114
124
  if critical_count > 0 or high_count >= 5:
115
125
  summary += "- Current security posture may not meet PCI-DSS, ISO 27001, or SOC 2 requirements\n"
116
126
  summary += "- Non-compliance could block customer contracts, certifications, or partnerships\n"
117
-
127
+
118
128
  # Financial impact estimate
119
129
  summary += "\n**Estimated Financial Impact (if exploited):**\n\n"
120
130
  if critical_count > 0:
121
131
  summary += f"- Incident response costs: ${50000 * critical_count:,} - ${200000 * critical_count:,}\n"
122
- summary += f"- Potential breach notification and remediation: ${100000:,}+\n"
132
+ summary += (
133
+ f"- Potential breach notification and remediation: ${100000:,}+\n"
134
+ )
123
135
  summary += "- Reputational damage and customer loss: Significant\n"
124
136
 
125
137
  # Top risks with business context
126
138
  if critical_count > 0 or high_count > 0:
127
139
  summary += "\n\n### TOP RISKS\n\n"
128
- top_findings = findings['critical'][:3] + findings['high'][:3]
140
+ top_findings = findings["critical"][:3] + findings["high"][:3]
129
141
  for idx, finding in enumerate(top_findings[:5], 1):
130
- severity = finding.get('severity', 'unknown').upper()
131
- title = finding['title']
142
+ severity = finding.get("severity", "unknown").upper()
143
+ title = finding["title"]
132
144
  summary += f"**{idx}. {title}** - {severity}\n"
133
-
145
+
134
146
  # Add business impact for executive reports
135
- if report_type == 'executive':
147
+ if report_type == "executive":
136
148
  impact = self._get_finding_business_context(finding)
137
149
  summary += f" *Impact:* {impact}\n"
138
-
150
+
139
151
  summary += "\n"
140
152
 
141
153
  # Action Timeline (for executive reports)
142
- if report_type == 'executive' and (critical_count > 0 or high_count > 0):
154
+ if report_type == "executive" and (critical_count > 0 or high_count > 0):
143
155
  summary += "### ACTION TIMELINE\n\n"
144
-
156
+
145
157
  if critical_count > 0:
146
158
  summary += f"**Immediate (This Week):** Address {critical_count} critical finding{'s' if critical_count != 1 else ''}\n"
147
159
  summary += "- Deploy emergency patches\n"
148
160
  summary += "- Implement temporary mitigations\n"
149
161
  summary += "- Begin incident response preparation\n\n"
150
-
162
+
151
163
  if high_count > 0:
152
164
  summary += f"**Short-Term (Within 2 Weeks):** Remediate {high_count} high-priority issue{'s' if high_count != 1 else ''}\n"
153
165
  summary += "- Plan and schedule fixes\n"
154
166
  summary += "- Update security configurations\n"
155
167
  summary += "- Review access controls\n\n"
156
-
168
+
157
169
  if medium_count > 0:
158
170
  summary += f"**Medium-Term (30 Days):** Address {medium_count} medium-severity finding{'s' if medium_count != 1 else ''}\n"
159
171
  summary += "- Systematic security improvements\n"
160
172
  summary += "- Policy and procedure updates\n\n"
161
-
173
+
162
174
  # Resource estimate
163
175
  total_hours = (critical_count * 4) + (high_count * 2) + (medium_count * 1)
164
176
  summary += f"**Estimated Remediation Effort:** {total_hours}-{total_hours * 2} hours\n"
165
- summary += "*See Recommendations section for detailed remediation guidance.*\n"
177
+ summary += (
178
+ "*See Recommendations section for detailed remediation guidance.*\n"
179
+ )
166
180
 
167
181
  summary += "\n---"
168
182
  return summary
169
-
183
+
170
184
  def _calculate_business_impacts(self, findings: List[Dict]) -> List[str]:
171
185
  """Calculate business impacts for top findings."""
172
186
  impacts = []
@@ -174,44 +188,57 @@ The penetration test identified **{critical_count} critical** and **{high_count}
174
188
  impact = self._get_finding_business_context(finding)
175
189
  impacts.append(impact)
176
190
  return impacts
177
-
191
+
178
192
  def _get_finding_business_context(self, finding: Dict) -> str:
179
193
  """Get business context for a specific finding."""
180
- title_lower = finding.get('title', '').lower()
181
-
194
+ title_lower = finding.get("title", "").lower()
195
+
182
196
  # Data breach scenarios
183
- if 'sql injection' in title_lower or 'data breach' in title_lower or 'dump' in title_lower:
197
+ if (
198
+ "sql injection" in title_lower
199
+ or "data breach" in title_lower
200
+ or "dump" in title_lower
201
+ ):
184
202
  return "Data breach risk with regulatory and reputational consequences"
185
-
203
+
186
204
  # Authentication/Access
187
- if 'authentication' in title_lower or 'credential' in title_lower or 'password' in title_lower:
205
+ if (
206
+ "authentication" in title_lower
207
+ or "credential" in title_lower
208
+ or "password" in title_lower
209
+ ):
188
210
  return "Unauthorized access could compromise confidential systems and data"
189
-
211
+
190
212
  # Injection attacks
191
- if 'injection' in title_lower or 'xss' in title_lower:
213
+ if "injection" in title_lower or "xss" in title_lower:
192
214
  return "User account compromise and potential malware distribution"
193
-
215
+
194
216
  # Information disclosure
195
- if 'disclosure' in title_lower or 'exposure' in title_lower:
217
+ if "disclosure" in title_lower or "exposure" in title_lower:
196
218
  return "Information leakage aids attacker reconnaissance and planning"
197
-
219
+
198
220
  # Configuration issues
199
- if 'configuration' in title_lower or 'misconfiguration' in title_lower:
221
+ if "configuration" in title_lower or "misconfiguration" in title_lower:
200
222
  return "Security weaknesses that lower defense effectiveness"
201
-
223
+
202
224
  # Default severity-based context
203
- severity = finding.get('severity', '').lower()
204
- if severity == 'critical':
225
+ severity = finding.get("severity", "").lower()
226
+ if severity == "critical":
205
227
  return "High-impact vulnerability requiring immediate attention"
206
- elif severity == 'high':
228
+ elif severity == "high":
207
229
  return "Significant security risk requiring timely remediation"
208
230
  else:
209
231
  return "Security issue requiring attention"
210
232
 
211
- def engagement_overview(self, engagement: Dict, tools_used: List[str] = None, report_type: str = 'technical') -> str:
233
+ def engagement_overview(
234
+ self,
235
+ engagement: Dict,
236
+ tools_used: List[str] = None,
237
+ report_type: str = "technical",
238
+ ) -> str:
212
239
  """
213
240
  Generate engagement overview section.
214
-
241
+
215
242
  Args:
216
243
  engagement: Engagement details
217
244
  tools_used: List of tools used in assessment
@@ -230,83 +257,152 @@ Testing was conducted against systems within the defined scope. All testing acti
230
257
  ### Tools Used
231
258
 
232
259
  """
233
-
260
+
234
261
  # If tools list provided, use it dynamically
235
262
  if tools_used:
236
263
  # Executive: Just show count
237
- if report_type == 'executive':
264
+ if report_type == "executive":
238
265
  section += f"{len(tools_used)} industry-standard security testing tools were utilized during this assessment.\n\n"
239
266
  section += "---"
240
267
  return section
241
-
268
+
242
269
  # Tool descriptions and categories
243
270
  tool_info = {
244
- 'nmap': {'desc': 'Port scanning and service enumeration', 'cat': 'Reconnaissance'},
245
- 'metasploit': {'desc': 'Exploitation and post-exploitation', 'cat': 'Exploitation'},
246
- 'msf': {'desc': 'Exploitation and post-exploitation', 'cat': 'Exploitation'},
247
- 'nuclei': {'desc': 'Web vulnerability scanning with templated checks', 'cat': 'Vulnerability Scanning'},
248
- 'sqlmap': {'desc': 'SQL injection testing and database exploitation', 'cat': 'Exploitation'},
249
- 'gobuster': {'desc': 'Directory and DNS brute forcing', 'cat': 'Enumeration'},
250
- 'ffuf': {'desc': 'Fast web fuzzer for directory and parameter discovery', 'cat': 'Enumeration'},
251
- 'hydra': {'desc': 'Credential brute forcing', 'cat': 'Password Attacks'},
252
- 'theharvester': {'desc': 'OSINT and email/subdomain gathering', 'cat': 'Reconnaissance'},
253
- 'dnsrecon': {'desc': 'DNS enumeration and zone transfer testing', 'cat': 'Reconnaissance'},
254
- 'whois': {'desc': 'Domain registration and ownership information', 'cat': 'Reconnaissance'},
255
- 'enum4linux': {'desc': 'SMB/Windows enumeration', 'cat': 'Enumeration'},
256
- 'smbmap': {'desc': 'SMB share enumeration and access testing', 'cat': 'Enumeration'},
257
- 'wpscan': {'desc': 'WordPress vulnerability scanning', 'cat': 'Vulnerability Scanning'},
258
- 'dirb': {'desc': 'Web content scanner', 'cat': 'Enumeration'},
259
- 'searchsploit': {'desc': 'Exploit database search', 'cat': 'Exploitation'},
260
- 'john': {'desc': 'Password cracking', 'cat': 'Password Attacks'},
261
- 'hashcat': {'desc': 'Advanced password recovery', 'cat': 'Password Attacks'},
262
- 'medusa': {'desc': 'Parallel password brute forcer', 'cat': 'Password Attacks'},
263
- 'crackmapexec': {'desc': 'Network authentication testing', 'cat': 'Exploitation'},
264
- 'responder': {'desc': 'LLMNR/NBT-NS/MDNS poisoner', 'cat': 'Exploitation'},
265
- 'bloodhound': {'desc': 'Active Directory attack path analysis', 'cat': 'Post-Exploitation'},
266
- 'mimikatz': {'desc': 'Credential extraction', 'cat': 'Post-Exploitation'},
267
- 'linpeas': {'desc': 'Linux privilege escalation checker', 'cat': 'Post-Exploitation'},
268
- 'winpeas': {'desc': 'Windows privilege escalation checker', 'cat': 'Post-Exploitation'},
271
+ "nmap": {
272
+ "desc": "Port scanning and service enumeration",
273
+ "cat": "Reconnaissance",
274
+ },
275
+ "metasploit": {
276
+ "desc": "Exploitation and post-exploitation",
277
+ "cat": "Exploitation",
278
+ },
279
+ "msf": {
280
+ "desc": "Exploitation and post-exploitation",
281
+ "cat": "Exploitation",
282
+ },
283
+ "nuclei": {
284
+ "desc": "Web vulnerability scanning with templated checks",
285
+ "cat": "Vulnerability Scanning",
286
+ },
287
+ "sqlmap": {
288
+ "desc": "SQL injection testing and database exploitation",
289
+ "cat": "Exploitation",
290
+ },
291
+ "gobuster": {
292
+ "desc": "Directory and DNS brute forcing",
293
+ "cat": "Enumeration",
294
+ },
295
+ "ffuf": {
296
+ "desc": "Fast web fuzzer for directory and parameter discovery",
297
+ "cat": "Enumeration",
298
+ },
299
+ "hydra": {
300
+ "desc": "Credential brute forcing",
301
+ "cat": "Password Attacks",
302
+ },
303
+ "theharvester": {
304
+ "desc": "OSINT and email/subdomain gathering",
305
+ "cat": "Reconnaissance",
306
+ },
307
+ "dnsrecon": {
308
+ "desc": "DNS enumeration and zone transfer testing",
309
+ "cat": "Reconnaissance",
310
+ },
311
+ "whois": {
312
+ "desc": "Domain registration and ownership information",
313
+ "cat": "Reconnaissance",
314
+ },
315
+ "enum4linux": {"desc": "SMB/Windows enumeration", "cat": "Enumeration"},
316
+ "smbmap": {
317
+ "desc": "SMB share enumeration and access testing",
318
+ "cat": "Enumeration",
319
+ },
320
+ "wpscan": {
321
+ "desc": "WordPress vulnerability scanning",
322
+ "cat": "Vulnerability Scanning",
323
+ },
324
+ "dirb": {"desc": "Web content scanner", "cat": "Enumeration"},
325
+ "searchsploit": {
326
+ "desc": "Exploit database search",
327
+ "cat": "Exploitation",
328
+ },
329
+ "john": {"desc": "Password cracking", "cat": "Password Attacks"},
330
+ "hashcat": {
331
+ "desc": "Advanced password recovery",
332
+ "cat": "Password Attacks",
333
+ },
334
+ "medusa": {
335
+ "desc": "Parallel password brute forcer",
336
+ "cat": "Password Attacks",
337
+ },
338
+ "crackmapexec": {
339
+ "desc": "Network authentication testing",
340
+ "cat": "Exploitation",
341
+ },
342
+ "responder": {
343
+ "desc": "LLMNR/NBT-NS/MDNS poisoner",
344
+ "cat": "Exploitation",
345
+ },
346
+ "bloodhound": {
347
+ "desc": "Active Directory attack path analysis",
348
+ "cat": "Post-Exploitation",
349
+ },
350
+ "mimikatz": {
351
+ "desc": "Credential extraction",
352
+ "cat": "Post-Exploitation",
353
+ },
354
+ "linpeas": {
355
+ "desc": "Linux privilege escalation checker",
356
+ "cat": "Post-Exploitation",
357
+ },
358
+ "winpeas": {
359
+ "desc": "Windows privilege escalation checker",
360
+ "cat": "Post-Exploitation",
361
+ },
269
362
  }
270
-
363
+
271
364
  # Group tools by category
272
365
  categorized = {}
273
366
  unknown_tools = []
274
-
367
+
275
368
  for tool in tools_used:
276
369
  tool_lower = tool.lower()
277
370
  info = tool_info.get(tool_lower)
278
-
371
+
279
372
  if info:
280
- category = info['cat']
373
+ category = info["cat"]
281
374
  if category not in categorized:
282
375
  categorized[category] = []
283
- categorized[category].append({
284
- 'name': tool,
285
- 'desc': info['desc']
286
- })
376
+ categorized[category].append({"name": tool, "desc": info["desc"]})
287
377
  else:
288
378
  unknown_tools.append(tool)
289
-
379
+
290
380
  # Display by category in logical order
291
381
  category_order = [
292
- 'Reconnaissance',
293
- 'Enumeration',
294
- 'Vulnerability Scanning',
295
- 'Exploitation',
296
- 'Password Attacks',
297
- 'Post-Exploitation'
382
+ "Reconnaissance",
383
+ "Enumeration",
384
+ "Vulnerability Scanning",
385
+ "Exploitation",
386
+ "Password Attacks",
387
+ "Post-Exploitation",
298
388
  ]
299
-
389
+
300
390
  for category in category_order:
301
391
  if category in categorized:
302
392
  section += f"**{category}:**\n"
303
- for tool in sorted(categorized[category], key=lambda x: x['name'].lower()):
393
+ for tool in sorted(
394
+ categorized[category], key=lambda x: x["name"].lower()
395
+ ):
304
396
  # Capitalize tool name properly
305
- tool_lower = tool['name'].lower()
306
- tool_name = tool['name'].upper() if tool_lower in ['smb', 'dns', 'osint'] else tool['name'].title()
397
+ tool_lower = tool["name"].lower()
398
+ tool_name = (
399
+ tool["name"].upper()
400
+ if tool_lower in ["smb", "dns", "osint"]
401
+ else tool["name"].title()
402
+ )
307
403
  section += f"- {tool_name} - {tool['desc']}\n"
308
404
  section += "\n"
309
-
405
+
310
406
  # Add any unknown tools at the end
311
407
  if unknown_tools:
312
408
  section += "**Other Tools:**\n"
@@ -331,14 +427,14 @@ Testing was conducted against systems within the defined scope. All testing acti
331
427
  - Gobuster - Directory brute forcing
332
428
 
333
429
  """
334
-
430
+
335
431
  section += "---"
336
432
  return section
337
433
 
338
434
  def attack_surface_section(self, attack_surface: Dict) -> str:
339
435
  """Generate attack surface analysis section."""
340
- hosts = attack_surface['hosts'][:5]
341
- overview = attack_surface['overview']
436
+ hosts = attack_surface["hosts"][:5]
437
+ overview = attack_surface["overview"]
342
438
 
343
439
  section = """## ATTACK SURFACE ANALYSIS
344
440
 
@@ -353,16 +449,18 @@ Testing was conducted against systems within the defined scope. All testing acti
353
449
  section += "### Top Targets (by attack surface score)\n\n"
354
450
 
355
451
  for idx, host in enumerate(hosts, 1):
356
- prog = host['exploitation_progress']
357
- pct = round((prog['exploited'] / prog['total'] * 100) if prog['total'] > 0 else 0, 0)
452
+ prog = host["exploitation_progress"]
453
+ pct = round(
454
+ (prog["exploited"] / prog["total"] * 100) if prog["total"] > 0 else 0, 0
455
+ )
358
456
 
359
457
  section += f"#### #{idx} {host['host']}"
360
- if host.get('hostname'):
458
+ if host.get("hostname"):
361
459
  section += f" ({host['hostname']})"
362
460
  section += f" - Score: {host['score']}\n\n"
363
461
 
364
462
  # Format services count properly (handle list, int, or empty)
365
- services_data = host.get('services', 0)
463
+ services_data = host.get("services", 0)
366
464
  if isinstance(services_data, list):
367
465
  services_count = len(services_data)
368
466
  else:
@@ -371,7 +469,7 @@ Testing was conducted against systems within the defined scope. All testing acti
371
469
  section += f"- **{host['open_ports']} open ports**\n"
372
470
  section += f"- **{services_count} services** identified\n"
373
471
  section += f"- **{host['findings']} vulnerabilities** found"
374
- if host['critical_findings'] > 0:
472
+ if host["critical_findings"] > 0:
375
473
  section += f" ({host['critical_findings']} critical)"
376
474
  section += "\n"
377
475
  section += f"- **Exploitation:** {prog['exploited']}/{prog['total']} services ({pct}%)\n\n"
@@ -381,11 +479,11 @@ Testing was conducted against systems within the defined scope. All testing acti
381
479
 
382
480
  def findings_summary(self, findings: Dict) -> str:
383
481
  """Generate findings summary section."""
384
- critical = len(findings['critical'])
385
- high = len(findings['high'])
386
- medium = len(findings['medium'])
387
- low = len(findings['low'])
388
- info = len(findings['info'])
482
+ critical = len(findings["critical"])
483
+ high = len(findings["high"])
484
+ medium = len(findings["medium"])
485
+ low = len(findings["low"])
486
+ info = len(findings["info"])
389
487
  total = critical + high + medium + low + info
390
488
 
391
489
  return f"""## FINDINGS SUMMARY
@@ -402,7 +500,7 @@ Testing was conducted against systems within the defined scope. All testing acti
402
500
  | **Total** | **{total}** | **100%** |
403
501
 
404
502
  ---"""
405
-
503
+
406
504
  def key_findings_summary(self, findings: Dict) -> str:
407
505
  """
408
506
  Generate key findings summary - shows top critical/high findings upfront.
@@ -413,56 +511,61 @@ Testing was conducted against systems within the defined scope. All testing acti
413
511
  *Quick overview of the most critical security issues discovered*
414
512
 
415
513
  """
416
-
417
- critical_findings = findings['critical']
418
- high_findings = findings['high']
419
-
514
+
515
+ critical_findings = findings["critical"]
516
+ high_findings = findings["high"]
517
+
420
518
  # Immediate Action Required (Critical)
421
519
  if critical_findings:
422
520
  section += "### 🚨 Immediate Action Required (Critical)\n\n"
423
521
  for idx, finding in enumerate(critical_findings[:5], 1): # Top 5 critical
424
- title = finding.get('title', 'Untitled Finding')
522
+ title = finding.get("title", "Untitled Finding")
425
523
  host = self._format_affected_host(finding)
426
524
  section += f"{idx}. **{title}**\n"
427
525
  section += f" - Host: {host}\n"
428
- if finding.get('description'):
526
+ if finding.get("description"):
429
527
  # Get first sentence or first 100 chars
430
- desc = finding['description'].split('.')[0][:100]
528
+ desc = finding["description"].split(".")[0][:100]
431
529
  section += f" - Impact: {desc}...\n"
432
530
  section += "\n"
433
-
531
+
434
532
  if len(critical_findings) > 5:
435
533
  section += f"*...and {len(critical_findings) - 5} more critical finding(s)*\n\n"
436
-
534
+
437
535
  # High Priority (Within 7 days)
438
536
  if high_findings:
439
537
  section += "### ⚠️ High Priority (Address within 7 days)\n\n"
440
538
  for idx, finding in enumerate(high_findings[:3], 1): # Top 3 high
441
- title = finding.get('title', 'Untitled Finding')
539
+ title = finding.get("title", "Untitled Finding")
442
540
  host = self._format_affected_host(finding)
443
541
  section += f"{idx}. **{title}** - {host}\n"
444
-
542
+
445
543
  if len(high_findings) > 3:
446
544
  section += f"\n*...and {len(high_findings) - 3} more high-priority finding(s)*\n"
447
545
  section += "\n"
448
-
546
+
449
547
  # Overall stats
450
548
  total_critical = len(critical_findings)
451
549
  total_high = len(high_findings)
452
- total_medium = len(findings['medium'])
453
- total_low = len(findings['low'])
454
-
550
+ total_medium = len(findings["medium"])
551
+ total_low = len(findings["low"])
552
+
455
553
  section += f"**Total Findings:** {total_critical} Critical, {total_high} High, {total_medium} Medium, {total_low} Low\n\n"
456
554
  section += "**Recommendation:** Address all critical findings immediately, high findings within 7 days.\n\n"
457
555
  section += "*See Detailed Findings section below for complete information.*\n\n"
458
556
  section += "---\n"
459
-
557
+
460
558
  return section
461
-
462
- def compliance_section(self, findings: List[Dict], compliance_data: Dict, report_type: str = 'technical') -> str:
559
+
560
+ def compliance_section(
561
+ self,
562
+ findings: List[Dict],
563
+ compliance_data: Dict,
564
+ report_type: str = "technical",
565
+ ) -> str:
463
566
  """
464
567
  Generate compliance mapping section.
465
-
568
+
466
569
  Args:
467
570
  findings: List of all findings
468
571
  compliance_data: Compliance coverage data
@@ -472,33 +575,35 @@ Testing was conducted against systems within the defined scope. All testing acti
472
575
  - summary: Brief summary only
473
576
  """
474
577
  from souleyez.reporting.compliance_mappings import ComplianceMappings
475
-
578
+
476
579
  mapper = ComplianceMappings()
477
580
  section = """## COMPLIANCE MAPPING
478
581
 
479
582
  ### OWASP Top 10 2021 Coverage
480
583
 
481
584
  """
482
-
483
- owasp_coverage = compliance_data['owasp']
585
+
586
+ owasp_coverage = compliance_data["owasp"]
484
587
  section += f"**Coverage: {owasp_coverage['coverage_percent']}%** ({len(owasp_coverage['covered'])}/{owasp_coverage['total']} categories)\n\n"
485
-
588
+
486
589
  # Count findings per category
487
590
  owasp_findings_count = {}
488
591
  for finding in findings:
489
592
  owasp_matches = mapper.map_finding_to_owasp(finding)
490
593
  for owasp_id in owasp_matches:
491
- owasp_findings_count[owasp_id] = owasp_findings_count.get(owasp_id, 0) + 1
492
-
594
+ owasp_findings_count[owasp_id] = (
595
+ owasp_findings_count.get(owasp_id, 0) + 1
596
+ )
597
+
493
598
  # OWASP Coverage Table
494
- if report_type == 'executive':
599
+ if report_type == "executive":
495
600
  # Executive: Only show matched categories (reduce clutter)
496
- if owasp_coverage['covered']:
601
+ if owasp_coverage["covered"]:
497
602
  section += "**OWASP Categories Identified:**\n\n"
498
603
  section += "| Category | Findings |\n"
499
604
  section += "|----------|----------|\n"
500
-
501
- for owasp_id in sorted(owasp_coverage['covered']):
605
+
606
+ for owasp_id in sorted(owasp_coverage["covered"]):
502
607
  name = mapper.get_owasp_name(owasp_id)
503
608
  count = owasp_findings_count.get(owasp_id, 0)
504
609
  section += f"| {owasp_id}: {name} | {count} |\n"
@@ -508,87 +613,89 @@ Testing was conducted against systems within the defined scope. All testing acti
508
613
  # Technical/Summary: Show all categories with status
509
614
  section += "| Category | Status | Findings |\n"
510
615
  section += "|----------|--------|----------|\n"
511
-
616
+
512
617
  for owasp_id in sorted(mapper.owasp_mappings.keys()):
513
618
  name = mapper.get_owasp_name(owasp_id)
514
- if owasp_id in owasp_coverage['covered']:
619
+ if owasp_id in owasp_coverage["covered"]:
515
620
  count = owasp_findings_count.get(owasp_id, 0)
516
621
  status = "✅ Covered"
517
622
  section += f"| {owasp_id}: {name} | {status} | {count} |\n"
518
623
  else:
519
624
  section += f"| {owasp_id}: {name} | ⚪ Not Found | 0 |\n"
520
-
625
+
521
626
  section += "\n### CWE Top 25 Coverage\n\n"
522
-
523
- cwe_coverage = compliance_data['cwe']
627
+
628
+ cwe_coverage = compliance_data["cwe"]
524
629
  section += f"**Coverage: {cwe_coverage['coverage_percent']}%** ({len(cwe_coverage['covered'])}/{cwe_coverage['total']} categories)\n\n"
525
-
526
- if cwe_coverage['covered']:
630
+
631
+ if cwe_coverage["covered"]:
527
632
  section += "**CWEs Identified:**\n\n"
528
-
633
+
529
634
  # Count findings per CWE
530
635
  cwe_findings_count = {}
531
636
  for finding in findings:
532
637
  cwe_matches = mapper.map_finding_to_cwe(finding)
533
638
  for cwe_id in cwe_matches:
534
639
  cwe_findings_count[cwe_id] = cwe_findings_count.get(cwe_id, 0) + 1
535
-
640
+
536
641
  # Limit to top 10 for executive reports
537
- cwe_list = sorted(cwe_coverage['covered'])
538
- if report_type == 'executive' and len(cwe_list) > 10:
642
+ cwe_list = sorted(cwe_coverage["covered"])
643
+ if report_type == "executive" and len(cwe_list) > 10:
539
644
  cwe_list = cwe_list[:10]
540
645
  show_more = True
541
646
  else:
542
647
  show_more = False
543
-
648
+
544
649
  for cwe_id in cwe_list:
545
650
  name = mapper.get_cwe_name(cwe_id)
546
651
  count = cwe_findings_count.get(cwe_id, 0)
547
652
  section += f"- **{cwe_id}**: {name} ({count} finding{'s' if count != 1 else ''})\n"
548
-
653
+
549
654
  if show_more:
550
655
  section += f"\n*...and {len(cwe_coverage['covered']) - 10} more CWEs*\n"
551
656
  else:
552
657
  section += "No CWE Top 25 vulnerabilities identified.\n"
553
-
658
+
554
659
  # Compliance Gaps (only for technical reports)
555
- if report_type == 'technical' and (compliance_data['owasp']['gaps'] or compliance_data['cwe']['gaps']):
660
+ if report_type == "technical" and (
661
+ compliance_data["owasp"]["gaps"] or compliance_data["cwe"]["gaps"]
662
+ ):
556
663
  section += "\n### Compliance Gaps\n\n"
557
-
558
- if compliance_data['owasp']['gaps']:
664
+
665
+ if compliance_data["owasp"]["gaps"]:
559
666
  section += f"**OWASP Categories Not Found:** {len(compliance_data['owasp']['gaps'])} categories not represented in findings.\n\n"
560
-
561
- if compliance_data['cwe']['gaps']:
667
+
668
+ if compliance_data["cwe"]["gaps"]:
562
669
  section += f"**CWE Categories Not Found:** {len(compliance_data['cwe']['gaps'])} weakness types not identified.\n\n"
563
-
670
+
564
671
  section += "*Note: Gaps indicate vulnerability types not found during testing, which may be positive (not present) or indicate areas requiring deeper assessment.*\n"
565
-
672
+
566
673
  section += "\n---"
567
674
  return section
568
675
 
569
- def detailed_findings(self, findings: Dict, report_type: str = 'technical') -> str:
676
+ def detailed_findings(self, findings: Dict, report_type: str = "technical") -> str:
570
677
  """Generate detailed findings section."""
571
678
  section = "## DETAILED FINDINGS\n\n"
572
679
 
573
680
  finding_number = 1
574
- for severity in ['critical', 'high', 'medium', 'low', 'info']:
681
+ for severity in ["critical", "high", "medium", "low", "info"]:
575
682
  for finding in findings[severity]:
576
683
  section += f"### Finding #{finding_number}: {finding['title']}\n\n"
577
684
 
578
685
  # Severity badge
579
686
  severity_upper = severity.upper()
580
687
  emoji_map = {
581
- 'critical': '🔴',
582
- 'high': '🟠',
583
- 'medium': '🟡',
584
- 'low': '🟢',
585
- 'info': 'ℹ️'
688
+ "critical": "🔴",
689
+ "high": "🟠",
690
+ "medium": "🟡",
691
+ "low": "🟢",
692
+ "info": "ℹ️",
586
693
  }
587
694
  section += f"**Severity:** {emoji_map[severity]} {severity_upper} \n"
588
695
 
589
- if finding.get('cvss'):
696
+ if finding.get("cvss"):
590
697
  section += f"**CVSS Score:** {finding['cvss']} \n"
591
- if finding.get('cve'):
698
+ if finding.get("cve"):
592
699
  section += f"**CVE:** {finding['cve']} \n"
593
700
 
594
701
  # Format affected host display
@@ -597,14 +704,16 @@ Testing was conducted against systems within the defined scope. All testing acti
597
704
  section += f"**Tool:** {finding['tool']} \n\n"
598
705
 
599
706
  # Description
600
- if finding.get('description'):
707
+ if finding.get("description"):
601
708
  section += f"**Description:**\n\n{finding['description']}\n\n"
602
709
 
603
710
  # Remediation - Always include
604
- remediation_text = finding.get('remediation', '')
711
+ remediation_text = finding.get("remediation", "")
605
712
  if not remediation_text:
606
- remediation_text = self._generate_default_remediation(finding, severity)
607
-
713
+ remediation_text = self._generate_default_remediation(
714
+ finding, severity
715
+ )
716
+
608
717
  section += f"**Recommendation:**\n\n{remediation_text}\n\n"
609
718
 
610
719
  section += "---\n\n"
@@ -622,7 +731,9 @@ Testing was conducted against systems within the defined scope. All testing acti
622
731
  section += f"- **Reconnaissance:** {evidence_counts['reconnaissance']} items\n"
623
732
  section += f"- **Enumeration:** {evidence_counts['enumeration']} items\n"
624
733
  section += f"- **Exploitation:** {evidence_counts['exploitation']} items\n"
625
- section += f"- **Post-Exploitation:** {evidence_counts['post_exploitation']} items\n\n"
734
+ section += (
735
+ f"- **Post-Exploitation:** {evidence_counts['post_exploitation']} items\n\n"
736
+ )
626
737
 
627
738
  if credentials:
628
739
  section += f"### Credentials Discovered ({len(credentials)} total)\n\n"
@@ -648,7 +759,7 @@ Testing was conducted against systems within the defined scope. All testing acti
648
759
  *Prioritized remediation roadmap with estimated effort and impact*
649
760
 
650
761
  """
651
-
762
+
652
763
  # Quick Wins (< 1 hour, high impact)
653
764
  quick_wins = self._identify_quick_wins(findings)
654
765
  if quick_wins:
@@ -658,29 +769,31 @@ Testing was conducted against systems within the defined scope. All testing acti
658
769
  section += f"{idx}. **{win['title']}** ⏱️ {win['effort']}\n"
659
770
  section += f" - **Action:** {win['action']}\n"
660
771
  section += f" - **Impact:** {win['impact']}\n\n"
661
-
772
+
662
773
  # Immediate Actions (Critical Priority - Today/This Week)
663
- section += "### 🚨 Immediate Actions (Critical Priority - Complete This Week)\n\n"
664
-
665
- if findings['critical']:
666
- total_critical = len(findings['critical'])
774
+ section += (
775
+ "### 🚨 Immediate Actions (Critical Priority - Complete This Week)\n\n"
776
+ )
777
+
778
+ if findings["critical"]:
779
+ total_critical = len(findings["critical"])
667
780
  section += f"**{total_critical} critical finding{'s' if total_critical != 1 else ''} requiring immediate attention:**\n\n"
668
-
669
- for idx, finding in enumerate(findings['critical'][:5], 1):
781
+
782
+ for idx, finding in enumerate(findings["critical"][:5], 1):
670
783
  section += f"**{idx}. {finding['title']}**\n"
671
-
784
+
672
785
  # Add specific remediation steps
673
- if finding.get('remediation'):
786
+ if finding.get("remediation"):
674
787
  section += f" - **Fix:** {finding['remediation']}\n"
675
-
788
+
676
789
  # Add resource estimate based on finding type
677
790
  effort = self._estimate_remediation_effort(finding)
678
791
  section += f" - **Estimated Effort:** {effort}\n"
679
-
792
+
680
793
  # Add business impact context
681
- impact = self._get_business_impact(finding, 'critical')
794
+ impact = self._get_business_impact(finding, "critical")
682
795
  section += f" - **Business Impact:** {impact}\n\n"
683
-
796
+
684
797
  if total_critical > 5:
685
798
  section += f"*...and {total_critical - 5} more critical finding(s). See Detailed Findings section.*\n\n"
686
799
  else:
@@ -689,30 +802,30 @@ Testing was conducted against systems within the defined scope. All testing acti
689
802
  # Short-Term (High Priority - 1-2 Weeks)
690
803
  section += "### ⚠️ Short-Term Actions (High Priority - Within 2 Weeks)\n\n"
691
804
 
692
- if findings['high']:
693
- total_high = len(findings['high'])
805
+ if findings["high"]:
806
+ total_high = len(findings["high"])
694
807
  section += f"**{total_high} high-priority finding{'s' if total_high != 1 else ''} to address:**\n\n"
695
-
696
- for idx, finding in enumerate(findings['high'][:3], 1):
808
+
809
+ for idx, finding in enumerate(findings["high"][:3], 1):
697
810
  section += f"{idx}. **{finding['title']}**\n"
698
- if finding.get('remediation'):
811
+ if finding.get("remediation"):
699
812
  # Shorten if too long
700
- rem = finding['remediation']
813
+ rem = finding["remediation"]
701
814
  if len(rem) > 150:
702
815
  rem = rem[:147] + "..."
703
816
  section += f" - {rem}\n"
704
817
  effort = self._estimate_remediation_effort(finding)
705
818
  section += f" - Effort: {effort}\n\n"
706
-
819
+
707
820
  if total_high > 3:
708
821
  section += f"*...and {total_high - 3} more high-priority findings.*\n\n"
709
822
  else:
710
823
  section += "✅ No high-priority findings identified.\n\n"
711
824
 
712
825
  # Medium-Term (Medium Severity - 30 Days)
713
- if findings['medium']:
826
+ if findings["medium"]:
714
827
  section += "### 📋 Medium-Term Actions (Within 30 Days)\n\n"
715
- total_medium = len(findings['medium'])
828
+ total_medium = len(findings["medium"])
716
829
  section += f"**{total_medium} medium-severity finding{'s' if total_medium != 1 else ''} identified.** "
717
830
  section += "Prioritize based on asset criticality and exposure.\n\n"
718
831
  section += "Review the Detailed Findings section for specific remediation guidance on each issue.\n\n"
@@ -729,125 +842,138 @@ Testing was conducted against systems within the defined scope. All testing acti
729
842
 
730
843
  section += "\n---"
731
844
  return section
732
-
845
+
733
846
  def _identify_quick_wins(self, findings: Dict) -> List[Dict]:
734
847
  """Identify quick-win fixes (< 1 hour, high impact)."""
735
848
  quick_wins = []
736
-
849
+
737
850
  # Common quick-win patterns
738
851
  quick_win_patterns = {
739
- 'information disclosure': {
740
- 'action': 'Remove or restrict access to exposed information',
741
- 'impact': 'Reduces reconnaissance opportunities for attackers',
742
- 'effort': '15-30 min'
852
+ "information disclosure": {
853
+ "action": "Remove or restrict access to exposed information",
854
+ "impact": "Reduces reconnaissance opportunities for attackers",
855
+ "effort": "15-30 min",
743
856
  },
744
- 'default credentials': {
745
- 'action': 'Change all default passwords immediately',
746
- 'impact': 'Prevents trivial unauthorized access',
747
- 'effort': '5-10 min per system'
857
+ "default credentials": {
858
+ "action": "Change all default passwords immediately",
859
+ "impact": "Prevents trivial unauthorized access",
860
+ "effort": "5-10 min per system",
748
861
  },
749
- 'missing security headers': {
750
- 'action': 'Configure web server security headers',
751
- 'impact': 'Protects against common web attacks',
752
- 'effort': '30-45 min'
862
+ "missing security headers": {
863
+ "action": "Configure web server security headers",
864
+ "impact": "Protects against common web attacks",
865
+ "effort": "30-45 min",
753
866
  },
754
- 'unencrypted': {
755
- 'action': 'Enable SSL/TLS encryption',
756
- 'impact': 'Protects data in transit',
757
- 'effort': '30-60 min'
867
+ "unencrypted": {
868
+ "action": "Enable SSL/TLS encryption",
869
+ "impact": "Protects data in transit",
870
+ "effort": "30-60 min",
871
+ },
872
+ "directory listing": {
873
+ "action": "Disable directory indexing in web server config",
874
+ "impact": "Prevents information disclosure",
875
+ "effort": "10-15 min",
758
876
  },
759
- 'directory listing': {
760
- 'action': 'Disable directory indexing in web server config',
761
- 'impact': 'Prevents information disclosure',
762
- 'effort': '10-15 min'
763
- }
764
877
  }
765
-
878
+
766
879
  # Check critical and high findings for quick wins
767
- for finding in findings['critical'] + findings['high']:
768
- title_lower = finding['title'].lower()
880
+ for finding in findings["critical"] + findings["high"]:
881
+ title_lower = finding["title"].lower()
769
882
  for pattern, details in quick_win_patterns.items():
770
883
  if pattern in title_lower:
771
- quick_wins.append({
772
- 'title': finding['title'],
773
- 'action': details['action'],
774
- 'impact': details['impact'],
775
- 'effort': details['effort']
776
- })
884
+ quick_wins.append(
885
+ {
886
+ "title": finding["title"],
887
+ "action": details["action"],
888
+ "impact": details["impact"],
889
+ "effort": details["effort"],
890
+ }
891
+ )
777
892
  break
778
-
893
+
779
894
  # Limit to top 5 quick wins
780
895
  if len(quick_wins) >= 5:
781
896
  break
782
-
897
+
783
898
  return quick_wins
784
-
899
+
785
900
  def _estimate_remediation_effort(self, finding: Dict) -> str:
786
901
  """Estimate remediation effort based on finding type."""
787
- title_lower = finding['title'].lower()
788
-
902
+ title_lower = finding["title"].lower()
903
+
789
904
  # High effort (4+ hours)
790
- if any(x in title_lower for x in ['architecture', 'redesign', 'refactor', 'encryption scheme']):
905
+ if any(
906
+ x in title_lower
907
+ for x in ["architecture", "redesign", "refactor", "encryption scheme"]
908
+ ):
791
909
  return "4-8 hours (Complex)"
792
-
910
+
793
911
  # Medium effort (1-4 hours)
794
- if any(x in title_lower for x in ['sql injection', 'xss', 'authentication', 'access control']):
912
+ if any(
913
+ x in title_lower
914
+ for x in ["sql injection", "xss", "authentication", "access control"]
915
+ ):
795
916
  return "2-4 hours (Moderate)"
796
-
917
+
797
918
  # Low effort (< 1 hour)
798
- if any(x in title_lower for x in ['default', 'disclosure', 'header', 'configuration']):
919
+ if any(
920
+ x in title_lower
921
+ for x in ["default", "disclosure", "header", "configuration"]
922
+ ):
799
923
  return "30-60 minutes (Simple)"
800
-
924
+
801
925
  # Default to medium
802
926
  return "1-3 hours (Moderate)"
803
-
927
+
804
928
  def _get_business_impact(self, finding: Dict, severity: str) -> str:
805
929
  """Get business impact context for finding."""
806
- title_lower = finding['title'].lower()
807
-
930
+ title_lower = finding["title"].lower()
931
+
808
932
  # Data breach scenarios
809
- if 'sql injection' in title_lower or 'data breach' in title_lower:
810
- return "Data breach, regulatory penalties (GDPR/PCI-DSS), reputational damage"
811
-
933
+ if "sql injection" in title_lower or "data breach" in title_lower:
934
+ return (
935
+ "Data breach, regulatory penalties (GDPR/PCI-DSS), reputational damage"
936
+ )
937
+
812
938
  # Authentication issues
813
- if 'authentication' in title_lower or 'credentials' in title_lower:
939
+ if "authentication" in title_lower or "credentials" in title_lower:
814
940
  return "Unauthorized access, potential system compromise"
815
-
941
+
816
942
  # XSS/injection
817
- if 'xss' in title_lower or 'injection' in title_lower:
943
+ if "xss" in title_lower or "injection" in title_lower:
818
944
  return "User account compromise, data theft, malware distribution"
819
-
945
+
820
946
  # Default by severity
821
- if severity == 'critical':
947
+ if severity == "critical":
822
948
  return "High risk of immediate exploitation and business disruption"
823
949
  else:
824
950
  return "Security exposure requiring timely remediation"
825
-
951
+
826
952
  def _calculate_resource_summary(self, findings: Dict) -> Dict:
827
953
  """Calculate total remediation effort summary."""
828
954
  effort_map = {
829
- '5-10 min per system': 0.2,
830
- '10-15 min': 0.25,
831
- '15-30 min': 0.5,
832
- '30-45 min': 0.75,
833
- '30-60 min': 1,
834
- '30-60 minutes (Simple)': 1,
835
- '1-3 hours (Moderate)': 2,
836
- '2-4 hours (Moderate)': 3,
837
- '4-8 hours (Complex)': 6
955
+ "5-10 min per system": 0.2,
956
+ "10-15 min": 0.25,
957
+ "15-30 min": 0.5,
958
+ "30-45 min": 0.75,
959
+ "30-60 min": 1,
960
+ "30-60 minutes (Simple)": 1,
961
+ "1-3 hours (Moderate)": 2,
962
+ "2-4 hours (Moderate)": 3,
963
+ "4-8 hours (Complex)": 6,
838
964
  }
839
-
840
- summary = {'critical': 0, 'high': 0, 'medium': 0}
841
-
842
- for severity in ['critical', 'high', 'medium']:
965
+
966
+ summary = {"critical": 0, "high": 0, "medium": 0}
967
+
968
+ for severity in ["critical", "high", "medium"]:
843
969
  for finding in findings[severity]:
844
970
  effort_str = self._estimate_remediation_effort(finding)
845
971
  # Default to 2 hours if not in map
846
972
  hours = effort_map.get(effort_str, 2)
847
973
  summary[severity] += hours
848
-
849
- summary['total'] = summary['critical'] + summary['high'] + summary['medium']
850
-
974
+
975
+ summary["total"] = summary["critical"] + summary["high"] + summary["medium"]
976
+
851
977
  return summary
852
978
 
853
979
  def methodology(self) -> str:
@@ -882,7 +1008,7 @@ This penetration test followed industry-standard methodology based on PTES (Pene
882
1008
  All testing was conducted in accordance with the agreed-upon rules of engagement and with explicit authorization.
883
1009
 
884
1010
  ---"""
885
-
1011
+
886
1012
  def attack_chain_section(
887
1013
  self, chain: Dict, summary: Dict, host_centric_chain: Dict = None
888
1014
  ) -> str:
@@ -894,14 +1020,15 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
894
1020
  host_centric_chain: New host-centric chain structure (preferred)
895
1021
  """
896
1022
  from souleyez.reporting.attack_chain import AttackChainAnalyzer
1023
+
897
1024
  analyzer = AttackChainAnalyzer()
898
1025
 
899
1026
  # Use host-centric chain if available
900
- if host_centric_chain and host_centric_chain.get('hosts'):
1027
+ if host_centric_chain and host_centric_chain.get("hosts"):
901
1028
  return self._host_centric_attack_section(host_centric_chain, analyzer)
902
1029
 
903
1030
  # Fallback to legacy visualization
904
- if not chain or not chain.get('nodes'):
1031
+ if not chain or not chain.get("nodes"):
905
1032
  return ""
906
1033
 
907
1034
  section = """## ATTACK CHAIN ANALYSIS
@@ -927,15 +1054,17 @@ The following diagram shows the progression of the attack from initial reconnais
927
1054
  section += f"- **Total Attack Steps:** {summary['total_nodes']} nodes, {summary['total_edges']} transitions\n"
928
1055
  section += f"- **Hosts Compromised:** {summary['hosts_compromised']}\n"
929
1056
  section += f"- **Active Phases:** {summary['phases_active']}/4 penetration testing phases\n"
930
- section += f"- **Attack Depth:** {summary['longest_path']} levels (longest path)\n"
1057
+ section += (
1058
+ f"- **Attack Depth:** {summary['longest_path']} levels (longest path)\n"
1059
+ )
931
1060
 
932
- if summary['critical_nodes']:
1061
+ if summary["critical_nodes"]:
933
1062
  section += f"- **Critical Nodes:** {len(summary['critical_nodes'])} high-connectivity points\n"
934
1063
 
935
1064
  section += "\n**Phase Breakdown:**\n\n"
936
- for phase, count in chain['phases'].items():
1065
+ for phase, count in chain["phases"].items():
937
1066
  if count > 0:
938
- phase_name = phase.replace('_', ' ').title()
1067
+ phase_name = phase.replace("_", " ").title()
939
1068
  section += f"- **{phase_name}:** {count} evidence items\n"
940
1069
 
941
1070
  section += "\n### Attack Flow Interpretation\n\n"
@@ -946,7 +1075,7 @@ The following diagram shows the progression of the attack from initial reconnais
946
1075
  section += "- Credential harvesting and access escalation\n"
947
1076
  section += "- Post-exploitation activities on compromised hosts\n"
948
1077
 
949
- if summary['hosts_compromised'] > 1:
1078
+ if summary["hosts_compromised"] > 1:
950
1079
  section += "\n**Lateral Movement:** The attack chain shows movement across "
951
1080
  section += f"{summary['hosts_compromised']} different hosts, indicating potential for lateral movement within the network.\n"
952
1081
 
@@ -956,8 +1085,8 @@ The following diagram shows the progression of the attack from initial reconnais
956
1085
  def _host_centric_attack_section(self, chain: Dict, analyzer) -> str:
957
1086
  """Generate host-centric attack chain visualization."""
958
1087
  summary = analyzer.get_host_centric_summary(chain)
959
- hosts = chain.get('hosts', [])
960
- lateral_edges = chain.get('lateral_edges', [])
1088
+ hosts = chain.get("hosts", [])
1089
+ lateral_edges = chain.get("lateral_edges", [])
961
1090
 
962
1091
  section = """## ATTACK PATH VISUALIZATION
963
1092
 
@@ -998,45 +1127,49 @@ Each box represents a target host with its attack progression. Arrows show the p
998
1127
  section += f"- **Hosts Exploited:** {summary['hosts_exploited']}\n"
999
1128
  section += f"- **Total Attack Steps:** {summary['total_nodes']}\n"
1000
1129
  section += f"- **Deepest Penetration:** {summary['deepest_attack']} phases"
1001
- if summary.get('deepest_host'):
1130
+ if summary.get("deepest_host"):
1002
1131
  section += f" (on {summary['deepest_host']})"
1003
1132
  section += "\n"
1004
1133
 
1005
- if summary['lateral_movements'] > 0:
1134
+ if summary["lateral_movements"] > 0:
1006
1135
  section += f"- **Lateral Movements:** {summary['lateral_movements']} cross-host transitions\n"
1007
1136
 
1008
1137
  # Phase breakdown
1009
- phase_counts = summary.get('phase_counts', {})
1138
+ phase_counts = summary.get("phase_counts", {})
1010
1139
  if any(v > 0 for v in phase_counts.values()):
1011
1140
  section += "\n**Attack Phase Distribution:**\n\n"
1012
1141
  phase_labels = {
1013
- 'discovery': 'Discovery',
1014
- 'enumeration': 'Enumeration',
1015
- 'vulnerability': 'Vulnerabilities',
1016
- 'exploitation': 'Exploitation',
1017
- 'credential': 'Credentials',
1018
- 'post_exploitation': 'Post-Exploitation'
1142
+ "discovery": "Discovery",
1143
+ "enumeration": "Enumeration",
1144
+ "vulnerability": "Vulnerabilities",
1145
+ "exploitation": "Exploitation",
1146
+ "credential": "Credentials",
1147
+ "post_exploitation": "Post-Exploitation",
1019
1148
  }
1020
1149
  for phase, count in phase_counts.items():
1021
1150
  if count > 0:
1022
- label = phase_labels.get(phase, phase.replace('_', ' ').title())
1151
+ label = phase_labels.get(phase, phase.replace("_", " ").title())
1023
1152
  section += f"- **{label}:** {count} instances\n"
1024
1153
 
1025
1154
  # Interpretation
1026
1155
  section += "\n### Attack Flow Interpretation\n\n"
1027
1156
 
1028
- if summary['hosts_exploited'] == 0:
1157
+ if summary["hosts_exploited"] == 0:
1029
1158
  section += "No hosts were fully exploited during this engagement. "
1030
- section += "The attack progressed through reconnaissance and enumeration phases.\n"
1031
- elif summary['hosts_exploited'] == 1:
1159
+ section += (
1160
+ "The attack progressed through reconnaissance and enumeration phases.\n"
1161
+ )
1162
+ elif summary["hosts_exploited"] == 1:
1032
1163
  section += f"One host was successfully exploited. "
1033
- if summary['lateral_movements'] > 0:
1164
+ if summary["lateral_movements"] > 0:
1034
1165
  section += "Lateral movement was detected, suggesting the attacker attempted to pivot to other systems.\n"
1035
1166
  else:
1036
1167
  section += "No lateral movement was detected.\n"
1037
1168
  else:
1038
- section += f"**{summary['hosts_exploited']} hosts** were successfully exploited. "
1039
- if summary['lateral_movements'] > 0:
1169
+ section += (
1170
+ f"**{summary['hosts_exploited']} hosts** were successfully exploited. "
1171
+ )
1172
+ if summary["lateral_movements"] > 0:
1040
1173
  section += f"**{summary['lateral_movements']} lateral movements** were detected, "
1041
1174
  section += "demonstrating the attacker's ability to pivot between systems using harvested credentials.\n"
1042
1175
  else:
@@ -1045,8 +1178,12 @@ Each box represents a target host with its attack progression. Arrows show the p
1045
1178
  section += "\n---"
1046
1179
  return section
1047
1180
 
1048
- def appendix(self, hosts: List[Dict], credentials: List[Dict],
1049
- include_methodology: bool = True) -> str:
1181
+ def appendix(
1182
+ self,
1183
+ hosts: List[Dict],
1184
+ credentials: List[Dict],
1185
+ include_methodology: bool = True,
1186
+ ) -> str:
1050
1187
  """Generate appendix section with optional methodology."""
1051
1188
  section = """## APPENDIX
1052
1189
 
@@ -1095,14 +1232,14 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1095
1232
 
1096
1233
  for host in hosts:
1097
1234
  # Format services count properly (handle list, int, or empty)
1098
- services_data = host.get('services', 0)
1235
+ services_data = host.get("services", 0)
1099
1236
  if isinstance(services_data, list):
1100
1237
  services_count = len(services_data)
1101
1238
  else:
1102
1239
  services_count = services_data
1103
1240
 
1104
1241
  section += f"- **{host['host']}**"
1105
- if host.get('hostname'):
1242
+ if host.get("hostname"):
1106
1243
  section += f" ({host['hostname']})"
1107
1244
  section += f" - {services_count} services, {host['findings']} findings\n"
1108
1245
 
@@ -1131,197 +1268,218 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1131
1268
 
1132
1269
  *This report contains confidential information. Unauthorized distribution is prohibited.*
1133
1270
  """
1134
-
1271
+
1135
1272
  def _format_affected_host(self, finding: Dict) -> str:
1136
1273
  """
1137
1274
  Format affected host display from finding data.
1138
-
1275
+
1139
1276
  Tries multiple fields in order:
1140
1277
  1. ip_address (from JOIN with hosts table)
1141
- 2. hostname (from JOIN with hosts table)
1278
+ 2. hostname (from JOIN with hosts table)
1142
1279
  3. path (for web findings with URL path)
1143
1280
  4. port (show port number)
1144
1281
  5. 'Unknown' as fallback
1145
-
1282
+
1146
1283
  Args:
1147
1284
  finding: Finding dictionary with host data
1148
-
1285
+
1149
1286
  Returns:
1150
1287
  Formatted host string
1151
1288
  """
1152
1289
  # Try IP address first (most common)
1153
- if finding.get('ip_address'):
1154
- host_str = finding['ip_address']
1290
+ if finding.get("ip_address"):
1291
+ host_str = finding["ip_address"]
1155
1292
  # Add hostname if available
1156
- if finding.get('hostname'):
1293
+ if finding.get("hostname"):
1157
1294
  host_str += f" ({finding['hostname']})"
1158
1295
  # Add port if available
1159
- if finding.get('port'):
1296
+ if finding.get("port"):
1160
1297
  host_str += f":{finding['port']}"
1161
1298
  # Add path for web findings - but only if it's a clean path (not a full URL)
1162
- if finding.get('path') and finding['path'] != '/' and not finding['path'].startswith('http'):
1163
- host_str += finding['path']
1299
+ if (
1300
+ finding.get("path")
1301
+ and finding["path"] != "/"
1302
+ and not finding["path"].startswith("http")
1303
+ ):
1304
+ host_str += finding["path"]
1164
1305
  return host_str
1165
-
1306
+
1166
1307
  # Try hostname alone
1167
- if finding.get('hostname'):
1168
- host_str = finding['hostname']
1169
- if finding.get('port'):
1308
+ if finding.get("hostname"):
1309
+ host_str = finding["hostname"]
1310
+ if finding.get("port"):
1170
1311
  host_str += f":{finding['port']}"
1171
1312
  # Only add path if it's a clean path (not a full URL)
1172
- if finding.get('path') and finding['path'] != '/' and not finding['path'].startswith('http'):
1173
- host_str += finding['path']
1313
+ if (
1314
+ finding.get("path")
1315
+ and finding["path"] != "/"
1316
+ and not finding["path"].startswith("http")
1317
+ ):
1318
+ host_str += finding["path"]
1174
1319
  return host_str
1175
-
1320
+
1176
1321
  # Try path alone (web findings without host) - but only if not a full URL
1177
- if finding.get('path') and not finding['path'].startswith('http'):
1178
- return finding['path']
1179
-
1322
+ if finding.get("path") and not finding["path"].startswith("http"):
1323
+ return finding["path"]
1324
+
1180
1325
  # Try port alone
1181
- if finding.get('port'):
1326
+ if finding.get("port"):
1182
1327
  return f"Port {finding['port']}"
1183
-
1328
+
1184
1329
  # Fallback
1185
- return 'Unknown'
1186
-
1330
+ return "Unknown"
1331
+
1187
1332
  def _generate_default_remediation(self, finding: Dict, severity: str) -> str:
1188
1333
  """Generate default remediation recommendation based on finding details."""
1189
- title_lower = finding.get('title', '').lower()
1190
- tool = finding.get('tool', '').lower()
1191
-
1334
+ title_lower = finding.get("title", "").lower()
1335
+ tool = finding.get("tool", "").lower()
1336
+
1192
1337
  # SQL Injection
1193
- if 'sql injection' in title_lower or 'sqli' in title_lower:
1338
+ if "sql injection" in title_lower or "sqli" in title_lower:
1194
1339
  return """1. Implement parameterized queries/prepared statements for all database operations
1195
1340
  2. Apply input validation and sanitization for all user inputs
1196
1341
  3. Use stored procedures with properly defined parameters
1197
1342
  4. Implement least privilege database access controls
1198
1343
  5. Deploy a Web Application Firewall (WAF) to block SQL injection attempts
1199
1344
  6. Review and patch all vulnerable parameters immediately"""
1200
-
1345
+
1201
1346
  # XSS
1202
- if 'xss' in title_lower or 'cross-site scripting' in title_lower:
1347
+ if "xss" in title_lower or "cross-site scripting" in title_lower:
1203
1348
  return """1. Implement proper output encoding for all user-controlled data
1204
1349
  2. Use Content Security Policy (CSP) headers
1205
1350
  3. Apply HTML sanitization on user inputs
1206
1351
  4. Enable HTTPOnly and Secure flags on cookies
1207
1352
  5. Validate and sanitize all input on server-side
1208
1353
  6. Use modern frameworks with built-in XSS protection"""
1209
-
1354
+
1210
1355
  # Authentication issues
1211
- if 'auth' in title_lower or 'login' in title_lower or 'password' in title_lower or 'credential' in title_lower:
1356
+ if (
1357
+ "auth" in title_lower
1358
+ or "login" in title_lower
1359
+ or "password" in title_lower
1360
+ or "credential" in title_lower
1361
+ ):
1212
1362
  return """1. Implement multi-factor authentication (MFA)
1213
1363
  2. Enforce strong password policies (minimum 12 characters, complexity)
1214
1364
  3. Use bcrypt or Argon2 for password hashing
1215
1365
  4. Implement account lockout after failed attempts
1216
1366
  5. Use secure session management with proper timeouts
1217
1367
  6. Review and update authentication mechanisms"""
1218
-
1368
+
1219
1369
  # SSL/TLS issues
1220
- if 'ssl' in title_lower or 'tls' in title_lower or 'certificate' in title_lower:
1370
+ if "ssl" in title_lower or "tls" in title_lower or "certificate" in title_lower:
1221
1371
  return """1. Update to TLS 1.2 or higher (disable TLS 1.0/1.1)
1222
1372
  2. Use strong cipher suites only
1223
1373
  3. Obtain and install valid SSL/TLS certificates
1224
1374
  4. Enable HSTS (HTTP Strict Transport Security)
1225
1375
  5. Configure proper certificate chain validation
1226
1376
  6. Regular certificate monitoring and renewal"""
1227
-
1377
+
1228
1378
  # File upload
1229
- if 'upload' in title_lower or 'file' in title_lower:
1379
+ if "upload" in title_lower or "file" in title_lower:
1230
1380
  return """1. Validate file types using whitelist approach
1231
1381
  2. Implement file size restrictions
1232
1382
  3. Scan uploaded files for malware
1233
1383
  4. Store uploaded files outside web root
1234
1384
  5. Use random filenames to prevent path traversal
1235
1385
  6. Implement proper access controls on uploaded content"""
1236
-
1386
+
1237
1387
  # Command injection
1238
- if 'command injection' in title_lower or 'command execution' in title_lower:
1388
+ if "command injection" in title_lower or "command execution" in title_lower:
1239
1389
  return """1. Avoid system calls that use user input
1240
1390
  2. Implement strict input validation and sanitization
1241
1391
  3. Use safe APIs that don't invoke shell commands
1242
1392
  4. Apply the principle of least privilege for process execution
1243
1393
  5. Use parameterized commands when shell execution is necessary
1244
1394
  6. Implement command whitelisting"""
1245
-
1395
+
1246
1396
  # Directory traversal
1247
- if 'directory traversal' in title_lower or 'path traversal' in title_lower:
1397
+ if "directory traversal" in title_lower or "path traversal" in title_lower:
1248
1398
  return """1. Validate and sanitize all file path inputs
1249
1399
  2. Use whitelisting for allowed paths
1250
1400
  3. Reject paths with '..' sequences
1251
1401
  4. Implement proper access controls on file systems
1252
1402
  5. Use secure file access APIs
1253
1403
  6. Chroot/jail file operations where possible"""
1254
-
1404
+
1255
1405
  # Information disclosure
1256
- if 'disclosure' in title_lower or 'exposure' in title_lower or 'leakage' in title_lower:
1406
+ if (
1407
+ "disclosure" in title_lower
1408
+ or "exposure" in title_lower
1409
+ or "leakage" in title_lower
1410
+ ):
1257
1411
  return """1. Remove or restrict access to sensitive information
1258
1412
  2. Implement proper error handling (don't expose stack traces)
1259
1413
  3. Review and remove debug information from production
1260
1414
  4. Apply proper authentication and authorization
1261
1415
  5. Use generic error messages for public-facing systems
1262
1416
  6. Review server configurations and headers"""
1263
-
1417
+
1264
1418
  # Misconfigurations
1265
- if 'misconfiguration' in title_lower or 'default' in title_lower:
1419
+ if "misconfiguration" in title_lower or "default" in title_lower:
1266
1420
  return """1. Change all default credentials immediately
1267
1421
  2. Disable unnecessary services and features
1268
1422
  3. Apply security hardening guidelines (CIS benchmarks)
1269
1423
  4. Review and update security configurations
1270
1424
  5. Implement regular security configuration audits
1271
1425
  6. Use infrastructure-as-code for consistent configurations"""
1272
-
1426
+
1273
1427
  # SMB/Windows issues
1274
- if 'smb' in title_lower or tool == 'enum4linux':
1428
+ if "smb" in title_lower or tool == "enum4linux":
1275
1429
  return """1. Disable SMBv1 protocol (use SMBv2/v3 only)
1276
1430
  2. Restrict anonymous access to SMB shares
1277
1431
  3. Implement proper access controls on shares
1278
1432
  4. Enable SMB signing and encryption
1279
1433
  5. Apply latest Windows security patches
1280
1434
  6. Use strong authentication mechanisms"""
1281
-
1435
+
1282
1436
  # DNS issues
1283
- if 'dns' in title_lower or tool == 'dnsrecon':
1437
+ if "dns" in title_lower or tool == "dnsrecon":
1284
1438
  return """1. Restrict zone transfers to authorized servers only
1285
1439
  2. Implement DNSSEC for data integrity
1286
1440
  3. Use split-horizon DNS for internal/external separation
1287
1441
  4. Apply rate limiting to prevent DNS amplification attacks
1288
1442
  5. Monitor DNS logs for suspicious activity
1289
1443
  6. Keep DNS software updated"""
1290
-
1444
+
1291
1445
  # Brute force / Weak credentials
1292
- if 'brute' in title_lower or 'weak password' in title_lower or tool in ['hydra', 'medusa']:
1446
+ if (
1447
+ "brute" in title_lower
1448
+ or "weak password" in title_lower
1449
+ or tool in ["hydra", "medusa"]
1450
+ ):
1293
1451
  return """1. Enforce strong password policies
1294
1452
  2. Implement account lockout mechanisms
1295
1453
  3. Deploy multi-factor authentication (MFA)
1296
1454
  4. Monitor and alert on failed login attempts
1297
1455
  5. Use CAPTCHA for login forms
1298
1456
  6. Implement rate limiting on authentication endpoints"""
1299
-
1457
+
1300
1458
  # Web vulnerabilities (generic)
1301
- if tool in ['nuclei', 'gobuster', 'ffuf']:
1459
+ if tool in ["nuclei", "gobuster", "ffuf"]:
1302
1460
  return """1. Review and patch identified web vulnerabilities
1303
1461
  2. Apply security updates to web server and applications
1304
1462
  3. Implement Web Application Firewall (WAF)
1305
1463
  4. Use security headers (CSP, X-Frame-Options, etc.)
1306
1464
  5. Perform regular security testing
1307
1465
  6. Follow OWASP Top 10 remediation guidance"""
1308
-
1466
+
1309
1467
  # Default by severity
1310
- if severity == 'critical':
1468
+ if severity == "critical":
1311
1469
  return """1. **IMMEDIATE ACTION REQUIRED** - Patch or mitigate this vulnerability within 24 hours
1312
1470
  2. Isolate affected systems if immediate patching isn't possible
1313
1471
  3. Implement monitoring for exploitation attempts
1314
1472
  4. Review logs for signs of compromise
1315
1473
  5. Notify security team and stakeholders
1316
1474
  6. Plan for emergency patching and testing"""
1317
- elif severity == 'high':
1475
+ elif severity == "high":
1318
1476
  return """1. Prioritize remediation within 7 days
1319
1477
  2. Apply vendor patches or security updates
1320
1478
  3. Implement temporary mitigations if patches unavailable
1321
1479
  4. Review and restrict access to affected systems
1322
1480
  5. Enhance monitoring and logging
1323
1481
  6. Document remediation efforts"""
1324
- elif severity == 'medium':
1482
+ elif severity == "medium":
1325
1483
  return """1. Address within 30 days as part of regular patching cycle
1326
1484
  2. Review and apply security best practices
1327
1485
  3. Implement defense-in-depth controls
@@ -1336,7 +1494,7 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1336
1494
  5. Monitor for any changes in risk level
1337
1495
  6. Consider as part of security posture improvement"""
1338
1496
 
1339
- def ai_executive_summary(self, content: str, provider: str = 'AI') -> str:
1497
+ def ai_executive_summary(self, content: str, provider: str = "AI") -> str:
1340
1498
  """
1341
1499
  Render AI-generated executive summary.
1342
1500
 
@@ -1355,7 +1513,7 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1355
1513
 
1356
1514
  ---"""
1357
1515
 
1358
- def ai_remediation_plan(self, content: str, provider: str = 'AI') -> str:
1516
+ def ai_remediation_plan(self, content: str, provider: str = "AI") -> str:
1359
1517
  """
1360
1518
  Render AI-generated remediation plan.
1361
1519
 
@@ -1374,7 +1532,9 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1374
1532
 
1375
1533
  ---"""
1376
1534
 
1377
- def ai_risk_rating(self, rating: str, justification: str, provider: str = 'AI') -> str:
1535
+ def ai_risk_rating(
1536
+ self, rating: str, justification: str, provider: str = "AI"
1537
+ ) -> str:
1378
1538
  """
1379
1539
  Render AI-generated risk rating.
1380
1540
 
@@ -1387,13 +1547,13 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1387
1547
  str: Formatted markdown section
1388
1548
  """
1389
1549
  emoji_map = {
1390
- 'CRITICAL': '🔴',
1391
- 'HIGH': '🟠',
1392
- 'MODERATE': '🟡',
1393
- 'LOW': '🟢',
1394
- 'UNKNOWN': ''
1550
+ "CRITICAL": "🔴",
1551
+ "HIGH": "🟠",
1552
+ "MODERATE": "🟡",
1553
+ "LOW": "🟢",
1554
+ "UNKNOWN": "",
1395
1555
  }
1396
- emoji = emoji_map.get(rating.upper(), '')
1556
+ emoji = emoji_map.get(rating.upper(), "")
1397
1557
 
1398
1558
  return f"""### AI Risk Assessment
1399
1559
 
@@ -1411,7 +1571,7 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1411
1571
  business_impact: str,
1412
1572
  attack_scenario: str,
1413
1573
  risk_context: str,
1414
- provider: str = 'AI'
1574
+ provider: str = "AI",
1415
1575
  ) -> str:
1416
1576
  """
1417
1577
  Render AI-enhanced finding context.
@@ -1429,16 +1589,22 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1429
1589
  sections = []
1430
1590
 
1431
1591
  if business_impact:
1432
- sections.append(f"""**Business Impact:**
1433
- {business_impact}""")
1592
+ sections.append(
1593
+ f"""**Business Impact:**
1594
+ {business_impact}"""
1595
+ )
1434
1596
 
1435
1597
  if attack_scenario:
1436
- sections.append(f"""**Attack Scenario:**
1437
- {attack_scenario}""")
1598
+ sections.append(
1599
+ f"""**Attack Scenario:**
1600
+ {attack_scenario}"""
1601
+ )
1438
1602
 
1439
1603
  if risk_context:
1440
- sections.append(f"""**Risk Context:**
1441
- {risk_context}""")
1604
+ sections.append(
1605
+ f"""**Risk Context:**
1606
+ {risk_context}"""
1607
+ )
1442
1608
 
1443
1609
  if not sections:
1444
1610
  return ""
@@ -1495,7 +1661,7 @@ All testing was conducted in accordance with the agreed-upon rules of engagement
1495
1661
  risk_msg = "Critical detection blindspots"
1496
1662
 
1497
1663
  # Use generated executive summary if available
1498
- exec_summary_text = getattr(data, 'executive_summary', '') or ''
1664
+ exec_summary_text = getattr(data, "executive_summary", "") or ""
1499
1665
 
1500
1666
  return f"""## EXECUTIVE SUMMARY
1501
1667
 
@@ -1534,9 +1700,17 @@ This section summarizes which penetration test attacks triggered SIEM alerts and
1534
1700
  # Status breakdown
1535
1701
  statuses = [
1536
1702
  ("Detected", summary.detected, "Attacks that triggered SIEM alerts"),
1537
- ("Not Detected", summary.not_detected, "Attacks that did NOT trigger alerts (gaps)"),
1703
+ (
1704
+ "Not Detected",
1705
+ summary.not_detected,
1706
+ "Attacks that did NOT trigger alerts (gaps)",
1707
+ ),
1538
1708
  ("Partial", summary.partial, "Attacks with some but incomplete detection"),
1539
- ("Offline", summary.offline, "Offline tools (no network detection expected)"),
1709
+ (
1710
+ "Offline",
1711
+ summary.offline,
1712
+ "Offline tools (no network detection expected)",
1713
+ ),
1540
1714
  ("Unknown", summary.unknown, "Validation errors or inconclusive results"),
1541
1715
  ]
1542
1716
 
@@ -1569,7 +1743,7 @@ The following matrix shows which MITRE ATT&CK techniques were tested during the
1569
1743
  # Group by tactic
1570
1744
  tactics_data = {}
1571
1745
  for item in data.heatmap_data:
1572
- tactic = item['tactic_name']
1746
+ tactic = item["tactic_name"]
1573
1747
  if tactic not in tactics_data:
1574
1748
  tactics_data[tactic] = []
1575
1749
  tactics_data[tactic].append(item)
@@ -1578,16 +1752,16 @@ The following matrix shows which MITRE ATT&CK techniques were tested during the
1578
1752
  section += f"#### {tactic}\n\n"
1579
1753
 
1580
1754
  for tech in techniques:
1581
- status = tech['status']
1755
+ status = tech["status"]
1582
1756
  icon = {
1583
- 'detected': '',
1584
- 'not_detected': '',
1585
- 'partial': '⚠️',
1586
- 'not_tested': ''
1587
- }.get(status, '')
1757
+ "detected": "",
1758
+ "not_detected": "",
1759
+ "partial": "⚠️",
1760
+ "not_tested": "",
1761
+ }.get(status, "")
1588
1762
 
1589
- rate = f" ({tech['detection_rate']}%)" if tech['tested'] > 0 else ""
1590
- tools = ', '.join(tech['tools_used']) if tech['tools_used'] else 'N/A'
1763
+ rate = f" ({tech['detection_rate']}%)" if tech["tested"] > 0 else ""
1764
+ tools = ", ".join(tech["tools_used"]) if tech["tools_used"] else "N/A"
1591
1765
 
1592
1766
  section += f"- {icon} **{tech['technique_id']}** - {tech['technique_name']}{rate}\n"
1593
1767
  section += f" - Tools: {tools}\n"
@@ -1599,7 +1773,7 @@ The following matrix shows which MITRE ATT&CK techniques were tested during the
1599
1773
 
1600
1774
  def detected_attacks_table(self, data) -> str:
1601
1775
  """Generate table of attacks that triggered SIEM alerts."""
1602
- detected = [r for r in data.detection_results if r.status == 'detected']
1776
+ detected = [r for r in data.detection_results if r.status == "detected"]
1603
1777
 
1604
1778
  if not detected:
1605
1779
  return """## DETECTED ATTACKS
@@ -1618,15 +1792,21 @@ The following {len(detected)} attack{'s' if len(detected) != 1 else ''} successf
1618
1792
  """
1619
1793
 
1620
1794
  for result in detected[:50]: # Limit to 50
1621
- attack_type = result.attack_type or 'Unknown'
1622
- target = getattr(result, 'target_ip', 'N/A') or 'N/A'
1795
+ attack_type = result.attack_type or "Unknown"
1796
+ target = getattr(result, "target_ip", "N/A") or "N/A"
1623
1797
  alerts = result.alerts_count
1624
- rules = ', '.join(result.rule_ids[:3]) if result.rule_ids else 'N/A'
1798
+ rules = ", ".join(result.rule_ids[:3]) if result.rule_ids else "N/A"
1625
1799
  if len(result.rule_ids) > 3:
1626
- rules += '...'
1627
- timestamp = result.checked_at.strftime('%Y-%m-%d %H:%M') if result.checked_at else 'N/A'
1800
+ rules += "..."
1801
+ timestamp = (
1802
+ result.checked_at.strftime("%Y-%m-%d %H:%M")
1803
+ if result.checked_at
1804
+ else "N/A"
1805
+ )
1628
1806
 
1629
- section += f"| {attack_type} | {target} | {alerts} | {rules} | {timestamp} |\n"
1807
+ section += (
1808
+ f"| {attack_type} | {target} | {alerts} | {rules} | {timestamp} |\n"
1809
+ )
1630
1810
 
1631
1811
  section += "\n---\n"
1632
1812
  return section
@@ -1653,20 +1833,27 @@ These represent blindspots in security monitoring that should be addressed.
1653
1833
  """
1654
1834
 
1655
1835
  for idx, gap in enumerate(gaps[:30], 1): # Limit to 30
1656
- attack_type = gap.attack_type or 'Unknown'
1657
- target = getattr(gap, 'target_ip', 'N/A') or 'N/A'
1836
+ attack_type = gap.attack_type or "Unknown"
1837
+ target = getattr(gap, "target_ip", "N/A") or "N/A"
1658
1838
 
1659
1839
  # Get MITRE technique
1660
1840
  from souleyez.detection.mitre_mappings import map_tool_to_techniques
1841
+
1661
1842
  techniques = map_tool_to_techniques(attack_type)
1662
- mitre = techniques[0]['id'] if techniques else 'N/A'
1843
+ mitre = techniques[0]["id"] if techniques else "N/A"
1663
1844
 
1664
1845
  # Priority based on attack type severity
1665
1846
  from souleyez.detection.attack_signatures import get_signature
1847
+
1666
1848
  sig = get_signature(attack_type)
1667
- severity = sig.get('severity', 'medium')
1668
- priority_map = {'critical': '🔴 Critical', 'high': '🟠 High', 'medium': '🟡 Medium', 'low': '🟢 Low'}
1669
- priority = priority_map.get(severity, '🟡 Medium')
1849
+ severity = sig.get("severity", "medium")
1850
+ priority_map = {
1851
+ "critical": "🔴 Critical",
1852
+ "high": "🟠 High",
1853
+ "medium": "🟡 Medium",
1854
+ "low": "🟢 Low",
1855
+ }
1856
+ priority = priority_map.get(severity, "🟡 Medium")
1670
1857
 
1671
1858
  section += f"| {priority} | {attack_type} | {target} | {mitre} | Add detection rules |\n"
1672
1859
 
@@ -1675,7 +1862,7 @@ These represent blindspots in security monitoring that should be addressed.
1675
1862
 
1676
1863
  def severity_breakdown_section(self, data) -> str:
1677
1864
  """Generate alert severity breakdown section."""
1678
- severity = getattr(data, 'severity_breakdown', None)
1865
+ severity = getattr(data, "severity_breakdown", None)
1679
1866
  if not severity or severity.total == 0:
1680
1867
  return """## ALERT SEVERITY BREAKDOWN
1681
1868
 
@@ -1695,11 +1882,17 @@ Distribution of {severity.total} alerts by severity level:
1695
1882
  ("High", severity.high),
1696
1883
  ("Medium", severity.medium),
1697
1884
  ("Low", severity.low),
1698
- ("Info", severity.info)
1885
+ ("Info", severity.info),
1699
1886
  ]:
1700
1887
  if count > 0:
1701
1888
  pct = round(count / severity.total * 100, 1)
1702
- icon = {"Critical": "🔴", "High": "🟠", "Medium": "🟡", "Low": "🟢", "Info": "⚪"}.get(level, "⚪")
1889
+ icon = {
1890
+ "Critical": "🔴",
1891
+ "High": "🟠",
1892
+ "Medium": "🟡",
1893
+ "Low": "🟢",
1894
+ "Info": "⚪",
1895
+ }.get(level, "⚪")
1703
1896
  section += f"| {icon} {level} | {count} | {pct}% |\n"
1704
1897
 
1705
1898
  section += "\n---\n"
@@ -1707,7 +1900,7 @@ Distribution of {severity.total} alerts by severity level:
1707
1900
 
1708
1901
  def top_rules_section(self, data) -> str:
1709
1902
  """Generate top triggered rules section."""
1710
- top_rules = getattr(data, 'top_rules', [])
1903
+ top_rules = getattr(data, "top_rules", [])
1711
1904
  if not top_rules:
1712
1905
  return """## TOP TRIGGERED RULES
1713
1906
 
@@ -1724,13 +1917,19 @@ The following SIEM rules generated the most alerts:
1724
1917
  """
1725
1918
  for idx, rule in enumerate(top_rules[:10], 1):
1726
1919
  sev_icon = {
1727
- 'critical': '🔴', 'crit': '🔴',
1728
- 'high': '🟠',
1729
- 'medium': '🟡', 'med': '🟡',
1730
- 'low': '🟢',
1731
- 'info': '⚪'
1732
- }.get(rule.severity.lower(), '⚪')
1733
- rule_name = rule.rule_name[:50] + "..." if len(rule.rule_name) > 50 else rule.rule_name
1920
+ "critical": "🔴",
1921
+ "crit": "🔴",
1922
+ "high": "🟠",
1923
+ "medium": "🟡",
1924
+ "med": "🟡",
1925
+ "low": "🟢",
1926
+ "info": "",
1927
+ }.get(rule.severity.lower(), "⚪")
1928
+ rule_name = (
1929
+ rule.rule_name[:50] + "..."
1930
+ if len(rule.rule_name) > 50
1931
+ else rule.rule_name
1932
+ )
1734
1933
  section += f"| {idx} | {rule.rule_id} | {rule_name} | {rule.count} | {sev_icon} {rule.severity.capitalize()} |\n"
1735
1934
 
1736
1935
  section += "\n---\n"
@@ -1738,7 +1937,7 @@ The following SIEM rules generated the most alerts:
1738
1937
 
1739
1938
  def sample_alerts_section(self, data) -> str:
1740
1939
  """Generate sample alerts section with actual alert content."""
1741
- samples = getattr(data, 'sample_alerts', [])
1940
+ samples = getattr(data, "sample_alerts", [])
1742
1941
  if not samples:
1743
1942
  return """## SAMPLE ALERTS
1744
1943
 
@@ -1753,12 +1952,14 @@ Representative alerts from the assessment (highest severity first):
1753
1952
  """
1754
1953
  for idx, alert in enumerate(samples[:5], 1):
1755
1954
  sev_icon = {
1756
- 'critical': '🔴', 'crit': '🔴',
1757
- 'high': '🟠',
1758
- 'medium': '🟡', 'med': '🟡',
1759
- 'low': '🟢',
1760
- 'info': '⚪'
1761
- }.get(alert.severity.lower(), '⚪')
1955
+ "critical": "🔴",
1956
+ "crit": "🔴",
1957
+ "high": "🟠",
1958
+ "medium": "🟡",
1959
+ "med": "🟡",
1960
+ "low": "🟢",
1961
+ "info": "⚪",
1962
+ }.get(alert.severity.lower(), "⚪")
1762
1963
 
1763
1964
  section += f"""### Alert {idx}: {alert.rule_name}
1764
1965
 
@@ -1783,7 +1984,7 @@ Representative alerts from the assessment (highest severity first):
1783
1984
 
1784
1985
  def vulnerability_section(self, data) -> str:
1785
1986
  """Generate Wazuh vulnerability section for detection report."""
1786
- vuln_data = getattr(data, 'vulnerability_section', None)
1987
+ vuln_data = getattr(data, "vulnerability_section", None)
1787
1988
 
1788
1989
  if not vuln_data or vuln_data.total_vulns == 0:
1789
1990
  return """## VULNERABILITY CONTEXT
@@ -1819,11 +2020,13 @@ Cross-referencing with attack targets reveals which vulnerable systems were test
1819
2020
  """
1820
2021
  for cve in vuln_data.top_cves[:10]:
1821
2022
  sev_icon = {
1822
- 'critical': '🔴', 'high': '🟠',
1823
- 'medium': '🟡', 'low': '🟢'
1824
- }.get(cve.severity.lower(), '⚪')
2023
+ "critical": "🔴",
2024
+ "high": "🟠",
2025
+ "medium": "🟡",
2026
+ "low": "🟢",
2027
+ }.get(cve.severity.lower(), "⚪")
1825
2028
  name = cve.name[:40] + "..." if len(cve.name) > 40 else cve.name
1826
- pkg = cve.package_name or 'N/A'
2029
+ pkg = cve.package_name or "N/A"
1827
2030
  section += f"| {cve.cve_id} | {name} | {sev_icon} {cve.severity} | {cve.cvss_score:.1f} | {pkg} |\n"
1828
2031
  section += "\n"
1829
2032
 
@@ -1848,7 +2051,12 @@ These hosts were targeted during the assessment and have known vulnerabilities:
1848
2051
  if host.top_vulns:
1849
2052
  section += f"**{host.host_ip}** - Top Vulnerabilities:\n"
1850
2053
  for v in host.top_vulns[:3]:
1851
- sev_icon = {'critical': '🔴', 'high': '🟠', 'medium': '🟡', 'low': '🟢'}.get(v.severity.lower(), '⚪')
2054
+ sev_icon = {
2055
+ "critical": "🔴",
2056
+ "high": "🟠",
2057
+ "medium": "🟡",
2058
+ "low": "🟢",
2059
+ }.get(v.severity.lower(), "⚪")
1852
2060
  section += f"- {sev_icon} **{v.cve_id}** (CVSS {v.cvss_score:.1f}) - {v.name[:60]}\n"
1853
2061
  section += "\n"
1854
2062
 
@@ -1884,11 +2092,11 @@ The following recommendations will help close detection gaps:
1884
2092
  """
1885
2093
  for idx, rec in enumerate(recs[:20], 1):
1886
2094
  priority_icon = {
1887
- 'critical': '🔴',
1888
- 'high': '🟠',
1889
- 'medium': '🟡',
1890
- 'low': '🟢'
1891
- }.get(rec.priority, '🟡')
2095
+ "critical": "🔴",
2096
+ "high": "🟠",
2097
+ "medium": "🟡",
2098
+ "low": "🟢",
2099
+ }.get(rec.priority, "🟡")
1892
2100
 
1893
2101
  section += f"""### {idx}. {rec.attack_type.upper()} Detection
1894
2102
 
@@ -1902,7 +2110,9 @@ The following recommendations will help close detection gaps:
1902
2110
 
1903
2111
  """
1904
2112
  if rec.suggested_rule_ids:
1905
- section += f"**Suggested Rule IDs:** {', '.join(rec.suggested_rule_ids)}\n\n"
2113
+ section += (
2114
+ f"**Suggested Rule IDs:** {', '.join(rec.suggested_rule_ids)}\n\n"
2115
+ )
1906
2116
 
1907
2117
  section += "---\n"
1908
2118
  return section
@@ -1931,7 +2141,11 @@ Detection coverage broken down by target host:
1931
2141
  sorted_hosts = sorted(hosts.values(), key=lambda h: h.coverage_percent)
1932
2142
 
1933
2143
  for host in sorted_hosts[:20]: # Limit to 20
1934
- coverage_icon = '🟢' if host.coverage_percent >= 75 else '🟡' if host.coverage_percent >= 50 else '🔴'
2144
+ coverage_icon = (
2145
+ "🟢"
2146
+ if host.coverage_percent >= 75
2147
+ else "🟡" if host.coverage_percent >= 50 else "🔴"
2148
+ )
1935
2149
  section += f"| {host.host_ip} | {host.total_attacks} | {host.detected} | {host.not_detected} | {coverage_icon} {host.coverage_percent}% |\n"
1936
2150
 
1937
2151
  section += "\n---\n"
@@ -2958,14 +3172,16 @@ class HTMLFormatter(MarkdownFormatter):
2958
3172
  <div class="container">
2959
3173
  """
2960
3174
 
2961
- def executive_one_pager(self, metrics: Dict, findings: Dict, engagement: Dict) -> str:
3175
+ def executive_one_pager(
3176
+ self, metrics: Dict, findings: Dict, engagement: Dict
3177
+ ) -> str:
2962
3178
  """Generate executive one-pager - single page summary for executives."""
2963
- risk_level = metrics.get('risk_level', 'MEDIUM').lower()
2964
- risk_score = metrics.get('risk_score', 50)
3179
+ risk_level = metrics.get("risk_level", "MEDIUM").lower()
3180
+ risk_score = metrics.get("risk_score", 50)
2965
3181
 
2966
3182
  # Get top findings
2967
- critical = findings.get('critical', [])
2968
- high = findings.get('high', [])
3183
+ critical = findings.get("critical", [])
3184
+ high = findings.get("high", [])
2969
3185
  top_findings = (critical + high)[:5]
2970
3186
 
2971
3187
  # Calculate estimated breach cost (rough estimate based on findings)
@@ -3015,10 +3231,10 @@ class HTMLFormatter(MarkdownFormatter):
3015
3231
  <h3>TOP SECURITY RISKS</h3>
3016
3232
  """
3017
3233
  for idx, finding in enumerate(top_findings, 1):
3018
- severity = finding.get('severity', 'high')
3019
- sev_class = 'high' if severity == 'high' else ''
3020
- title = finding.get('title', 'Finding')[:50]
3021
- host = finding.get('ip_address', finding.get('hostname', 'Unknown'))
3234
+ severity = finding.get("severity", "high")
3235
+ sev_class = "high" if severity == "high" else ""
3236
+ title = finding.get("title", "Finding")[:50]
3237
+ host = finding.get("ip_address", finding.get("hostname", "Unknown"))
3022
3238
 
3023
3239
  html += f"""<div class="exec-finding-row {sev_class}">
3024
3240
  <div class="exec-finding-num">{idx}</div>
@@ -3029,14 +3245,14 @@ class HTMLFormatter(MarkdownFormatter):
3029
3245
  html += "</div>\n"
3030
3246
 
3031
3247
  # Bottom line recommendation
3032
- if risk_level in ['critical', 'high']:
3033
- bottom_class = ''
3248
+ if risk_level in ["critical", "high"]:
3249
+ bottom_class = ""
3034
3250
  bottom_msg = "IMMEDIATE ACTION REQUIRED: Critical vulnerabilities expose your organization to significant risk. Remediation should begin within 24-48 hours."
3035
- elif risk_level == 'medium':
3036
- bottom_class = 'medium'
3251
+ elif risk_level == "medium":
3252
+ bottom_class = "medium"
3037
3253
  bottom_msg = "ACTION RECOMMENDED: Several security issues require attention within the next 1-2 weeks to maintain security posture."
3038
3254
  else:
3039
- bottom_class = 'low'
3255
+ bottom_class = "low"
3040
3256
  bottom_msg = "GOOD STANDING: Minor issues identified. Continue regular security maintenance and monitoring."
3041
3257
 
3042
3258
  html += f"""<div class="exec-bottom-line {bottom_class}">
@@ -3051,7 +3267,7 @@ class HTMLFormatter(MarkdownFormatter):
3051
3267
  all_findings = []
3052
3268
  for severity, items in findings.items():
3053
3269
  for f in items:
3054
- f['_severity'] = severity
3270
+ f["_severity"] = severity
3055
3271
  all_findings.append(f)
3056
3272
 
3057
3273
  if not all_findings:
@@ -3064,28 +3280,30 @@ class HTMLFormatter(MarkdownFormatter):
3064
3280
  # Low Impact + Hard to Exploit = Low (bottom-left)
3065
3281
 
3066
3282
  quadrants = {
3067
- 'critical': [], # High impact, easy exploit
3068
- 'high': [], # High impact, hard exploit
3069
- 'medium': [], # Low impact, easy exploit
3070
- 'low': [] # Low impact, hard exploit
3283
+ "critical": [], # High impact, easy exploit
3284
+ "high": [], # High impact, hard exploit
3285
+ "medium": [], # Low impact, easy exploit
3286
+ "low": [], # Low impact, hard exploit
3071
3287
  }
3072
3288
 
3073
3289
  for f in all_findings:
3074
- sev = f.get('_severity', 'info')
3075
- if sev == 'critical':
3076
- quadrants['critical'].append(f)
3077
- elif sev == 'high':
3078
- quadrants['high'].append(f)
3079
- elif sev == 'medium':
3080
- quadrants['medium'].append(f)
3081
- elif sev == 'low':
3082
- quadrants['low'].append(f)
3290
+ sev = f.get("_severity", "info")
3291
+ if sev == "critical":
3292
+ quadrants["critical"].append(f)
3293
+ elif sev == "high":
3294
+ quadrants["high"].append(f)
3295
+ elif sev == "medium":
3296
+ quadrants["medium"].append(f)
3297
+ elif sev == "low":
3298
+ quadrants["low"].append(f)
3083
3299
 
3084
3300
  def render_dots(findings_list, severity, max_dots=12):
3085
3301
  dots = ""
3086
3302
  for i, f in enumerate(findings_list[:max_dots]):
3087
- title = f.get('title', 'Finding')[:20]
3088
- dots += f'<div class="quadrant-dot {severity}" title="{title}">{i+1}</div>'
3303
+ title = f.get("title", "Finding")[:20]
3304
+ dots += (
3305
+ f'<div class="quadrant-dot {severity}" title="{title}">{i+1}</div>'
3306
+ )
3089
3307
  if len(findings_list) > max_dots:
3090
3308
  dots += f'<div class="quadrant-dot {severity}">+{len(findings_list) - max_dots}</div>'
3091
3309
  return dots
@@ -3104,14 +3322,14 @@ class HTMLFormatter(MarkdownFormatter):
3104
3322
  <div class="quadrant-title">Monitor</div>
3105
3323
  <div class="quadrant-findings">
3106
3324
  """
3107
- html += render_dots(quadrants['high'], 'high')
3325
+ html += render_dots(quadrants["high"], "high")
3108
3326
  html += """ </div>
3109
3327
  </div>
3110
3328
  <div class="quadrant-cell critical">
3111
3329
  <div class="quadrant-title">Fix Now</div>
3112
3330
  <div class="quadrant-findings">
3113
3331
  """
3114
- html += render_dots(quadrants['critical'], 'critical')
3332
+ html += render_dots(quadrants["critical"], "critical")
3115
3333
  html += """ </div>
3116
3334
  </div>
3117
3335
 
@@ -3120,14 +3338,14 @@ class HTMLFormatter(MarkdownFormatter):
3120
3338
  <div class="quadrant-title">Accept Risk</div>
3121
3339
  <div class="quadrant-findings">
3122
3340
  """
3123
- html += render_dots(quadrants['low'], 'low')
3341
+ html += render_dots(quadrants["low"], "low")
3124
3342
  html += """ </div>
3125
3343
  </div>
3126
3344
  <div class="quadrant-cell medium">
3127
3345
  <div class="quadrant-title">Schedule Fix</div>
3128
3346
  <div class="quadrant-findings">
3129
3347
  """
3130
- html += render_dots(quadrants['medium'], 'medium')
3348
+ html += render_dots(quadrants["medium"], "medium")
3131
3349
  html += """ </div>
3132
3350
  </div>
3133
3351
 
@@ -3141,13 +3359,13 @@ class HTMLFormatter(MarkdownFormatter):
3141
3359
 
3142
3360
  def remediation_timeline(self, metrics: Dict) -> str:
3143
3361
  """Generate visual remediation timeline."""
3144
- timeline = metrics.get('remediation_timeline', {})
3145
- total_days = timeline.get('total_days', 30)
3362
+ timeline = metrics.get("remediation_timeline", {})
3363
+ total_days = timeline.get("total_days", 30)
3146
3364
 
3147
- critical_days = timeline.get('critical', 2)
3148
- high_days = timeline.get('high', 7)
3149
- medium_days = timeline.get('medium', 14)
3150
- low_days = timeline.get('low', 7)
3365
+ critical_days = timeline.get("critical", 2)
3366
+ high_days = timeline.get("high", 7)
3367
+ medium_days = timeline.get("medium", 14)
3368
+ low_days = timeline.get("low", 7)
3151
3369
 
3152
3370
  # Calculate percentages
3153
3371
  if total_days > 0:
@@ -3206,7 +3424,7 @@ class HTMLFormatter(MarkdownFormatter):
3206
3424
 
3207
3425
  <div class="dashboard">
3208
3426
  """
3209
-
3427
+
3210
3428
  # Risk Score Card
3211
3429
  html += f""" <div class="metric-card {risk_class}">
3212
3430
  <div class="metric-label">OVERALL RISK SCORE</div>
@@ -3214,7 +3432,7 @@ class HTMLFormatter(MarkdownFormatter):
3214
3432
  <div class="metric-label">{metrics['risk_level']}</div>
3215
3433
  </div>
3216
3434
  """
3217
-
3435
+
3218
3436
  # Total Findings Card
3219
3437
  html += f""" <div class="metric-card">
3220
3438
  <div class="metric-label">TOTAL FINDINGS</div>
@@ -3222,7 +3440,7 @@ class HTMLFormatter(MarkdownFormatter):
3222
3440
  <div class="metric-label">{metrics['critical_findings']} Critical | {metrics['high_findings']} High</div>
3223
3441
  </div>
3224
3442
  """
3225
-
3443
+
3226
3444
  # Hosts Assessed Card
3227
3445
  html += f""" <div class="metric-card">
3228
3446
  <div class="metric-label">HOSTS ASSESSED</div>
@@ -3230,7 +3448,7 @@ class HTMLFormatter(MarkdownFormatter):
3230
3448
  <div class="metric-label">{metrics['vulnerable_hosts']} Vulnerable</div>
3231
3449
  </div>
3232
3450
  """
3233
-
3451
+
3234
3452
  # Exploitation Rate Card
3235
3453
  html += f""" <div class="metric-card">
3236
3454
  <div class="metric-label">EXPLOITATION RATE</div>
@@ -3238,16 +3456,16 @@ class HTMLFormatter(MarkdownFormatter):
3238
3456
  <div class="metric-label">{metrics['exploited_services']}/{metrics['total_services']} Services</div>
3239
3457
  </div>
3240
3458
  """
3241
-
3459
+
3242
3460
  # Remediation Timeline Card
3243
- timeline = metrics['remediation_timeline']
3461
+ timeline = metrics["remediation_timeline"]
3244
3462
  html += f""" <div class="metric-card">
3245
3463
  <div class="metric-label">ESTIMATED REMEDIATION</div>
3246
3464
  <div class="metric-value">{timeline['weeks']}</div>
3247
3465
  <div class="metric-label">Weeks (~{timeline['total_days']} days)</div>
3248
3466
  </div>
3249
3467
  """
3250
-
3468
+
3251
3469
  # Credentials Found Card
3252
3470
  html += f""" <div class="metric-card">
3253
3471
  <div class="metric-label">CREDENTIALS FOUND</div>
@@ -3255,7 +3473,7 @@ class HTMLFormatter(MarkdownFormatter):
3255
3473
  <div class="metric-label">Valid Credentials</div>
3256
3474
  </div>
3257
3475
  """
3258
-
3476
+
3259
3477
  html += "</div>\n</div>\n\n---\n\n"
3260
3478
  return html
3261
3479
 
@@ -3264,15 +3482,15 @@ class HTMLFormatter(MarkdownFormatter):
3264
3482
  if not attack_surface:
3265
3483
  return ""
3266
3484
 
3267
- overview = attack_surface.get('overview', {})
3268
- hosts = attack_surface.get('hosts', [])
3485
+ overview = attack_surface.get("overview", {})
3486
+ hosts = attack_surface.get("hosts", [])
3269
3487
 
3270
3488
  if not hosts:
3271
3489
  return ""
3272
3490
 
3273
3491
  # Calculate gap count (services not exploited)
3274
- total_services = overview.get('total_services', 0)
3275
- exploited_services = overview.get('exploited_services', 0)
3492
+ total_services = overview.get("total_services", 0)
3493
+ exploited_services = overview.get("exploited_services", 0)
3276
3494
  gap_count = total_services - exploited_services
3277
3495
 
3278
3496
  html = """<div class="intel-hub">
@@ -3291,14 +3509,16 @@ class HTMLFormatter(MarkdownFormatter):
3291
3509
  # Top target callout
3292
3510
  if hosts:
3293
3511
  top = hosts[0]
3294
- top_ip = top.get('host', 'unknown')
3295
- top_hostname = top.get('hostname', '')
3296
- top_score = top.get('score', 0)
3297
- top_services = top.get('services', [])
3298
- top_exploited = sum(1 for s in top_services if s.get('status') == 'exploited')
3512
+ top_ip = top.get("host", "unknown")
3513
+ top_hostname = top.get("hostname", "")
3514
+ top_score = top.get("score", 0)
3515
+ top_services = top.get("services", [])
3516
+ top_exploited = sum(
3517
+ 1 for s in top_services if s.get("status") == "exploited"
3518
+ )
3299
3519
  top_total_svc = len(top_services)
3300
- top_critical = top.get('critical_findings', 0)
3301
- top_findings = top.get('findings', 0)
3520
+ top_critical = top.get("critical_findings", 0)
3521
+ top_findings = top.get("findings", 0)
3302
3522
 
3303
3523
  top_display = top_ip
3304
3524
  if top_hostname:
@@ -3309,11 +3529,11 @@ class HTMLFormatter(MarkdownFormatter):
3309
3529
  html += f' <div class="top-target-host">{top_display}</div>\n'
3310
3530
  html += f' <div class="top-target-stats">Score: {top_score} pts | {top_exploited}/{top_total_svc} services exploited | '
3311
3531
  if top_critical > 0:
3312
- html += f'{top_critical} critical, {top_findings - top_critical} high findings'
3532
+ html += f"{top_critical} critical, {top_findings - top_critical} high findings"
3313
3533
  else:
3314
- html += f'{top_findings} findings'
3315
- html += '</div>\n'
3316
- html += '</div>\n\n'
3534
+ html += f"{top_findings} findings"
3535
+ html += "</div>\n"
3536
+ html += "</div>\n\n"
3317
3537
 
3318
3538
  # Host table
3319
3539
  # Limit to top 10 hosts for readability
@@ -3336,21 +3556,21 @@ class HTMLFormatter(MarkdownFormatter):
3336
3556
  """
3337
3557
 
3338
3558
  for idx, host in enumerate(display_hosts, 1):
3339
- host_ip = host.get('host', 'unknown')
3340
- hostname = host.get('hostname', '')
3341
- score = host.get('score', 0)
3342
- services = host.get('services', [])
3559
+ host_ip = host.get("host", "unknown")
3560
+ hostname = host.get("hostname", "")
3561
+ score = host.get("score", 0)
3562
+ services = host.get("services", [])
3343
3563
  service_count = len(services)
3344
- findings = host.get('findings', 0)
3345
- critical = host.get('critical_findings', 0)
3564
+ findings = host.get("findings", 0)
3565
+ critical = host.get("critical_findings", 0)
3346
3566
 
3347
3567
  # Score color class
3348
3568
  if score >= 70:
3349
- score_class = 'score-critical'
3569
+ score_class = "score-critical"
3350
3570
  elif score >= 50:
3351
- score_class = 'score-high'
3571
+ score_class = "score-high"
3352
3572
  else:
3353
- score_class = 'score-medium'
3573
+ score_class = "score-medium"
3354
3574
 
3355
3575
  # Host display
3356
3576
  host_display = host_ip
@@ -3364,8 +3584,10 @@ class HTMLFormatter(MarkdownFormatter):
3364
3584
  findings_display = str(findings)
3365
3585
 
3366
3586
  # Exploitation progress
3367
- exploited = sum(1 for s in services if s.get('status') == 'exploited')
3368
- progress_filled = min(8, int((exploited / service_count * 8) if service_count > 0 else 0))
3587
+ exploited = sum(1 for s in services if s.get("status") == "exploited")
3588
+ progress_filled = min(
3589
+ 8, int((exploited / service_count * 8) if service_count > 0 else 0)
3590
+ )
3369
3591
  progress_empty = 8 - progress_filled
3370
3592
 
3371
3593
  progress_bar = f'<span class="progress-filled">{"█" * progress_filled}</span><span class="progress-empty">{"░" * progress_empty}</span>'
@@ -3394,8 +3616,13 @@ class HTMLFormatter(MarkdownFormatter):
3394
3616
  """
3395
3617
  return html
3396
3618
 
3397
- def compare_to_previous(self, current_metrics: Dict, previous_metrics: Dict,
3398
- current_engagement: Dict, previous_engagement: Dict) -> str:
3619
+ def compare_to_previous(
3620
+ self,
3621
+ current_metrics: Dict,
3622
+ previous_metrics: Dict,
3623
+ current_engagement: Dict,
3624
+ previous_engagement: Dict,
3625
+ ) -> str:
3399
3626
  """Generate comparison section showing improvement/regression from previous engagement.
3400
3627
 
3401
3628
  Args:
@@ -3408,20 +3635,20 @@ class HTMLFormatter(MarkdownFormatter):
3408
3635
  return ""
3409
3636
 
3410
3637
  # Calculate deltas
3411
- current_risk = current_metrics.get('risk_score', 0)
3412
- prev_risk = previous_metrics.get('risk_score', 0)
3638
+ current_risk = current_metrics.get("risk_score", 0)
3639
+ prev_risk = previous_metrics.get("risk_score", 0)
3413
3640
  risk_delta = current_risk - prev_risk
3414
3641
 
3415
- current_critical = current_metrics.get('severity_counts', {}).get('critical', 0)
3416
- prev_critical = previous_metrics.get('severity_counts', {}).get('critical', 0)
3642
+ current_critical = current_metrics.get("severity_counts", {}).get("critical", 0)
3643
+ prev_critical = previous_metrics.get("severity_counts", {}).get("critical", 0)
3417
3644
  critical_delta = current_critical - prev_critical
3418
3645
 
3419
- current_high = current_metrics.get('severity_counts', {}).get('high', 0)
3420
- prev_high = previous_metrics.get('severity_counts', {}).get('high', 0)
3646
+ current_high = current_metrics.get("severity_counts", {}).get("high", 0)
3647
+ prev_high = previous_metrics.get("severity_counts", {}).get("high", 0)
3421
3648
  high_delta = current_high - prev_high
3422
3649
 
3423
- current_total = current_metrics.get('total_findings', 0)
3424
- prev_total = previous_metrics.get('total_findings', 0)
3650
+ current_total = current_metrics.get("total_findings", 0)
3651
+ prev_total = previous_metrics.get("total_findings", 0)
3425
3652
  total_delta = current_total - prev_total
3426
3653
 
3427
3654
  # Determine overall trend
@@ -3446,9 +3673,9 @@ class HTMLFormatter(MarkdownFormatter):
3446
3673
  trend_text = "NO CHANGE"
3447
3674
  trend_color = "#6b7280"
3448
3675
 
3449
- prev_date = previous_engagement.get('created_at', 'Unknown')
3450
- if hasattr(prev_date, 'strftime'):
3451
- prev_date = prev_date.strftime('%B %d, %Y')
3676
+ prev_date = previous_engagement.get("created_at", "Unknown")
3677
+ if hasattr(prev_date, "strftime"):
3678
+ prev_date = prev_date.strftime("%B %d, %Y")
3452
3679
 
3453
3680
  html = f"""<div class="compare-section" style="margin: 30px 0; padding: 25px; background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border-radius: 12px; border: 2px solid #0ea5e9;">
3454
3681
 
@@ -3552,136 +3779,136 @@ class HTMLFormatter(MarkdownFormatter):
3552
3779
  """Generate charts section with Chart.js visualizations."""
3553
3780
  if not charts:
3554
3781
  return ""
3555
-
3782
+
3556
3783
  html = """<div id="charts-section">
3557
3784
  <h2>VISUAL ANALYSIS</h2>
3558
3785
 
3559
3786
  <div class="charts-grid">
3560
3787
  """
3561
-
3788
+
3562
3789
  # Phase 1 Charts
3563
3790
  # Severity Distribution Chart
3564
- if 'severity_distribution' in charts:
3791
+ if "severity_distribution" in charts:
3565
3792
  html += """ <div class="chart-container">
3566
3793
  <canvas id="severityChart"></canvas>
3567
3794
  </div>
3568
3795
  """
3569
-
3796
+
3570
3797
  # Host Impact Chart
3571
- if 'host_impact' in charts:
3798
+ if "host_impact" in charts:
3572
3799
  html += """ <div class="chart-container">
3573
3800
  <canvas id="hostChart"></canvas>
3574
3801
  </div>
3575
3802
  """
3576
-
3803
+
3577
3804
  # Exploitation Progress Chart
3578
- if 'exploitation_progress' in charts:
3805
+ if "exploitation_progress" in charts:
3579
3806
  html += """ <div class="chart-container">
3580
3807
  <canvas id="exploitationChart"></canvas>
3581
3808
  </div>
3582
3809
  """
3583
-
3810
+
3584
3811
  # Phase 2 Charts
3585
3812
  # Timeline Chart
3586
- if 'timeline' in charts:
3813
+ if "timeline" in charts:
3587
3814
  html += """ <div class="chart-container">
3588
3815
  <canvas id="timelineChart"></canvas>
3589
3816
  </div>
3590
3817
  """
3591
-
3818
+
3592
3819
  # Evidence by Phase Chart
3593
- if 'evidence_by_phase' in charts:
3820
+ if "evidence_by_phase" in charts:
3594
3821
  html += """ <div class="chart-container">
3595
3822
  <canvas id="evidencePhaseChart"></canvas>
3596
3823
  </div>
3597
3824
  """
3598
-
3825
+
3599
3826
  # Service Exposure Chart
3600
- if 'service_exposure' in charts:
3827
+ if "service_exposure" in charts:
3601
3828
  html += """ <div class="chart-container">
3602
3829
  <canvas id="serviceExposureChart"></canvas>
3603
3830
  </div>
3604
3831
  """
3605
-
3832
+
3606
3833
  # Credentials by Service Chart
3607
- if 'credentials_by_service' in charts:
3834
+ if "credentials_by_service" in charts:
3608
3835
  html += """ <div class="chart-container">
3609
3836
  <canvas id="credentialsChart"></canvas>
3610
3837
  </div>
3611
3838
  """
3612
-
3839
+
3613
3840
  html += "</div>\n</div>\n\n---\n\n"
3614
-
3841
+
3615
3842
  # Add Chart.js initialization scripts
3616
3843
  html += "<script>\n"
3617
-
3844
+
3618
3845
  # Phase 1 Charts
3619
- if 'severity_distribution' in charts:
3846
+ if "severity_distribution" in charts:
3620
3847
  html += f"""
3621
3848
  const severityCtx = document.getElementById('severityChart');
3622
3849
  if (severityCtx) {{
3623
3850
  new Chart(severityCtx, {charts['severity_distribution']});
3624
3851
  }}
3625
3852
  """
3626
-
3627
- if 'host_impact' in charts:
3853
+
3854
+ if "host_impact" in charts:
3628
3855
  html += f"""
3629
3856
  const hostCtx = document.getElementById('hostChart');
3630
3857
  if (hostCtx) {{
3631
3858
  new Chart(hostCtx, {charts['host_impact']});
3632
3859
  }}
3633
3860
  """
3634
-
3635
- if 'exploitation_progress' in charts:
3861
+
3862
+ if "exploitation_progress" in charts:
3636
3863
  html += f"""
3637
3864
  const exploitationCtx = document.getElementById('exploitationChart');
3638
3865
  if (exploitationCtx) {{
3639
3866
  new Chart(exploitationCtx, {charts['exploitation_progress']});
3640
3867
  }}
3641
3868
  """
3642
-
3869
+
3643
3870
  # Phase 2 Charts
3644
- if 'timeline' in charts:
3871
+ if "timeline" in charts:
3645
3872
  html += f"""
3646
3873
  const timelineCtx = document.getElementById('timelineChart');
3647
3874
  if (timelineCtx) {{
3648
3875
  new Chart(timelineCtx, {charts['timeline']});
3649
3876
  }}
3650
3877
  """
3651
-
3652
- if 'evidence_by_phase' in charts:
3878
+
3879
+ if "evidence_by_phase" in charts:
3653
3880
  html += f"""
3654
3881
  const evidencePhaseCtx = document.getElementById('evidencePhaseChart');
3655
3882
  if (evidencePhaseCtx) {{
3656
3883
  new Chart(evidencePhaseCtx, {charts['evidence_by_phase']});
3657
3884
  }}
3658
3885
  """
3659
-
3660
- if 'service_exposure' in charts:
3886
+
3887
+ if "service_exposure" in charts:
3661
3888
  html += f"""
3662
3889
  const serviceExposureCtx = document.getElementById('serviceExposureChart');
3663
3890
  if (serviceExposureCtx) {{
3664
3891
  new Chart(serviceExposureCtx, {charts['service_exposure']});
3665
3892
  }}
3666
3893
  """
3667
-
3668
- if 'credentials_by_service' in charts:
3894
+
3895
+ if "credentials_by_service" in charts:
3669
3896
  html += f"""
3670
3897
  const credentialsCtx = document.getElementById('credentialsChart');
3671
3898
  if (credentialsCtx) {{
3672
3899
  new Chart(credentialsCtx, {charts['credentials_by_service']});
3673
3900
  }}
3674
3901
  """
3675
-
3902
+
3676
3903
  html += "</script>\n\n"
3677
3904
  return html
3678
-
3905
+
3679
3906
  def detailed_findings_collapsible(self, findings: Dict) -> str:
3680
3907
  """Generate detailed findings with collapsible sections by severity."""
3681
3908
  from souleyez.reporting.compliance_mappings import ComplianceMappings
3682
-
3909
+
3683
3910
  mapper = ComplianceMappings()
3684
-
3911
+
3685
3912
  section = """## DETAILED FINDINGS
3686
3913
 
3687
3914
  <div class="collapse-controls">
@@ -3690,29 +3917,29 @@ if (credentialsCtx) {{
3690
3917
  </div>
3691
3918
 
3692
3919
  """
3693
-
3694
- severity_order = ['critical', 'high', 'medium', 'low', 'info']
3920
+
3921
+ severity_order = ["critical", "high", "medium", "low", "info"]
3695
3922
  emoji_map = {
3696
- 'critical': '🔴',
3697
- 'high': '🟠',
3698
- 'medium': '🟡',
3699
- 'low': '🟢',
3700
- 'info': '🔵'
3923
+ "critical": "🔴",
3924
+ "high": "🟠",
3925
+ "medium": "🟡",
3926
+ "low": "🟢",
3927
+ "info": "🔵",
3701
3928
  }
3702
-
3929
+
3703
3930
  finding_number = 1
3704
-
3931
+
3705
3932
  for severity in severity_order:
3706
3933
  if not findings.get(severity):
3707
3934
  continue
3708
-
3935
+
3709
3936
  count = len(findings[severity])
3710
3937
  severity_title = severity.upper()
3711
- emoji = emoji_map.get(severity, '')
3712
-
3938
+ emoji = emoji_map.get(severity, "")
3939
+
3713
3940
  # Create collapsible section
3714
3941
  # Auto-expand Critical and High findings for quick visibility
3715
- open_attr = ' open' if severity in ['critical', 'high'] else ''
3942
+ open_attr = " open" if severity in ["critical", "high"] else ""
3716
3943
  section_id = f"findings-{severity}"
3717
3944
  section += f"""<details id="{section_id}"{open_attr}>
3718
3945
  <summary class="severity-{severity}">
@@ -3724,10 +3951,10 @@ if (credentialsCtx) {{
3724
3951
  <div class="findings-content">
3725
3952
 
3726
3953
  """
3727
-
3954
+
3728
3955
  # Add each finding
3729
3956
  for finding in findings[severity]:
3730
- severity_lower = finding.get('severity', severity).lower()
3957
+ severity_lower = finding.get("severity", severity).lower()
3731
3958
 
3732
3959
  # Use HTML formatting instead of markdown for proper rendering
3733
3960
  section += f"""<div class="finding-card">
@@ -3749,9 +3976,11 @@ if (credentialsCtx) {{
3749
3976
  badges.append(f"<code>{cwe_id}</code>")
3750
3977
  section += " ".join(badges) + "</p>\n"
3751
3978
 
3752
- if finding.get('cvss'):
3753
- section += f"<p><strong>CVSS Score:</strong> {finding['cvss']}</p>\n"
3754
- if finding.get('cve'):
3979
+ if finding.get("cvss"):
3980
+ section += (
3981
+ f"<p><strong>CVSS Score:</strong> {finding['cvss']}</p>\n"
3982
+ )
3983
+ if finding.get("cve"):
3755
3984
  section += f"<p><strong>CVE:</strong> {finding['cve']}</p>\n"
3756
3985
 
3757
3986
  # Format affected host display
@@ -3760,15 +3989,17 @@ if (credentialsCtx) {{
3760
3989
  section += f"<p><strong>Tool:</strong> {finding['tool']}</p>\n"
3761
3990
 
3762
3991
  # Description
3763
- if finding.get('description'):
3764
- desc = finding['description'].replace('\n', '<br>\n')
3992
+ if finding.get("description"):
3993
+ desc = finding["description"].replace("\n", "<br>\n")
3765
3994
  section += f"<p><strong>Description:</strong></p>\n<p>{desc}</p>\n"
3766
3995
 
3767
3996
  # Evidence (if available) - shows proof of vulnerability
3768
- evidence_text = finding.get('evidence', '')
3997
+ evidence_text = finding.get("evidence", "")
3769
3998
  if evidence_text and len(evidence_text.strip()) > 0:
3770
3999
  # Escape HTML in evidence
3771
- safe_evidence = evidence_text.replace('<', '&lt;').replace('>', '&gt;')
4000
+ safe_evidence = evidence_text.replace("<", "&lt;").replace(
4001
+ ">", "&gt;"
4002
+ )
3772
4003
  section += f"""<div class="finding-evidence">
3773
4004
  <div class="finding-evidence-label">Evidence / Proof</div>
3774
4005
  <pre>{safe_evidence}</pre>
@@ -3776,21 +4007,23 @@ if (credentialsCtx) {{
3776
4007
  """
3777
4008
 
3778
4009
  # Remediation - Add recommendations
3779
- remediation_text = finding.get('remediation', '')
4010
+ remediation_text = finding.get("remediation", "")
3780
4011
 
3781
4012
  # If no remediation provided, generate a basic one
3782
4013
  if not remediation_text:
3783
- remediation_text = self._generate_default_remediation(finding, severity)
4014
+ remediation_text = self._generate_default_remediation(
4015
+ finding, severity
4016
+ )
3784
4017
 
3785
4018
  if remediation_text:
3786
- remediation_html = remediation_text.replace('\n', '<br>\n')
4019
+ remediation_html = remediation_text.replace("\n", "<br>\n")
3787
4020
  section += f"<p><strong>Remediation:</strong></p>\n<p>{remediation_html}</p>\n"
3788
4021
 
3789
4022
  section += "</div>\n<hr>\n\n"
3790
4023
  finding_number += 1
3791
-
4024
+
3792
4025
  section += " </div>\n</details>\n\n"
3793
-
4026
+
3794
4027
  return section
3795
4028
 
3796
4029
  # =========================================================================
@@ -3813,17 +4046,17 @@ if (credentialsCtx) {{
3813
4046
  # Group by tactic
3814
4047
  tactics_data = {}
3815
4048
  for item in data.heatmap_data:
3816
- tactic = item['tactic_name']
4049
+ tactic = item["tactic_name"]
3817
4050
  if tactic not in tactics_data:
3818
4051
  tactics_data[tactic] = {
3819
- 'id': item['tactic_id'],
3820
- 'order': item['tactic_order'],
3821
- 'techniques': []
4052
+ "id": item["tactic_id"],
4053
+ "order": item["tactic_order"],
4054
+ "techniques": [],
3822
4055
  }
3823
- tactics_data[tactic]['techniques'].append(item)
4056
+ tactics_data[tactic]["techniques"].append(item)
3824
4057
 
3825
4058
  # Sort tactics by order
3826
- sorted_tactics = sorted(tactics_data.items(), key=lambda x: x[1]['order'])
4059
+ sorted_tactics = sorted(tactics_data.items(), key=lambda x: x[1]["order"])
3827
4060
 
3828
4061
  html = """
3829
4062
  <style>
@@ -3970,7 +4203,7 @@ if (credentialsCtx) {{
3970
4203
  """
3971
4204
 
3972
4205
  for tactic_name, tactic_data in sorted_tactics:
3973
- techniques = tactic_data['techniques']
4206
+ techniques = tactic_data["techniques"]
3974
4207
  if not techniques:
3975
4208
  continue
3976
4209
 
@@ -3981,12 +4214,14 @@ if (credentialsCtx) {{
3981
4214
  """
3982
4215
 
3983
4216
  for tech in techniques:
3984
- status_class = tech['status'].replace('_', '-')
4217
+ status_class = tech["status"].replace("_", "-")
3985
4218
  rate_html = ""
3986
- if tech['tested'] > 0:
4219
+ if tech["tested"] > 0:
3987
4220
  rate_html = f'<span class="mitre-technique-rate">{tech["detection_rate"]}%</span>'
3988
4221
 
3989
- tools_title = ', '.join(tech['tools_used']) if tech['tools_used'] else 'N/A'
4222
+ tools_title = (
4223
+ ", ".join(tech["tools_used"]) if tech["tools_used"] else "N/A"
4224
+ )
3990
4225
 
3991
4226
  html += f"""
3992
4227
  <div class="mitre-technique {status_class}" title="Tools: {tools_title}">
@@ -4118,7 +4353,9 @@ if (credentialsCtx) {{
4118
4353
  """
4119
4354
 
4120
4355
  # Insert CSS before closing </head>
4121
- return base_header.replace('</style>\n</head>', '</style>\n' + detection_css + '</head>')
4356
+ return base_header.replace(
4357
+ "</style>\n</head>", "</style>\n" + detection_css + "</head>"
4358
+ )
4122
4359
 
4123
4360
  def html_footer(self) -> str:
4124
4361
  """Generate HTML footer."""