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/formatters.py
CHANGED
|
@@ -35,21 +35,27 @@ class MarkdownFormatter:
|
|
|
35
35
|
|
|
36
36
|
---"""
|
|
37
37
|
|
|
38
|
-
def executive_summary(
|
|
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[
|
|
49
|
-
high_count = len(findings[
|
|
50
|
-
medium_count = len(findings[
|
|
51
|
-
total_hosts = overview[
|
|
52
|
-
exploited = overview[
|
|
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 ==
|
|
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 =
|
|
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(
|
|
101
|
-
|
|
102
|
-
|
|
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 +=
|
|
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[
|
|
140
|
+
top_findings = findings["critical"][:3] + findings["high"][:3]
|
|
129
141
|
for idx, finding in enumerate(top_findings[:5], 1):
|
|
130
|
-
severity = finding.get(
|
|
131
|
-
title = finding[
|
|
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 ==
|
|
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 ==
|
|
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 +=
|
|
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(
|
|
181
|
-
|
|
194
|
+
title_lower = finding.get("title", "").lower()
|
|
195
|
+
|
|
182
196
|
# Data breach scenarios
|
|
183
|
-
if
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
204
|
-
if severity ==
|
|
225
|
+
severity = finding.get("severity", "").lower()
|
|
226
|
+
if severity == "critical":
|
|
205
227
|
return "High-impact vulnerability requiring immediate attention"
|
|
206
|
-
elif severity ==
|
|
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(
|
|
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 ==
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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[
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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(
|
|
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[
|
|
306
|
-
tool_name =
|
|
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[
|
|
341
|
-
overview = attack_surface[
|
|
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[
|
|
357
|
-
pct = round(
|
|
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(
|
|
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(
|
|
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[
|
|
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[
|
|
385
|
-
high = len(findings[
|
|
386
|
-
medium = len(findings[
|
|
387
|
-
low = len(findings[
|
|
388
|
-
info = len(findings[
|
|
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[
|
|
418
|
-
high_findings = findings[
|
|
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(
|
|
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(
|
|
526
|
+
if finding.get("description"):
|
|
429
527
|
# Get first sentence or first 100 chars
|
|
430
|
-
desc = finding[
|
|
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(
|
|
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[
|
|
453
|
-
total_low = len(findings[
|
|
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(
|
|
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[
|
|
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] =
|
|
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 ==
|
|
599
|
+
if report_type == "executive":
|
|
495
600
|
# Executive: Only show matched categories (reduce clutter)
|
|
496
|
-
if owasp_coverage[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
538
|
-
if report_type ==
|
|
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 ==
|
|
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[
|
|
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[
|
|
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 =
|
|
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 [
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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(
|
|
696
|
+
if finding.get("cvss"):
|
|
590
697
|
section += f"**CVSS Score:** {finding['cvss']} \n"
|
|
591
|
-
if finding.get(
|
|
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(
|
|
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(
|
|
711
|
+
remediation_text = finding.get("remediation", "")
|
|
605
712
|
if not remediation_text:
|
|
606
|
-
remediation_text = self._generate_default_remediation(
|
|
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 +=
|
|
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 +=
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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[
|
|
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(
|
|
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,
|
|
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[
|
|
693
|
-
total_high = len(findings[
|
|
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[
|
|
808
|
+
|
|
809
|
+
for idx, finding in enumerate(findings["high"][:3], 1):
|
|
697
810
|
section += f"{idx}. **{finding['title']}**\n"
|
|
698
|
-
if finding.get(
|
|
811
|
+
if finding.get("remediation"):
|
|
699
812
|
# Shorten if too long
|
|
700
|
-
rem = finding[
|
|
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[
|
|
826
|
+
if findings["medium"]:
|
|
714
827
|
section += "### 📋 Medium-Term Actions (Within 30 Days)\n\n"
|
|
715
|
-
total_medium = len(findings[
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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[
|
|
768
|
-
title_lower = finding[
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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[
|
|
788
|
-
|
|
902
|
+
title_lower = finding["title"].lower()
|
|
903
|
+
|
|
789
904
|
# High effort (4+ hours)
|
|
790
|
-
if any(
|
|
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(
|
|
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(
|
|
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[
|
|
807
|
-
|
|
930
|
+
title_lower = finding["title"].lower()
|
|
931
|
+
|
|
808
932
|
# Data breach scenarios
|
|
809
|
-
if
|
|
810
|
-
return
|
|
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
|
|
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
|
|
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 ==
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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 = {
|
|
841
|
-
|
|
842
|
-
for severity in [
|
|
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[
|
|
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(
|
|
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(
|
|
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 +=
|
|
1057
|
+
section += (
|
|
1058
|
+
f"- **Attack Depth:** {summary['longest_path']} levels (longest path)\n"
|
|
1059
|
+
)
|
|
931
1060
|
|
|
932
|
-
if summary[
|
|
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[
|
|
1065
|
+
for phase, count in chain["phases"].items():
|
|
937
1066
|
if count > 0:
|
|
938
|
-
phase_name = phase.replace(
|
|
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[
|
|
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(
|
|
960
|
-
lateral_edges = chain.get(
|
|
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(
|
|
1130
|
+
if summary.get("deepest_host"):
|
|
1002
1131
|
section += f" (on {summary['deepest_host']})"
|
|
1003
1132
|
section += "\n"
|
|
1004
1133
|
|
|
1005
|
-
if summary[
|
|
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(
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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(
|
|
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[
|
|
1157
|
+
if summary["hosts_exploited"] == 0:
|
|
1029
1158
|
section += "No hosts were fully exploited during this engagement. "
|
|
1030
|
-
section +=
|
|
1031
|
-
|
|
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[
|
|
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 +=
|
|
1039
|
-
|
|
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(
|
|
1049
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1154
|
-
host_str = finding[
|
|
1290
|
+
if finding.get("ip_address"):
|
|
1291
|
+
host_str = finding["ip_address"]
|
|
1155
1292
|
# Add hostname if available
|
|
1156
|
-
if finding.get(
|
|
1293
|
+
if finding.get("hostname"):
|
|
1157
1294
|
host_str += f" ({finding['hostname']})"
|
|
1158
1295
|
# Add port if available
|
|
1159
|
-
if finding.get(
|
|
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
|
|
1163
|
-
|
|
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(
|
|
1168
|
-
host_str = finding[
|
|
1169
|
-
if finding.get(
|
|
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
|
|
1173
|
-
|
|
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(
|
|
1178
|
-
return finding[
|
|
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(
|
|
1326
|
+
if finding.get("port"):
|
|
1182
1327
|
return f"Port {finding['port']}"
|
|
1183
|
-
|
|
1328
|
+
|
|
1184
1329
|
# Fallback
|
|
1185
|
-
return
|
|
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(
|
|
1190
|
-
tool = finding.get(
|
|
1191
|
-
|
|
1334
|
+
title_lower = finding.get("title", "").lower()
|
|
1335
|
+
tool = finding.get("tool", "").lower()
|
|
1336
|
+
|
|
1192
1337
|
# SQL Injection
|
|
1193
|
-
if
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 [
|
|
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 ==
|
|
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 ==
|
|
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 ==
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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 =
|
|
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(
|
|
1433
|
-
|
|
1592
|
+
sections.append(
|
|
1593
|
+
f"""**Business Impact:**
|
|
1594
|
+
{business_impact}"""
|
|
1595
|
+
)
|
|
1434
1596
|
|
|
1435
1597
|
if attack_scenario:
|
|
1436
|
-
sections.append(
|
|
1437
|
-
|
|
1598
|
+
sections.append(
|
|
1599
|
+
f"""**Attack Scenario:**
|
|
1600
|
+
{attack_scenario}"""
|
|
1601
|
+
)
|
|
1438
1602
|
|
|
1439
1603
|
if risk_context:
|
|
1440
|
-
sections.append(
|
|
1441
|
-
|
|
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,
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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[
|
|
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[
|
|
1755
|
+
status = tech["status"]
|
|
1582
1756
|
icon = {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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[
|
|
1590
|
-
tools =
|
|
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 ==
|
|
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
|
|
1622
|
-
target = getattr(result,
|
|
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 =
|
|
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 =
|
|
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 +=
|
|
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
|
|
1657
|
-
target = getattr(gap,
|
|
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][
|
|
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(
|
|
1668
|
-
priority_map = {
|
|
1669
|
-
|
|
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,
|
|
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 = {
|
|
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,
|
|
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
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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,
|
|
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
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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,
|
|
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
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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 +=
|
|
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 =
|
|
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(
|
|
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(
|
|
2964
|
-
risk_score = metrics.get(
|
|
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(
|
|
2968
|
-
high = findings.get(
|
|
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(
|
|
3019
|
-
sev_class =
|
|
3020
|
-
title = finding.get(
|
|
3021
|
-
host = finding.get(
|
|
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 [
|
|
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 ==
|
|
3036
|
-
bottom_class =
|
|
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 =
|
|
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[
|
|
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
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
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(
|
|
3075
|
-
if sev ==
|
|
3076
|
-
quadrants[
|
|
3077
|
-
elif sev ==
|
|
3078
|
-
quadrants[
|
|
3079
|
-
elif sev ==
|
|
3080
|
-
quadrants[
|
|
3081
|
-
elif sev ==
|
|
3082
|
-
quadrants[
|
|
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(
|
|
3088
|
-
dots +=
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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(
|
|
3145
|
-
total_days = timeline.get(
|
|
3362
|
+
timeline = metrics.get("remediation_timeline", {})
|
|
3363
|
+
total_days = timeline.get("total_days", 30)
|
|
3146
3364
|
|
|
3147
|
-
critical_days = timeline.get(
|
|
3148
|
-
high_days = timeline.get(
|
|
3149
|
-
medium_days = timeline.get(
|
|
3150
|
-
low_days = timeline.get(
|
|
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[
|
|
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(
|
|
3268
|
-
hosts = attack_surface.get(
|
|
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(
|
|
3275
|
-
exploited_services = overview.get(
|
|
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(
|
|
3295
|
-
top_hostname = top.get(
|
|
3296
|
-
top_score = top.get(
|
|
3297
|
-
top_services = top.get(
|
|
3298
|
-
top_exploited = sum(
|
|
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(
|
|
3301
|
-
top_findings = top.get(
|
|
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
|
|
3532
|
+
html += f"{top_critical} critical, {top_findings - top_critical} high findings"
|
|
3313
3533
|
else:
|
|
3314
|
-
html += f
|
|
3315
|
-
html +=
|
|
3316
|
-
html +=
|
|
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(
|
|
3340
|
-
hostname = host.get(
|
|
3341
|
-
score = host.get(
|
|
3342
|
-
services = host.get(
|
|
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(
|
|
3345
|
-
critical = host.get(
|
|
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 =
|
|
3569
|
+
score_class = "score-critical"
|
|
3350
3570
|
elif score >= 50:
|
|
3351
|
-
score_class =
|
|
3571
|
+
score_class = "score-high"
|
|
3352
3572
|
else:
|
|
3353
|
-
score_class =
|
|
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(
|
|
3368
|
-
progress_filled = min(
|
|
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(
|
|
3398
|
-
|
|
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(
|
|
3412
|
-
prev_risk = previous_metrics.get(
|
|
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(
|
|
3416
|
-
prev_critical = previous_metrics.get(
|
|
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(
|
|
3420
|
-
prev_high = previous_metrics.get(
|
|
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(
|
|
3424
|
-
prev_total = previous_metrics.get(
|
|
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(
|
|
3450
|
-
if hasattr(prev_date,
|
|
3451
|
-
prev_date = prev_date.strftime(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = [
|
|
3920
|
+
|
|
3921
|
+
severity_order = ["critical", "high", "medium", "low", "info"]
|
|
3695
3922
|
emoji_map = {
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
3753
|
-
section +=
|
|
3754
|
-
|
|
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(
|
|
3764
|
-
desc = finding[
|
|
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(
|
|
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(
|
|
4000
|
+
safe_evidence = evidence_text.replace("<", "<").replace(
|
|
4001
|
+
">", ">"
|
|
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(
|
|
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(
|
|
4014
|
+
remediation_text = self._generate_default_remediation(
|
|
4015
|
+
finding, severity
|
|
4016
|
+
)
|
|
3784
4017
|
|
|
3785
4018
|
if remediation_text:
|
|
3786
|
-
remediation_html = remediation_text.replace(
|
|
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[
|
|
4049
|
+
tactic = item["tactic_name"]
|
|
3817
4050
|
if tactic not in tactics_data:
|
|
3818
4051
|
tactics_data[tactic] = {
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
4052
|
+
"id": item["tactic_id"],
|
|
4053
|
+
"order": item["tactic_order"],
|
|
4054
|
+
"techniques": [],
|
|
3822
4055
|
}
|
|
3823
|
-
tactics_data[tactic][
|
|
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][
|
|
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[
|
|
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[
|
|
4217
|
+
status_class = tech["status"].replace("_", "-")
|
|
3985
4218
|
rate_html = ""
|
|
3986
|
-
if tech[
|
|
4219
|
+
if tech["tested"] > 0:
|
|
3987
4220
|
rate_html = f'<span class="mitre-technique-rate">{tech["detection_rate"]}%</span>'
|
|
3988
4221
|
|
|
3989
|
-
tools_title =
|
|
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(
|
|
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."""
|