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/template_selector.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Template selection interface for deliverables - Interactive table style."""
|
|
2
|
+
|
|
2
3
|
import click
|
|
3
4
|
from typing import Dict, List, Optional, Any
|
|
4
5
|
from rich.console import Console
|
|
@@ -10,6 +11,7 @@ from souleyez.ui.design_system import DesignSystem
|
|
|
10
11
|
# Key codes
|
|
11
12
|
try:
|
|
12
13
|
import readchar
|
|
14
|
+
|
|
13
15
|
KEY_UP = readchar.key.UP
|
|
14
16
|
KEY_DOWN = readchar.key.DOWN
|
|
15
17
|
KEY_PAGE_UP = readchar.key.PAGE_UP
|
|
@@ -18,51 +20,52 @@ try:
|
|
|
18
20
|
KEY_ENTER = readchar.key.ENTER
|
|
19
21
|
KEY_SPACE = readchar.key.SPACE
|
|
20
22
|
except (ImportError, AttributeError):
|
|
21
|
-
KEY_UP =
|
|
22
|
-
KEY_DOWN =
|
|
23
|
-
KEY_PAGE_UP =
|
|
24
|
-
KEY_PAGE_DOWN =
|
|
25
|
-
KEY_ESCAPE =
|
|
26
|
-
KEY_ENTER =
|
|
27
|
-
KEY_SPACE =
|
|
23
|
+
KEY_UP = "\x1b[A"
|
|
24
|
+
KEY_DOWN = "\x1b[B"
|
|
25
|
+
KEY_PAGE_UP = "\x1b[5~"
|
|
26
|
+
KEY_PAGE_DOWN = "\x1b[6~"
|
|
27
|
+
KEY_ESCAPE = "\x1b"
|
|
28
|
+
KEY_ENTER = "\r"
|
|
29
|
+
KEY_SPACE = " "
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def _get_key() -> str:
|
|
31
33
|
"""Read a single keypress."""
|
|
32
34
|
try:
|
|
33
35
|
import readchar
|
|
36
|
+
|
|
34
37
|
return readchar.readkey()
|
|
35
38
|
except ImportError:
|
|
36
39
|
pass
|
|
37
40
|
|
|
38
41
|
try:
|
|
39
42
|
ch = click.getchar()
|
|
40
|
-
if ch ==
|
|
43
|
+
if ch == "\x1b" or (len(ch) > 1 and ch.startswith("\x1b")):
|
|
41
44
|
if len(ch) >= 3:
|
|
42
|
-
if ch ==
|
|
45
|
+
if ch == "\x1b[A":
|
|
43
46
|
return KEY_UP
|
|
44
|
-
elif ch ==
|
|
47
|
+
elif ch == "\x1b[B":
|
|
45
48
|
return KEY_DOWN
|
|
46
|
-
elif ch in (
|
|
49
|
+
elif ch in ("\x1b[5~", "\x1b[5"):
|
|
47
50
|
return KEY_PAGE_UP
|
|
48
|
-
elif ch in (
|
|
51
|
+
elif ch in ("\x1b[6~", "\x1b[6"):
|
|
49
52
|
return KEY_PAGE_DOWN
|
|
50
|
-
elif ch ==
|
|
53
|
+
elif ch == "\x1b":
|
|
51
54
|
try:
|
|
52
55
|
ch2 = click.getchar()
|
|
53
|
-
if ch2 ==
|
|
56
|
+
if ch2 == "[":
|
|
54
57
|
ch3 = click.getchar()
|
|
55
|
-
if ch3 ==
|
|
58
|
+
if ch3 == "A":
|
|
56
59
|
return KEY_UP
|
|
57
|
-
elif ch3 ==
|
|
60
|
+
elif ch3 == "B":
|
|
58
61
|
return KEY_DOWN
|
|
59
|
-
elif ch3 in (
|
|
62
|
+
elif ch3 in ("5", "6"):
|
|
60
63
|
click.getchar()
|
|
61
|
-
return KEY_PAGE_UP if ch3 ==
|
|
62
|
-
return
|
|
64
|
+
return KEY_PAGE_UP if ch3 == "5" else KEY_PAGE_DOWN
|
|
65
|
+
return ""
|
|
63
66
|
except Exception:
|
|
64
67
|
return KEY_ESCAPE
|
|
65
|
-
return
|
|
68
|
+
return ""
|
|
66
69
|
return ch
|
|
67
70
|
except Exception:
|
|
68
71
|
return KEY_ESCAPE
|
|
@@ -71,17 +74,36 @@ def _get_key() -> str:
|
|
|
71
74
|
class TemplateSelector:
|
|
72
75
|
"""Interactive template selector with keyboard navigation."""
|
|
73
76
|
|
|
74
|
-
CURSOR =
|
|
75
|
-
NO_CURSOR =
|
|
76
|
-
RADIO_EMPTY =
|
|
77
|
-
RADIO_SELECTED =
|
|
77
|
+
CURSOR = "▶"
|
|
78
|
+
NO_CURSOR = " "
|
|
79
|
+
RADIO_EMPTY = "○"
|
|
80
|
+
RADIO_SELECTED = "●"
|
|
78
81
|
|
|
79
82
|
# Category filters
|
|
80
83
|
CATEGORIES = [
|
|
81
|
-
(
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
("all", "All", None),
|
|
85
|
+
(
|
|
86
|
+
"compliance",
|
|
87
|
+
"🏛️ Compliance",
|
|
88
|
+
[
|
|
89
|
+
"hipaa",
|
|
90
|
+
"pci-dss",
|
|
91
|
+
"nist",
|
|
92
|
+
"owasp",
|
|
93
|
+
"soc2",
|
|
94
|
+
"iso27001",
|
|
95
|
+
"cis",
|
|
96
|
+
"cmmc",
|
|
97
|
+
"gdpr",
|
|
98
|
+
"glba",
|
|
99
|
+
],
|
|
100
|
+
),
|
|
101
|
+
(
|
|
102
|
+
"pentest",
|
|
103
|
+
"🎯 Pentest",
|
|
104
|
+
["ptes", "internal", "external", "webapp", "redteam", "cloud", "ad"],
|
|
105
|
+
),
|
|
106
|
+
("industry", "🏭 Industry", ["nerc-cip", "hitrust", "ffiec"]),
|
|
85
107
|
]
|
|
86
108
|
|
|
87
109
|
def __init__(self, templates: List[Dict], engagement_id: int):
|
|
@@ -96,12 +118,12 @@ class TemplateSelector:
|
|
|
96
118
|
self.selected_template = None
|
|
97
119
|
self.selected_idx = None # Currently selected row (for inline options)
|
|
98
120
|
self.show_inline_options = False
|
|
99
|
-
self.current_filter =
|
|
121
|
+
self.current_filter = "all"
|
|
100
122
|
|
|
101
123
|
def run(self) -> Optional[Dict]:
|
|
102
124
|
"""Run the interactive selector. Returns selected template or None."""
|
|
103
125
|
if not self.templates:
|
|
104
|
-
click.echo(click.style(" No templates available.", fg=
|
|
126
|
+
click.echo(click.style(" No templates available.", fg="yellow"))
|
|
105
127
|
click.pause()
|
|
106
128
|
return None
|
|
107
129
|
|
|
@@ -122,29 +144,44 @@ class TemplateSelector:
|
|
|
122
144
|
self.selected_idx = None
|
|
123
145
|
self.show_inline_options = False
|
|
124
146
|
|
|
125
|
-
if filter_key ==
|
|
147
|
+
if filter_key == "all":
|
|
126
148
|
self.templates = self.all_templates
|
|
127
149
|
else:
|
|
128
150
|
# Find the filter
|
|
129
151
|
for key, label, frameworks in self.CATEGORIES:
|
|
130
152
|
if key == filter_key and frameworks:
|
|
131
|
-
self.templates = [
|
|
153
|
+
self.templates = [
|
|
154
|
+
t
|
|
155
|
+
for t in self.all_templates
|
|
156
|
+
if t.get("framework") in frameworks
|
|
157
|
+
]
|
|
132
158
|
break
|
|
133
159
|
|
|
134
160
|
def _get_category(self, template: Dict) -> str:
|
|
135
161
|
"""Get display category for template."""
|
|
136
|
-
framework = template.get(
|
|
137
|
-
compliance = [
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
framework = template.get("framework", "")
|
|
163
|
+
compliance = [
|
|
164
|
+
"hipaa",
|
|
165
|
+
"pci-dss",
|
|
166
|
+
"nist",
|
|
167
|
+
"owasp",
|
|
168
|
+
"soc2",
|
|
169
|
+
"iso27001",
|
|
170
|
+
"cis",
|
|
171
|
+
"cmmc",
|
|
172
|
+
"gdpr",
|
|
173
|
+
"glba",
|
|
174
|
+
]
|
|
175
|
+
pentest = ["ptes", "internal", "external", "webapp", "redteam", "cloud", "ad"]
|
|
176
|
+
industry = ["nerc-cip", "hitrust", "ffiec"]
|
|
140
177
|
|
|
141
178
|
if framework in compliance:
|
|
142
|
-
return
|
|
179
|
+
return "🏛️"
|
|
143
180
|
elif framework in pentest:
|
|
144
|
-
return
|
|
181
|
+
return "🎯"
|
|
145
182
|
elif framework in industry:
|
|
146
|
-
return
|
|
147
|
-
return
|
|
183
|
+
return "🏭"
|
|
184
|
+
return "📋"
|
|
148
185
|
|
|
149
186
|
def _render(self):
|
|
150
187
|
"""Render the table."""
|
|
@@ -154,7 +191,15 @@ class TemplateSelector:
|
|
|
154
191
|
# Title
|
|
155
192
|
click.echo()
|
|
156
193
|
click.echo("┌" + "─" * (width - 2) + "┐")
|
|
157
|
-
click.echo(
|
|
194
|
+
click.echo(
|
|
195
|
+
"│"
|
|
196
|
+
+ click.style(
|
|
197
|
+
" 🎯 INITIALIZE DELIVERABLES FROM TEMPLATE ".center(width - 2),
|
|
198
|
+
bold=True,
|
|
199
|
+
fg="cyan",
|
|
200
|
+
)
|
|
201
|
+
+ "│"
|
|
202
|
+
)
|
|
158
203
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
159
204
|
click.echo()
|
|
160
205
|
|
|
@@ -162,7 +207,9 @@ class TemplateSelector:
|
|
|
162
207
|
filter_parts = []
|
|
163
208
|
for idx, (key, label, _) in enumerate(self.CATEGORIES):
|
|
164
209
|
if key == self.current_filter:
|
|
165
|
-
filter_parts.append(
|
|
210
|
+
filter_parts.append(
|
|
211
|
+
click.style(f"[{idx+1}] {label}", fg="cyan", bold=True)
|
|
212
|
+
)
|
|
166
213
|
else:
|
|
167
214
|
filter_parts.append(f"[{idx+1}] {label}")
|
|
168
215
|
click.echo(" " + " | ".join(filter_parts))
|
|
@@ -173,12 +220,14 @@ class TemplateSelector:
|
|
|
173
220
|
selected_info = ""
|
|
174
221
|
if self.selected_idx is not None:
|
|
175
222
|
selected_info = f" | {click.style('Selected:', bold=True, fg='green')} 1"
|
|
176
|
-
click.echo(
|
|
223
|
+
click.echo(
|
|
224
|
+
f" {click.style('Total:', bold=True)} {total} templates{selected_info}"
|
|
225
|
+
)
|
|
177
226
|
click.echo()
|
|
178
227
|
|
|
179
228
|
# Calculate visible items
|
|
180
229
|
page_end = min(self.page_start + self.page_size, len(self.templates))
|
|
181
|
-
visible_templates = self.templates[self.page_start:page_end]
|
|
230
|
+
visible_templates = self.templates[self.page_start : page_end]
|
|
182
231
|
|
|
183
232
|
# Create table
|
|
184
233
|
table = Table(
|
|
@@ -186,7 +235,7 @@ class TemplateSelector:
|
|
|
186
235
|
header_style="bold cyan",
|
|
187
236
|
box=DesignSystem.TABLE_BOX,
|
|
188
237
|
padding=(0, 1),
|
|
189
|
-
expand=True
|
|
238
|
+
expand=True,
|
|
190
239
|
)
|
|
191
240
|
|
|
192
241
|
table.add_column(" ", width=2, justify="center", no_wrap=True)
|
|
@@ -199,21 +248,21 @@ class TemplateSelector:
|
|
|
199
248
|
|
|
200
249
|
for idx, template in enumerate(visible_templates):
|
|
201
250
|
absolute_idx = self.page_start + idx
|
|
202
|
-
is_cursor =
|
|
203
|
-
is_selected =
|
|
251
|
+
is_cursor = absolute_idx == self.cursor_pos
|
|
252
|
+
is_selected = absolute_idx == self.selected_idx
|
|
204
253
|
|
|
205
254
|
cursor = self.CURSOR if is_cursor else self.NO_CURSOR
|
|
206
255
|
radio = self.RADIO_SELECTED if is_selected else self.RADIO_EMPTY
|
|
207
256
|
|
|
208
257
|
category_icon = self._get_category(template)
|
|
209
|
-
deliverable_count = len(template.get(
|
|
210
|
-
framework = (template.get(
|
|
258
|
+
deliverable_count = len(template.get("deliverables", []))
|
|
259
|
+
framework = (template.get("framework") or "").upper()
|
|
211
260
|
|
|
212
261
|
row = [
|
|
213
262
|
cursor,
|
|
214
263
|
radio,
|
|
215
264
|
str(absolute_idx + 1),
|
|
216
|
-
template[
|
|
265
|
+
template["name"],
|
|
217
266
|
str(deliverable_count),
|
|
218
267
|
category_icon,
|
|
219
268
|
framework,
|
|
@@ -244,7 +293,9 @@ class TemplateSelector:
|
|
|
244
293
|
if self.show_inline_options and self.selected_idx is not None:
|
|
245
294
|
# Inline options when template is selected
|
|
246
295
|
template = self.templates[self.selected_idx]
|
|
247
|
-
click.echo(
|
|
296
|
+
click.echo(
|
|
297
|
+
f" {click.style('Selected:', bold=True, fg='green')} {template['name']} ({len(template.get('deliverables', []))} deliverables)"
|
|
298
|
+
)
|
|
248
299
|
click.echo()
|
|
249
300
|
click.echo(f" [v] Preview - View template details")
|
|
250
301
|
click.echo(f" [l] Load - Load this template")
|
|
@@ -266,16 +317,16 @@ class TemplateSelector:
|
|
|
266
317
|
"""Handle keypress."""
|
|
267
318
|
# Handle inline options mode first
|
|
268
319
|
if self.show_inline_options and self.selected_idx is not None:
|
|
269
|
-
if key ==
|
|
320
|
+
if key == "v":
|
|
270
321
|
# Preview selected template
|
|
271
322
|
self._preview_template(self.selected_idx)
|
|
272
323
|
return
|
|
273
|
-
elif key ==
|
|
324
|
+
elif key == "l":
|
|
274
325
|
# Load selected template
|
|
275
326
|
self.selected_template = self.templates[self.selected_idx]
|
|
276
327
|
self.running = False
|
|
277
328
|
return
|
|
278
|
-
elif key in (
|
|
329
|
+
elif key in ("b", KEY_ESCAPE):
|
|
279
330
|
# Back - deselect
|
|
280
331
|
self.selected_idx = None
|
|
281
332
|
self.show_inline_options = False
|
|
@@ -285,42 +336,42 @@ class TemplateSelector:
|
|
|
285
336
|
self.show_inline_options = False
|
|
286
337
|
|
|
287
338
|
# Filter keys (1-4)
|
|
288
|
-
if key ==
|
|
289
|
-
self._apply_filter(
|
|
339
|
+
if key == "1":
|
|
340
|
+
self._apply_filter("all")
|
|
290
341
|
return
|
|
291
|
-
elif key ==
|
|
292
|
-
self._apply_filter(
|
|
342
|
+
elif key == "2":
|
|
343
|
+
self._apply_filter("compliance")
|
|
293
344
|
return
|
|
294
|
-
elif key ==
|
|
295
|
-
self._apply_filter(
|
|
345
|
+
elif key == "3":
|
|
346
|
+
self._apply_filter("pentest")
|
|
296
347
|
return
|
|
297
|
-
elif key ==
|
|
298
|
-
self._apply_filter(
|
|
348
|
+
elif key == "4":
|
|
349
|
+
self._apply_filter("industry")
|
|
299
350
|
return
|
|
300
351
|
|
|
301
352
|
# Navigation - Up
|
|
302
|
-
if key in (KEY_UP,
|
|
353
|
+
if key in (KEY_UP, "k"):
|
|
303
354
|
if self.cursor_pos > 0:
|
|
304
355
|
self.cursor_pos -= 1
|
|
305
356
|
if self.cursor_pos < self.page_start:
|
|
306
357
|
self.page_start = max(0, self.page_start - self.page_size)
|
|
307
358
|
|
|
308
359
|
# Navigation - Down
|
|
309
|
-
elif key in (KEY_DOWN,
|
|
360
|
+
elif key in (KEY_DOWN, "j"):
|
|
310
361
|
if self.cursor_pos < len(self.templates) - 1:
|
|
311
362
|
self.cursor_pos += 1
|
|
312
363
|
if self.cursor_pos >= self.page_start + self.page_size:
|
|
313
364
|
self.page_start += self.page_size
|
|
314
365
|
|
|
315
366
|
# Page Up / Previous Page
|
|
316
|
-
elif key in (KEY_PAGE_UP,
|
|
367
|
+
elif key in (KEY_PAGE_UP, "p", "[", "<"):
|
|
317
368
|
current_page = self.page_start // self.page_size
|
|
318
369
|
if current_page > 0:
|
|
319
370
|
self.page_start = (current_page - 1) * self.page_size
|
|
320
371
|
self.cursor_pos = self.page_start
|
|
321
372
|
|
|
322
373
|
# Page Down / Next Page
|
|
323
|
-
elif key in (KEY_PAGE_DOWN,
|
|
374
|
+
elif key in (KEY_PAGE_DOWN, "n", "]", ">"):
|
|
324
375
|
total_pages = (len(self.templates) + self.page_size - 1) // self.page_size
|
|
325
376
|
current_page = self.page_start // self.page_size
|
|
326
377
|
if current_page < total_pages - 1:
|
|
@@ -328,15 +379,15 @@ class TemplateSelector:
|
|
|
328
379
|
self.cursor_pos = self.page_start
|
|
329
380
|
|
|
330
381
|
# Preview (v key)
|
|
331
|
-
elif key ==
|
|
382
|
+
elif key == "v":
|
|
332
383
|
self._preview_template()
|
|
333
384
|
|
|
334
385
|
# Import
|
|
335
|
-
elif key ==
|
|
386
|
+
elif key == "i":
|
|
336
387
|
self._import_template()
|
|
337
388
|
|
|
338
389
|
# Space - Select/toggle template and show inline options
|
|
339
|
-
elif key in (KEY_SPACE,
|
|
390
|
+
elif key in (KEY_SPACE, " "):
|
|
340
391
|
if self.selected_idx == self.cursor_pos:
|
|
341
392
|
# Deselect if same item
|
|
342
393
|
self.selected_idx = None
|
|
@@ -347,12 +398,12 @@ class TemplateSelector:
|
|
|
347
398
|
self.show_inline_options = True
|
|
348
399
|
|
|
349
400
|
# Enter - Quick load (bypass inline options)
|
|
350
|
-
elif key in (KEY_ENTER,
|
|
401
|
+
elif key in (KEY_ENTER, "\r", "\n"):
|
|
351
402
|
self.selected_template = self.templates[self.cursor_pos]
|
|
352
403
|
self.running = False
|
|
353
404
|
|
|
354
405
|
# Cancel - q or Escape
|
|
355
|
-
elif key in (KEY_ESCAPE,
|
|
406
|
+
elif key in (KEY_ESCAPE, "q", "\x03"):
|
|
356
407
|
self.selected_template = None
|
|
357
408
|
self.running = False
|
|
358
409
|
|
|
@@ -368,26 +419,28 @@ class TemplateSelector:
|
|
|
368
419
|
"""Import a template from JSON file."""
|
|
369
420
|
DesignSystem.clear_screen()
|
|
370
421
|
click.echo()
|
|
371
|
-
click.echo(click.style(" 📦 IMPORT TEMPLATE", bold=True, fg=
|
|
422
|
+
click.echo(click.style(" 📦 IMPORT TEMPLATE", bold=True, fg="cyan"))
|
|
372
423
|
click.echo()
|
|
373
424
|
|
|
374
|
-
file_path = click.prompt(
|
|
425
|
+
file_path = click.prompt(
|
|
426
|
+
" Enter path to template JSON file", type=str, default=""
|
|
427
|
+
)
|
|
375
428
|
if not file_path:
|
|
376
429
|
return
|
|
377
430
|
|
|
378
431
|
try:
|
|
379
432
|
tm = TemplateManager()
|
|
380
|
-
with open(file_path,
|
|
433
|
+
with open(file_path, "r") as f:
|
|
381
434
|
json_data = f.read()
|
|
382
435
|
template_id = tm.import_template(json_data)
|
|
383
436
|
template = tm.get_template(template_id)
|
|
384
437
|
click.echo()
|
|
385
|
-
click.echo(click.style(f" ✅ Imported: {template['name']}", fg=
|
|
438
|
+
click.echo(click.style(f" ✅ Imported: {template['name']}", fg="green"))
|
|
386
439
|
click.pause()
|
|
387
440
|
# Refresh templates list
|
|
388
441
|
self.templates = tm.list_templates()
|
|
389
442
|
except Exception as e:
|
|
390
|
-
click.echo(click.style(f" ❌ Import failed: {e}", fg=
|
|
443
|
+
click.echo(click.style(f" ❌ Import failed: {e}", fg="red"))
|
|
391
444
|
click.pause()
|
|
392
445
|
|
|
393
446
|
|
|
@@ -405,10 +458,21 @@ def show_template_selector(engagement_id: int) -> bool:
|
|
|
405
458
|
|
|
406
459
|
# Sort by category then name
|
|
407
460
|
def sort_key(t):
|
|
408
|
-
framework = t.get(
|
|
409
|
-
compliance = [
|
|
410
|
-
|
|
411
|
-
|
|
461
|
+
framework = t.get("framework", "") or ""
|
|
462
|
+
compliance = [
|
|
463
|
+
"hipaa",
|
|
464
|
+
"pci-dss",
|
|
465
|
+
"nist",
|
|
466
|
+
"owasp",
|
|
467
|
+
"soc2",
|
|
468
|
+
"iso27001",
|
|
469
|
+
"cis",
|
|
470
|
+
"cmmc",
|
|
471
|
+
"gdpr",
|
|
472
|
+
"glba",
|
|
473
|
+
]
|
|
474
|
+
pentest = ["ptes", "internal", "external", "webapp", "redteam", "cloud", "ad"]
|
|
475
|
+
industry = ["nerc-cip", "hitrust", "ffiec"]
|
|
412
476
|
|
|
413
477
|
if framework in compliance:
|
|
414
478
|
cat = 0
|
|
@@ -418,7 +482,7 @@ def show_template_selector(engagement_id: int) -> bool:
|
|
|
418
482
|
cat = 2
|
|
419
483
|
else:
|
|
420
484
|
cat = 3
|
|
421
|
-
return (cat, t[
|
|
485
|
+
return (cat, t["name"])
|
|
422
486
|
|
|
423
487
|
all_templates.sort(key=sort_key)
|
|
424
488
|
|
|
@@ -444,23 +508,34 @@ def show_template_selector(engagement_id: int) -> bool:
|
|
|
444
508
|
continue
|
|
445
509
|
|
|
446
510
|
try:
|
|
447
|
-
count = tm.apply_template(selected[
|
|
511
|
+
count = tm.apply_template(selected["id"], engagement_id)
|
|
448
512
|
click.echo()
|
|
449
|
-
click.echo(
|
|
450
|
-
|
|
451
|
-
|
|
513
|
+
click.echo(
|
|
514
|
+
click.style(
|
|
515
|
+
f" ✅ Loaded {count} deliverables from template", fg="green"
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
auto_val_count = sum(
|
|
520
|
+
1 for d in selected["deliverables"] if d.get("auto_validate")
|
|
521
|
+
)
|
|
452
522
|
if auto_val_count > 0:
|
|
453
|
-
click.echo(
|
|
523
|
+
click.echo(
|
|
524
|
+
click.style(
|
|
525
|
+
f" ✅ {auto_val_count} deliverables have auto-validation enabled",
|
|
526
|
+
fg="green",
|
|
527
|
+
)
|
|
528
|
+
)
|
|
454
529
|
|
|
455
530
|
click.echo()
|
|
456
|
-
click.echo(click.style(" 💡 Next steps:", fg=
|
|
531
|
+
click.echo(click.style(" 💡 Next steps:", fg="cyan"))
|
|
457
532
|
click.echo(" • Press [V] to validate and see current progress")
|
|
458
533
|
click.echo(" • Start testing and link evidence as you go")
|
|
459
534
|
click.echo(" • Export report when complete")
|
|
460
535
|
click.pause()
|
|
461
536
|
return True
|
|
462
537
|
except Exception as e:
|
|
463
|
-
click.echo(click.style(f" ❌ Failed to load template: {e}", fg=
|
|
538
|
+
click.echo(click.style(f" ❌ Failed to load template: {e}", fg="red"))
|
|
464
539
|
click.pause()
|
|
465
540
|
return False
|
|
466
541
|
|
|
@@ -471,46 +546,62 @@ def _show_template_preview(template: dict):
|
|
|
471
546
|
width = DesignSystem.get_terminal_width()
|
|
472
547
|
|
|
473
548
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
474
|
-
click.echo(
|
|
549
|
+
click.echo(
|
|
550
|
+
"│"
|
|
551
|
+
+ click.style(" 📋 TEMPLATE PREVIEW ".center(width - 2), bold=True, fg="cyan")
|
|
552
|
+
+ "│"
|
|
553
|
+
)
|
|
475
554
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
476
555
|
click.echo()
|
|
477
556
|
|
|
478
557
|
click.echo(f" Name: {click.style(template['name'], bold=True)}")
|
|
479
|
-
if template.get(
|
|
558
|
+
if template.get("description"):
|
|
480
559
|
click.echo(f" Description: {template['description']}")
|
|
481
|
-
if template.get(
|
|
560
|
+
if template.get("framework"):
|
|
482
561
|
click.echo(f" Framework: {template['framework'].upper()}")
|
|
483
562
|
click.echo(f" Total Deliverables: {len(template.get('deliverables', []))}")
|
|
484
563
|
|
|
485
|
-
auto_val_count = sum(
|
|
486
|
-
|
|
564
|
+
auto_val_count = sum(
|
|
565
|
+
1 for d in template.get("deliverables", []) if d.get("auto_validate")
|
|
566
|
+
)
|
|
567
|
+
click.echo(
|
|
568
|
+
f" Auto-validation: {auto_val_count}/{len(template.get('deliverables', []))} deliverables"
|
|
569
|
+
)
|
|
487
570
|
|
|
488
571
|
click.echo()
|
|
489
|
-
click.echo(click.style(" DELIVERABLES", bold=True, fg=
|
|
572
|
+
click.echo(click.style(" DELIVERABLES", bold=True, fg="cyan"))
|
|
490
573
|
click.echo(" " + "═" * (width - 4))
|
|
491
574
|
click.echo()
|
|
492
575
|
|
|
493
576
|
# Group by category
|
|
494
577
|
categories = {}
|
|
495
|
-
for d in template.get(
|
|
496
|
-
cat = d.get(
|
|
578
|
+
for d in template.get("deliverables", []):
|
|
579
|
+
cat = d.get("category", "other")
|
|
497
580
|
if cat not in categories:
|
|
498
581
|
categories[cat] = []
|
|
499
582
|
categories[cat].append(d)
|
|
500
583
|
|
|
501
584
|
for category, deliverables in categories.items():
|
|
502
|
-
click.echo(
|
|
585
|
+
click.echo(
|
|
586
|
+
click.style(f" {category.upper().replace('_', ' ')}", bold=True, fg="cyan")
|
|
587
|
+
)
|
|
503
588
|
click.echo(" " + "─" * (width - 4))
|
|
504
589
|
|
|
505
590
|
for d in deliverables:
|
|
506
591
|
priority_color = {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}.get(d.get(
|
|
512
|
-
|
|
513
|
-
auto_val_icon =
|
|
514
|
-
click.echo(
|
|
592
|
+
"critical": "red",
|
|
593
|
+
"high": "yellow",
|
|
594
|
+
"medium": "white",
|
|
595
|
+
"low": "bright_black",
|
|
596
|
+
}.get(d.get("priority", "medium"), "white")
|
|
597
|
+
|
|
598
|
+
auto_val_icon = "✓" if d.get("auto_validate") else " "
|
|
599
|
+
click.echo(
|
|
600
|
+
f" [{auto_val_icon}] "
|
|
601
|
+
+ click.style(
|
|
602
|
+
f"[{d.get('priority', 'medium').upper()}]", fg=priority_color
|
|
603
|
+
)
|
|
604
|
+
+ f" {d['title']}"
|
|
605
|
+
)
|
|
515
606
|
|
|
516
607
|
click.echo()
|
souleyez/ui/terminal.py
CHANGED
|
@@ -31,16 +31,16 @@ def init_readline():
|
|
|
31
31
|
atexit.register(readline.write_history_file, histfile)
|
|
32
32
|
|
|
33
33
|
# Enable tab completion (can be customized later)
|
|
34
|
-
readline.parse_and_bind(
|
|
34
|
+
readline.parse_and_bind("tab: complete")
|
|
35
35
|
|
|
36
36
|
# Set up proper keybindings for editing
|
|
37
37
|
# These handle backspace, delete, arrow keys properly
|
|
38
|
-
readline.parse_and_bind(
|
|
38
|
+
readline.parse_and_bind("set editing-mode emacs")
|
|
39
39
|
|
|
40
40
|
# Ensure proper character handling
|
|
41
|
-
if hasattr(readline,
|
|
41
|
+
if hasattr(readline, "set_completer_delims"):
|
|
42
42
|
# Set delimiters for word completion
|
|
43
|
-
readline.set_completer_delims(
|
|
43
|
+
readline.set_completer_delims(" \t\n;")
|
|
44
44
|
|
|
45
45
|
# Try to fix terminal settings
|
|
46
46
|
try:
|
|
@@ -55,7 +55,7 @@ def init_readline():
|
|
|
55
55
|
|
|
56
56
|
# Set ERASE character to handle backspace
|
|
57
57
|
# VERASE is typically index 2 in the cc array
|
|
58
|
-
new_settings[6][termios.VERASE] = b
|
|
58
|
+
new_settings[6][termios.VERASE] = b"\x7f" # DEL character
|
|
59
59
|
|
|
60
60
|
# Apply the settings
|
|
61
61
|
termios.tcsetattr(fd, termios.TCSANOW, new_settings)
|
|
@@ -77,7 +77,7 @@ def setup_terminal():
|
|
|
77
77
|
init_readline()
|
|
78
78
|
|
|
79
79
|
# Ensure stdout is unbuffered for immediate output
|
|
80
|
-
if hasattr(sys.stdout,
|
|
80
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
81
81
|
try:
|
|
82
82
|
sys.stdout.reconfigure(line_buffering=True)
|
|
83
83
|
except (AttributeError, ValueError):
|