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
|
@@ -18,27 +18,28 @@ console = Console()
|
|
|
18
18
|
|
|
19
19
|
# Severity colors
|
|
20
20
|
SEVERITY_COLORS = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
"Critical": "red",
|
|
22
|
+
"High": "yellow",
|
|
23
|
+
"Medium": "white",
|
|
24
|
+
"Low": "bright_black",
|
|
25
|
+
"critical": "red",
|
|
26
|
+
"high": "yellow",
|
|
27
|
+
"medium": "white",
|
|
28
|
+
"low": "bright_black",
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
SEVERITY_ICONS = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
"Critical": "[red bold]C[/red bold]",
|
|
33
|
+
"High": "[yellow bold]H[/yellow bold]",
|
|
34
|
+
"Medium": "[white]M[/white]",
|
|
35
|
+
"Low": "[bright_black]L[/bright_black]",
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
@dataclass
|
|
40
40
|
class SplunkVulnGap:
|
|
41
41
|
"""Represents a vulnerability gap between detection sources."""
|
|
42
|
+
|
|
42
43
|
cve_id: str
|
|
43
44
|
severity: str
|
|
44
45
|
host_ip: str
|
|
@@ -52,6 +53,7 @@ class SplunkVulnGap:
|
|
|
52
53
|
@dataclass
|
|
53
54
|
class SplunkGapResult:
|
|
54
55
|
"""Result of Splunk gap analysis."""
|
|
56
|
+
|
|
55
57
|
splunk_total: int = 0
|
|
56
58
|
scan_total: int = 0
|
|
57
59
|
splunk_only: List[SplunkVulnGap] = field(default_factory=list)
|
|
@@ -60,7 +62,9 @@ class SplunkGapResult:
|
|
|
60
62
|
coverage_pct: float = 0.0
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
def show_splunk_gap_analysis_view(
|
|
65
|
+
def show_splunk_gap_analysis_view(
|
|
66
|
+
engagement_id: int, engagement_name: str = ""
|
|
67
|
+
) -> None:
|
|
64
68
|
"""
|
|
65
69
|
Display Splunk gap analysis view.
|
|
66
70
|
|
|
@@ -76,17 +80,36 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
|
|
|
76
80
|
|
|
77
81
|
# Header
|
|
78
82
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
79
|
-
click.echo(
|
|
83
|
+
click.echo(
|
|
84
|
+
"│"
|
|
85
|
+
+ click.style(
|
|
86
|
+
" SPLUNK GAP ANALYSIS ".center(width - 2), bold=True, fg="cyan"
|
|
87
|
+
)
|
|
88
|
+
+ "│"
|
|
89
|
+
)
|
|
80
90
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
81
91
|
click.echo()
|
|
82
|
-
click.echo(
|
|
92
|
+
click.echo(
|
|
93
|
+
click.style(
|
|
94
|
+
" Compare Splunk (passive/synced) vs Scan (active) findings",
|
|
95
|
+
fg="bright_black",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
83
98
|
click.echo()
|
|
84
99
|
|
|
85
100
|
# Check if Splunk is configured
|
|
86
101
|
config = WazuhConfig.get_config(engagement_id)
|
|
87
102
|
|
|
88
|
-
if
|
|
89
|
-
|
|
103
|
+
if (
|
|
104
|
+
not config
|
|
105
|
+
or config.get("siem_type") != "splunk"
|
|
106
|
+
or not config.get("enabled")
|
|
107
|
+
):
|
|
108
|
+
click.echo(
|
|
109
|
+
click.style(
|
|
110
|
+
" Splunk is not configured for this engagement.", fg="yellow"
|
|
111
|
+
)
|
|
112
|
+
)
|
|
90
113
|
click.echo()
|
|
91
114
|
click.echo(" Configure Splunk in Settings -> SIEM Integration")
|
|
92
115
|
click.echo()
|
|
@@ -95,7 +118,7 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
|
|
|
95
118
|
click.echo(" [q] Back")
|
|
96
119
|
click.echo()
|
|
97
120
|
try:
|
|
98
|
-
if click.getchar().lower() ==
|
|
121
|
+
if click.getchar().lower() == "q":
|
|
99
122
|
return
|
|
100
123
|
except (KeyboardInterrupt, EOFError):
|
|
101
124
|
return
|
|
@@ -104,20 +127,23 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
|
|
|
104
127
|
# Get Splunk client and run analysis
|
|
105
128
|
try:
|
|
106
129
|
from souleyez.integrations.siem.splunk import SplunkSIEMClient
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
|
|
131
|
+
client = SplunkSIEMClient.from_config(
|
|
132
|
+
{
|
|
133
|
+
"api_url": config.get("api_url", ""),
|
|
134
|
+
"username": config.get("username", ""),
|
|
135
|
+
"password": config.get("password", ""),
|
|
136
|
+
"verify_ssl": config.get("verify_ssl", False),
|
|
137
|
+
"default_index": config.get("default_index", "main"),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
114
140
|
|
|
115
141
|
# Run gap analysis
|
|
116
142
|
result = _analyze_gaps(engagement_id, client)
|
|
117
143
|
stats = _get_coverage_stats(result)
|
|
118
144
|
|
|
119
145
|
except Exception as e:
|
|
120
|
-
click.echo(click.style(f" Error connecting to Splunk: {e}", fg=
|
|
146
|
+
click.echo(click.style(f" Error connecting to Splunk: {e}", fg="red"))
|
|
121
147
|
click.echo()
|
|
122
148
|
click.echo(" Make sure the wazuh_vulns index exists and has data.")
|
|
123
149
|
click.echo(" Run: python3 scripts/wazuh_vuln_to_splunk.py --all")
|
|
@@ -142,17 +168,17 @@ def show_splunk_gap_analysis_view(engagement_id: int, engagement_name: str = "")
|
|
|
142
168
|
try:
|
|
143
169
|
choice = input(" Select option: ").strip().lower()
|
|
144
170
|
|
|
145
|
-
if choice ==
|
|
171
|
+
if choice == "q":
|
|
146
172
|
return
|
|
147
|
-
elif choice ==
|
|
173
|
+
elif choice == "1":
|
|
148
174
|
_show_splunk_only(result, width)
|
|
149
|
-
elif choice ==
|
|
175
|
+
elif choice == "2":
|
|
150
176
|
_show_scan_only(result, width)
|
|
151
|
-
elif choice ==
|
|
177
|
+
elif choice == "3":
|
|
152
178
|
_show_confirmed(result, width)
|
|
153
|
-
elif choice ==
|
|
179
|
+
elif choice == "a":
|
|
154
180
|
_show_actionable_gaps(result, width)
|
|
155
|
-
elif choice ==
|
|
181
|
+
elif choice == "r":
|
|
156
182
|
continue # Refresh
|
|
157
183
|
except (KeyboardInterrupt, EOFError):
|
|
158
184
|
return
|
|
@@ -179,13 +205,13 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
|
|
|
179
205
|
# Group by CVE ID to find matches
|
|
180
206
|
splunk_by_cve: Dict[str, List[Dict]] = {}
|
|
181
207
|
for v in splunk_vulns:
|
|
182
|
-
cve = v.get(
|
|
208
|
+
cve = v.get("cve_id")
|
|
183
209
|
if cve:
|
|
184
210
|
splunk_by_cve.setdefault(cve, []).append(v)
|
|
185
211
|
|
|
186
212
|
scan_by_cve: Dict[str, List[Dict]] = {}
|
|
187
213
|
for f in scan_findings:
|
|
188
|
-
cve = f.get(
|
|
214
|
+
cve = f.get("cve_id")
|
|
189
215
|
if cve:
|
|
190
216
|
scan_by_cve.setdefault(cve, []).append(f)
|
|
191
217
|
|
|
@@ -202,16 +228,18 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
|
|
|
202
228
|
splunk_v = splunk_entries[0]
|
|
203
229
|
scan_f = scan_entries[0]
|
|
204
230
|
|
|
205
|
-
result.confirmed.append(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
231
|
+
result.confirmed.append(
|
|
232
|
+
SplunkVulnGap(
|
|
233
|
+
cve_id=cve_id,
|
|
234
|
+
severity=splunk_v.get("severity", "Medium"),
|
|
235
|
+
host_ip=f"{splunk_v.get('host_ip', '-')} / {scan_f.get('host_ip', '-')}",
|
|
236
|
+
source="both",
|
|
237
|
+
splunk_details=splunk_v,
|
|
238
|
+
scan_details=scan_f,
|
|
239
|
+
recommendation=f"Confirmed by both sources ({len(splunk_entries)} Splunk, {len(scan_entries)} scan)",
|
|
240
|
+
confidence="high",
|
|
241
|
+
)
|
|
242
|
+
)
|
|
215
243
|
|
|
216
244
|
# Find Splunk-only CVEs (scan didn't find this CVE anywhere)
|
|
217
245
|
splunk_only_cves = all_splunk_cves - all_scan_cves
|
|
@@ -219,15 +247,17 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
|
|
|
219
247
|
splunk_entries = splunk_by_cve[cve_id]
|
|
220
248
|
splunk_v = splunk_entries[0]
|
|
221
249
|
|
|
222
|
-
result.splunk_only.append(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
250
|
+
result.splunk_only.append(
|
|
251
|
+
SplunkVulnGap(
|
|
252
|
+
cve_id=cve_id,
|
|
253
|
+
severity=splunk_v.get("severity", "Medium"),
|
|
254
|
+
host_ip=splunk_v.get("host_ip", "-"),
|
|
255
|
+
source="splunk",
|
|
256
|
+
splunk_details=splunk_v,
|
|
257
|
+
recommendation="Package-level vulnerability - network scans typically can't detect",
|
|
258
|
+
confidence="medium",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
231
261
|
|
|
232
262
|
# Find Scan-only CVEs (Splunk/Wazuh didn't detect this CVE)
|
|
233
263
|
scan_only_cves = all_scan_cves - all_splunk_cves
|
|
@@ -235,15 +265,17 @@ def _analyze_gaps(engagement_id: int, client) -> SplunkGapResult:
|
|
|
235
265
|
scan_entries = scan_by_cve[cve_id]
|
|
236
266
|
scan_f = scan_entries[0]
|
|
237
267
|
|
|
238
|
-
result.scan_only.append(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
268
|
+
result.scan_only.append(
|
|
269
|
+
SplunkVulnGap(
|
|
270
|
+
cve_id=cve_id,
|
|
271
|
+
severity=scan_f.get("severity", "Medium"),
|
|
272
|
+
host_ip=scan_f.get("host_ip", "-"),
|
|
273
|
+
source="scan",
|
|
274
|
+
scan_details=scan_f,
|
|
275
|
+
recommendation="Network-exposed CVE - Wazuh agent may not be installed on this host",
|
|
276
|
+
confidence="medium",
|
|
277
|
+
)
|
|
278
|
+
)
|
|
247
279
|
|
|
248
280
|
# Calculate coverage based on unique CVEs
|
|
249
281
|
total_unique_cves = len(all_splunk_cves | all_scan_cves)
|
|
@@ -262,41 +294,45 @@ def _get_splunk_cves(client) -> List[Dict[str, Any]]:
|
|
|
262
294
|
result = []
|
|
263
295
|
for v in vulns:
|
|
264
296
|
# Map agent_name to host_ip if available
|
|
265
|
-
host_ip = v.get(
|
|
297
|
+
host_ip = v.get("agent_ip") or v.get("agent_name", "-")
|
|
266
298
|
# Normalize CVE ID to uppercase for matching
|
|
267
|
-
cve_id = v.get(
|
|
299
|
+
cve_id = v.get("cve_id", "")
|
|
268
300
|
if cve_id:
|
|
269
301
|
cve_id = cve_id.upper()
|
|
270
|
-
result.append(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
302
|
+
result.append(
|
|
303
|
+
{
|
|
304
|
+
"cve_id": cve_id,
|
|
305
|
+
"severity": v.get("severity", "Medium"),
|
|
306
|
+
"host_ip": host_ip,
|
|
307
|
+
"package_name": v.get("package_name"),
|
|
308
|
+
"package_version": v.get("package_version"),
|
|
309
|
+
"cvss_score": v.get("cvss_score"),
|
|
310
|
+
"agent_name": v.get("agent_name"),
|
|
311
|
+
"os_name": v.get("os_name"),
|
|
312
|
+
}
|
|
313
|
+
)
|
|
280
314
|
return result
|
|
281
315
|
except Exception as e:
|
|
282
|
-
click.echo(
|
|
316
|
+
click.echo(
|
|
317
|
+
click.style(f" Warning: Error fetching Splunk vulns: {e}", fg="yellow")
|
|
318
|
+
)
|
|
283
319
|
return []
|
|
284
320
|
|
|
285
321
|
|
|
286
322
|
def _get_scan_cves(engagement_id: int) -> List[Dict[str, Any]]:
|
|
287
323
|
"""Get CVE findings from active scans."""
|
|
288
324
|
import re
|
|
325
|
+
|
|
289
326
|
db = get_db()
|
|
290
|
-
cve_pattern = re.compile(r
|
|
327
|
+
cve_pattern = re.compile(r"CVE-\d{4}-\d+", re.IGNORECASE)
|
|
291
328
|
result = []
|
|
292
329
|
|
|
293
330
|
# Get hosts for this engagement
|
|
294
331
|
hosts = db.execute(
|
|
295
|
-
"SELECT id, ip_address FROM hosts WHERE engagement_id = ?",
|
|
296
|
-
(engagement_id,)
|
|
332
|
+
"SELECT id, ip_address FROM hosts WHERE engagement_id = ?", (engagement_id,)
|
|
297
333
|
)
|
|
298
334
|
|
|
299
|
-
host_map = {h[
|
|
335
|
+
host_map = {h["id"]: h["ip_address"] for h in hosts}
|
|
300
336
|
|
|
301
337
|
if not host_map:
|
|
302
338
|
return []
|
|
@@ -304,58 +340,65 @@ def _get_scan_cves(engagement_id: int) -> List[Dict[str, Any]]:
|
|
|
304
340
|
# 1. Get nuclei_findings with CVE IDs (they have cve_id column)
|
|
305
341
|
nuclei_findings = db.execute(
|
|
306
342
|
"SELECT * FROM nuclei_findings WHERE engagement_id = ? AND cve_id IS NOT NULL",
|
|
307
|
-
(engagement_id,)
|
|
343
|
+
(engagement_id,),
|
|
308
344
|
)
|
|
309
345
|
|
|
310
346
|
for f in nuclei_findings:
|
|
311
|
-
cve_id = f.get(
|
|
347
|
+
cve_id = f.get("cve_id")
|
|
312
348
|
if cve_id:
|
|
313
349
|
# Extract host IP from matched_at if available
|
|
314
|
-
matched_at = f.get(
|
|
350
|
+
matched_at = f.get("matched_at", "")
|
|
315
351
|
host_ip = None
|
|
316
352
|
# Try to extract IP from matched_at URL
|
|
317
|
-
ip_match = re.search(r
|
|
353
|
+
ip_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", matched_at)
|
|
318
354
|
if ip_match:
|
|
319
355
|
host_ip = ip_match.group(1)
|
|
320
356
|
|
|
321
|
-
result.append(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
357
|
+
result.append(
|
|
358
|
+
{
|
|
359
|
+
"cve_id": cve_id.upper(),
|
|
360
|
+
"host_ip": host_ip or "-",
|
|
361
|
+
"severity": (f.get("severity") or "Medium").title(),
|
|
362
|
+
"title": f.get("name"),
|
|
363
|
+
"tool": "nuclei",
|
|
364
|
+
"port": None,
|
|
365
|
+
"service": None,
|
|
366
|
+
}
|
|
367
|
+
)
|
|
330
368
|
|
|
331
369
|
# 2. Get regular findings and extract CVEs from title/refs
|
|
332
370
|
host_ids = list(host_map.keys())
|
|
333
|
-
placeholders =
|
|
334
|
-
findings = db.execute(
|
|
371
|
+
placeholders = ",".join("?" * len(host_ids))
|
|
372
|
+
findings = db.execute(
|
|
373
|
+
f"""
|
|
335
374
|
SELECT f.*, h.ip_address
|
|
336
375
|
FROM findings f
|
|
337
376
|
JOIN hosts h ON f.host_id = h.id
|
|
338
377
|
WHERE f.host_id IN ({placeholders})
|
|
339
378
|
AND (f.title LIKE '%CVE-%' OR f.refs LIKE '%CVE-%')
|
|
340
|
-
""",
|
|
379
|
+
""",
|
|
380
|
+
tuple(host_ids),
|
|
381
|
+
)
|
|
341
382
|
|
|
342
383
|
for f in findings:
|
|
343
384
|
# Try to extract CVE from title
|
|
344
|
-
title = f.get(
|
|
345
|
-
refs = f.get(
|
|
346
|
-
combined = title +
|
|
385
|
+
title = f.get("title", "") or ""
|
|
386
|
+
refs = f.get("refs", "") or ""
|
|
387
|
+
combined = title + " " + refs
|
|
347
388
|
|
|
348
389
|
matches = cve_pattern.findall(combined)
|
|
349
390
|
for cve_id in matches:
|
|
350
|
-
result.append(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
391
|
+
result.append(
|
|
392
|
+
{
|
|
393
|
+
"cve_id": cve_id.upper(),
|
|
394
|
+
"host_ip": f.get("ip_address"),
|
|
395
|
+
"severity": (f.get("severity") or "Medium").title(),
|
|
396
|
+
"title": f.get("title"),
|
|
397
|
+
"tool": f.get("tool"),
|
|
398
|
+
"port": f.get("port"),
|
|
399
|
+
"service": None,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
359
402
|
|
|
360
403
|
return result
|
|
361
404
|
|
|
@@ -363,26 +406,28 @@ def _get_scan_cves(engagement_id: int) -> List[Dict[str, Any]]:
|
|
|
363
406
|
def _get_coverage_stats(result: SplunkGapResult) -> Dict[str, Any]:
|
|
364
407
|
"""Calculate coverage statistics."""
|
|
365
408
|
stats = {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
'splunk_only': {},
|
|
369
|
-
'scan_only': {},
|
|
370
|
-
'confirmed': {}
|
|
371
|
-
}
|
|
409
|
+
"coverage_pct": result.coverage_pct,
|
|
410
|
+
"by_severity": {"splunk_only": {}, "scan_only": {}, "confirmed": {}},
|
|
372
411
|
}
|
|
373
412
|
|
|
374
413
|
# Count by severity
|
|
375
414
|
for gap in result.splunk_only:
|
|
376
415
|
sev = gap.severity
|
|
377
|
-
stats[
|
|
416
|
+
stats["by_severity"]["splunk_only"][sev] = (
|
|
417
|
+
stats["by_severity"]["splunk_only"].get(sev, 0) + 1
|
|
418
|
+
)
|
|
378
419
|
|
|
379
420
|
for gap in result.scan_only:
|
|
380
421
|
sev = gap.severity
|
|
381
|
-
stats[
|
|
422
|
+
stats["by_severity"]["scan_only"][sev] = (
|
|
423
|
+
stats["by_severity"]["scan_only"].get(sev, 0) + 1
|
|
424
|
+
)
|
|
382
425
|
|
|
383
426
|
for gap in result.confirmed:
|
|
384
427
|
sev = gap.severity
|
|
385
|
-
stats[
|
|
428
|
+
stats["by_severity"]["confirmed"][sev] = (
|
|
429
|
+
stats["by_severity"]["confirmed"].get(sev, 0) + 1
|
|
430
|
+
)
|
|
386
431
|
|
|
387
432
|
return stats
|
|
388
433
|
|
|
@@ -394,48 +439,73 @@ def _render_summary_dashboard(result: SplunkGapResult, stats: Dict, width: int)
|
|
|
394
439
|
confirmed = len(result.confirmed)
|
|
395
440
|
splunk_only = len(result.splunk_only)
|
|
396
441
|
scan_only = len(result.scan_only)
|
|
397
|
-
coverage = stats.get(
|
|
442
|
+
coverage = stats.get("coverage_pct", 0)
|
|
398
443
|
|
|
399
444
|
# Coverage color
|
|
400
445
|
if coverage >= 80:
|
|
401
|
-
coverage_color =
|
|
446
|
+
coverage_color = "green"
|
|
402
447
|
elif coverage >= 50:
|
|
403
|
-
coverage_color =
|
|
448
|
+
coverage_color = "yellow"
|
|
404
449
|
else:
|
|
405
|
-
coverage_color =
|
|
450
|
+
coverage_color = "red"
|
|
406
451
|
|
|
407
452
|
# Detection Sources
|
|
408
453
|
click.echo(click.style(" DETECTION SOURCES", bold=True))
|
|
409
|
-
click.echo(
|
|
410
|
-
|
|
454
|
+
click.echo(
|
|
455
|
+
f" Splunk (passive): {click.style(str(splunk_total), fg='cyan', bold=True)} CVEs"
|
|
456
|
+
)
|
|
457
|
+
click.echo(
|
|
458
|
+
f" Scans (active): {click.style(str(scan_total), fg='cyan', bold=True)} CVEs"
|
|
459
|
+
)
|
|
411
460
|
click.echo()
|
|
412
461
|
|
|
413
462
|
# Analysis Results
|
|
414
463
|
click.echo(click.style(" ANALYSIS RESULTS", bold=True))
|
|
415
|
-
click.echo(
|
|
416
|
-
|
|
417
|
-
|
|
464
|
+
click.echo(
|
|
465
|
+
f" ✓ Confirmed (both): {click.style(str(confirmed), fg='green', bold=True)}"
|
|
466
|
+
)
|
|
467
|
+
click.echo(
|
|
468
|
+
f" ⚠ Splunk Only: {click.style(str(splunk_only), fg='yellow', bold=True)} <- Scans missed these"
|
|
469
|
+
)
|
|
470
|
+
click.echo(
|
|
471
|
+
f" ◐ Scan Only: {click.style(str(scan_only), fg='blue', bold=True)} <- Splunk missed these"
|
|
472
|
+
)
|
|
418
473
|
click.echo()
|
|
419
474
|
|
|
420
475
|
# Coverage
|
|
421
|
-
click.echo(
|
|
422
|
-
|
|
476
|
+
click.echo(
|
|
477
|
+
f" Coverage: "
|
|
478
|
+
+ click.style(f"{coverage:.1f}%", fg=coverage_color, bold=True)
|
|
479
|
+
+ " of Splunk vulns confirmed by scans"
|
|
480
|
+
)
|
|
423
481
|
click.echo()
|
|
424
482
|
|
|
425
483
|
# Add explanation note for low coverage
|
|
426
484
|
if coverage < 20:
|
|
427
|
-
click.echo(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
485
|
+
click.echo(
|
|
486
|
+
click.style(" NOTE: ", fg="cyan", bold=True)
|
|
487
|
+
+ click.style(
|
|
488
|
+
"Low overlap is expected. Splunk/Wazuh detects ", fg="bright_black"
|
|
489
|
+
)
|
|
490
|
+
+ click.style("package-level", fg="cyan")
|
|
491
|
+
+ click.style(" vulns", fg="bright_black")
|
|
492
|
+
)
|
|
493
|
+
click.echo(
|
|
494
|
+
click.style(
|
|
495
|
+
" (installed software), while scans find ", fg="bright_black"
|
|
496
|
+
)
|
|
497
|
+
+ click.style("network-exposed", fg="cyan")
|
|
498
|
+
+ click.style(" vulns (services).", fg="bright_black")
|
|
499
|
+
)
|
|
434
500
|
click.echo()
|
|
435
501
|
|
|
436
502
|
# Severity breakdown
|
|
437
|
-
sev_breakdown = stats.get(
|
|
438
|
-
if
|
|
503
|
+
sev_breakdown = stats.get("by_severity", {})
|
|
504
|
+
if (
|
|
505
|
+
sev_breakdown.get("splunk_only")
|
|
506
|
+
or sev_breakdown.get("scan_only")
|
|
507
|
+
or sev_breakdown.get("confirmed")
|
|
508
|
+
):
|
|
439
509
|
_render_severity_breakdown(sev_breakdown, width)
|
|
440
510
|
|
|
441
511
|
|
|
@@ -446,7 +516,7 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
|
|
|
446
516
|
header_style="bold",
|
|
447
517
|
box=box.SIMPLE,
|
|
448
518
|
padding=(0, 2),
|
|
449
|
-
expand=False
|
|
519
|
+
expand=False,
|
|
450
520
|
)
|
|
451
521
|
|
|
452
522
|
table.add_column("Severity", width=14)
|
|
@@ -454,17 +524,17 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
|
|
|
454
524
|
table.add_column("Scan Only", width=12, justify="right")
|
|
455
525
|
table.add_column("Confirmed", width=12, justify="right")
|
|
456
526
|
|
|
457
|
-
for sev in [
|
|
458
|
-
color = SEVERITY_COLORS.get(sev,
|
|
459
|
-
splunk_only = breakdown.get(
|
|
460
|
-
scan_only = breakdown.get(
|
|
461
|
-
confirmed = breakdown.get(
|
|
527
|
+
for sev in ["Critical", "High", "Medium", "Low"]:
|
|
528
|
+
color = SEVERITY_COLORS.get(sev, "white")
|
|
529
|
+
splunk_only = breakdown.get("splunk_only", {}).get(sev, 0)
|
|
530
|
+
scan_only = breakdown.get("scan_only", {}).get(sev, 0)
|
|
531
|
+
confirmed = breakdown.get("confirmed", {}).get(sev, 0)
|
|
462
532
|
|
|
463
533
|
table.add_row(
|
|
464
534
|
f"[{color}]{sev}[/{color}]",
|
|
465
535
|
str(splunk_only) if splunk_only else "-",
|
|
466
536
|
str(scan_only) if scan_only else "-",
|
|
467
|
-
str(confirmed) if confirmed else "-"
|
|
537
|
+
str(confirmed) if confirmed else "-",
|
|
468
538
|
)
|
|
469
539
|
|
|
470
540
|
console.print(" ", table)
|
|
@@ -483,16 +553,31 @@ def _show_splunk_only(result: SplunkGapResult, width: int) -> None:
|
|
|
483
553
|
width = DesignSystem.get_terminal_width()
|
|
484
554
|
|
|
485
555
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
486
|
-
click.echo(
|
|
556
|
+
click.echo(
|
|
557
|
+
"│"
|
|
558
|
+
+ click.style(
|
|
559
|
+
" SPLUNK ONLY - SCANS MISSED ".center(width - 2), bold=True, fg="yellow"
|
|
560
|
+
)
|
|
561
|
+
+ "│"
|
|
562
|
+
)
|
|
487
563
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
488
564
|
click.echo()
|
|
489
565
|
|
|
490
566
|
click.echo(f" {len(gaps)} CVEs detected by Splunk but NOT by active scans.")
|
|
491
|
-
click.echo(
|
|
567
|
+
click.echo(
|
|
568
|
+
click.style(
|
|
569
|
+
" These may be local/package vulnerabilities not exposed to network scanning.",
|
|
570
|
+
fg="bright_black",
|
|
571
|
+
)
|
|
572
|
+
)
|
|
492
573
|
click.echo()
|
|
493
574
|
|
|
494
575
|
if not gaps:
|
|
495
|
-
click.echo(
|
|
576
|
+
click.echo(
|
|
577
|
+
click.style(
|
|
578
|
+
" V No gaps - all Splunk vulns confirmed by scans!", fg="green"
|
|
579
|
+
)
|
|
580
|
+
)
|
|
496
581
|
click.echo()
|
|
497
582
|
click.pause(" Press any key to return...")
|
|
498
583
|
return
|
|
@@ -508,7 +593,13 @@ def _show_splunk_only(result: SplunkGapResult, width: int) -> None:
|
|
|
508
593
|
end_idx = min(start_idx + page_size, len(gaps))
|
|
509
594
|
page_gaps = gaps[start_idx:end_idx]
|
|
510
595
|
|
|
511
|
-
_render_gaps_table(
|
|
596
|
+
_render_gaps_table(
|
|
597
|
+
page_gaps,
|
|
598
|
+
show_package=True,
|
|
599
|
+
page=page,
|
|
600
|
+
page_size=page_size,
|
|
601
|
+
view_all=view_all,
|
|
602
|
+
)
|
|
512
603
|
|
|
513
604
|
# Pagination info
|
|
514
605
|
if view_all:
|
|
@@ -534,17 +625,17 @@ def _show_splunk_only(result: SplunkGapResult, width: int) -> None:
|
|
|
534
625
|
try:
|
|
535
626
|
choice = input(" Select option: ").strip().lower()
|
|
536
627
|
|
|
537
|
-
if choice ==
|
|
628
|
+
if choice == "q":
|
|
538
629
|
return
|
|
539
|
-
elif choice ==
|
|
630
|
+
elif choice == "i":
|
|
540
631
|
_interactive_gaps_mode(gaps, "SPLUNK ONLY GAPS", show_package=True)
|
|
541
|
-
elif choice ==
|
|
632
|
+
elif choice == "t":
|
|
542
633
|
view_all = not view_all
|
|
543
634
|
if not view_all:
|
|
544
635
|
page = 0
|
|
545
|
-
elif choice ==
|
|
636
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
546
637
|
page += 1
|
|
547
|
-
elif choice ==
|
|
638
|
+
elif choice == "p" and not view_all and page > 0:
|
|
548
639
|
page -= 1
|
|
549
640
|
elif choice.isdigit():
|
|
550
641
|
idx = int(choice) - 1
|
|
@@ -566,16 +657,31 @@ def _show_scan_only(result: SplunkGapResult, width: int) -> None:
|
|
|
566
657
|
width = DesignSystem.get_terminal_width()
|
|
567
658
|
|
|
568
659
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
569
|
-
click.echo(
|
|
660
|
+
click.echo(
|
|
661
|
+
"│"
|
|
662
|
+
+ click.style(
|
|
663
|
+
" SCAN ONLY - SPLUNK MISSED ".center(width - 2), bold=True, fg="blue"
|
|
664
|
+
)
|
|
665
|
+
+ "│"
|
|
666
|
+
)
|
|
570
667
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
571
668
|
click.echo()
|
|
572
669
|
|
|
573
670
|
click.echo(f" {len(gaps)} CVEs detected by active scans but NOT by Splunk.")
|
|
574
|
-
click.echo(
|
|
671
|
+
click.echo(
|
|
672
|
+
click.style(
|
|
673
|
+
" This may indicate: missing Wazuh agent, detection rule gap, or network-only vuln.",
|
|
674
|
+
fg="bright_black",
|
|
675
|
+
)
|
|
676
|
+
)
|
|
575
677
|
click.echo()
|
|
576
678
|
|
|
577
679
|
if not gaps:
|
|
578
|
-
click.echo(
|
|
680
|
+
click.echo(
|
|
681
|
+
click.style(
|
|
682
|
+
" V No gaps - Splunk detected all scan findings!", fg="green"
|
|
683
|
+
)
|
|
684
|
+
)
|
|
579
685
|
click.echo()
|
|
580
686
|
click.pause(" Press any key to return...")
|
|
581
687
|
return
|
|
@@ -591,7 +697,9 @@ def _show_scan_only(result: SplunkGapResult, width: int) -> None:
|
|
|
591
697
|
end_idx = min(start_idx + page_size, len(gaps))
|
|
592
698
|
page_gaps = gaps[start_idx:end_idx]
|
|
593
699
|
|
|
594
|
-
_render_gaps_table(
|
|
700
|
+
_render_gaps_table(
|
|
701
|
+
page_gaps, show_tool=True, page=page, page_size=page_size, view_all=view_all
|
|
702
|
+
)
|
|
595
703
|
|
|
596
704
|
# Pagination info
|
|
597
705
|
if view_all:
|
|
@@ -617,17 +725,17 @@ def _show_scan_only(result: SplunkGapResult, width: int) -> None:
|
|
|
617
725
|
try:
|
|
618
726
|
choice = input(" Select option: ").strip().lower()
|
|
619
727
|
|
|
620
|
-
if choice ==
|
|
728
|
+
if choice == "q":
|
|
621
729
|
return
|
|
622
|
-
elif choice ==
|
|
730
|
+
elif choice == "i":
|
|
623
731
|
_interactive_gaps_mode(gaps, "SCAN ONLY GAPS", show_tool=True)
|
|
624
|
-
elif choice ==
|
|
732
|
+
elif choice == "t":
|
|
625
733
|
view_all = not view_all
|
|
626
734
|
if not view_all:
|
|
627
735
|
page = 0
|
|
628
|
-
elif choice ==
|
|
736
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
629
737
|
page += 1
|
|
630
|
-
elif choice ==
|
|
738
|
+
elif choice == "p" and not view_all and page > 0:
|
|
631
739
|
page -= 1
|
|
632
740
|
elif choice.isdigit():
|
|
633
741
|
idx = int(choice) - 1
|
|
@@ -649,16 +757,31 @@ def _show_confirmed(result: SplunkGapResult, width: int) -> None:
|
|
|
649
757
|
width = DesignSystem.get_terminal_width()
|
|
650
758
|
|
|
651
759
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
652
|
-
click.echo(
|
|
760
|
+
click.echo(
|
|
761
|
+
"│"
|
|
762
|
+
+ click.style(
|
|
763
|
+
" CONFIRMED - BOTH SOURCES ".center(width - 2), bold=True, fg="green"
|
|
764
|
+
)
|
|
765
|
+
+ "│"
|
|
766
|
+
)
|
|
653
767
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
654
768
|
click.echo()
|
|
655
769
|
|
|
656
770
|
click.echo(f" {len(gaps)} CVEs detected by BOTH Splunk and active scans.")
|
|
657
|
-
click.echo(
|
|
771
|
+
click.echo(
|
|
772
|
+
click.style(
|
|
773
|
+
" High confidence - prioritize these for exploitation.",
|
|
774
|
+
fg="bright_black",
|
|
775
|
+
)
|
|
776
|
+
)
|
|
658
777
|
click.echo()
|
|
659
778
|
|
|
660
779
|
if not gaps:
|
|
661
|
-
click.echo(
|
|
780
|
+
click.echo(
|
|
781
|
+
click.style(
|
|
782
|
+
" No confirmed matches between Splunk and scans.", fg="yellow"
|
|
783
|
+
)
|
|
784
|
+
)
|
|
662
785
|
click.echo()
|
|
663
786
|
click.pause(" Press any key to return...")
|
|
664
787
|
return
|
|
@@ -674,7 +797,9 @@ def _show_confirmed(result: SplunkGapResult, width: int) -> None:
|
|
|
674
797
|
end_idx = min(start_idx + page_size, len(gaps))
|
|
675
798
|
page_gaps = gaps[start_idx:end_idx]
|
|
676
799
|
|
|
677
|
-
_render_gaps_table(
|
|
800
|
+
_render_gaps_table(
|
|
801
|
+
page_gaps, show_both=True, page=page, page_size=page_size, view_all=view_all
|
|
802
|
+
)
|
|
678
803
|
|
|
679
804
|
# Pagination info
|
|
680
805
|
if view_all:
|
|
@@ -700,17 +825,17 @@ def _show_confirmed(result: SplunkGapResult, width: int) -> None:
|
|
|
700
825
|
try:
|
|
701
826
|
choice = input(" Select option: ").strip().lower()
|
|
702
827
|
|
|
703
|
-
if choice ==
|
|
828
|
+
if choice == "q":
|
|
704
829
|
return
|
|
705
|
-
elif choice ==
|
|
830
|
+
elif choice == "i":
|
|
706
831
|
_interactive_gaps_mode(gaps, "CONFIRMED GAPS", show_both=True)
|
|
707
|
-
elif choice ==
|
|
832
|
+
elif choice == "t":
|
|
708
833
|
view_all = not view_all
|
|
709
834
|
if not view_all:
|
|
710
835
|
page = 0
|
|
711
|
-
elif choice ==
|
|
836
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
712
837
|
page += 1
|
|
713
|
-
elif choice ==
|
|
838
|
+
elif choice == "p" and not view_all and page > 0:
|
|
714
839
|
page -= 1
|
|
715
840
|
elif choice.isdigit():
|
|
716
841
|
idx = int(choice) - 1
|
|
@@ -729,35 +854,53 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
|
|
|
729
854
|
# Get high-priority gaps (Critical/High severity from Splunk that scans missed)
|
|
730
855
|
gaps = []
|
|
731
856
|
for g in result.splunk_only:
|
|
732
|
-
if g.severity in (
|
|
733
|
-
gaps.append(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
857
|
+
if g.severity in ("Critical", "High"):
|
|
858
|
+
gaps.append(
|
|
859
|
+
{
|
|
860
|
+
"cve_id": g.cve_id,
|
|
861
|
+
"severity": g.severity,
|
|
862
|
+
"host_ip": g.host_ip,
|
|
863
|
+
"package": (
|
|
864
|
+
g.splunk_details.get("package_name", "-")
|
|
865
|
+
if g.splunk_details
|
|
866
|
+
else "-"
|
|
867
|
+
),
|
|
868
|
+
"package_version": (
|
|
869
|
+
g.splunk_details.get("package_version", "-")
|
|
870
|
+
if g.splunk_details
|
|
871
|
+
else "-"
|
|
872
|
+
),
|
|
873
|
+
"recommendation": g.recommendation,
|
|
874
|
+
"priority": "high" if g.severity == "Critical" else "medium",
|
|
875
|
+
}
|
|
876
|
+
)
|
|
742
877
|
|
|
743
878
|
# Sort by severity
|
|
744
|
-
severity_order = {
|
|
745
|
-
gaps.sort(key=lambda x: severity_order.get(x[
|
|
879
|
+
severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
|
|
880
|
+
gaps.sort(key=lambda x: severity_order.get(x["severity"], 4))
|
|
746
881
|
|
|
747
882
|
while True:
|
|
748
883
|
DesignSystem.clear_screen()
|
|
749
884
|
width = DesignSystem.get_terminal_width()
|
|
750
885
|
|
|
751
886
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
752
|
-
click.echo(
|
|
887
|
+
click.echo(
|
|
888
|
+
"│"
|
|
889
|
+
+ click.style(" ACTIONABLE GAPS ".center(width - 2), bold=True, fg="yellow")
|
|
890
|
+
+ "│"
|
|
891
|
+
)
|
|
753
892
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
754
893
|
click.echo()
|
|
755
894
|
|
|
756
|
-
click.echo(
|
|
895
|
+
click.echo(
|
|
896
|
+
f" {len(gaps)} prioritized vulnerabilities from Splunk that need targeted scanning."
|
|
897
|
+
)
|
|
757
898
|
click.echo()
|
|
758
899
|
|
|
759
900
|
if not gaps:
|
|
760
|
-
click.echo(
|
|
901
|
+
click.echo(
|
|
902
|
+
click.style(" V No actionable gaps - great scan coverage!", fg="green")
|
|
903
|
+
)
|
|
761
904
|
click.echo()
|
|
762
905
|
click.pause(" Press any key to return...")
|
|
763
906
|
return
|
|
@@ -791,21 +934,23 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
|
|
|
791
934
|
else:
|
|
792
935
|
display_idx = (page * page_size) + idx + 1
|
|
793
936
|
|
|
794
|
-
priority = gap.get(
|
|
795
|
-
priority_display =
|
|
937
|
+
priority = gap.get("priority", "medium")
|
|
938
|
+
priority_display = (
|
|
939
|
+
"[red]HIGH[/red]" if priority == "high" else "[yellow]MEDIUM[/yellow]"
|
|
940
|
+
)
|
|
796
941
|
|
|
797
|
-
severity = gap.get(
|
|
798
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
942
|
+
severity = gap.get("severity", "Medium")
|
|
943
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
799
944
|
|
|
800
945
|
table.add_row(
|
|
801
946
|
"○",
|
|
802
947
|
str(display_idx),
|
|
803
948
|
priority_display,
|
|
804
|
-
gap.get(
|
|
949
|
+
gap.get("cve_id", "-"),
|
|
805
950
|
f"[{sev_color}]{severity}[/{sev_color}]",
|
|
806
|
-
gap.get(
|
|
807
|
-
gap.get(
|
|
808
|
-
gap.get(
|
|
951
|
+
gap.get("host_ip", "-"),
|
|
952
|
+
gap.get("package", "-")[:19],
|
|
953
|
+
gap.get("recommendation", "-")[:34],
|
|
809
954
|
)
|
|
810
955
|
|
|
811
956
|
console.print(table)
|
|
@@ -834,17 +979,17 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
|
|
|
834
979
|
try:
|
|
835
980
|
choice = input(" Select option: ").strip().lower()
|
|
836
981
|
|
|
837
|
-
if choice ==
|
|
982
|
+
if choice == "q":
|
|
838
983
|
return
|
|
839
|
-
elif choice ==
|
|
984
|
+
elif choice == "i":
|
|
840
985
|
_interactive_actionable_gaps_mode(gaps)
|
|
841
|
-
elif choice ==
|
|
986
|
+
elif choice == "t":
|
|
842
987
|
view_all = not view_all
|
|
843
988
|
if not view_all:
|
|
844
989
|
page = 0
|
|
845
|
-
elif choice ==
|
|
990
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
846
991
|
page += 1
|
|
847
|
-
elif choice ==
|
|
992
|
+
elif choice == "p" and not view_all and page > 0:
|
|
848
993
|
page -= 1
|
|
849
994
|
elif choice.isdigit():
|
|
850
995
|
idx = int(choice) - 1
|
|
@@ -855,8 +1000,13 @@ def _show_actionable_gaps(result: SplunkGapResult, width: int) -> None:
|
|
|
855
1000
|
|
|
856
1001
|
|
|
857
1002
|
def _render_gaps_table(
|
|
858
|
-
gaps: List,
|
|
859
|
-
|
|
1003
|
+
gaps: List,
|
|
1004
|
+
show_package: bool = False,
|
|
1005
|
+
show_tool: bool = False,
|
|
1006
|
+
show_both: bool = False,
|
|
1007
|
+
page: int = 0,
|
|
1008
|
+
page_size: int = 20,
|
|
1009
|
+
view_all: bool = False,
|
|
860
1010
|
) -> None:
|
|
861
1011
|
"""Render gaps table with pagination support."""
|
|
862
1012
|
table = DesignSystem.create_table()
|
|
@@ -885,27 +1035,35 @@ def _render_gaps_table(
|
|
|
885
1035
|
display_idx = (page * page_size) + idx + 1
|
|
886
1036
|
|
|
887
1037
|
severity = gap.severity
|
|
888
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
1038
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
889
1039
|
|
|
890
1040
|
row = [
|
|
891
1041
|
"○",
|
|
892
1042
|
str(display_idx),
|
|
893
1043
|
gap.cve_id,
|
|
894
1044
|
f"[{sev_color}]{severity}[/{sev_color}]",
|
|
895
|
-
gap.host_ip or "-"
|
|
1045
|
+
gap.host_ip or "-",
|
|
896
1046
|
]
|
|
897
1047
|
|
|
898
1048
|
if show_package:
|
|
899
|
-
package =
|
|
1049
|
+
package = (
|
|
1050
|
+
gap.splunk_details.get("package_name", "-")
|
|
1051
|
+
if gap.splunk_details
|
|
1052
|
+
else "-"
|
|
1053
|
+
)
|
|
900
1054
|
row.append(package[:24])
|
|
901
1055
|
row.append(gap.recommendation[:34])
|
|
902
1056
|
elif show_tool:
|
|
903
|
-
tool = gap.scan_details.get(
|
|
1057
|
+
tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
904
1058
|
row.append(tool)
|
|
905
1059
|
row.append(gap.recommendation[:39])
|
|
906
1060
|
elif show_both:
|
|
907
|
-
package =
|
|
908
|
-
|
|
1061
|
+
package = (
|
|
1062
|
+
gap.splunk_details.get("package_name", "-")
|
|
1063
|
+
if gap.splunk_details
|
|
1064
|
+
else "-"
|
|
1065
|
+
)
|
|
1066
|
+
tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
909
1067
|
row.append(package[:19])
|
|
910
1068
|
row.append(tool)
|
|
911
1069
|
row.append(f"[green]{gap.confidence}[/green]")
|
|
@@ -920,12 +1078,14 @@ def _show_gap_detail(gap: SplunkVulnGap) -> None:
|
|
|
920
1078
|
DesignSystem.clear_screen()
|
|
921
1079
|
width = DesignSystem.get_terminal_width()
|
|
922
1080
|
|
|
923
|
-
cve = gap.cve_id or
|
|
924
|
-
severity = gap.severity or
|
|
925
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
1081
|
+
cve = gap.cve_id or "Unknown"
|
|
1082
|
+
severity = gap.severity or "Medium"
|
|
1083
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
926
1084
|
|
|
927
1085
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
928
|
-
click.echo(
|
|
1086
|
+
click.echo(
|
|
1087
|
+
"│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
|
|
1088
|
+
)
|
|
929
1089
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
930
1090
|
click.echo()
|
|
931
1091
|
|
|
@@ -964,20 +1124,24 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
|
|
|
964
1124
|
DesignSystem.clear_screen()
|
|
965
1125
|
width = DesignSystem.get_terminal_width()
|
|
966
1126
|
|
|
967
|
-
cve = gap.get(
|
|
968
|
-
severity = gap.get(
|
|
969
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
1127
|
+
cve = gap.get("cve_id", "Unknown")
|
|
1128
|
+
severity = gap.get("severity", "Medium")
|
|
1129
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
970
1130
|
|
|
971
1131
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
972
|
-
click.echo(
|
|
1132
|
+
click.echo(
|
|
1133
|
+
"│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
|
|
1134
|
+
)
|
|
973
1135
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
974
1136
|
click.echo()
|
|
975
1137
|
|
|
976
|
-
priority = gap.get(
|
|
977
|
-
priority_color =
|
|
1138
|
+
priority = gap.get("priority", "medium")
|
|
1139
|
+
priority_color = "red" if priority == "high" else "yellow"
|
|
978
1140
|
|
|
979
1141
|
click.echo(f" Severity: " + click.style(severity, fg=sev_color, bold=True))
|
|
980
|
-
click.echo(
|
|
1142
|
+
click.echo(
|
|
1143
|
+
f" Priority: " + click.style(priority.upper(), fg=priority_color, bold=True)
|
|
1144
|
+
)
|
|
981
1145
|
click.echo(f" Host: {gap.get('host_ip', '-')}")
|
|
982
1146
|
click.echo()
|
|
983
1147
|
|
|
@@ -986,7 +1150,7 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
|
|
|
986
1150
|
click.echo(f" Version: {gap.get('package_version', '-')}")
|
|
987
1151
|
click.echo()
|
|
988
1152
|
|
|
989
|
-
if gap.get(
|
|
1153
|
+
if gap.get("recommendation"):
|
|
990
1154
|
click.echo(click.style(" Recommendation:", bold=True))
|
|
991
1155
|
click.echo(f" {gap.get('recommendation')}")
|
|
992
1156
|
click.echo()
|
|
@@ -995,13 +1159,17 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
|
|
|
995
1159
|
|
|
996
1160
|
|
|
997
1161
|
def _interactive_gaps_mode(
|
|
998
|
-
gaps: List,
|
|
1162
|
+
gaps: List,
|
|
1163
|
+
title: str,
|
|
1164
|
+
show_package: bool = False,
|
|
1165
|
+
show_tool: bool = False,
|
|
1166
|
+
show_both: bool = False,
|
|
999
1167
|
) -> None:
|
|
1000
1168
|
"""Interactive selection mode for gaps."""
|
|
1001
1169
|
from souleyez.ui.interactive_selector import interactive_select
|
|
1002
1170
|
|
|
1003
1171
|
if not gaps:
|
|
1004
|
-
click.echo(click.style(" No gaps to select.", fg=
|
|
1172
|
+
click.echo(click.style(" No gaps to select.", fg="yellow"))
|
|
1005
1173
|
click.pause()
|
|
1006
1174
|
return
|
|
1007
1175
|
|
|
@@ -1009,57 +1177,69 @@ def _interactive_gaps_mode(
|
|
|
1009
1177
|
gap_items = []
|
|
1010
1178
|
for gap in gaps:
|
|
1011
1179
|
item = {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1180
|
+
"id": id(gap),
|
|
1181
|
+
"cve_id": gap.cve_id or "-",
|
|
1182
|
+
"severity": gap.severity or "Medium",
|
|
1183
|
+
"host": gap.host_ip or "-",
|
|
1184
|
+
"raw": gap,
|
|
1017
1185
|
}
|
|
1018
1186
|
if show_package:
|
|
1019
|
-
item[
|
|
1187
|
+
item["package"] = (
|
|
1188
|
+
gap.splunk_details.get("package_name", "-")[:20]
|
|
1189
|
+
if gap.splunk_details
|
|
1190
|
+
else "-"
|
|
1191
|
+
)
|
|
1020
1192
|
elif show_tool:
|
|
1021
|
-
item[
|
|
1193
|
+
item["tool"] = (
|
|
1194
|
+
gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
1195
|
+
)
|
|
1022
1196
|
elif show_both:
|
|
1023
|
-
item[
|
|
1024
|
-
|
|
1197
|
+
item["package"] = (
|
|
1198
|
+
gap.splunk_details.get("package_name", "-")[:15]
|
|
1199
|
+
if gap.splunk_details
|
|
1200
|
+
else "-"
|
|
1201
|
+
)
|
|
1202
|
+
item["tool"] = (
|
|
1203
|
+
gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
1204
|
+
)
|
|
1025
1205
|
gap_items.append(item)
|
|
1026
1206
|
|
|
1027
1207
|
columns = [
|
|
1028
|
-
{
|
|
1029
|
-
{
|
|
1030
|
-
{
|
|
1208
|
+
{"name": "CVE", "key": "cve_id", "width": 18},
|
|
1209
|
+
{"name": "Severity", "key": "severity", "width": 10},
|
|
1210
|
+
{"name": "Host", "key": "host", "width": 15},
|
|
1031
1211
|
]
|
|
1032
1212
|
|
|
1033
1213
|
if show_package:
|
|
1034
|
-
columns.append({
|
|
1214
|
+
columns.append({"name": "Package", "key": "package", "width": 20})
|
|
1035
1215
|
elif show_tool:
|
|
1036
|
-
columns.append({
|
|
1216
|
+
columns.append({"name": "Tool", "key": "tool", "width": 15})
|
|
1037
1217
|
elif show_both:
|
|
1038
|
-
columns.append({
|
|
1039
|
-
columns.append({
|
|
1218
|
+
columns.append({"name": "Package", "key": "package", "width": 15})
|
|
1219
|
+
columns.append({"name": "Tool", "key": "tool", "width": 15})
|
|
1040
1220
|
|
|
1041
1221
|
def format_cell(item: Dict, key: str) -> str:
|
|
1042
|
-
if key ==
|
|
1043
|
-
sev = item.get(
|
|
1044
|
-
color = SEVERITY_COLORS.get(sev,
|
|
1222
|
+
if key == "severity":
|
|
1223
|
+
sev = item.get("severity", "Medium")
|
|
1224
|
+
color = SEVERITY_COLORS.get(sev, "white")
|
|
1045
1225
|
return f"[{color}]{sev}[/{color}]"
|
|
1046
|
-
return str(item.get(key,
|
|
1226
|
+
return str(item.get(key, "-"))
|
|
1047
1227
|
|
|
1048
1228
|
selected_ids: set = set()
|
|
1049
1229
|
interactive_select(
|
|
1050
1230
|
items=gap_items,
|
|
1051
1231
|
columns=columns,
|
|
1052
1232
|
selected_ids=selected_ids,
|
|
1053
|
-
get_id=lambda g: g[
|
|
1233
|
+
get_id=lambda g: g["id"],
|
|
1054
1234
|
title=title,
|
|
1055
|
-
format_cell=format_cell
|
|
1235
|
+
format_cell=format_cell,
|
|
1056
1236
|
)
|
|
1057
1237
|
|
|
1058
1238
|
# Show details of first selected
|
|
1059
1239
|
if selected_ids:
|
|
1060
1240
|
for item in gap_items:
|
|
1061
|
-
if item[
|
|
1062
|
-
_show_gap_detail(item[
|
|
1241
|
+
if item["id"] in selected_ids:
|
|
1242
|
+
_show_gap_detail(item["raw"])
|
|
1063
1243
|
break
|
|
1064
1244
|
|
|
1065
1245
|
|
|
@@ -1068,55 +1248,57 @@ def _interactive_actionable_gaps_mode(gaps: List[Dict]) -> None:
|
|
|
1068
1248
|
from souleyez.ui.interactive_selector import interactive_select
|
|
1069
1249
|
|
|
1070
1250
|
if not gaps:
|
|
1071
|
-
click.echo(click.style(" No gaps to select.", fg=
|
|
1251
|
+
click.echo(click.style(" No gaps to select.", fg="yellow"))
|
|
1072
1252
|
click.pause()
|
|
1073
1253
|
return
|
|
1074
1254
|
|
|
1075
1255
|
# Prepare items for interactive selector
|
|
1076
1256
|
gap_items = []
|
|
1077
1257
|
for gap in gaps:
|
|
1078
|
-
gap_items.append(
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1258
|
+
gap_items.append(
|
|
1259
|
+
{
|
|
1260
|
+
"id": id(gap),
|
|
1261
|
+
"cve_id": gap.get("cve_id", "-"),
|
|
1262
|
+
"severity": gap.get("severity", "Medium"),
|
|
1263
|
+
"priority": gap.get("priority", "medium").upper(),
|
|
1264
|
+
"host": gap.get("host_ip", "-"),
|
|
1265
|
+
"package": gap.get("package", "-")[:20],
|
|
1266
|
+
"raw": gap,
|
|
1267
|
+
}
|
|
1268
|
+
)
|
|
1087
1269
|
|
|
1088
1270
|
columns = [
|
|
1089
|
-
{
|
|
1090
|
-
{
|
|
1091
|
-
{
|
|
1092
|
-
{
|
|
1093
|
-
{
|
|
1271
|
+
{"name": "Priority", "key": "priority", "width": 8},
|
|
1272
|
+
{"name": "CVE", "key": "cve_id", "width": 18},
|
|
1273
|
+
{"name": "Severity", "key": "severity", "width": 10},
|
|
1274
|
+
{"name": "Host", "key": "host", "width": 15},
|
|
1275
|
+
{"name": "Package", "key": "package", "width": 20},
|
|
1094
1276
|
]
|
|
1095
1277
|
|
|
1096
1278
|
def format_cell(item: Dict, key: str) -> str:
|
|
1097
|
-
if key ==
|
|
1098
|
-
sev = item.get(
|
|
1099
|
-
color = SEVERITY_COLORS.get(sev,
|
|
1279
|
+
if key == "severity":
|
|
1280
|
+
sev = item.get("severity", "Medium")
|
|
1281
|
+
color = SEVERITY_COLORS.get(sev, "white")
|
|
1100
1282
|
return f"[{color}]{sev}[/{color}]"
|
|
1101
|
-
elif key ==
|
|
1102
|
-
pri = item.get(
|
|
1103
|
-
color =
|
|
1283
|
+
elif key == "priority":
|
|
1284
|
+
pri = item.get("priority", "MEDIUM")
|
|
1285
|
+
color = "red" if pri == "HIGH" else "yellow"
|
|
1104
1286
|
return f"[{color}]{pri}[/{color}]"
|
|
1105
|
-
return str(item.get(key,
|
|
1287
|
+
return str(item.get(key, "-"))
|
|
1106
1288
|
|
|
1107
1289
|
selected_ids: set = set()
|
|
1108
1290
|
interactive_select(
|
|
1109
1291
|
items=gap_items,
|
|
1110
1292
|
columns=columns,
|
|
1111
1293
|
selected_ids=selected_ids,
|
|
1112
|
-
get_id=lambda g: g[
|
|
1294
|
+
get_id=lambda g: g["id"],
|
|
1113
1295
|
title="ACTIONABLE GAPS",
|
|
1114
|
-
format_cell=format_cell
|
|
1296
|
+
format_cell=format_cell,
|
|
1115
1297
|
)
|
|
1116
1298
|
|
|
1117
1299
|
# Show details of first selected
|
|
1118
1300
|
if selected_ids:
|
|
1119
1301
|
for item in gap_items:
|
|
1120
|
-
if item[
|
|
1121
|
-
_show_actionable_gap_detail(item[
|
|
1302
|
+
if item["id"] in selected_ids:
|
|
1303
|
+
_show_actionable_gap_detail(item["raw"])
|
|
1122
1304
|
break
|