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
|
@@ -28,6 +28,7 @@ from souleyez.storage.database import get_db
|
|
|
28
28
|
@dataclass
|
|
29
29
|
class RuleRecommendation:
|
|
30
30
|
"""Recommendation for improving detection coverage."""
|
|
31
|
+
|
|
31
32
|
attack_type: str
|
|
32
33
|
gap_description: str
|
|
33
34
|
priority: str # critical, high, medium, low
|
|
@@ -40,6 +41,7 @@ class RuleRecommendation:
|
|
|
40
41
|
@dataclass
|
|
41
42
|
class HostDetectionStats:
|
|
42
43
|
"""Detection statistics for a single host."""
|
|
44
|
+
|
|
43
45
|
host_ip: str
|
|
44
46
|
total_attacks: int = 0
|
|
45
47
|
detected: int = 0
|
|
@@ -53,6 +55,7 @@ class HostDetectionStats:
|
|
|
53
55
|
@dataclass
|
|
54
56
|
class SeverityBreakdown:
|
|
55
57
|
"""Breakdown of alerts by severity level."""
|
|
58
|
+
|
|
56
59
|
critical: int = 0
|
|
57
60
|
high: int = 0
|
|
58
61
|
medium: int = 0
|
|
@@ -64,6 +67,7 @@ class SeverityBreakdown:
|
|
|
64
67
|
@dataclass
|
|
65
68
|
class TopRule:
|
|
66
69
|
"""A frequently triggered rule/sourcetype."""
|
|
70
|
+
|
|
67
71
|
rule_id: str
|
|
68
72
|
rule_name: str
|
|
69
73
|
count: int
|
|
@@ -74,6 +78,7 @@ class TopRule:
|
|
|
74
78
|
@dataclass
|
|
75
79
|
class SampleAlert:
|
|
76
80
|
"""A sample alert for display in the report."""
|
|
81
|
+
|
|
77
82
|
rule_id: str
|
|
78
83
|
rule_name: str
|
|
79
84
|
severity: str
|
|
@@ -86,6 +91,7 @@ class SampleAlert:
|
|
|
86
91
|
@dataclass
|
|
87
92
|
class HostVulnerability:
|
|
88
93
|
"""Vulnerability info for a host."""
|
|
94
|
+
|
|
89
95
|
cve_id: str
|
|
90
96
|
name: str
|
|
91
97
|
severity: str
|
|
@@ -97,6 +103,7 @@ class HostVulnerability:
|
|
|
97
103
|
@dataclass
|
|
98
104
|
class HostVulnerabilitySummary:
|
|
99
105
|
"""Vulnerability summary for a single host."""
|
|
106
|
+
|
|
100
107
|
host_ip: str
|
|
101
108
|
agent_name: str = ""
|
|
102
109
|
total_vulns: int = 0
|
|
@@ -111,6 +118,7 @@ class HostVulnerabilitySummary:
|
|
|
111
118
|
@dataclass
|
|
112
119
|
class VulnerabilitySection:
|
|
113
120
|
"""Vulnerability data for the report."""
|
|
121
|
+
|
|
114
122
|
total_vulns: int = 0
|
|
115
123
|
hosts_with_vulns: int = 0
|
|
116
124
|
critical_count: int = 0
|
|
@@ -124,6 +132,7 @@ class VulnerabilitySection:
|
|
|
124
132
|
@dataclass
|
|
125
133
|
class DetectionReportData:
|
|
126
134
|
"""Complete data structure for a detection coverage report."""
|
|
135
|
+
|
|
127
136
|
engagement: Dict[str, Any]
|
|
128
137
|
summary: EngagementDetectionSummary
|
|
129
138
|
detection_results: List[DetectionResult]
|
|
@@ -143,7 +152,9 @@ class DetectionReportData:
|
|
|
143
152
|
executive_summary: str = ""
|
|
144
153
|
risk_level: str = "UNKNOWN" # CRITICAL, HIGH, MEDIUM, LOW
|
|
145
154
|
avg_detection_latency: str = ""
|
|
146
|
-
vulnerability_section: VulnerabilitySection = field(
|
|
155
|
+
vulnerability_section: VulnerabilitySection = field(
|
|
156
|
+
default_factory=VulnerabilitySection
|
|
157
|
+
)
|
|
147
158
|
|
|
148
159
|
|
|
149
160
|
class DetectionReportGatherer:
|
|
@@ -226,8 +237,7 @@ class DetectionReportGatherer:
|
|
|
226
237
|
)
|
|
227
238
|
|
|
228
239
|
def _build_per_host_analysis(
|
|
229
|
-
self,
|
|
230
|
-
results: List[DetectionResult]
|
|
240
|
+
self, results: List[DetectionResult]
|
|
231
241
|
) -> Dict[str, HostDetectionStats]:
|
|
232
242
|
"""
|
|
233
243
|
Build detection statistics grouped by target host.
|
|
@@ -242,21 +252,21 @@ class DetectionReportGatherer:
|
|
|
242
252
|
|
|
243
253
|
for result in results:
|
|
244
254
|
# Get target IP - handle both object and dict
|
|
245
|
-
target_ip = getattr(result,
|
|
255
|
+
target_ip = getattr(result, "target_ip", None)
|
|
246
256
|
if target_ip is None and isinstance(result, dict):
|
|
247
|
-
target_ip = result.get(
|
|
257
|
+
target_ip = result.get("target_ip")
|
|
248
258
|
if not target_ip:
|
|
249
259
|
target_ip = "unknown"
|
|
250
260
|
|
|
251
261
|
# Get attack type
|
|
252
|
-
attack_type = getattr(result,
|
|
262
|
+
attack_type = getattr(result, "attack_type", None)
|
|
253
263
|
if attack_type is None and isinstance(result, dict):
|
|
254
|
-
attack_type = result.get(
|
|
264
|
+
attack_type = result.get("attack_type", "unknown")
|
|
255
265
|
|
|
256
266
|
# Get status
|
|
257
|
-
status = getattr(result,
|
|
267
|
+
status = getattr(result, "status", None)
|
|
258
268
|
if status is None and isinstance(result, dict):
|
|
259
|
-
status = result.get(
|
|
269
|
+
status = result.get("status", "unknown")
|
|
260
270
|
|
|
261
271
|
# Initialize host stats if needed
|
|
262
272
|
if target_ip not in hosts:
|
|
@@ -283,16 +293,12 @@ class DetectionReportGatherer:
|
|
|
283
293
|
for host in hosts.values():
|
|
284
294
|
countable = host.detected + host.not_detected + host.partial
|
|
285
295
|
if countable > 0:
|
|
286
|
-
host.coverage_percent = round(
|
|
287
|
-
(host.detected / countable) * 100, 1
|
|
288
|
-
)
|
|
296
|
+
host.coverage_percent = round((host.detected / countable) * 100, 1)
|
|
289
297
|
|
|
290
298
|
return hosts
|
|
291
299
|
|
|
292
300
|
def _generate_rule_recommendations(
|
|
293
|
-
self,
|
|
294
|
-
gaps: List[DetectionResult],
|
|
295
|
-
mitre_gaps: List[TechniqueResult]
|
|
301
|
+
self, gaps: List[DetectionResult], mitre_gaps: List[TechniqueResult]
|
|
296
302
|
) -> List[RuleRecommendation]:
|
|
297
303
|
"""
|
|
298
304
|
Generate SIEM rule recommendations for detection gaps.
|
|
@@ -309,9 +315,9 @@ class DetectionReportGatherer:
|
|
|
309
315
|
|
|
310
316
|
for gap in gaps:
|
|
311
317
|
# Get attack type
|
|
312
|
-
attack_type = getattr(gap,
|
|
318
|
+
attack_type = getattr(gap, "attack_type", None)
|
|
313
319
|
if attack_type is None and isinstance(gap, dict):
|
|
314
|
-
attack_type = gap.get(
|
|
320
|
+
attack_type = gap.get("attack_type")
|
|
315
321
|
if not attack_type or attack_type in seen_attack_types:
|
|
316
322
|
continue
|
|
317
323
|
|
|
@@ -361,9 +367,7 @@ class DetectionReportGatherer:
|
|
|
361
367
|
return recommendations
|
|
362
368
|
|
|
363
369
|
def _get_detection_guidance(
|
|
364
|
-
self,
|
|
365
|
-
attack_type: str,
|
|
366
|
-
signature: Dict[str, Any]
|
|
370
|
+
self, attack_type: str, signature: Dict[str, Any]
|
|
367
371
|
) -> str:
|
|
368
372
|
"""
|
|
369
373
|
Generate human-readable detection guidance for an attack type.
|
|
@@ -453,13 +457,11 @@ class DetectionReportGatherer:
|
|
|
453
457
|
}
|
|
454
458
|
|
|
455
459
|
return category_guidance.get(
|
|
456
|
-
category,
|
|
457
|
-
"Review SIEM rule configuration for this attack category."
|
|
460
|
+
category, "Review SIEM rule configuration for this attack category."
|
|
458
461
|
)
|
|
459
462
|
|
|
460
463
|
def _build_severity_breakdown(
|
|
461
|
-
self,
|
|
462
|
-
results: List[DetectionResult]
|
|
464
|
+
self, results: List[DetectionResult]
|
|
463
465
|
) -> SeverityBreakdown:
|
|
464
466
|
"""
|
|
465
467
|
Build severity breakdown from detected alerts.
|
|
@@ -473,20 +475,20 @@ class DetectionReportGatherer:
|
|
|
473
475
|
breakdown = SeverityBreakdown()
|
|
474
476
|
|
|
475
477
|
for result in results:
|
|
476
|
-
if result.status !=
|
|
478
|
+
if result.status != "detected" or not result.alerts:
|
|
477
479
|
continue
|
|
478
480
|
|
|
479
481
|
for alert in result.alerts:
|
|
480
|
-
severity = str(alert.get(
|
|
482
|
+
severity = str(alert.get("severity", "info")).lower()
|
|
481
483
|
breakdown.total += 1
|
|
482
484
|
|
|
483
|
-
if severity in (
|
|
485
|
+
if severity in ("critical", "crit"):
|
|
484
486
|
breakdown.critical += 1
|
|
485
|
-
elif severity ==
|
|
487
|
+
elif severity == "high":
|
|
486
488
|
breakdown.high += 1
|
|
487
|
-
elif severity in (
|
|
489
|
+
elif severity in ("medium", "med"):
|
|
488
490
|
breakdown.medium += 1
|
|
489
|
-
elif severity ==
|
|
491
|
+
elif severity == "low":
|
|
490
492
|
breakdown.low += 1
|
|
491
493
|
else:
|
|
492
494
|
breakdown.info += 1
|
|
@@ -494,9 +496,7 @@ class DetectionReportGatherer:
|
|
|
494
496
|
return breakdown
|
|
495
497
|
|
|
496
498
|
def _get_top_rules(
|
|
497
|
-
self,
|
|
498
|
-
results: List[DetectionResult],
|
|
499
|
-
limit: int = 10
|
|
499
|
+
self, results: List[DetectionResult], limit: int = 10
|
|
500
500
|
) -> List[TopRule]:
|
|
501
501
|
"""
|
|
502
502
|
Get the most frequently triggered rules.
|
|
@@ -511,47 +511,43 @@ class DetectionReportGatherer:
|
|
|
511
511
|
rule_counts: Dict[str, Dict[str, Any]] = {}
|
|
512
512
|
|
|
513
513
|
for result in results:
|
|
514
|
-
if result.status !=
|
|
514
|
+
if result.status != "detected" or not result.alerts:
|
|
515
515
|
continue
|
|
516
516
|
|
|
517
517
|
for alert in result.alerts:
|
|
518
|
-
rule_id = str(alert.get(
|
|
519
|
-
rule_name = alert.get(
|
|
520
|
-
severity = str(alert.get(
|
|
521
|
-
description = alert.get(
|
|
518
|
+
rule_id = str(alert.get("rule_id", "unknown"))
|
|
519
|
+
rule_name = alert.get("rule_name", alert.get("name", "Unknown Rule"))
|
|
520
|
+
severity = str(alert.get("severity", "info")).lower()
|
|
521
|
+
description = alert.get("description", "")
|
|
522
522
|
|
|
523
523
|
if rule_id not in rule_counts:
|
|
524
524
|
rule_counts[rule_id] = {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
525
|
+
"rule_id": rule_id,
|
|
526
|
+
"rule_name": rule_name,
|
|
527
|
+
"severity": severity,
|
|
528
|
+
"description": description,
|
|
529
|
+
"count": 0,
|
|
530
530
|
}
|
|
531
|
-
rule_counts[rule_id][
|
|
531
|
+
rule_counts[rule_id]["count"] += 1
|
|
532
532
|
|
|
533
533
|
# Sort by count descending
|
|
534
534
|
sorted_rules = sorted(
|
|
535
|
-
rule_counts.values(),
|
|
536
|
-
key=lambda r: r['count'],
|
|
537
|
-
reverse=True
|
|
535
|
+
rule_counts.values(), key=lambda r: r["count"], reverse=True
|
|
538
536
|
)[:limit]
|
|
539
537
|
|
|
540
538
|
return [
|
|
541
539
|
TopRule(
|
|
542
|
-
rule_id=r[
|
|
543
|
-
rule_name=r[
|
|
544
|
-
count=r[
|
|
545
|
-
severity=r[
|
|
546
|
-
description=r[
|
|
540
|
+
rule_id=r["rule_id"],
|
|
541
|
+
rule_name=r["rule_name"],
|
|
542
|
+
count=r["count"],
|
|
543
|
+
severity=r["severity"],
|
|
544
|
+
description=r["description"][:200] if r["description"] else "",
|
|
547
545
|
)
|
|
548
546
|
for r in sorted_rules
|
|
549
547
|
]
|
|
550
548
|
|
|
551
549
|
def _get_sample_alerts(
|
|
552
|
-
self,
|
|
553
|
-
results: List[DetectionResult],
|
|
554
|
-
limit: int = 5
|
|
550
|
+
self, results: List[DetectionResult], limit: int = 5
|
|
555
551
|
) -> List[SampleAlert]:
|
|
556
552
|
"""
|
|
557
553
|
Get sample alerts for display in the report.
|
|
@@ -566,11 +562,19 @@ class DetectionReportGatherer:
|
|
|
566
562
|
samples: List[SampleAlert] = []
|
|
567
563
|
|
|
568
564
|
# Prioritize alerts from detected attacks, favor higher severity
|
|
569
|
-
severity_order = {
|
|
565
|
+
severity_order = {
|
|
566
|
+
"critical": 0,
|
|
567
|
+
"crit": 0,
|
|
568
|
+
"high": 1,
|
|
569
|
+
"medium": 2,
|
|
570
|
+
"med": 2,
|
|
571
|
+
"low": 3,
|
|
572
|
+
"info": 4,
|
|
573
|
+
}
|
|
570
574
|
|
|
571
575
|
all_alerts = []
|
|
572
576
|
for result in results:
|
|
573
|
-
if result.status !=
|
|
577
|
+
if result.status != "detected" or not result.alerts:
|
|
574
578
|
continue
|
|
575
579
|
for alert in result.alerts[:3]: # Max 3 per result
|
|
576
580
|
all_alerts.append((result.attack_type, alert))
|
|
@@ -578,38 +582,40 @@ class DetectionReportGatherer:
|
|
|
578
582
|
# Sort by severity
|
|
579
583
|
all_alerts.sort(
|
|
580
584
|
key=lambda x: severity_order.get(
|
|
581
|
-
str(x[1].get(
|
|
585
|
+
str(x[1].get("severity", "info")).lower(), 5
|
|
582
586
|
)
|
|
583
587
|
)
|
|
584
588
|
|
|
585
589
|
for attack_type, alert in all_alerts[:limit]:
|
|
586
590
|
# Extract raw snippet from alert data
|
|
587
|
-
raw_data = alert.get(
|
|
591
|
+
raw_data = alert.get("raw_data", {})
|
|
588
592
|
raw_snippet = ""
|
|
589
593
|
if isinstance(raw_data, dict):
|
|
590
594
|
# Try to get meaningful snippet
|
|
591
|
-
full_log = raw_data.get(
|
|
595
|
+
full_log = raw_data.get("full_log", "")
|
|
592
596
|
if full_log:
|
|
593
597
|
raw_snippet = full_log[:300]
|
|
594
598
|
else:
|
|
595
599
|
# Try message or description
|
|
596
|
-
raw_snippet = raw_data.get(
|
|
600
|
+
raw_snippet = raw_data.get("message", "")[:300]
|
|
597
601
|
elif isinstance(raw_data, str):
|
|
598
602
|
raw_snippet = raw_data[:300]
|
|
599
603
|
|
|
600
|
-
timestamp = alert.get(
|
|
601
|
-
if hasattr(timestamp,
|
|
604
|
+
timestamp = alert.get("timestamp", "")
|
|
605
|
+
if hasattr(timestamp, "isoformat"):
|
|
602
606
|
timestamp = timestamp.isoformat()
|
|
603
607
|
|
|
604
|
-
samples.append(
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
608
|
+
samples.append(
|
|
609
|
+
SampleAlert(
|
|
610
|
+
rule_id=str(alert.get("rule_id", "N/A")),
|
|
611
|
+
rule_name=alert.get("rule_name", alert.get("name", "Unknown")),
|
|
612
|
+
severity=str(alert.get("severity", "info")),
|
|
613
|
+
timestamp=str(timestamp),
|
|
614
|
+
source=attack_type,
|
|
615
|
+
description=alert.get("description", "")[:200],
|
|
616
|
+
raw_snippet=raw_snippet,
|
|
617
|
+
)
|
|
618
|
+
)
|
|
613
619
|
|
|
614
620
|
return samples
|
|
615
621
|
|
|
@@ -618,7 +624,7 @@ class DetectionReportGatherer:
|
|
|
618
624
|
summary: EngagementDetectionSummary,
|
|
619
625
|
severity: SeverityBreakdown,
|
|
620
626
|
gaps: List[DetectionResult],
|
|
621
|
-
mitre_gaps: List[TechniqueResult]
|
|
627
|
+
mitre_gaps: List[TechniqueResult],
|
|
622
628
|
) -> str:
|
|
623
629
|
"""
|
|
624
630
|
Generate an executive summary paragraph for the report.
|
|
@@ -699,9 +705,7 @@ class DetectionReportGatherer:
|
|
|
699
705
|
return " ".join(parts)
|
|
700
706
|
|
|
701
707
|
def _calculate_risk_level(
|
|
702
|
-
self,
|
|
703
|
-
coverage_percent: float,
|
|
704
|
-
gaps: List[DetectionResult]
|
|
708
|
+
self, coverage_percent: float, gaps: List[DetectionResult]
|
|
705
709
|
) -> str:
|
|
706
710
|
"""
|
|
707
711
|
Calculate overall risk level based on coverage and gap severity.
|
|
@@ -716,9 +720,9 @@ class DetectionReportGatherer:
|
|
|
716
720
|
# Check for critical gaps
|
|
717
721
|
critical_gaps = 0
|
|
718
722
|
for gap in gaps:
|
|
719
|
-
attack_type = gap.attack_type if hasattr(gap,
|
|
723
|
+
attack_type = gap.attack_type if hasattr(gap, "attack_type") else ""
|
|
720
724
|
signature = get_signature(attack_type)
|
|
721
|
-
if signature.get(
|
|
725
|
+
if signature.get("severity") in ("critical", "high"):
|
|
722
726
|
critical_gaps += 1
|
|
723
727
|
|
|
724
728
|
# Determine risk level
|
|
@@ -732,8 +736,7 @@ class DetectionReportGatherer:
|
|
|
732
736
|
return "LOW"
|
|
733
737
|
|
|
734
738
|
def _gather_vulnerability_data(
|
|
735
|
-
self,
|
|
736
|
-
per_host_analysis: Dict[str, HostDetectionStats]
|
|
739
|
+
self, per_host_analysis: Dict[str, HostDetectionStats]
|
|
737
740
|
) -> VulnerabilitySection:
|
|
738
741
|
"""
|
|
739
742
|
Gather vulnerability data for hosts that were attacked.
|
|
@@ -756,8 +759,7 @@ class DetectionReportGatherer:
|
|
|
756
759
|
|
|
757
760
|
# Get all vulnerabilities for this engagement
|
|
758
761
|
all_vulns = vulns_manager.list_vulnerabilities(
|
|
759
|
-
engagement_id=self.engagement_id,
|
|
760
|
-
limit=1000
|
|
762
|
+
engagement_id=self.engagement_id, limit=1000
|
|
761
763
|
)
|
|
762
764
|
|
|
763
765
|
if not all_vulns:
|
|
@@ -769,12 +771,12 @@ class DetectionReportGatherer:
|
|
|
769
771
|
# Build summary by severity
|
|
770
772
|
for vuln in all_vulns:
|
|
771
773
|
section.total_vulns += 1
|
|
772
|
-
severity = (vuln.get(
|
|
773
|
-
if severity ==
|
|
774
|
+
severity = (vuln.get("severity") or "Low").lower()
|
|
775
|
+
if severity == "critical":
|
|
774
776
|
section.critical_count += 1
|
|
775
|
-
elif severity ==
|
|
777
|
+
elif severity == "high":
|
|
776
778
|
section.high_count += 1
|
|
777
|
-
elif severity ==
|
|
779
|
+
elif severity == "medium":
|
|
778
780
|
section.medium_count += 1
|
|
779
781
|
else:
|
|
780
782
|
section.low_count += 1
|
|
@@ -782,7 +784,7 @@ class DetectionReportGatherer:
|
|
|
782
784
|
# Group vulnerabilities by host/agent IP
|
|
783
785
|
host_vulns: Dict[str, List[Dict]] = {}
|
|
784
786
|
for vuln in all_vulns:
|
|
785
|
-
agent_ip = vuln.get(
|
|
787
|
+
agent_ip = vuln.get("agent_ip") or vuln.get("host_ip") or "unknown"
|
|
786
788
|
if agent_ip not in host_vulns:
|
|
787
789
|
host_vulns[agent_ip] = []
|
|
788
790
|
host_vulns[agent_ip].append(vuln)
|
|
@@ -795,39 +797,39 @@ class DetectionReportGatherer:
|
|
|
795
797
|
|
|
796
798
|
host_summary = HostVulnerabilitySummary(
|
|
797
799
|
host_ip=host_ip,
|
|
798
|
-
agent_name=vulns[0].get(
|
|
800
|
+
agent_name=vulns[0].get("agent_name", "") if vulns else "",
|
|
799
801
|
total_vulns=len(vulns),
|
|
800
|
-
was_attacked=was_attacked
|
|
802
|
+
was_attacked=was_attacked,
|
|
801
803
|
)
|
|
802
804
|
|
|
803
805
|
# Count by severity for this host
|
|
804
806
|
for v in vulns:
|
|
805
|
-
sev = (v.get(
|
|
806
|
-
if sev ==
|
|
807
|
+
sev = (v.get("severity") or "Low").lower()
|
|
808
|
+
if sev == "critical":
|
|
807
809
|
host_summary.critical += 1
|
|
808
|
-
elif sev ==
|
|
810
|
+
elif sev == "high":
|
|
809
811
|
host_summary.high += 1
|
|
810
|
-
elif sev ==
|
|
812
|
+
elif sev == "medium":
|
|
811
813
|
host_summary.medium += 1
|
|
812
814
|
else:
|
|
813
815
|
host_summary.low += 1
|
|
814
816
|
|
|
815
817
|
# Get top 5 vulns for this host (by CVSS score)
|
|
816
818
|
sorted_vulns = sorted(
|
|
817
|
-
vulns,
|
|
818
|
-
key=lambda x: x.get('cvss_score') or 0,
|
|
819
|
-
reverse=True
|
|
819
|
+
vulns, key=lambda x: x.get("cvss_score") or 0, reverse=True
|
|
820
820
|
)[:5]
|
|
821
821
|
|
|
822
822
|
for v in sorted_vulns:
|
|
823
|
-
host_summary.top_vulns.append(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
823
|
+
host_summary.top_vulns.append(
|
|
824
|
+
HostVulnerability(
|
|
825
|
+
cve_id=v.get("cve_id", "N/A"),
|
|
826
|
+
name=v.get("name", "")[:100],
|
|
827
|
+
severity=v.get("severity", "Low"),
|
|
828
|
+
cvss_score=v.get("cvss_score") or 0.0,
|
|
829
|
+
package_name=v.get("package_name", ""),
|
|
830
|
+
package_version=v.get("package_version", ""),
|
|
831
|
+
)
|
|
832
|
+
)
|
|
831
833
|
|
|
832
834
|
section.host_summaries.append(host_summary)
|
|
833
835
|
|
|
@@ -839,33 +841,36 @@ class DetectionReportGatherer:
|
|
|
839
841
|
# Get top 10 CVEs across all hosts
|
|
840
842
|
all_cves: Dict[str, Dict] = {}
|
|
841
843
|
for vuln in all_vulns:
|
|
842
|
-
cve_id = vuln.get(
|
|
844
|
+
cve_id = vuln.get("cve_id")
|
|
843
845
|
if not cve_id:
|
|
844
846
|
continue
|
|
845
847
|
if cve_id not in all_cves:
|
|
846
848
|
all_cves[cve_id] = vuln
|
|
847
|
-
elif (vuln.get(
|
|
849
|
+
elif (vuln.get("cvss_score") or 0) > (
|
|
850
|
+
all_cves[cve_id].get("cvss_score") or 0
|
|
851
|
+
):
|
|
848
852
|
all_cves[cve_id] = vuln
|
|
849
853
|
|
|
850
854
|
sorted_cves = sorted(
|
|
851
|
-
all_cves.values(),
|
|
852
|
-
key=lambda x: x.get('cvss_score') or 0,
|
|
853
|
-
reverse=True
|
|
855
|
+
all_cves.values(), key=lambda x: x.get("cvss_score") or 0, reverse=True
|
|
854
856
|
)[:10]
|
|
855
857
|
|
|
856
858
|
for v in sorted_cves:
|
|
857
|
-
section.top_cves.append(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
859
|
+
section.top_cves.append(
|
|
860
|
+
HostVulnerability(
|
|
861
|
+
cve_id=v.get("cve_id", "N/A"),
|
|
862
|
+
name=v.get("name", "")[:100],
|
|
863
|
+
severity=v.get("severity", "Low"),
|
|
864
|
+
cvss_score=v.get("cvss_score") or 0.0,
|
|
865
|
+
package_name=v.get("package_name", ""),
|
|
866
|
+
package_version=v.get("package_version", ""),
|
|
867
|
+
)
|
|
868
|
+
)
|
|
865
869
|
|
|
866
870
|
except Exception as e:
|
|
867
871
|
# Log but don't fail the report
|
|
868
872
|
import logging
|
|
873
|
+
|
|
869
874
|
logging.getLogger(__name__).warning(
|
|
870
875
|
f"Failed to gather vulnerability data: {e}"
|
|
871
876
|
)
|
|
@@ -894,12 +899,10 @@ class DetectionReportGatherer:
|
|
|
894
899
|
|
|
895
900
|
# Count tactics with coverage
|
|
896
901
|
tactics_tested = sum(
|
|
897
|
-
1 for t in data.tactic_summary.values()
|
|
898
|
-
if t.techniques_tested > 0
|
|
902
|
+
1 for t in data.tactic_summary.values() if t.techniques_tested > 0
|
|
899
903
|
)
|
|
900
904
|
tactics_with_gaps = sum(
|
|
901
|
-
1 for t in data.tactic_summary.values()
|
|
902
|
-
if t.techniques_not_detected > 0
|
|
905
|
+
1 for t in data.tactic_summary.values() if t.techniques_not_detected > 0
|
|
903
906
|
)
|
|
904
907
|
|
|
905
908
|
return {
|
|
@@ -912,13 +915,11 @@ class DetectionReportGatherer:
|
|
|
912
915
|
"tactics_with_gaps": tactics_with_gaps,
|
|
913
916
|
"techniques_tested": len(data.mitre_coverage),
|
|
914
917
|
"techniques_detected": sum(
|
|
915
|
-
1 for t in data.mitre_coverage.values()
|
|
916
|
-
if t.detected > 0
|
|
918
|
+
1 for t in data.mitre_coverage.values() if t.detected > 0
|
|
917
919
|
),
|
|
918
920
|
"hosts_tested": len(data.per_host_analysis),
|
|
919
921
|
"critical_recommendations": sum(
|
|
920
|
-
1 for r in data.rule_recommendations
|
|
921
|
-
if r.priority == "critical"
|
|
922
|
+
1 for r in data.rule_recommendations if r.priority == "critical"
|
|
922
923
|
),
|
|
923
924
|
}
|
|
924
925
|
|