souleyez 2.43.29__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9564 -2881
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +564 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +409 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +417 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +913 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +219 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +237 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23034 -10679
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/METADATA +2 -2
- souleyez-3.0.0.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-3.0.0.dist-info}/top_level.txt +0 -0
souleyez/ui/gap_analysis_view.py
CHANGED
|
@@ -17,14 +17,14 @@ console = Console()
|
|
|
17
17
|
|
|
18
18
|
# Severity colors for click
|
|
19
19
|
SEVERITY_COLORS = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
"Critical": "red",
|
|
21
|
+
"High": "yellow",
|
|
22
|
+
"Medium": "white",
|
|
23
|
+
"Low": "bright_black",
|
|
24
|
+
"critical": "red",
|
|
25
|
+
"high": "yellow",
|
|
26
|
+
"medium": "white",
|
|
27
|
+
"low": "bright_black",
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
|
|
@@ -42,16 +42,30 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
|
|
|
42
42
|
|
|
43
43
|
# Header
|
|
44
44
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
45
|
-
click.echo(
|
|
45
|
+
click.echo(
|
|
46
|
+
"│"
|
|
47
|
+
+ click.style(
|
|
48
|
+
" WAZUH GAP ANALYSIS ".center(width - 2), bold=True, fg="cyan"
|
|
49
|
+
)
|
|
50
|
+
+ "│"
|
|
51
|
+
)
|
|
46
52
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
47
53
|
click.echo()
|
|
48
|
-
click.echo(
|
|
54
|
+
click.echo(
|
|
55
|
+
click.style(
|
|
56
|
+
" Compare Wazuh (passive) vs Scan (active) findings", fg="bright_black"
|
|
57
|
+
)
|
|
58
|
+
)
|
|
49
59
|
click.echo()
|
|
50
60
|
|
|
51
61
|
# Check if Wazuh is configured
|
|
52
62
|
config = WazuhConfig.get_config(engagement_id)
|
|
53
|
-
if not config or not config.get(
|
|
54
|
-
click.echo(
|
|
63
|
+
if not config or not config.get("enabled"):
|
|
64
|
+
click.echo(
|
|
65
|
+
click.style(
|
|
66
|
+
" ⚠️ Wazuh is not configured for this engagement.", fg="yellow"
|
|
67
|
+
)
|
|
68
|
+
)
|
|
55
69
|
click.echo()
|
|
56
70
|
click.echo(" Configure Wazuh in Settings → Integrations → Wazuh SIEM")
|
|
57
71
|
click.echo(" Then sync vulnerabilities before running gap analysis.")
|
|
@@ -60,7 +74,7 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
|
|
|
60
74
|
click.echo()
|
|
61
75
|
click.echo(" [q] Back")
|
|
62
76
|
click.echo()
|
|
63
|
-
if click.getchar().lower() ==
|
|
77
|
+
if click.getchar().lower() == "q":
|
|
64
78
|
return
|
|
65
79
|
continue
|
|
66
80
|
|
|
@@ -68,8 +82,8 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
|
|
|
68
82
|
sync = WazuhVulnSync(engagement_id)
|
|
69
83
|
sync_status = sync.get_sync_status()
|
|
70
84
|
|
|
71
|
-
if not sync_status.get(
|
|
72
|
-
click.echo(click.style(" ⚠️ No Wazuh data synced yet.", fg=
|
|
85
|
+
if not sync_status.get("synced"):
|
|
86
|
+
click.echo(click.style(" ⚠️ No Wazuh data synced yet.", fg="yellow"))
|
|
73
87
|
click.echo()
|
|
74
88
|
click.echo(" Press [s] to sync vulnerabilities from Wazuh first.")
|
|
75
89
|
click.echo()
|
|
@@ -80,9 +94,9 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
|
|
|
80
94
|
click.echo()
|
|
81
95
|
|
|
82
96
|
key = click.getchar().lower()
|
|
83
|
-
if key ==
|
|
97
|
+
if key == "s":
|
|
84
98
|
_do_sync(engagement_id)
|
|
85
|
-
elif key ==
|
|
99
|
+
elif key == "q":
|
|
86
100
|
return
|
|
87
101
|
continue
|
|
88
102
|
|
|
@@ -108,60 +122,79 @@ def show_gap_analysis_view(engagement_id: int, engagement_name: str = "") -> Non
|
|
|
108
122
|
try:
|
|
109
123
|
choice = input(" Select option: ").strip().lower()
|
|
110
124
|
|
|
111
|
-
if choice ==
|
|
125
|
+
if choice == "q":
|
|
112
126
|
return
|
|
113
|
-
elif choice ==
|
|
127
|
+
elif choice == "1":
|
|
114
128
|
_show_wazuh_only(result, width)
|
|
115
|
-
elif choice ==
|
|
129
|
+
elif choice == "2":
|
|
116
130
|
_show_scan_only(result, width)
|
|
117
|
-
elif choice ==
|
|
131
|
+
elif choice == "3":
|
|
118
132
|
_show_confirmed(result, width)
|
|
119
|
-
elif choice ==
|
|
133
|
+
elif choice == "a":
|
|
120
134
|
_show_actionable_gaps(engagement_id, width)
|
|
121
|
-
elif choice ==
|
|
135
|
+
elif choice == "s":
|
|
122
136
|
_do_sync(engagement_id)
|
|
123
137
|
except (KeyboardInterrupt, EOFError):
|
|
124
138
|
return
|
|
125
139
|
|
|
126
140
|
|
|
127
|
-
def _render_summary_dashboard(
|
|
141
|
+
def _render_summary_dashboard(
|
|
142
|
+
result: GapAnalysisResult, stats: Dict, width: int
|
|
143
|
+
) -> None:
|
|
128
144
|
"""Render the summary dashboard."""
|
|
129
145
|
wazuh_total = result.wazuh_total
|
|
130
146
|
scan_total = result.scan_total
|
|
131
147
|
confirmed = len(result.confirmed)
|
|
132
148
|
wazuh_only = len(result.wazuh_only)
|
|
133
149
|
scan_only = len(result.scan_only)
|
|
134
|
-
coverage = stats.get(
|
|
150
|
+
coverage = stats.get("coverage_pct", 0)
|
|
135
151
|
|
|
136
152
|
# Coverage color
|
|
137
153
|
if coverage >= 80:
|
|
138
|
-
coverage_color =
|
|
154
|
+
coverage_color = "green"
|
|
139
155
|
elif coverage >= 50:
|
|
140
|
-
coverage_color =
|
|
156
|
+
coverage_color = "yellow"
|
|
141
157
|
else:
|
|
142
|
-
coverage_color =
|
|
158
|
+
coverage_color = "red"
|
|
143
159
|
|
|
144
160
|
# Detection Sources
|
|
145
161
|
click.echo(click.style(" DETECTION SOURCES", bold=True))
|
|
146
|
-
click.echo(
|
|
147
|
-
|
|
162
|
+
click.echo(
|
|
163
|
+
f" Wazuh (passive): {click.style(str(wazuh_total), fg='cyan', bold=True)} CVEs"
|
|
164
|
+
)
|
|
165
|
+
click.echo(
|
|
166
|
+
f" Scans (active): {click.style(str(scan_total), fg='cyan', bold=True)} CVEs"
|
|
167
|
+
)
|
|
148
168
|
click.echo()
|
|
149
169
|
|
|
150
170
|
# Analysis Results
|
|
151
171
|
click.echo(click.style(" ANALYSIS RESULTS", bold=True))
|
|
152
|
-
click.echo(
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
click.echo(
|
|
173
|
+
f" {click.style('✓', fg='green')} Confirmed (both): {click.style(str(confirmed), bold=True)}"
|
|
174
|
+
)
|
|
175
|
+
click.echo(
|
|
176
|
+
f" {click.style('⚠', fg='yellow')} Wazuh Only: {click.style(str(wazuh_only), bold=True)} ← Scans missed these"
|
|
177
|
+
)
|
|
178
|
+
click.echo(
|
|
179
|
+
f" {click.style('○', fg='blue')} Scan Only: {click.style(str(scan_only), bold=True)} ← Wazuh missed these"
|
|
180
|
+
)
|
|
155
181
|
click.echo()
|
|
156
182
|
|
|
157
183
|
# Coverage
|
|
158
|
-
click.echo(
|
|
159
|
-
|
|
184
|
+
click.echo(
|
|
185
|
+
f" Coverage: "
|
|
186
|
+
+ click.style(f"{coverage:.1f}%", fg=coverage_color, bold=True)
|
|
187
|
+
+ " of Wazuh vulns confirmed by scans"
|
|
188
|
+
)
|
|
160
189
|
click.echo()
|
|
161
190
|
|
|
162
191
|
# Severity breakdown
|
|
163
|
-
sev_breakdown = stats.get(
|
|
164
|
-
if
|
|
192
|
+
sev_breakdown = stats.get("by_severity", {})
|
|
193
|
+
if (
|
|
194
|
+
sev_breakdown.get("wazuh_only")
|
|
195
|
+
or sev_breakdown.get("scan_only")
|
|
196
|
+
or sev_breakdown.get("confirmed")
|
|
197
|
+
):
|
|
165
198
|
_render_severity_breakdown(sev_breakdown, width)
|
|
166
199
|
|
|
167
200
|
|
|
@@ -170,19 +203,14 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
|
|
|
170
203
|
from rich.table import Table
|
|
171
204
|
from rich import box
|
|
172
205
|
|
|
173
|
-
SEVERITY_ICONS = {
|
|
174
|
-
'Critical': '🔴',
|
|
175
|
-
'High': '🟠',
|
|
176
|
-
'Medium': '🟡',
|
|
177
|
-
'Low': '⚪'
|
|
178
|
-
}
|
|
206
|
+
SEVERITY_ICONS = {"Critical": "🔴", "High": "🟠", "Medium": "🟡", "Low": "⚪"}
|
|
179
207
|
|
|
180
208
|
table = Table(
|
|
181
209
|
show_header=True,
|
|
182
210
|
header_style="bold",
|
|
183
211
|
box=box.SIMPLE,
|
|
184
212
|
padding=(0, 2),
|
|
185
|
-
expand=False
|
|
213
|
+
expand=False,
|
|
186
214
|
)
|
|
187
215
|
|
|
188
216
|
table.add_column("Severity", width=14)
|
|
@@ -190,18 +218,18 @@ def _render_severity_breakdown(breakdown: Dict, width: int) -> None:
|
|
|
190
218
|
table.add_column("Scan Only", width=12, justify="right")
|
|
191
219
|
table.add_column("Confirmed", width=12, justify="right")
|
|
192
220
|
|
|
193
|
-
for sev in [
|
|
194
|
-
icon = SEVERITY_ICONS.get(sev,
|
|
195
|
-
color = SEVERITY_COLORS.get(sev,
|
|
196
|
-
wazuh_only = breakdown.get(
|
|
197
|
-
scan_only = breakdown.get(
|
|
198
|
-
confirmed = breakdown.get(
|
|
221
|
+
for sev in ["Critical", "High", "Medium", "Low"]:
|
|
222
|
+
icon = SEVERITY_ICONS.get(sev, "")
|
|
223
|
+
color = SEVERITY_COLORS.get(sev, "white")
|
|
224
|
+
wazuh_only = breakdown.get("wazuh_only", {}).get(sev, 0)
|
|
225
|
+
scan_only = breakdown.get("scan_only", {}).get(sev, 0)
|
|
226
|
+
confirmed = breakdown.get("confirmed", {}).get(sev, 0)
|
|
199
227
|
|
|
200
228
|
table.add_row(
|
|
201
229
|
f"{icon} [{color}]{sev}[/{color}]",
|
|
202
230
|
str(wazuh_only) if wazuh_only else "-",
|
|
203
231
|
str(scan_only) if scan_only else "-",
|
|
204
|
-
str(confirmed) if confirmed else "-"
|
|
232
|
+
str(confirmed) if confirmed else "-",
|
|
205
233
|
)
|
|
206
234
|
|
|
207
235
|
console.print(" ", table)
|
|
@@ -220,16 +248,31 @@ def _show_wazuh_only(result: GapAnalysisResult, width: int) -> None:
|
|
|
220
248
|
width = DesignSystem.get_terminal_width()
|
|
221
249
|
|
|
222
250
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
223
|
-
click.echo(
|
|
251
|
+
click.echo(
|
|
252
|
+
"│"
|
|
253
|
+
+ click.style(
|
|
254
|
+
" WAZUH ONLY - SCANS MISSED ".center(width - 2), bold=True, fg="yellow"
|
|
255
|
+
)
|
|
256
|
+
+ "│"
|
|
257
|
+
)
|
|
224
258
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
225
259
|
click.echo()
|
|
226
260
|
|
|
227
261
|
click.echo(f" {len(gaps)} CVEs detected by Wazuh but NOT by active scans.")
|
|
228
|
-
click.echo(
|
|
262
|
+
click.echo(
|
|
263
|
+
click.style(
|
|
264
|
+
" These may be local/package vulnerabilities not exposed to network scanning.",
|
|
265
|
+
fg="bright_black",
|
|
266
|
+
)
|
|
267
|
+
)
|
|
229
268
|
click.echo()
|
|
230
269
|
|
|
231
270
|
if not gaps:
|
|
232
|
-
click.echo(
|
|
271
|
+
click.echo(
|
|
272
|
+
click.style(
|
|
273
|
+
" ✓ No gaps - all Wazuh vulns confirmed by scans!", fg="green"
|
|
274
|
+
)
|
|
275
|
+
)
|
|
233
276
|
click.echo()
|
|
234
277
|
click.pause(" Press any key to return...")
|
|
235
278
|
return
|
|
@@ -245,7 +288,14 @@ def _show_wazuh_only(result: GapAnalysisResult, width: int) -> None:
|
|
|
245
288
|
end_idx = min(start_idx + page_size, len(gaps))
|
|
246
289
|
page_gaps = gaps[start_idx:end_idx]
|
|
247
290
|
|
|
248
|
-
_render_gaps_table(
|
|
291
|
+
_render_gaps_table(
|
|
292
|
+
page_gaps,
|
|
293
|
+
width,
|
|
294
|
+
show_package=True,
|
|
295
|
+
page=page,
|
|
296
|
+
page_size=page_size,
|
|
297
|
+
view_all=view_all,
|
|
298
|
+
)
|
|
249
299
|
|
|
250
300
|
# Pagination info
|
|
251
301
|
if view_all:
|
|
@@ -270,17 +320,17 @@ def _show_wazuh_only(result: GapAnalysisResult, width: int) -> None:
|
|
|
270
320
|
try:
|
|
271
321
|
choice = input(" Select option: ").strip().lower()
|
|
272
322
|
|
|
273
|
-
if choice ==
|
|
323
|
+
if choice == "q":
|
|
274
324
|
return
|
|
275
|
-
elif choice ==
|
|
325
|
+
elif choice == "i":
|
|
276
326
|
_interactive_gaps_mode(gaps, "WAZUH ONLY GAPS", show_package=True)
|
|
277
|
-
elif choice ==
|
|
327
|
+
elif choice == "t":
|
|
278
328
|
view_all = not view_all
|
|
279
329
|
if not view_all:
|
|
280
330
|
page = 0
|
|
281
|
-
elif choice ==
|
|
331
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
282
332
|
page += 1
|
|
283
|
-
elif choice ==
|
|
333
|
+
elif choice == "p" and not view_all and page > 0:
|
|
284
334
|
page -= 1
|
|
285
335
|
elif choice.isdigit():
|
|
286
336
|
idx = int(choice) - 1
|
|
@@ -302,16 +352,31 @@ def _show_scan_only(result: GapAnalysisResult, width: int) -> None:
|
|
|
302
352
|
width = DesignSystem.get_terminal_width()
|
|
303
353
|
|
|
304
354
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
305
|
-
click.echo(
|
|
355
|
+
click.echo(
|
|
356
|
+
"│"
|
|
357
|
+
+ click.style(
|
|
358
|
+
" SCAN ONLY - WAZUH MISSED ".center(width - 2), bold=True, fg="blue"
|
|
359
|
+
)
|
|
360
|
+
+ "│"
|
|
361
|
+
)
|
|
306
362
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
307
363
|
click.echo()
|
|
308
364
|
|
|
309
365
|
click.echo(f" {len(gaps)} CVEs detected by active scans but NOT by Wazuh.")
|
|
310
|
-
click.echo(
|
|
366
|
+
click.echo(
|
|
367
|
+
click.style(
|
|
368
|
+
" This may indicate: missing Wazuh agent, detection rule gap, or network-only vuln.",
|
|
369
|
+
fg="bright_black",
|
|
370
|
+
)
|
|
371
|
+
)
|
|
311
372
|
click.echo()
|
|
312
373
|
|
|
313
374
|
if not gaps:
|
|
314
|
-
click.echo(
|
|
375
|
+
click.echo(
|
|
376
|
+
click.style(
|
|
377
|
+
" ✓ No gaps - Wazuh detected all scan findings!", fg="green"
|
|
378
|
+
)
|
|
379
|
+
)
|
|
315
380
|
click.echo()
|
|
316
381
|
click.pause(" Press any key to return...")
|
|
317
382
|
return
|
|
@@ -327,7 +392,14 @@ def _show_scan_only(result: GapAnalysisResult, width: int) -> None:
|
|
|
327
392
|
end_idx = min(start_idx + page_size, len(gaps))
|
|
328
393
|
page_gaps = gaps[start_idx:end_idx]
|
|
329
394
|
|
|
330
|
-
_render_gaps_table(
|
|
395
|
+
_render_gaps_table(
|
|
396
|
+
page_gaps,
|
|
397
|
+
width,
|
|
398
|
+
show_tool=True,
|
|
399
|
+
page=page,
|
|
400
|
+
page_size=page_size,
|
|
401
|
+
view_all=view_all,
|
|
402
|
+
)
|
|
331
403
|
|
|
332
404
|
# Pagination info
|
|
333
405
|
if view_all:
|
|
@@ -352,17 +424,17 @@ def _show_scan_only(result: GapAnalysisResult, width: int) -> None:
|
|
|
352
424
|
try:
|
|
353
425
|
choice = input(" Select option: ").strip().lower()
|
|
354
426
|
|
|
355
|
-
if choice ==
|
|
427
|
+
if choice == "q":
|
|
356
428
|
return
|
|
357
|
-
elif choice ==
|
|
429
|
+
elif choice == "i":
|
|
358
430
|
_interactive_gaps_mode(gaps, "SCAN ONLY GAPS", show_tool=True)
|
|
359
|
-
elif choice ==
|
|
431
|
+
elif choice == "t":
|
|
360
432
|
view_all = not view_all
|
|
361
433
|
if not view_all:
|
|
362
434
|
page = 0
|
|
363
|
-
elif choice ==
|
|
435
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
364
436
|
page += 1
|
|
365
|
-
elif choice ==
|
|
437
|
+
elif choice == "p" and not view_all and page > 0:
|
|
366
438
|
page -= 1
|
|
367
439
|
elif choice.isdigit():
|
|
368
440
|
idx = int(choice) - 1
|
|
@@ -384,16 +456,31 @@ def _show_confirmed(result: GapAnalysisResult, width: int) -> None:
|
|
|
384
456
|
width = DesignSystem.get_terminal_width()
|
|
385
457
|
|
|
386
458
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
387
|
-
click.echo(
|
|
459
|
+
click.echo(
|
|
460
|
+
"│"
|
|
461
|
+
+ click.style(
|
|
462
|
+
" CONFIRMED - BOTH SOURCES ".center(width - 2), bold=True, fg="green"
|
|
463
|
+
)
|
|
464
|
+
+ "│"
|
|
465
|
+
)
|
|
388
466
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
389
467
|
click.echo()
|
|
390
468
|
|
|
391
469
|
click.echo(f" {len(gaps)} CVEs detected by BOTH Wazuh and active scans.")
|
|
392
|
-
click.echo(
|
|
470
|
+
click.echo(
|
|
471
|
+
click.style(
|
|
472
|
+
" High confidence - prioritize these for exploitation.",
|
|
473
|
+
fg="bright_black",
|
|
474
|
+
)
|
|
475
|
+
)
|
|
393
476
|
click.echo()
|
|
394
477
|
|
|
395
478
|
if not gaps:
|
|
396
|
-
click.echo(
|
|
479
|
+
click.echo(
|
|
480
|
+
click.style(
|
|
481
|
+
" No confirmed matches between Wazuh and scans.", fg="yellow"
|
|
482
|
+
)
|
|
483
|
+
)
|
|
397
484
|
click.echo()
|
|
398
485
|
click.pause(" Press any key to return...")
|
|
399
486
|
return
|
|
@@ -409,7 +496,14 @@ def _show_confirmed(result: GapAnalysisResult, width: int) -> None:
|
|
|
409
496
|
end_idx = min(start_idx + page_size, len(gaps))
|
|
410
497
|
page_gaps = gaps[start_idx:end_idx]
|
|
411
498
|
|
|
412
|
-
_render_gaps_table(
|
|
499
|
+
_render_gaps_table(
|
|
500
|
+
page_gaps,
|
|
501
|
+
width,
|
|
502
|
+
show_both=True,
|
|
503
|
+
page=page,
|
|
504
|
+
page_size=page_size,
|
|
505
|
+
view_all=view_all,
|
|
506
|
+
)
|
|
413
507
|
|
|
414
508
|
# Pagination info
|
|
415
509
|
if view_all:
|
|
@@ -434,17 +528,17 @@ def _show_confirmed(result: GapAnalysisResult, width: int) -> None:
|
|
|
434
528
|
try:
|
|
435
529
|
choice = input(" Select option: ").strip().lower()
|
|
436
530
|
|
|
437
|
-
if choice ==
|
|
531
|
+
if choice == "q":
|
|
438
532
|
return
|
|
439
|
-
elif choice ==
|
|
533
|
+
elif choice == "i":
|
|
440
534
|
_interactive_gaps_mode(gaps, "CONFIRMED GAPS", show_both=True)
|
|
441
|
-
elif choice ==
|
|
535
|
+
elif choice == "t":
|
|
442
536
|
view_all = not view_all
|
|
443
537
|
if not view_all:
|
|
444
538
|
page = 0
|
|
445
|
-
elif choice ==
|
|
539
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
446
540
|
page += 1
|
|
447
|
-
elif choice ==
|
|
541
|
+
elif choice == "p" and not view_all and page > 0:
|
|
448
542
|
page -= 1
|
|
449
543
|
elif choice.isdigit():
|
|
450
544
|
idx = int(choice) - 1
|
|
@@ -468,15 +562,23 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
|
|
|
468
562
|
width = DesignSystem.get_terminal_width()
|
|
469
563
|
|
|
470
564
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
471
|
-
click.echo(
|
|
565
|
+
click.echo(
|
|
566
|
+
"│"
|
|
567
|
+
+ click.style(" ACTIONABLE GAPS ".center(width - 2), bold=True, fg="yellow")
|
|
568
|
+
+ "│"
|
|
569
|
+
)
|
|
472
570
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
473
571
|
click.echo()
|
|
474
572
|
|
|
475
|
-
click.echo(
|
|
573
|
+
click.echo(
|
|
574
|
+
f" {len(gaps)} prioritized vulnerabilities from Wazuh that need targeted scanning."
|
|
575
|
+
)
|
|
476
576
|
click.echo()
|
|
477
577
|
|
|
478
578
|
if not gaps:
|
|
479
|
-
click.echo(
|
|
579
|
+
click.echo(
|
|
580
|
+
click.style(" ✓ No actionable gaps - great scan coverage!", fg="green")
|
|
581
|
+
)
|
|
480
582
|
click.echo()
|
|
481
583
|
click.pause(" Press any key to return...")
|
|
482
584
|
return
|
|
@@ -511,21 +613,23 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
|
|
|
511
613
|
else:
|
|
512
614
|
display_idx = (page * page_size) + idx + 1
|
|
513
615
|
|
|
514
|
-
priority = gap.get(
|
|
515
|
-
priority_display =
|
|
616
|
+
priority = gap.get("priority", "medium")
|
|
617
|
+
priority_display = (
|
|
618
|
+
"[red]HIGH[/red]" if priority == "high" else "[yellow]MEDIUM[/yellow]"
|
|
619
|
+
)
|
|
516
620
|
|
|
517
|
-
severity = gap.get(
|
|
518
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
621
|
+
severity = gap.get("severity", "Medium")
|
|
622
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
519
623
|
|
|
520
624
|
table.add_row(
|
|
521
625
|
"○",
|
|
522
626
|
str(display_idx),
|
|
523
627
|
priority_display,
|
|
524
|
-
gap.get(
|
|
628
|
+
gap.get("cve_id", "-"),
|
|
525
629
|
f"[{sev_color}]{severity}[/{sev_color}]",
|
|
526
|
-
gap.get(
|
|
527
|
-
gap.get(
|
|
528
|
-
gap.get(
|
|
630
|
+
gap.get("host_ip", "-"),
|
|
631
|
+
gap.get("package", "-")[:19],
|
|
632
|
+
gap.get("recommendation", "-")[:34],
|
|
529
633
|
)
|
|
530
634
|
|
|
531
635
|
console.print(table)
|
|
@@ -553,17 +657,17 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
|
|
|
553
657
|
try:
|
|
554
658
|
choice = input(" Select option: ").strip().lower()
|
|
555
659
|
|
|
556
|
-
if choice ==
|
|
660
|
+
if choice == "q":
|
|
557
661
|
return
|
|
558
|
-
elif choice ==
|
|
662
|
+
elif choice == "i":
|
|
559
663
|
_interactive_actionable_gaps_mode(gaps, "ACTIONABLE GAPS")
|
|
560
|
-
elif choice ==
|
|
664
|
+
elif choice == "t":
|
|
561
665
|
view_all = not view_all
|
|
562
666
|
if not view_all:
|
|
563
667
|
page = 0
|
|
564
|
-
elif choice ==
|
|
668
|
+
elif choice == "n" and not view_all and page < total_pages - 1:
|
|
565
669
|
page += 1
|
|
566
|
-
elif choice ==
|
|
670
|
+
elif choice == "p" and not view_all and page > 0:
|
|
567
671
|
page -= 1
|
|
568
672
|
elif choice.isdigit():
|
|
569
673
|
idx = int(choice) - 1
|
|
@@ -574,8 +678,14 @@ def _show_actionable_gaps(engagement_id: int, width: int) -> None:
|
|
|
574
678
|
|
|
575
679
|
|
|
576
680
|
def _render_gaps_table(
|
|
577
|
-
gaps: List,
|
|
578
|
-
|
|
681
|
+
gaps: List,
|
|
682
|
+
width: int,
|
|
683
|
+
show_package: bool = False,
|
|
684
|
+
show_tool: bool = False,
|
|
685
|
+
show_both: bool = False,
|
|
686
|
+
page: int = 0,
|
|
687
|
+
page_size: int = 20,
|
|
688
|
+
view_all: bool = False,
|
|
579
689
|
) -> None:
|
|
580
690
|
"""Render gaps table with pagination support."""
|
|
581
691
|
table = DesignSystem.create_table()
|
|
@@ -605,27 +715,31 @@ def _render_gaps_table(
|
|
|
605
715
|
display_idx = (page * page_size) + idx + 1
|
|
606
716
|
|
|
607
717
|
severity = gap.severity
|
|
608
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
718
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
609
719
|
|
|
610
720
|
row = [
|
|
611
721
|
"○",
|
|
612
722
|
str(display_idx),
|
|
613
723
|
gap.cve_id,
|
|
614
724
|
f"[{sev_color}]{severity}[/{sev_color}]",
|
|
615
|
-
gap.host_ip or "-"
|
|
725
|
+
gap.host_ip or "-",
|
|
616
726
|
]
|
|
617
727
|
|
|
618
728
|
if show_package:
|
|
619
|
-
package =
|
|
729
|
+
package = (
|
|
730
|
+
gap.wazuh_details.get("package_name", "-") if gap.wazuh_details else "-"
|
|
731
|
+
)
|
|
620
732
|
row.append(package[:24])
|
|
621
733
|
row.append(gap.recommendation[:34])
|
|
622
734
|
elif show_tool:
|
|
623
|
-
tool = gap.scan_details.get(
|
|
735
|
+
tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
624
736
|
row.append(tool)
|
|
625
737
|
row.append(gap.recommendation[:39])
|
|
626
738
|
elif show_both:
|
|
627
|
-
package =
|
|
628
|
-
|
|
739
|
+
package = (
|
|
740
|
+
gap.wazuh_details.get("package_name", "-") if gap.wazuh_details else "-"
|
|
741
|
+
)
|
|
742
|
+
tool = gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
629
743
|
row.append(package[:19])
|
|
630
744
|
row.append(tool)
|
|
631
745
|
row.append(f"[green]{gap.confidence}[/green]")
|
|
@@ -640,12 +754,14 @@ def _show_gap_detail(gap) -> None:
|
|
|
640
754
|
DesignSystem.clear_screen()
|
|
641
755
|
width = DesignSystem.get_terminal_width()
|
|
642
756
|
|
|
643
|
-
cve = gap.cve_id or
|
|
644
|
-
severity = gap.severity or
|
|
645
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
757
|
+
cve = gap.cve_id or "Unknown"
|
|
758
|
+
severity = gap.severity or "Medium"
|
|
759
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
646
760
|
|
|
647
761
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
648
|
-
click.echo(
|
|
762
|
+
click.echo(
|
|
763
|
+
"│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
|
|
764
|
+
)
|
|
649
765
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
650
766
|
click.echo()
|
|
651
767
|
|
|
@@ -682,20 +798,24 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
|
|
|
682
798
|
DesignSystem.clear_screen()
|
|
683
799
|
width = DesignSystem.get_terminal_width()
|
|
684
800
|
|
|
685
|
-
cve = gap.get(
|
|
686
|
-
severity = gap.get(
|
|
687
|
-
sev_color = SEVERITY_COLORS.get(severity,
|
|
801
|
+
cve = gap.get("cve_id", "Unknown")
|
|
802
|
+
severity = gap.get("severity", "Medium")
|
|
803
|
+
sev_color = SEVERITY_COLORS.get(severity, "white")
|
|
688
804
|
|
|
689
805
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
690
|
-
click.echo(
|
|
806
|
+
click.echo(
|
|
807
|
+
"│" + click.style(f" {cve} ".center(width - 2), bold=True, fg="cyan") + "│"
|
|
808
|
+
)
|
|
691
809
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
692
810
|
click.echo()
|
|
693
811
|
|
|
694
|
-
priority = gap.get(
|
|
695
|
-
priority_color =
|
|
812
|
+
priority = gap.get("priority", "medium")
|
|
813
|
+
priority_color = "red" if priority == "high" else "yellow"
|
|
696
814
|
|
|
697
815
|
click.echo(f" Severity: " + click.style(severity, fg=sev_color, bold=True))
|
|
698
|
-
click.echo(
|
|
816
|
+
click.echo(
|
|
817
|
+
f" Priority: " + click.style(priority.upper(), fg=priority_color, bold=True)
|
|
818
|
+
)
|
|
699
819
|
click.echo(f" Host: {gap.get('host_ip', '-')}")
|
|
700
820
|
click.echo()
|
|
701
821
|
|
|
@@ -704,12 +824,12 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
|
|
|
704
824
|
click.echo(f" Version: {gap.get('package_version', '-')}")
|
|
705
825
|
click.echo()
|
|
706
826
|
|
|
707
|
-
if gap.get(
|
|
827
|
+
if gap.get("recommendation"):
|
|
708
828
|
click.echo(click.style(" Recommendation:", bold=True))
|
|
709
829
|
click.echo(f" {gap.get('recommendation')}")
|
|
710
830
|
click.echo()
|
|
711
831
|
|
|
712
|
-
if gap.get(
|
|
832
|
+
if gap.get("scan_command"):
|
|
713
833
|
click.echo(click.style(" Suggested Scan Command:", bold=True))
|
|
714
834
|
click.echo(f" {gap.get('scan_command')}")
|
|
715
835
|
click.echo()
|
|
@@ -717,12 +837,18 @@ def _show_actionable_gap_detail(gap: Dict) -> None:
|
|
|
717
837
|
click.pause(" Press any key to return...")
|
|
718
838
|
|
|
719
839
|
|
|
720
|
-
def _interactive_gaps_mode(
|
|
840
|
+
def _interactive_gaps_mode(
|
|
841
|
+
gaps: List,
|
|
842
|
+
title: str,
|
|
843
|
+
show_package: bool = False,
|
|
844
|
+
show_tool: bool = False,
|
|
845
|
+
show_both: bool = False,
|
|
846
|
+
) -> None:
|
|
721
847
|
"""Interactive selection mode for gaps."""
|
|
722
848
|
from souleyez.ui.interactive_selector import interactive_select
|
|
723
849
|
|
|
724
850
|
if not gaps:
|
|
725
|
-
click.echo(click.style(" No gaps to select.", fg=
|
|
851
|
+
click.echo(click.style(" No gaps to select.", fg="yellow"))
|
|
726
852
|
click.pause()
|
|
727
853
|
return
|
|
728
854
|
|
|
@@ -730,57 +856,69 @@ def _interactive_gaps_mode(gaps: List, title: str, show_package: bool = False, s
|
|
|
730
856
|
gap_items = []
|
|
731
857
|
for gap in gaps:
|
|
732
858
|
item = {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
859
|
+
"id": id(gap),
|
|
860
|
+
"cve_id": gap.cve_id or "-",
|
|
861
|
+
"severity": gap.severity or "Medium",
|
|
862
|
+
"host": gap.host_ip or "-",
|
|
863
|
+
"raw": gap,
|
|
738
864
|
}
|
|
739
865
|
if show_package:
|
|
740
|
-
item[
|
|
866
|
+
item["package"] = (
|
|
867
|
+
gap.wazuh_details.get("package_name", "-")[:20]
|
|
868
|
+
if gap.wazuh_details
|
|
869
|
+
else "-"
|
|
870
|
+
)
|
|
741
871
|
elif show_tool:
|
|
742
|
-
item[
|
|
872
|
+
item["tool"] = (
|
|
873
|
+
gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
874
|
+
)
|
|
743
875
|
elif show_both:
|
|
744
|
-
item[
|
|
745
|
-
|
|
876
|
+
item["package"] = (
|
|
877
|
+
gap.wazuh_details.get("package_name", "-")[:15]
|
|
878
|
+
if gap.wazuh_details
|
|
879
|
+
else "-"
|
|
880
|
+
)
|
|
881
|
+
item["tool"] = (
|
|
882
|
+
gap.scan_details.get("tool", "-") if gap.scan_details else "-"
|
|
883
|
+
)
|
|
746
884
|
gap_items.append(item)
|
|
747
885
|
|
|
748
886
|
columns = [
|
|
749
|
-
{
|
|
750
|
-
{
|
|
751
|
-
{
|
|
887
|
+
{"name": "CVE", "key": "cve_id", "width": 18},
|
|
888
|
+
{"name": "Severity", "key": "severity", "width": 10},
|
|
889
|
+
{"name": "Host", "key": "host", "width": 15},
|
|
752
890
|
]
|
|
753
891
|
|
|
754
892
|
if show_package:
|
|
755
|
-
columns.append({
|
|
893
|
+
columns.append({"name": "Package", "key": "package", "width": 20})
|
|
756
894
|
elif show_tool:
|
|
757
|
-
columns.append({
|
|
895
|
+
columns.append({"name": "Tool", "key": "tool", "width": 15})
|
|
758
896
|
elif show_both:
|
|
759
|
-
columns.append({
|
|
760
|
-
columns.append({
|
|
897
|
+
columns.append({"name": "Package", "key": "package", "width": 15})
|
|
898
|
+
columns.append({"name": "Tool", "key": "tool", "width": 15})
|
|
761
899
|
|
|
762
900
|
def format_cell(item: Dict, key: str) -> str:
|
|
763
|
-
if key ==
|
|
764
|
-
sev = item.get(
|
|
765
|
-
color = SEVERITY_COLORS.get(sev,
|
|
901
|
+
if key == "severity":
|
|
902
|
+
sev = item.get("severity", "Medium")
|
|
903
|
+
color = SEVERITY_COLORS.get(sev, "white")
|
|
766
904
|
return f"[{color}]{sev}[/{color}]"
|
|
767
|
-
return str(item.get(key,
|
|
905
|
+
return str(item.get(key, "-"))
|
|
768
906
|
|
|
769
907
|
selected_ids: set = set()
|
|
770
908
|
interactive_select(
|
|
771
909
|
items=gap_items,
|
|
772
910
|
columns=columns,
|
|
773
911
|
selected_ids=selected_ids,
|
|
774
|
-
get_id=lambda g: g[
|
|
912
|
+
get_id=lambda g: g["id"],
|
|
775
913
|
title=title,
|
|
776
|
-
format_cell=format_cell
|
|
914
|
+
format_cell=format_cell,
|
|
777
915
|
)
|
|
778
916
|
|
|
779
917
|
# Show details of first selected
|
|
780
918
|
if selected_ids:
|
|
781
919
|
for item in gap_items:
|
|
782
|
-
if item[
|
|
783
|
-
_show_gap_detail(item[
|
|
920
|
+
if item["id"] in selected_ids:
|
|
921
|
+
_show_gap_detail(item["raw"])
|
|
784
922
|
break
|
|
785
923
|
|
|
786
924
|
|
|
@@ -789,57 +927,59 @@ def _interactive_actionable_gaps_mode(gaps: List[Dict], title: str) -> None:
|
|
|
789
927
|
from souleyez.ui.interactive_selector import interactive_select
|
|
790
928
|
|
|
791
929
|
if not gaps:
|
|
792
|
-
click.echo(click.style(" No gaps to select.", fg=
|
|
930
|
+
click.echo(click.style(" No gaps to select.", fg="yellow"))
|
|
793
931
|
click.pause()
|
|
794
932
|
return
|
|
795
933
|
|
|
796
934
|
# Prepare items for interactive selector
|
|
797
935
|
gap_items = []
|
|
798
936
|
for gap in gaps:
|
|
799
|
-
gap_items.append(
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
937
|
+
gap_items.append(
|
|
938
|
+
{
|
|
939
|
+
"id": id(gap),
|
|
940
|
+
"cve_id": gap.get("cve_id", "-"),
|
|
941
|
+
"severity": gap.get("severity", "Medium"),
|
|
942
|
+
"priority": gap.get("priority", "medium").upper(),
|
|
943
|
+
"host": gap.get("host_ip", "-"),
|
|
944
|
+
"package": gap.get("package", "-")[:20],
|
|
945
|
+
"raw": gap,
|
|
946
|
+
}
|
|
947
|
+
)
|
|
808
948
|
|
|
809
949
|
columns = [
|
|
810
|
-
{
|
|
811
|
-
{
|
|
812
|
-
{
|
|
813
|
-
{
|
|
814
|
-
{
|
|
950
|
+
{"name": "Priority", "key": "priority", "width": 8},
|
|
951
|
+
{"name": "CVE", "key": "cve_id", "width": 18},
|
|
952
|
+
{"name": "Severity", "key": "severity", "width": 10},
|
|
953
|
+
{"name": "Host", "key": "host", "width": 15},
|
|
954
|
+
{"name": "Package", "key": "package", "width": 20},
|
|
815
955
|
]
|
|
816
956
|
|
|
817
957
|
def format_cell(item: Dict, key: str) -> str:
|
|
818
|
-
if key ==
|
|
819
|
-
sev = item.get(
|
|
820
|
-
color = SEVERITY_COLORS.get(sev,
|
|
958
|
+
if key == "severity":
|
|
959
|
+
sev = item.get("severity", "Medium")
|
|
960
|
+
color = SEVERITY_COLORS.get(sev, "white")
|
|
821
961
|
return f"[{color}]{sev}[/{color}]"
|
|
822
|
-
elif key ==
|
|
823
|
-
pri = item.get(
|
|
824
|
-
color =
|
|
962
|
+
elif key == "priority":
|
|
963
|
+
pri = item.get("priority", "MEDIUM")
|
|
964
|
+
color = "red" if pri == "HIGH" else "yellow"
|
|
825
965
|
return f"[{color}]{pri}[/{color}]"
|
|
826
|
-
return str(item.get(key,
|
|
966
|
+
return str(item.get(key, "-"))
|
|
827
967
|
|
|
828
968
|
selected_ids: set = set()
|
|
829
969
|
interactive_select(
|
|
830
970
|
items=gap_items,
|
|
831
971
|
columns=columns,
|
|
832
972
|
selected_ids=selected_ids,
|
|
833
|
-
get_id=lambda g: g[
|
|
973
|
+
get_id=lambda g: g["id"],
|
|
834
974
|
title=title,
|
|
835
|
-
format_cell=format_cell
|
|
975
|
+
format_cell=format_cell,
|
|
836
976
|
)
|
|
837
977
|
|
|
838
978
|
# Show details of first selected
|
|
839
979
|
if selected_ids:
|
|
840
980
|
for item in gap_items:
|
|
841
|
-
if item[
|
|
842
|
-
_show_actionable_gap_detail(item[
|
|
981
|
+
if item["id"] in selected_ids:
|
|
982
|
+
_show_actionable_gap_detail(item["raw"])
|
|
843
983
|
break
|
|
844
984
|
|
|
845
985
|
|
|
@@ -849,7 +989,11 @@ def _do_sync(engagement_id: int) -> None:
|
|
|
849
989
|
width = DesignSystem.get_terminal_width()
|
|
850
990
|
|
|
851
991
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
852
|
-
click.echo(
|
|
992
|
+
click.echo(
|
|
993
|
+
"│"
|
|
994
|
+
+ click.style(" SYNCING FROM WAZUH ".center(width - 2), bold=True, fg="blue")
|
|
995
|
+
+ "│"
|
|
996
|
+
)
|
|
853
997
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
854
998
|
click.echo()
|
|
855
999
|
|
|
@@ -861,13 +1005,13 @@ def _do_sync(engagement_id: int) -> None:
|
|
|
861
1005
|
click.echo()
|
|
862
1006
|
|
|
863
1007
|
if result.success:
|
|
864
|
-
click.echo(click.style(" ✓ Sync complete!", fg=
|
|
1008
|
+
click.echo(click.style(" ✓ Sync complete!", fg="green", bold=True))
|
|
865
1009
|
click.echo(f" Fetched: {result.total_fetched}")
|
|
866
1010
|
click.echo(f" Mapped hosts: {result.mapped_hosts}")
|
|
867
1011
|
else:
|
|
868
|
-
click.echo(click.style(" ✗ Sync failed", fg=
|
|
1012
|
+
click.echo(click.style(" ✗ Sync failed", fg="red", bold=True))
|
|
869
1013
|
for err in result.errors:
|
|
870
|
-
click.echo(click.style(f" {err}", fg=
|
|
1014
|
+
click.echo(click.style(f" {err}", fg="red"))
|
|
871
1015
|
|
|
872
1016
|
click.echo()
|
|
873
1017
|
click.pause(" Press any key to continue...")
|