souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +9526 -2879
- 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 +563 -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 +408 -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 +371 -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 +854 -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 +173 -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 +223 -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 +23434 -10286
- 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.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/reporting/charts.py
CHANGED
|
@@ -9,516 +9,473 @@ from typing import Dict, List
|
|
|
9
9
|
|
|
10
10
|
class ChartGenerator:
|
|
11
11
|
"""Generate Chart.js chart configurations."""
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
def __init__(self):
|
|
14
14
|
self.colors = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
"critical": "#dc3545",
|
|
16
|
+
"high": "#fd7e14",
|
|
17
|
+
"medium": "#ffc107",
|
|
18
|
+
"low": "#28a745",
|
|
19
|
+
"info": "#17a2b8",
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
def severity_distribution_chart(self, findings_by_severity: Dict) -> str:
|
|
23
23
|
"""Generate pie chart showing finding distribution by severity."""
|
|
24
24
|
data = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
"Critical": len(findings_by_severity.get("critical", [])),
|
|
26
|
+
"High": len(findings_by_severity.get("high", [])),
|
|
27
|
+
"Medium": len(findings_by_severity.get("medium", [])),
|
|
28
|
+
"Low": len(findings_by_severity.get("low", [])),
|
|
29
|
+
"Info": len(findings_by_severity.get("info", [])),
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
# Filter out zero values
|
|
33
33
|
filtered_data = {k: v for k, v in data.items() if v > 0}
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
if not filtered_data:
|
|
36
36
|
return ""
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
labels = list(filtered_data.keys())
|
|
39
39
|
values = list(filtered_data.values())
|
|
40
40
|
colors = [self.colors[k.lower()] for k in labels]
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
config = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
"type": "doughnut",
|
|
44
|
+
"data": {
|
|
45
|
+
"labels": labels,
|
|
46
|
+
"datasets": [
|
|
47
|
+
{
|
|
48
|
+
"data": values,
|
|
49
|
+
"backgroundColor": colors,
|
|
50
|
+
"borderWidth": 2,
|
|
51
|
+
"borderColor": "#fff",
|
|
52
|
+
}
|
|
53
|
+
],
|
|
52
54
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
}
|
|
55
|
+
"options": {
|
|
56
|
+
"responsive": True,
|
|
57
|
+
"maintainAspectRatio": True,
|
|
58
|
+
"plugins": {
|
|
59
|
+
"legend": {"position": "right", "labels": {"font": {"size": 14}}},
|
|
60
|
+
"title": {
|
|
61
|
+
"display": True,
|
|
62
|
+
"text": "Findings by Severity",
|
|
63
|
+
"font": {"size": 16, "weight": "bold"},
|
|
64
64
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'text': 'Findings by Severity',
|
|
68
|
-
'font': {
|
|
69
|
-
'size': 16,
|
|
70
|
-
'weight': 'bold'
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
75
67
|
}
|
|
76
|
-
|
|
68
|
+
|
|
77
69
|
return json.dumps(config)
|
|
78
|
-
|
|
70
|
+
|
|
79
71
|
def host_impact_chart(self, attack_surface: Dict) -> str:
|
|
80
72
|
"""Generate bar chart showing findings per host."""
|
|
81
|
-
hosts = attack_surface.get(
|
|
82
|
-
|
|
73
|
+
hosts = attack_surface.get("hosts", [])
|
|
74
|
+
|
|
83
75
|
# Sort by findings count (descending) and take top 10
|
|
84
|
-
sorted_hosts = sorted(hosts, key=lambda h: h.get(
|
|
85
|
-
|
|
76
|
+
sorted_hosts = sorted(hosts, key=lambda h: h.get("findings", 0), reverse=True)[
|
|
77
|
+
:10
|
|
78
|
+
]
|
|
79
|
+
|
|
86
80
|
if not sorted_hosts:
|
|
87
81
|
return ""
|
|
88
|
-
|
|
82
|
+
|
|
89
83
|
labels = []
|
|
90
84
|
critical_data = []
|
|
91
85
|
high_data = []
|
|
92
86
|
other_data = []
|
|
93
|
-
|
|
87
|
+
|
|
94
88
|
for host in sorted_hosts:
|
|
95
89
|
# Use hostname or IP
|
|
96
|
-
label = host.get(
|
|
90
|
+
label = host.get("hostname") or host.get("host", "Unknown")
|
|
97
91
|
labels.append(label)
|
|
98
|
-
|
|
99
|
-
critical_data.append(host.get(
|
|
100
|
-
high_findings = host.get(
|
|
92
|
+
|
|
93
|
+
critical_data.append(host.get("critical_findings", 0))
|
|
94
|
+
high_findings = host.get("findings", 0) - host.get("critical_findings", 0)
|
|
101
95
|
# Assume 30% of non-critical are high, rest are medium/low (simplified)
|
|
102
96
|
high_count = int(high_findings * 0.3)
|
|
103
97
|
other_count = high_findings - high_count
|
|
104
|
-
|
|
98
|
+
|
|
105
99
|
high_data.append(high_count)
|
|
106
100
|
other_data.append(other_count)
|
|
107
|
-
|
|
101
|
+
|
|
108
102
|
config = {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
"type": "bar",
|
|
104
|
+
"data": {
|
|
105
|
+
"labels": labels,
|
|
106
|
+
"datasets": [
|
|
113
107
|
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
"label": "Critical",
|
|
109
|
+
"data": critical_data,
|
|
110
|
+
"backgroundColor": self.colors["critical"],
|
|
111
|
+
"stack": "stack0",
|
|
118
112
|
},
|
|
119
113
|
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
"label": "High",
|
|
115
|
+
"data": high_data,
|
|
116
|
+
"backgroundColor": self.colors["high"],
|
|
117
|
+
"stack": "stack0",
|
|
124
118
|
},
|
|
125
119
|
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
]
|
|
120
|
+
"label": "Medium/Low",
|
|
121
|
+
"data": other_data,
|
|
122
|
+
"backgroundColor": self.colors["medium"],
|
|
123
|
+
"stack": "stack0",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
132
126
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
127
|
+
"options": {
|
|
128
|
+
"responsive": True,
|
|
129
|
+
"maintainAspectRatio": True,
|
|
130
|
+
"indexAxis": "y",
|
|
131
|
+
"plugins": {
|
|
132
|
+
"legend": {"display": True, "position": "top"},
|
|
133
|
+
"title": {
|
|
134
|
+
"display": True,
|
|
135
|
+
"text": "Top 10 Hosts by Finding Count",
|
|
136
|
+
"font": {"size": 16, "weight": "bold"},
|
|
141
137
|
},
|
|
142
|
-
'title': {
|
|
143
|
-
'display': True,
|
|
144
|
-
'text': 'Top 10 Hosts by Finding Count',
|
|
145
|
-
'font': {
|
|
146
|
-
'size': 16,
|
|
147
|
-
'weight': 'bold'
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
138
|
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'display': True,
|
|
156
|
-
'text': 'Number of Findings'
|
|
157
|
-
}
|
|
139
|
+
"scales": {
|
|
140
|
+
"x": {
|
|
141
|
+
"stacked": True,
|
|
142
|
+
"title": {"display": True, "text": "Number of Findings"},
|
|
158
143
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
}
|
|
144
|
+
"y": {"stacked": True},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
164
147
|
}
|
|
165
|
-
|
|
148
|
+
|
|
166
149
|
return json.dumps(config)
|
|
167
|
-
|
|
150
|
+
|
|
168
151
|
def exploitation_progress_chart(self, attack_surface: Dict) -> str:
|
|
169
152
|
"""Generate gauge/progress chart for exploitation rate."""
|
|
170
|
-
overview = attack_surface.get(
|
|
171
|
-
total_services = overview.get(
|
|
172
|
-
exploited = overview.get(
|
|
173
|
-
|
|
153
|
+
overview = attack_surface.get("overview", {})
|
|
154
|
+
total_services = overview.get("total_services", 0)
|
|
155
|
+
exploited = overview.get("exploited_services", 0)
|
|
156
|
+
|
|
174
157
|
if total_services == 0:
|
|
175
158
|
return ""
|
|
176
|
-
|
|
159
|
+
|
|
177
160
|
exploitation_rate = round((exploited / total_services) * 100, 1)
|
|
178
161
|
remaining = 100 - exploitation_rate
|
|
179
|
-
|
|
162
|
+
|
|
180
163
|
# Determine color based on rate
|
|
181
164
|
if exploitation_rate >= 50:
|
|
182
|
-
color = self.colors[
|
|
165
|
+
color = self.colors["critical"]
|
|
183
166
|
elif exploitation_rate >= 25:
|
|
184
|
-
color = self.colors[
|
|
167
|
+
color = self.colors["high"]
|
|
185
168
|
elif exploitation_rate >= 10:
|
|
186
|
-
color = self.colors[
|
|
169
|
+
color = self.colors["medium"]
|
|
187
170
|
else:
|
|
188
|
-
color = self.colors[
|
|
189
|
-
|
|
171
|
+
color = self.colors["low"]
|
|
172
|
+
|
|
190
173
|
config = {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
174
|
+
"type": "doughnut",
|
|
175
|
+
"data": {
|
|
176
|
+
"labels": ["Exploited", "Not Exploited"],
|
|
177
|
+
"datasets": [
|
|
178
|
+
{
|
|
179
|
+
"data": [exploitation_rate, remaining],
|
|
180
|
+
"backgroundColor": [color, "#e9ecef"],
|
|
181
|
+
"borderWidth": 0,
|
|
182
|
+
"circumference": 180,
|
|
183
|
+
"rotation": 270,
|
|
184
|
+
}
|
|
185
|
+
],
|
|
201
186
|
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
187
|
+
"options": {
|
|
188
|
+
"responsive": True,
|
|
189
|
+
"maintainAspectRatio": True,
|
|
190
|
+
"plugins": {
|
|
191
|
+
"legend": {"display": False},
|
|
192
|
+
"title": {
|
|
193
|
+
"display": True,
|
|
194
|
+
"text": f"Exploitation Rate: {exploitation_rate}%",
|
|
195
|
+
"font": {"size": 16, "weight": "bold"},
|
|
208
196
|
},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'font': {
|
|
213
|
-
'size': 16,
|
|
214
|
-
'weight': 'bold'
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
'tooltip': {
|
|
218
|
-
'enabled': True
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
197
|
+
"tooltip": {"enabled": True},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
222
200
|
}
|
|
223
|
-
|
|
201
|
+
|
|
224
202
|
return json.dumps(config)
|
|
225
|
-
|
|
203
|
+
|
|
226
204
|
def timeline_chart(self, evidence: Dict) -> str:
|
|
227
205
|
"""Generate timeline chart showing findings discovered over time."""
|
|
228
206
|
from datetime import datetime
|
|
229
|
-
|
|
207
|
+
|
|
230
208
|
# Group evidence by date
|
|
231
209
|
timeline_data = {}
|
|
232
|
-
|
|
210
|
+
|
|
233
211
|
for phase, items in evidence.items():
|
|
234
212
|
if isinstance(items, list):
|
|
235
213
|
for item in items:
|
|
236
|
-
timestamp = item.get(
|
|
214
|
+
timestamp = item.get("timestamp", "")
|
|
237
215
|
if timestamp:
|
|
238
216
|
# Extract date (YYYY-MM-DD)
|
|
239
217
|
date = timestamp[:10] if len(timestamp) >= 10 else timestamp
|
|
240
218
|
if date not in timeline_data:
|
|
241
219
|
timeline_data[date] = 0
|
|
242
220
|
timeline_data[date] += 1
|
|
243
|
-
|
|
221
|
+
|
|
244
222
|
if not timeline_data:
|
|
245
223
|
return ""
|
|
246
|
-
|
|
224
|
+
|
|
247
225
|
# Sort by date
|
|
248
226
|
sorted_dates = sorted(timeline_data.keys())
|
|
249
227
|
counts = [timeline_data[date] for date in sorted_dates]
|
|
250
|
-
|
|
228
|
+
|
|
251
229
|
config = {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
230
|
+
"type": "line",
|
|
231
|
+
"data": {
|
|
232
|
+
"labels": sorted_dates,
|
|
233
|
+
"datasets": [
|
|
234
|
+
{
|
|
235
|
+
"label": "Evidence Items Collected",
|
|
236
|
+
"data": counts,
|
|
237
|
+
"borderColor": self.colors["info"],
|
|
238
|
+
"backgroundColor": "rgba(23, 162, 184, 0.1)",
|
|
239
|
+
"fill": True,
|
|
240
|
+
"tension": 0.3,
|
|
241
|
+
"pointRadius": 5,
|
|
242
|
+
"pointHoverRadius": 7,
|
|
243
|
+
}
|
|
244
|
+
],
|
|
265
245
|
},
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
246
|
+
"options": {
|
|
247
|
+
"responsive": True,
|
|
248
|
+
"maintainAspectRatio": True,
|
|
249
|
+
"plugins": {
|
|
250
|
+
"legend": {"display": True, "position": "top"},
|
|
251
|
+
"title": {
|
|
252
|
+
"display": True,
|
|
253
|
+
"text": "Evidence Collection Timeline",
|
|
254
|
+
"font": {"size": 16, "weight": "bold"},
|
|
273
255
|
},
|
|
274
|
-
'title': {
|
|
275
|
-
'display': True,
|
|
276
|
-
'text': 'Evidence Collection Timeline',
|
|
277
|
-
'font': {
|
|
278
|
-
'size': 16,
|
|
279
|
-
'weight': 'bold'
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
256
|
},
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
'display': True,
|
|
288
|
-
'text': 'Items Collected'
|
|
289
|
-
}
|
|
257
|
+
"scales": {
|
|
258
|
+
"y": {
|
|
259
|
+
"beginAtZero": True,
|
|
260
|
+
"title": {"display": True, "text": "Items Collected"},
|
|
290
261
|
},
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
'text': 'Date'
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
262
|
+
"x": {"title": {"display": True, "text": "Date"}},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
299
265
|
}
|
|
300
|
-
|
|
266
|
+
|
|
301
267
|
return json.dumps(config)
|
|
302
|
-
|
|
268
|
+
|
|
303
269
|
def evidence_by_phase_chart(self, evidence_counts: Dict) -> str:
|
|
304
270
|
"""Generate stacked bar chart for evidence by phase."""
|
|
305
271
|
if not evidence_counts:
|
|
306
272
|
return ""
|
|
307
|
-
|
|
308
|
-
phases = [
|
|
273
|
+
|
|
274
|
+
phases = ["Reconnaissance", "Enumeration", "Exploitation", "Post-Exploitation"]
|
|
309
275
|
counts = [
|
|
310
|
-
evidence_counts.get(
|
|
311
|
-
evidence_counts.get(
|
|
312
|
-
evidence_counts.get(
|
|
313
|
-
evidence_counts.get(
|
|
276
|
+
evidence_counts.get("reconnaissance", 0),
|
|
277
|
+
evidence_counts.get("enumeration", 0),
|
|
278
|
+
evidence_counts.get("exploitation", 0),
|
|
279
|
+
evidence_counts.get("post_exploitation", 0),
|
|
314
280
|
]
|
|
315
|
-
|
|
281
|
+
|
|
316
282
|
if sum(counts) == 0:
|
|
317
283
|
return ""
|
|
318
|
-
|
|
284
|
+
|
|
319
285
|
config = {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
286
|
+
"type": "bar",
|
|
287
|
+
"data": {
|
|
288
|
+
"labels": phases,
|
|
289
|
+
"datasets": [
|
|
290
|
+
{
|
|
291
|
+
"label": "Evidence Count",
|
|
292
|
+
"data": counts,
|
|
293
|
+
"backgroundColor": [
|
|
294
|
+
self.colors["info"],
|
|
295
|
+
self.colors["low"],
|
|
296
|
+
self.colors["medium"],
|
|
297
|
+
self.colors["high"],
|
|
298
|
+
],
|
|
299
|
+
"borderWidth": 1,
|
|
300
|
+
}
|
|
301
|
+
],
|
|
334
302
|
},
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
303
|
+
"options": {
|
|
304
|
+
"responsive": True,
|
|
305
|
+
"maintainAspectRatio": True,
|
|
306
|
+
"plugins": {
|
|
307
|
+
"legend": {"display": False},
|
|
308
|
+
"title": {
|
|
309
|
+
"display": True,
|
|
310
|
+
"text": "Evidence by Testing Phase",
|
|
311
|
+
"font": {"size": 16, "weight": "bold"},
|
|
341
312
|
},
|
|
342
|
-
'title': {
|
|
343
|
-
'display': True,
|
|
344
|
-
'text': 'Evidence by Testing Phase',
|
|
345
|
-
'font': {
|
|
346
|
-
'size': 16,
|
|
347
|
-
'weight': 'bold'
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
313
|
},
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
'display': True,
|
|
356
|
-
'text': 'Evidence Items'
|
|
357
|
-
}
|
|
314
|
+
"scales": {
|
|
315
|
+
"y": {
|
|
316
|
+
"beginAtZero": True,
|
|
317
|
+
"title": {"display": True, "text": "Evidence Items"},
|
|
358
318
|
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
319
|
+
},
|
|
320
|
+
},
|
|
361
321
|
}
|
|
362
|
-
|
|
322
|
+
|
|
363
323
|
return json.dumps(config)
|
|
364
|
-
|
|
324
|
+
|
|
365
325
|
def service_exposure_chart(self, attack_surface: Dict) -> str:
|
|
366
326
|
"""Generate chart showing service exposure distribution."""
|
|
367
|
-
hosts = attack_surface.get(
|
|
368
|
-
|
|
327
|
+
hosts = attack_surface.get("hosts", [])
|
|
328
|
+
|
|
369
329
|
if not hosts:
|
|
370
330
|
return ""
|
|
371
|
-
|
|
331
|
+
|
|
372
332
|
# Count services across all hosts
|
|
373
333
|
service_counts = {}
|
|
374
334
|
for host in hosts:
|
|
375
|
-
services_data = host.get(
|
|
335
|
+
services_data = host.get("services", [])
|
|
376
336
|
if isinstance(services_data, list):
|
|
377
337
|
for service in services_data:
|
|
378
|
-
service_name = service.get(
|
|
379
|
-
service_counts[service_name] =
|
|
380
|
-
|
|
338
|
+
service_name = service.get("service", "unknown")
|
|
339
|
+
service_counts[service_name] = (
|
|
340
|
+
service_counts.get(service_name, 0) + 1
|
|
341
|
+
)
|
|
342
|
+
|
|
381
343
|
if not service_counts:
|
|
382
344
|
return ""
|
|
383
|
-
|
|
345
|
+
|
|
384
346
|
# Take top 10 services
|
|
385
|
-
sorted_services = sorted(
|
|
347
|
+
sorted_services = sorted(
|
|
348
|
+
service_counts.items(), key=lambda x: x[1], reverse=True
|
|
349
|
+
)[:10]
|
|
386
350
|
labels = [s[0] for s in sorted_services]
|
|
387
351
|
counts = [s[1] for s in sorted_services]
|
|
388
|
-
|
|
352
|
+
|
|
389
353
|
config = {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
354
|
+
"type": "bar",
|
|
355
|
+
"data": {
|
|
356
|
+
"labels": labels,
|
|
357
|
+
"datasets": [
|
|
358
|
+
{
|
|
359
|
+
"label": "Occurrences",
|
|
360
|
+
"data": counts,
|
|
361
|
+
"backgroundColor": self.colors["info"],
|
|
362
|
+
"borderWidth": 1,
|
|
363
|
+
}
|
|
364
|
+
],
|
|
399
365
|
},
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
366
|
+
"options": {
|
|
367
|
+
"responsive": True,
|
|
368
|
+
"maintainAspectRatio": True,
|
|
369
|
+
"indexAxis": "y",
|
|
370
|
+
"plugins": {
|
|
371
|
+
"legend": {"display": False},
|
|
372
|
+
"title": {
|
|
373
|
+
"display": True,
|
|
374
|
+
"text": "Top 10 Exposed Services",
|
|
375
|
+
"font": {"size": 16, "weight": "bold"},
|
|
407
376
|
},
|
|
408
|
-
'title': {
|
|
409
|
-
'display': True,
|
|
410
|
-
'text': 'Top 10 Exposed Services',
|
|
411
|
-
'font': {
|
|
412
|
-
'size': 16,
|
|
413
|
-
'weight': 'bold'
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
377
|
},
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
'display': True,
|
|
422
|
-
'text': 'Number of Instances'
|
|
423
|
-
}
|
|
378
|
+
"scales": {
|
|
379
|
+
"x": {
|
|
380
|
+
"beginAtZero": True,
|
|
381
|
+
"title": {"display": True, "text": "Number of Instances"},
|
|
424
382
|
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
383
|
+
},
|
|
384
|
+
},
|
|
427
385
|
}
|
|
428
|
-
|
|
386
|
+
|
|
429
387
|
return json.dumps(config)
|
|
430
|
-
|
|
388
|
+
|
|
431
389
|
def credentials_by_service_chart(self, credentials: List[Dict]) -> str:
|
|
432
390
|
"""Generate chart showing credentials found by service type."""
|
|
433
391
|
if not credentials:
|
|
434
392
|
return ""
|
|
435
|
-
|
|
393
|
+
|
|
436
394
|
service_counts = {}
|
|
437
395
|
for cred in credentials:
|
|
438
|
-
service = cred.get(
|
|
396
|
+
service = cred.get("service", "unknown")
|
|
439
397
|
service_counts[service] = service_counts.get(service, 0) + 1
|
|
440
|
-
|
|
398
|
+
|
|
441
399
|
if not service_counts:
|
|
442
400
|
return ""
|
|
443
|
-
|
|
401
|
+
|
|
444
402
|
labels = list(service_counts.keys())
|
|
445
403
|
counts = list(service_counts.values())
|
|
446
|
-
|
|
404
|
+
|
|
447
405
|
config = {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
406
|
+
"type": "pie",
|
|
407
|
+
"data": {
|
|
408
|
+
"labels": labels,
|
|
409
|
+
"datasets": [
|
|
410
|
+
{
|
|
411
|
+
"data": counts,
|
|
412
|
+
"backgroundColor": [
|
|
413
|
+
self.colors["critical"],
|
|
414
|
+
self.colors["high"],
|
|
415
|
+
self.colors["medium"],
|
|
416
|
+
self.colors["low"],
|
|
417
|
+
self.colors["info"],
|
|
418
|
+
"#6c757d",
|
|
419
|
+
"#17a2b8",
|
|
420
|
+
"#28a745",
|
|
421
|
+
],
|
|
422
|
+
"borderWidth": 2,
|
|
423
|
+
"borderColor": "#fff",
|
|
424
|
+
}
|
|
425
|
+
],
|
|
466
426
|
},
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
427
|
+
"options": {
|
|
428
|
+
"responsive": True,
|
|
429
|
+
"maintainAspectRatio": True,
|
|
430
|
+
"plugins": {
|
|
431
|
+
"legend": {"position": "right"},
|
|
432
|
+
"title": {
|
|
433
|
+
"display": True,
|
|
434
|
+
"text": "Credentials by Service",
|
|
435
|
+
"font": {"size": 16, "weight": "bold"},
|
|
473
436
|
},
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
'text': 'Credentials by Service',
|
|
477
|
-
'font': {
|
|
478
|
-
'size': 16,
|
|
479
|
-
'weight': 'bold'
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
437
|
+
},
|
|
438
|
+
},
|
|
484
439
|
}
|
|
485
|
-
|
|
440
|
+
|
|
486
441
|
return json.dumps(config)
|
|
487
|
-
|
|
442
|
+
|
|
488
443
|
def generate_all_charts(self, data: Dict) -> Dict:
|
|
489
444
|
"""Generate all chart configurations."""
|
|
490
445
|
charts = {}
|
|
491
|
-
|
|
446
|
+
|
|
492
447
|
# Phase 1 charts
|
|
493
|
-
severity_chart = self.severity_distribution_chart(data[
|
|
448
|
+
severity_chart = self.severity_distribution_chart(data["findings_by_severity"])
|
|
494
449
|
if severity_chart:
|
|
495
|
-
charts[
|
|
496
|
-
|
|
497
|
-
host_chart = self.host_impact_chart(data[
|
|
450
|
+
charts["severity_distribution"] = severity_chart
|
|
451
|
+
|
|
452
|
+
host_chart = self.host_impact_chart(data["attack_surface"])
|
|
498
453
|
if host_chart:
|
|
499
|
-
charts[
|
|
500
|
-
|
|
501
|
-
exploitation_chart = self.exploitation_progress_chart(data[
|
|
454
|
+
charts["host_impact"] = host_chart
|
|
455
|
+
|
|
456
|
+
exploitation_chart = self.exploitation_progress_chart(data["attack_surface"])
|
|
502
457
|
if exploitation_chart:
|
|
503
|
-
charts[
|
|
504
|
-
|
|
458
|
+
charts["exploitation_progress"] = exploitation_chart
|
|
459
|
+
|
|
505
460
|
# Phase 2 charts
|
|
506
|
-
timeline = self.timeline_chart(data.get(
|
|
461
|
+
timeline = self.timeline_chart(data.get("evidence", {}))
|
|
507
462
|
if timeline:
|
|
508
|
-
charts[
|
|
509
|
-
|
|
510
|
-
evidence_phase = self.evidence_by_phase_chart(data.get(
|
|
463
|
+
charts["timeline"] = timeline
|
|
464
|
+
|
|
465
|
+
evidence_phase = self.evidence_by_phase_chart(data.get("evidence_counts", {}))
|
|
511
466
|
if evidence_phase:
|
|
512
|
-
charts[
|
|
513
|
-
|
|
514
|
-
service_exposure = self.service_exposure_chart(data[
|
|
467
|
+
charts["evidence_by_phase"] = evidence_phase
|
|
468
|
+
|
|
469
|
+
service_exposure = self.service_exposure_chart(data["attack_surface"])
|
|
515
470
|
if service_exposure:
|
|
516
|
-
charts[
|
|
517
|
-
|
|
518
|
-
credentials_chart = self.credentials_by_service_chart(
|
|
471
|
+
charts["service_exposure"] = service_exposure
|
|
472
|
+
|
|
473
|
+
credentials_chart = self.credentials_by_service_chart(
|
|
474
|
+
data.get("credentials", [])
|
|
475
|
+
)
|
|
519
476
|
if credentials_chart:
|
|
520
|
-
charts[
|
|
521
|
-
|
|
477
|
+
charts["credentials_by_service"] = credentials_chart
|
|
478
|
+
|
|
522
479
|
return charts
|
|
523
480
|
|
|
524
481
|
# =========================================================================
|
|
@@ -536,22 +493,22 @@ class ChartGenerator:
|
|
|
536
493
|
Chart.js JSON config string
|
|
537
494
|
"""
|
|
538
495
|
# Handle both object and dict
|
|
539
|
-
if hasattr(summary,
|
|
496
|
+
if hasattr(summary, "detected"):
|
|
540
497
|
detected = summary.detected
|
|
541
498
|
not_detected = summary.not_detected
|
|
542
|
-
partial = getattr(summary,
|
|
543
|
-
offline = getattr(summary,
|
|
499
|
+
partial = getattr(summary, "partial", 0)
|
|
500
|
+
offline = getattr(summary, "offline", 0)
|
|
544
501
|
else:
|
|
545
|
-
detected = summary.get(
|
|
546
|
-
not_detected = summary.get(
|
|
547
|
-
partial = summary.get(
|
|
548
|
-
offline = summary.get(
|
|
502
|
+
detected = summary.get("detected", 0)
|
|
503
|
+
not_detected = summary.get("not_detected", 0)
|
|
504
|
+
partial = summary.get("partial", 0)
|
|
505
|
+
offline = summary.get("offline", 0)
|
|
549
506
|
|
|
550
507
|
data = {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
508
|
+
"Detected": detected,
|
|
509
|
+
"Not Detected": not_detected,
|
|
510
|
+
"Partial": partial,
|
|
511
|
+
"Offline": offline,
|
|
555
512
|
}
|
|
556
513
|
|
|
557
514
|
# Filter out zero values
|
|
@@ -565,44 +522,38 @@ class ChartGenerator:
|
|
|
565
522
|
|
|
566
523
|
# Detection-specific colors
|
|
567
524
|
color_map = {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
525
|
+
"Detected": "#28a745", # Green
|
|
526
|
+
"Not Detected": "#dc3545", # Red
|
|
527
|
+
"Partial": "#ffc107", # Yellow
|
|
528
|
+
"Offline": "#6c757d", # Gray
|
|
572
529
|
}
|
|
573
|
-
colors = [color_map.get(k,
|
|
530
|
+
colors = [color_map.get(k, "#17a2b8") for k in labels]
|
|
574
531
|
|
|
575
532
|
config = {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
533
|
+
"type": "doughnut",
|
|
534
|
+
"data": {
|
|
535
|
+
"labels": labels,
|
|
536
|
+
"datasets": [
|
|
537
|
+
{
|
|
538
|
+
"data": values,
|
|
539
|
+
"backgroundColor": colors,
|
|
540
|
+
"borderWidth": 2,
|
|
541
|
+
"borderColor": "#fff",
|
|
542
|
+
}
|
|
543
|
+
],
|
|
585
544
|
},
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
545
|
+
"options": {
|
|
546
|
+
"responsive": True,
|
|
547
|
+
"maintainAspectRatio": True,
|
|
548
|
+
"plugins": {
|
|
549
|
+
"legend": {"position": "right", "labels": {"font": {"size": 14}}},
|
|
550
|
+
"title": {
|
|
551
|
+
"display": True,
|
|
552
|
+
"text": "Detection Coverage",
|
|
553
|
+
"font": {"size": 16, "weight": "bold"},
|
|
595
554
|
},
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
'text': 'Detection Coverage',
|
|
599
|
-
'font': {
|
|
600
|
-
'size': 16,
|
|
601
|
-
'weight': 'bold'
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
555
|
+
},
|
|
556
|
+
},
|
|
606
557
|
}
|
|
607
558
|
|
|
608
559
|
return json.dumps(config)
|
|
@@ -624,7 +575,7 @@ class ChartGenerator:
|
|
|
624
575
|
tested_tactics = [
|
|
625
576
|
(tid, tactic)
|
|
626
577
|
for tid, tactic in tactic_summary.items()
|
|
627
|
-
if hasattr(tactic,
|
|
578
|
+
if hasattr(tactic, "techniques_tested") and tactic.techniques_tested > 0
|
|
628
579
|
]
|
|
629
580
|
|
|
630
581
|
if not tested_tactics:
|
|
@@ -636,64 +587,57 @@ class ChartGenerator:
|
|
|
636
587
|
not_detected_data = []
|
|
637
588
|
|
|
638
589
|
for tid, tactic in tested_tactics:
|
|
639
|
-
labels.append(tactic.tactic_name if hasattr(tactic,
|
|
590
|
+
labels.append(tactic.tactic_name if hasattr(tactic, "tactic_name") else tid)
|
|
640
591
|
detected_data.append(
|
|
641
|
-
tactic.techniques_detected
|
|
592
|
+
tactic.techniques_detected
|
|
593
|
+
if hasattr(tactic, "techniques_detected")
|
|
594
|
+
else 0
|
|
642
595
|
)
|
|
643
596
|
not_detected_data.append(
|
|
644
|
-
tactic.techniques_not_detected
|
|
597
|
+
tactic.techniques_not_detected
|
|
598
|
+
if hasattr(tactic, "techniques_not_detected")
|
|
599
|
+
else 0
|
|
645
600
|
)
|
|
646
601
|
|
|
647
602
|
config = {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
603
|
+
"type": "bar",
|
|
604
|
+
"data": {
|
|
605
|
+
"labels": labels,
|
|
606
|
+
"datasets": [
|
|
652
607
|
{
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
608
|
+
"label": "Detected",
|
|
609
|
+
"data": detected_data,
|
|
610
|
+
"backgroundColor": "#28a745",
|
|
611
|
+
"stack": "stack0",
|
|
657
612
|
},
|
|
658
613
|
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
]
|
|
614
|
+
"label": "Not Detected",
|
|
615
|
+
"data": not_detected_data,
|
|
616
|
+
"backgroundColor": "#dc3545",
|
|
617
|
+
"stack": "stack0",
|
|
618
|
+
},
|
|
619
|
+
],
|
|
665
620
|
},
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
621
|
+
"options": {
|
|
622
|
+
"responsive": True,
|
|
623
|
+
"maintainAspectRatio": True,
|
|
624
|
+
"indexAxis": "y",
|
|
625
|
+
"plugins": {
|
|
626
|
+
"legend": {"display": True, "position": "top"},
|
|
627
|
+
"title": {
|
|
628
|
+
"display": True,
|
|
629
|
+
"text": "Detection Coverage by MITRE ATT&CK Tactic",
|
|
630
|
+
"font": {"size": 16, "weight": "bold"},
|
|
674
631
|
},
|
|
675
|
-
'title': {
|
|
676
|
-
'display': True,
|
|
677
|
-
'text': 'Detection Coverage by MITRE ATT&CK Tactic',
|
|
678
|
-
'font': {
|
|
679
|
-
'size': 16,
|
|
680
|
-
'weight': 'bold'
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
632
|
},
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
'display': True,
|
|
689
|
-
'text': 'Techniques'
|
|
690
|
-
}
|
|
633
|
+
"scales": {
|
|
634
|
+
"x": {
|
|
635
|
+
"stacked": True,
|
|
636
|
+
"title": {"display": True, "text": "Techniques"},
|
|
691
637
|
},
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
}
|
|
638
|
+
"y": {"stacked": True},
|
|
639
|
+
},
|
|
640
|
+
},
|
|
697
641
|
}
|
|
698
642
|
|
|
699
643
|
return json.dumps(config)
|
|
@@ -717,84 +661,75 @@ class ChartGenerator:
|
|
|
717
661
|
categories = {}
|
|
718
662
|
for result in detection_results:
|
|
719
663
|
# Get attack type
|
|
720
|
-
attack_type = getattr(result,
|
|
664
|
+
attack_type = getattr(result, "attack_type", None)
|
|
721
665
|
if not attack_type and isinstance(result, dict):
|
|
722
|
-
attack_type = result.get(
|
|
666
|
+
attack_type = result.get("attack_type")
|
|
723
667
|
if not attack_type:
|
|
724
668
|
continue
|
|
725
669
|
|
|
726
670
|
# Get status
|
|
727
|
-
status = getattr(result,
|
|
671
|
+
status = getattr(result, "status", None)
|
|
728
672
|
if not status and isinstance(result, dict):
|
|
729
|
-
status = result.get(
|
|
673
|
+
status = result.get("status")
|
|
730
674
|
|
|
731
675
|
# Get category from signature
|
|
732
676
|
sig = get_signature(attack_type)
|
|
733
|
-
category = sig.get(
|
|
677
|
+
category = sig.get("category", "unknown")
|
|
734
678
|
|
|
735
679
|
if category not in categories:
|
|
736
|
-
categories[category] = {
|
|
680
|
+
categories[category] = {"detected": 0, "not_detected": 0}
|
|
737
681
|
|
|
738
|
-
if status ==
|
|
739
|
-
categories[category][
|
|
740
|
-
elif status ==
|
|
741
|
-
categories[category][
|
|
682
|
+
if status == "detected":
|
|
683
|
+
categories[category]["detected"] += 1
|
|
684
|
+
elif status == "not_detected":
|
|
685
|
+
categories[category]["not_detected"] += 1
|
|
742
686
|
|
|
743
687
|
if not categories:
|
|
744
688
|
return ""
|
|
745
689
|
|
|
746
690
|
# Sort and build chart data
|
|
747
691
|
labels = list(categories.keys())
|
|
748
|
-
detected_data = [categories[c][
|
|
749
|
-
not_detected_data = [categories[c][
|
|
692
|
+
detected_data = [categories[c]["detected"] for c in labels]
|
|
693
|
+
not_detected_data = [categories[c]["not_detected"] for c in labels]
|
|
750
694
|
|
|
751
695
|
# Capitalize labels
|
|
752
|
-
labels = [label.replace(
|
|
696
|
+
labels = [label.replace("_", " ").title() for label in labels]
|
|
753
697
|
|
|
754
698
|
config = {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
699
|
+
"type": "bar",
|
|
700
|
+
"data": {
|
|
701
|
+
"labels": labels,
|
|
702
|
+
"datasets": [
|
|
759
703
|
{
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
704
|
+
"label": "Detected",
|
|
705
|
+
"data": detected_data,
|
|
706
|
+
"backgroundColor": "#28a745",
|
|
763
707
|
},
|
|
764
708
|
{
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
]
|
|
709
|
+
"label": "Not Detected",
|
|
710
|
+
"data": not_detected_data,
|
|
711
|
+
"backgroundColor": "#dc3545",
|
|
712
|
+
},
|
|
713
|
+
],
|
|
770
714
|
},
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
715
|
+
"options": {
|
|
716
|
+
"responsive": True,
|
|
717
|
+
"maintainAspectRatio": True,
|
|
718
|
+
"plugins": {
|
|
719
|
+
"legend": {"display": True, "position": "top"},
|
|
720
|
+
"title": {
|
|
721
|
+
"display": True,
|
|
722
|
+
"text": "Detection by Attack Category",
|
|
723
|
+
"font": {"size": 16, "weight": "bold"},
|
|
778
724
|
},
|
|
779
|
-
'title': {
|
|
780
|
-
'display': True,
|
|
781
|
-
'text': 'Detection by Attack Category',
|
|
782
|
-
'font': {
|
|
783
|
-
'size': 16,
|
|
784
|
-
'weight': 'bold'
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
725
|
},
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
'display': True,
|
|
793
|
-
'text': 'Attack Count'
|
|
794
|
-
}
|
|
726
|
+
"scales": {
|
|
727
|
+
"y": {
|
|
728
|
+
"beginAtZero": True,
|
|
729
|
+
"title": {"display": True, "text": "Attack Count"},
|
|
795
730
|
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
731
|
+
},
|
|
732
|
+
},
|
|
798
733
|
}
|
|
799
734
|
|
|
800
735
|
return json.dumps(config)
|
|
@@ -812,21 +747,21 @@ class ChartGenerator:
|
|
|
812
747
|
charts = {}
|
|
813
748
|
|
|
814
749
|
# Coverage pie chart
|
|
815
|
-
if hasattr(data,
|
|
750
|
+
if hasattr(data, "summary"):
|
|
816
751
|
coverage_chart = self.detection_coverage_pie_chart(data.summary)
|
|
817
752
|
if coverage_chart:
|
|
818
|
-
charts[
|
|
753
|
+
charts["detection_coverage"] = coverage_chart
|
|
819
754
|
|
|
820
755
|
# Tactic bar chart
|
|
821
|
-
if hasattr(data,
|
|
756
|
+
if hasattr(data, "tactic_summary"):
|
|
822
757
|
tactic_chart = self.detection_by_tactic_chart(data.tactic_summary)
|
|
823
758
|
if tactic_chart:
|
|
824
|
-
charts[
|
|
759
|
+
charts["detection_by_tactic"] = tactic_chart
|
|
825
760
|
|
|
826
761
|
# Category chart
|
|
827
|
-
if hasattr(data,
|
|
762
|
+
if hasattr(data, "detection_results"):
|
|
828
763
|
category_chart = self.detection_by_category_chart(data.detection_results)
|
|
829
764
|
if category_chart:
|
|
830
|
-
charts[
|
|
765
|
+
charts["detection_by_category"] = category_chart
|
|
831
766
|
|
|
832
767
|
return charts
|