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