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
|
@@ -10,9 +10,17 @@ import math
|
|
|
10
10
|
import click
|
|
11
11
|
from typing import List, Set, Tuple
|
|
12
12
|
from souleyez.core.pending_chains import (
|
|
13
|
-
list_pending_chains,
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
list_pending_chains,
|
|
14
|
+
get_pending_chain,
|
|
15
|
+
approve_chain,
|
|
16
|
+
reject_chain,
|
|
17
|
+
approve_all_pending,
|
|
18
|
+
reject_all_pending,
|
|
19
|
+
get_chain_stats,
|
|
20
|
+
CHAIN_PENDING,
|
|
21
|
+
CHAIN_APPROVED,
|
|
22
|
+
CHAIN_REJECTED,
|
|
23
|
+
CHAIN_EXECUTED,
|
|
16
24
|
)
|
|
17
25
|
from souleyez.core.tool_chaining import ToolChaining
|
|
18
26
|
from souleyez.ui.menu_components import StandardMenu
|
|
@@ -21,7 +29,7 @@ from souleyez.ui.design_system import DesignSystem
|
|
|
21
29
|
|
|
22
30
|
def manage_pending_chains(engagement_id: int = None):
|
|
23
31
|
"""Main interface for reviewing and approving pending chains.
|
|
24
|
-
|
|
32
|
+
|
|
25
33
|
Args:
|
|
26
34
|
engagement_id: Filter chains by engagement (None = show all)
|
|
27
35
|
"""
|
|
@@ -29,31 +37,57 @@ def manage_pending_chains(engagement_id: int = None):
|
|
|
29
37
|
preview_page = 0 # Track current page for preview pagination
|
|
30
38
|
|
|
31
39
|
while True:
|
|
32
|
-
preview_page, total_pages = _display_pending_dashboard(
|
|
40
|
+
preview_page, total_pages = _display_pending_dashboard(
|
|
41
|
+
chaining, preview_page, engagement_id
|
|
42
|
+
)
|
|
33
43
|
|
|
34
44
|
options = [
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{
|
|
45
|
+
{
|
|
46
|
+
"number": 1,
|
|
47
|
+
"label": "Review & Approve Chains",
|
|
48
|
+
"description": "View pending chains and approve/reject them",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"number": 2,
|
|
52
|
+
"label": "Approve All Pending",
|
|
53
|
+
"description": "Approve all pending chains at once",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"number": 3,
|
|
57
|
+
"label": "Reject All Pending",
|
|
58
|
+
"description": "Reject all pending chains",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"number": 4,
|
|
62
|
+
"label": "Execute Approved",
|
|
63
|
+
"description": "Run all approved chains now",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"number": 5,
|
|
67
|
+
"label": "Toggle Approval Mode",
|
|
68
|
+
"description": "Switch between auto/approval modes",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"number": 6,
|
|
72
|
+
"label": "View Chain History",
|
|
73
|
+
"description": "See executed and rejected chains",
|
|
74
|
+
},
|
|
41
75
|
]
|
|
42
76
|
|
|
43
77
|
# Build shortcuts for page navigation
|
|
44
|
-
shortcuts = {
|
|
78
|
+
shortcuts = {"?": -3} # Help shortcut
|
|
45
79
|
if total_pages > 1:
|
|
46
80
|
if preview_page > 0:
|
|
47
|
-
shortcuts[
|
|
81
|
+
shortcuts["p"] = -1 # Previous page
|
|
48
82
|
if preview_page < total_pages - 1:
|
|
49
|
-
shortcuts[
|
|
83
|
+
shortcuts["n"] = -2 # Next page
|
|
50
84
|
|
|
51
85
|
try:
|
|
52
86
|
choice = StandardMenu.render(
|
|
53
87
|
options,
|
|
54
88
|
shortcuts=shortcuts,
|
|
55
89
|
show_shortcuts=False,
|
|
56
|
-
tip="Type ? for Active Orchestration help guide"
|
|
90
|
+
tip="Type ? for Active Orchestration help guide",
|
|
57
91
|
)
|
|
58
92
|
|
|
59
93
|
# Handle page navigation and help
|
|
@@ -88,7 +122,9 @@ def manage_pending_chains(engagement_id: int = None):
|
|
|
88
122
|
return
|
|
89
123
|
|
|
90
124
|
|
|
91
|
-
def _display_pending_dashboard(
|
|
125
|
+
def _display_pending_dashboard(
|
|
126
|
+
chaining: ToolChaining, preview_page: int = 0, engagement_id: int = None
|
|
127
|
+
) -> Tuple[int, int]:
|
|
92
128
|
"""Display pending chains dashboard with paginated preview.
|
|
93
129
|
|
|
94
130
|
Args:
|
|
@@ -106,39 +142,51 @@ def _display_pending_dashboard(chaining: ToolChaining, preview_page: int = 0, en
|
|
|
106
142
|
|
|
107
143
|
# Header
|
|
108
144
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
109
|
-
click.echo(
|
|
145
|
+
click.echo(
|
|
146
|
+
"│"
|
|
147
|
+
+ click.style(
|
|
148
|
+
" PENDING CHAINS - ACTIVE ORCHESTRATION ".center(width - 2),
|
|
149
|
+
bold=True,
|
|
150
|
+
fg="cyan",
|
|
151
|
+
)
|
|
152
|
+
+ "│"
|
|
153
|
+
)
|
|
110
154
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
111
155
|
click.echo()
|
|
112
156
|
|
|
113
157
|
# Mode indicator
|
|
114
158
|
if chaining.is_approval_mode():
|
|
115
|
-
mode_text = click.style("APPROVAL MODE", fg=
|
|
159
|
+
mode_text = click.style("APPROVAL MODE", fg="yellow", bold=True)
|
|
116
160
|
mode_desc = "Chains queue for your review before execution"
|
|
117
161
|
else:
|
|
118
|
-
mode_text = click.style("AUTO MODE", fg=
|
|
162
|
+
mode_text = click.style("AUTO MODE", fg="green", bold=True)
|
|
119
163
|
mode_desc = "Chains execute automatically (use option [5] to enable approval)"
|
|
120
164
|
|
|
121
165
|
click.echo(f" Mode: {mode_text}")
|
|
122
|
-
click.echo(click.style(f" {mode_desc}", fg=
|
|
166
|
+
click.echo(click.style(f" {mode_desc}", fg="bright_black"))
|
|
123
167
|
click.echo()
|
|
124
168
|
|
|
125
169
|
# Stats
|
|
126
170
|
stats = get_chain_stats(engagement_id)
|
|
127
|
-
pending = stats[
|
|
128
|
-
approved = stats[
|
|
129
|
-
rejected = stats[
|
|
130
|
-
executed = stats[
|
|
171
|
+
pending = stats["pending"]
|
|
172
|
+
approved = stats["approved"]
|
|
173
|
+
rejected = stats["rejected"]
|
|
174
|
+
executed = stats["executed"]
|
|
131
175
|
|
|
132
|
-
click.echo(click.style("📊 CHAIN STATISTICS", bold=True, fg=
|
|
176
|
+
click.echo(click.style("📊 CHAIN STATISTICS", bold=True, fg="cyan"))
|
|
133
177
|
click.echo("─" * width)
|
|
134
178
|
|
|
135
179
|
if pending > 0:
|
|
136
|
-
click.echo(
|
|
180
|
+
click.echo(
|
|
181
|
+
f" ⏳ Pending: {click.style(str(pending), fg='yellow', bold=True)} chains awaiting your decision"
|
|
182
|
+
)
|
|
137
183
|
else:
|
|
138
184
|
click.echo(f" ⏳ Pending: {pending}")
|
|
139
185
|
|
|
140
186
|
if approved > 0:
|
|
141
|
-
click.echo(
|
|
187
|
+
click.echo(
|
|
188
|
+
f" ✓ Approved: {click.style(str(approved), fg='green', bold=True)} ready to execute"
|
|
189
|
+
)
|
|
142
190
|
else:
|
|
143
191
|
click.echo(f" ✓ Approved: {approved}")
|
|
144
192
|
|
|
@@ -154,22 +202,33 @@ def _display_pending_dashboard(chaining: ToolChaining, preview_page: int = 0, en
|
|
|
154
202
|
|
|
155
203
|
# Get chains for current page
|
|
156
204
|
offset = preview_page * PAGE_SIZE
|
|
157
|
-
pending_chains = list_pending_chains(
|
|
205
|
+
pending_chains = list_pending_chains(
|
|
206
|
+
status=CHAIN_PENDING,
|
|
207
|
+
engagement_id=engagement_id,
|
|
208
|
+
limit=PAGE_SIZE,
|
|
209
|
+
offset=offset,
|
|
210
|
+
)
|
|
158
211
|
|
|
159
212
|
# Header with page info
|
|
160
213
|
page_info = f"Page {preview_page + 1}/{total_pages}" if total_pages > 1 else ""
|
|
161
|
-
click.echo(
|
|
162
|
-
|
|
214
|
+
click.echo(
|
|
215
|
+
click.style("📋 PENDING CHAINS PREVIEW ", bold=True, fg="cyan")
|
|
216
|
+
+ click.style(page_info, fg="bright_black")
|
|
217
|
+
)
|
|
163
218
|
click.echo("─" * width)
|
|
164
219
|
|
|
165
220
|
for chain in pending_chains:
|
|
166
|
-
tool = chain.get(
|
|
167
|
-
target = chain.get(
|
|
168
|
-
priority = chain.get(
|
|
221
|
+
tool = chain.get("tool", "unknown")
|
|
222
|
+
target = chain.get("target", "")[:30]
|
|
223
|
+
priority = chain.get("priority", 5)
|
|
169
224
|
|
|
170
|
-
pri_color =
|
|
171
|
-
|
|
172
|
-
|
|
225
|
+
pri_color = (
|
|
226
|
+
"green" if priority >= 8 else "yellow" if priority >= 5 else "white"
|
|
227
|
+
)
|
|
228
|
+
click.echo(
|
|
229
|
+
f" • {click.style(tool.upper(), fg='magenta')} → {target} "
|
|
230
|
+
f"(Priority: {click.style(str(priority), fg=pri_color)})"
|
|
231
|
+
)
|
|
173
232
|
|
|
174
233
|
# Page navigation hints
|
|
175
234
|
if total_pages > 1:
|
|
@@ -179,7 +238,7 @@ def _display_pending_dashboard(chaining: ToolChaining, preview_page: int = 0, en
|
|
|
179
238
|
if preview_page < total_pages - 1:
|
|
180
239
|
nav_hints.append("[n] Next")
|
|
181
240
|
if nav_hints:
|
|
182
|
-
click.echo(click.style(f" {' │ '.join(nav_hints)}", fg=
|
|
241
|
+
click.echo(click.style(f" {' │ '.join(nav_hints)}", fg="bright_black"))
|
|
183
242
|
click.echo()
|
|
184
243
|
|
|
185
244
|
return preview_page, total_pages
|
|
@@ -187,7 +246,7 @@ def _display_pending_dashboard(chaining: ToolChaining, preview_page: int = 0, en
|
|
|
187
246
|
|
|
188
247
|
def _review_pending_chains(chaining: ToolChaining, engagement_id: int = None):
|
|
189
248
|
"""Interactive review of pending chains with approve/reject actions.
|
|
190
|
-
|
|
249
|
+
|
|
191
250
|
Args:
|
|
192
251
|
chaining: ToolChaining instance
|
|
193
252
|
engagement_id: Filter by engagement (None = show all)
|
|
@@ -196,13 +255,17 @@ def _review_pending_chains(chaining: ToolChaining, engagement_id: int = None):
|
|
|
196
255
|
|
|
197
256
|
while True:
|
|
198
257
|
# Get pending chains
|
|
199
|
-
pending_chains = list_pending_chains(
|
|
258
|
+
pending_chains = list_pending_chains(
|
|
259
|
+
status=CHAIN_PENDING, engagement_id=engagement_id, limit=200
|
|
260
|
+
)
|
|
200
261
|
total_pending = len(pending_chains)
|
|
201
262
|
|
|
202
263
|
if total_pending == 0:
|
|
203
264
|
DesignSystem.clear_screen()
|
|
204
|
-
click.echo("\n" + click.style(" No pending chains to review!", fg=
|
|
205
|
-
click.echo(
|
|
265
|
+
click.echo("\n" + click.style(" No pending chains to review!", fg="green"))
|
|
266
|
+
click.echo(
|
|
267
|
+
click.style(" All chains have been processed.", fg="bright_black")
|
|
268
|
+
)
|
|
206
269
|
click.echo()
|
|
207
270
|
click.pause()
|
|
208
271
|
return
|
|
@@ -210,49 +273,51 @@ def _review_pending_chains(chaining: ToolChaining, engagement_id: int = None):
|
|
|
210
273
|
# Convert chains to dicts for selector
|
|
211
274
|
chain_items = []
|
|
212
275
|
for chain in pending_chains:
|
|
213
|
-
chain_items.append(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
276
|
+
chain_items.append(
|
|
277
|
+
{
|
|
278
|
+
"id": chain["id"],
|
|
279
|
+
"tool": chain.get("tool", ""),
|
|
280
|
+
"target": chain.get("target", ""),
|
|
281
|
+
"priority": chain.get("priority", 5),
|
|
282
|
+
"parent_id": chain.get("parent_job_id", "?"),
|
|
283
|
+
"args": chain.get("args", []),
|
|
284
|
+
}
|
|
285
|
+
)
|
|
221
286
|
|
|
222
287
|
selected_ids: Set[int] = set()
|
|
223
288
|
columns = [
|
|
224
|
-
{
|
|
225
|
-
{
|
|
226
|
-
{
|
|
227
|
-
{
|
|
228
|
-
{
|
|
289
|
+
{"name": "#", "width": 5, "key": "id", "justify": "right"},
|
|
290
|
+
{"name": "Tool", "width": 15, "key": "tool"},
|
|
291
|
+
{"name": "Target", "width": 35, "key": "target"},
|
|
292
|
+
{"name": "Priority", "width": 8, "key": "priority", "justify": "center"},
|
|
293
|
+
{"name": "From", "width": 10, "key": "parent_id"},
|
|
229
294
|
]
|
|
230
295
|
|
|
231
296
|
def format_chain_cell(item: dict, key: str) -> str:
|
|
232
297
|
value = item.get(key)
|
|
233
|
-
if key ==
|
|
298
|
+
if key == "priority":
|
|
234
299
|
pri = value or 5
|
|
235
300
|
if pri >= 8:
|
|
236
|
-
return f
|
|
301
|
+
return f"[green]{pri}[/green]"
|
|
237
302
|
elif pri >= 5:
|
|
238
|
-
return f
|
|
303
|
+
return f"[yellow]{pri}[/yellow]"
|
|
239
304
|
return str(pri)
|
|
240
|
-
if key ==
|
|
241
|
-
return f
|
|
242
|
-
if key ==
|
|
243
|
-
target = str(value) if value else
|
|
305
|
+
if key == "parent_id":
|
|
306
|
+
return f"Job #{value}"
|
|
307
|
+
if key == "target":
|
|
308
|
+
target = str(value) if value else ""
|
|
244
309
|
return target[:35] if len(target) > 35 else target
|
|
245
|
-
return str(value) if value else
|
|
310
|
+
return str(value) if value else "-"
|
|
246
311
|
|
|
247
312
|
# Run interactive selector
|
|
248
313
|
interactive_select(
|
|
249
314
|
items=chain_items,
|
|
250
315
|
columns=columns,
|
|
251
316
|
selected_ids=selected_ids,
|
|
252
|
-
get_id=lambda c: c.get(
|
|
253
|
-
title=f
|
|
317
|
+
get_id=lambda c: c.get("id"),
|
|
318
|
+
title=f"SELECT PENDING CHAINS ({total_pending} total)",
|
|
254
319
|
format_cell=format_chain_cell,
|
|
255
|
-
page_size=20
|
|
320
|
+
page_size=20,
|
|
256
321
|
)
|
|
257
322
|
|
|
258
323
|
# After selection, show action menu
|
|
@@ -263,50 +328,72 @@ def _review_pending_chains(chaining: ToolChaining, engagement_id: int = None):
|
|
|
263
328
|
width = DesignSystem.get_terminal_width()
|
|
264
329
|
|
|
265
330
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
266
|
-
click.echo(
|
|
331
|
+
click.echo(
|
|
332
|
+
"│"
|
|
333
|
+
+ click.style(" CHAIN ACTIONS ".center(width - 2), bold=True, fg="cyan")
|
|
334
|
+
+ "│"
|
|
335
|
+
)
|
|
267
336
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
268
337
|
click.echo()
|
|
269
338
|
|
|
270
|
-
click.echo(
|
|
339
|
+
click.echo(
|
|
340
|
+
f" Selected: {click.style(str(len(selected_ids)), fg='cyan', bold=True)} chain(s)"
|
|
341
|
+
)
|
|
271
342
|
click.echo()
|
|
272
343
|
|
|
273
344
|
# Show selected chains summary
|
|
274
345
|
for chain_id in list(selected_ids)[:10]:
|
|
275
|
-
chain = next((c for c in chain_items if c[
|
|
346
|
+
chain = next((c for c in chain_items if c["id"] == chain_id), None)
|
|
276
347
|
if chain:
|
|
277
348
|
click.echo(f" #{chain_id}: {chain['tool']} → {chain['target'][:40]}")
|
|
278
349
|
if len(selected_ids) > 10:
|
|
279
350
|
click.echo(f" ... and {len(selected_ids) - 10} more")
|
|
280
351
|
click.echo()
|
|
281
352
|
|
|
282
|
-
click.echo(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
click.echo(" " + click.style("[
|
|
353
|
+
click.echo(
|
|
354
|
+
" " + click.style("[a]", fg="green", bold=True) + " Approve selected"
|
|
355
|
+
)
|
|
356
|
+
click.echo(" " + click.style("[r]", fg="red", bold=True) + " Reject selected")
|
|
357
|
+
click.echo(
|
|
358
|
+
" "
|
|
359
|
+
+ click.style("[d]", fg="cyan", bold=True)
|
|
360
|
+
+ " View details (first selected)"
|
|
361
|
+
)
|
|
362
|
+
click.echo(
|
|
363
|
+
" " + click.style("[q]", fg="white", bold=True) + " Back to selection"
|
|
364
|
+
)
|
|
286
365
|
click.echo()
|
|
287
366
|
|
|
288
367
|
try:
|
|
289
|
-
choice =
|
|
368
|
+
choice = (
|
|
369
|
+
click.prompt("Select option", default="0", show_default=False)
|
|
370
|
+
.strip()
|
|
371
|
+
.lower()
|
|
372
|
+
)
|
|
290
373
|
|
|
291
|
-
if choice ==
|
|
374
|
+
if choice == "q":
|
|
292
375
|
continue # Go back to selection
|
|
293
|
-
elif choice ==
|
|
376
|
+
elif choice == "a":
|
|
294
377
|
approved = 0
|
|
295
378
|
for chain_id in list(selected_ids):
|
|
296
379
|
if approve_chain(chain_id):
|
|
297
380
|
approved += 1
|
|
298
|
-
click.echo(
|
|
381
|
+
click.echo(
|
|
382
|
+
click.style(f"\n ✓ Approved {approved} chain(s)", fg="green")
|
|
383
|
+
)
|
|
299
384
|
click.pause()
|
|
300
385
|
# Continue loop to show remaining
|
|
301
|
-
elif choice ==
|
|
386
|
+
elif choice == "r":
|
|
302
387
|
rejected = 0
|
|
303
388
|
for chain_id in list(selected_ids):
|
|
304
389
|
if reject_chain(chain_id):
|
|
305
390
|
rejected += 1
|
|
306
|
-
click.echo(
|
|
391
|
+
click.echo(
|
|
392
|
+
click.style(f"\n ✗ Rejected {rejected} chain(s)", fg="yellow")
|
|
393
|
+
)
|
|
307
394
|
click.pause()
|
|
308
395
|
# Continue loop to show remaining
|
|
309
|
-
elif choice ==
|
|
396
|
+
elif choice == "d":
|
|
310
397
|
first_id = list(selected_ids)[0]
|
|
311
398
|
_show_chain_details(first_id)
|
|
312
399
|
continue
|
|
@@ -321,7 +408,7 @@ def _show_chain_details(chain_id: int):
|
|
|
321
408
|
"""Display detailed information about a chain."""
|
|
322
409
|
chain = get_pending_chain(chain_id)
|
|
323
410
|
if not chain:
|
|
324
|
-
click.echo(click.style(f"\n Chain #{chain_id} not found", fg=
|
|
411
|
+
click.echo(click.style(f"\n Chain #{chain_id} not found", fg="red"))
|
|
325
412
|
click.pause()
|
|
326
413
|
return
|
|
327
414
|
|
|
@@ -330,20 +417,20 @@ def _show_chain_details(chain_id: int):
|
|
|
330
417
|
|
|
331
418
|
# Header
|
|
332
419
|
click.echo("\n" + "═" * width)
|
|
333
|
-
click.echo(click.style(f" CHAIN #{chain_id} DETAILS", bold=True, fg=
|
|
420
|
+
click.echo(click.style(f" CHAIN #{chain_id} DETAILS", bold=True, fg="cyan"))
|
|
334
421
|
click.echo("═" * width)
|
|
335
422
|
click.echo()
|
|
336
423
|
|
|
337
424
|
# Status
|
|
338
|
-
status = chain.get(
|
|
425
|
+
status = chain.get("status", "unknown")
|
|
339
426
|
if status == CHAIN_PENDING:
|
|
340
|
-
status_display = click.style("⏳ PENDING", fg=
|
|
427
|
+
status_display = click.style("⏳ PENDING", fg="yellow", bold=True)
|
|
341
428
|
elif status == CHAIN_APPROVED:
|
|
342
|
-
status_display = click.style("✓ APPROVED", fg=
|
|
429
|
+
status_display = click.style("✓ APPROVED", fg="green", bold=True)
|
|
343
430
|
elif status == CHAIN_REJECTED:
|
|
344
|
-
status_display = click.style("✗ REJECTED", fg=
|
|
431
|
+
status_display = click.style("✗ REJECTED", fg="red", bold=True)
|
|
345
432
|
elif status == CHAIN_EXECUTED:
|
|
346
|
-
status_display = click.style("▶ EXECUTED", fg=
|
|
433
|
+
status_display = click.style("▶ EXECUTED", fg="blue", bold=True)
|
|
347
434
|
else:
|
|
348
435
|
status_display = status
|
|
349
436
|
|
|
@@ -352,23 +439,25 @@ def _show_chain_details(chain_id: int):
|
|
|
352
439
|
|
|
353
440
|
# Tool info
|
|
354
441
|
click.echo(click.style(" Tool Information:", bold=True))
|
|
355
|
-
click.echo(
|
|
442
|
+
click.echo(
|
|
443
|
+
f" Tool: {click.style(chain.get('tool', 'unknown').upper(), fg='magenta')}"
|
|
444
|
+
)
|
|
356
445
|
click.echo(f" Target: {chain.get('target', 'N/A')}")
|
|
357
446
|
click.echo(f" Priority: {chain.get('priority', 5)}/10")
|
|
358
447
|
click.echo()
|
|
359
448
|
|
|
360
449
|
# Arguments
|
|
361
|
-
args = chain.get(
|
|
450
|
+
args = chain.get("args", [])
|
|
362
451
|
if args:
|
|
363
452
|
click.echo(click.style(" Arguments:", bold=True))
|
|
364
|
-
args_str =
|
|
453
|
+
args_str = " ".join(str(a) for a in args)
|
|
365
454
|
if len(args_str) > width - 10:
|
|
366
|
-
args_str = args_str[:width-13] + "..."
|
|
455
|
+
args_str = args_str[: width - 13] + "..."
|
|
367
456
|
click.echo(f" {args_str}")
|
|
368
457
|
click.echo()
|
|
369
458
|
|
|
370
459
|
# Rule description
|
|
371
|
-
rule_desc = chain.get(
|
|
460
|
+
rule_desc = chain.get("rule_description", "")
|
|
372
461
|
if rule_desc:
|
|
373
462
|
click.echo(click.style(" Triggered By:", bold=True))
|
|
374
463
|
click.echo(f" {rule_desc}")
|
|
@@ -377,16 +466,16 @@ def _show_chain_details(chain_id: int):
|
|
|
377
466
|
# Timestamps
|
|
378
467
|
click.echo(click.style(" Timeline:", bold=True))
|
|
379
468
|
click.echo(f" Created: {chain.get('created_at', 'N/A')}")
|
|
380
|
-
if chain.get(
|
|
469
|
+
if chain.get("decided_at"):
|
|
381
470
|
click.echo(f" Decided: {chain.get('decided_at')}")
|
|
382
|
-
if chain.get(
|
|
471
|
+
if chain.get("executed_at"):
|
|
383
472
|
click.echo(f" Executed: {chain.get('executed_at')}")
|
|
384
473
|
click.echo()
|
|
385
474
|
|
|
386
475
|
# Parent job and resulting job
|
|
387
476
|
click.echo(click.style(" Related Jobs:", bold=True))
|
|
388
477
|
click.echo(f" Parent Job: #{chain.get('parent_job_id', 'N/A')}")
|
|
389
|
-
if chain.get(
|
|
478
|
+
if chain.get("job_id"):
|
|
390
479
|
click.echo(f" Result Job: #{chain.get('job_id')}")
|
|
391
480
|
click.echo()
|
|
392
481
|
|
|
@@ -397,18 +486,22 @@ def _show_chain_details(chain_id: int):
|
|
|
397
486
|
click.echo()
|
|
398
487
|
|
|
399
488
|
try:
|
|
400
|
-
action =
|
|
401
|
-
|
|
489
|
+
action = (
|
|
490
|
+
click.prompt("Select option", default="0", show_default=False)
|
|
491
|
+
.strip()
|
|
492
|
+
.lower()
|
|
493
|
+
)
|
|
494
|
+
if action == "a":
|
|
402
495
|
if approve_chain(chain_id):
|
|
403
|
-
click.echo(click.style("\n ✓ Chain approved!", fg=
|
|
496
|
+
click.echo(click.style("\n ✓ Chain approved!", fg="green"))
|
|
404
497
|
else:
|
|
405
|
-
click.echo(click.style("\n Failed to approve chain", fg=
|
|
498
|
+
click.echo(click.style("\n Failed to approve chain", fg="red"))
|
|
406
499
|
click.pause()
|
|
407
|
-
elif action ==
|
|
500
|
+
elif action == "r":
|
|
408
501
|
if reject_chain(chain_id):
|
|
409
|
-
click.echo(click.style("\n ✗ Chain rejected", fg=
|
|
502
|
+
click.echo(click.style("\n ✗ Chain rejected", fg="yellow"))
|
|
410
503
|
else:
|
|
411
|
-
click.echo(click.style("\n Failed to reject chain", fg=
|
|
504
|
+
click.echo(click.style("\n Failed to reject chain", fg="red"))
|
|
412
505
|
click.pause()
|
|
413
506
|
except (KeyboardInterrupt, EOFError):
|
|
414
507
|
pass
|
|
@@ -418,83 +511,96 @@ def _show_chain_details(chain_id: int):
|
|
|
418
511
|
|
|
419
512
|
def _approve_all_interactive(chaining: ToolChaining, engagement_id: int = None):
|
|
420
513
|
"""Approve all pending chains with confirmation.
|
|
421
|
-
|
|
514
|
+
|
|
422
515
|
Args:
|
|
423
516
|
chaining: ToolChaining instance
|
|
424
517
|
engagement_id: Filter by engagement (None = show all)
|
|
425
518
|
"""
|
|
426
519
|
stats = get_chain_stats(engagement_id)
|
|
427
|
-
pending = stats[
|
|
520
|
+
pending = stats["pending"]
|
|
428
521
|
|
|
429
522
|
if pending == 0:
|
|
430
|
-
click.echo(click.style("\n No pending chains to approve!", fg=
|
|
523
|
+
click.echo(click.style("\n No pending chains to approve!", fg="green"))
|
|
431
524
|
click.pause()
|
|
432
525
|
return
|
|
433
526
|
|
|
434
527
|
click.echo()
|
|
435
|
-
click.echo(
|
|
528
|
+
click.echo(
|
|
529
|
+
f" This will approve {click.style(str(pending), fg='yellow', bold=True)} pending chains."
|
|
530
|
+
)
|
|
436
531
|
click.echo()
|
|
437
532
|
|
|
438
533
|
if click.confirm(" Approve all?", default=True):
|
|
439
534
|
count = approve_all_pending(engagement_id)
|
|
440
|
-
click.echo(click.style(f"\n ✓ Approved {count} chains", fg=
|
|
535
|
+
click.echo(click.style(f"\n ✓ Approved {count} chains", fg="green"))
|
|
441
536
|
|
|
442
537
|
# Offer to execute immediately
|
|
443
538
|
if click.confirm(" Execute approved chains now?", default=True):
|
|
444
539
|
job_ids = chaining.execute_approved_chains(engagement_id)
|
|
445
|
-
click.echo(click.style(f"\n ✓ Created {len(job_ids)} jobs", fg=
|
|
540
|
+
click.echo(click.style(f"\n ✓ Created {len(job_ids)} jobs", fg="green"))
|
|
446
541
|
|
|
447
542
|
click.pause()
|
|
448
543
|
|
|
449
544
|
|
|
450
545
|
def _reject_all_interactive(engagement_id: int = None):
|
|
451
546
|
"""Reject all pending chains with confirmation.
|
|
452
|
-
|
|
547
|
+
|
|
453
548
|
Args:
|
|
454
549
|
engagement_id: Filter by engagement (None = show all)
|
|
455
550
|
"""
|
|
456
551
|
stats = get_chain_stats(engagement_id)
|
|
457
|
-
pending = stats[
|
|
552
|
+
pending = stats["pending"]
|
|
458
553
|
|
|
459
554
|
if pending == 0:
|
|
460
|
-
click.echo(click.style("\n No pending chains to reject!", fg=
|
|
555
|
+
click.echo(click.style("\n No pending chains to reject!", fg="green"))
|
|
461
556
|
click.pause()
|
|
462
557
|
return
|
|
463
558
|
|
|
464
559
|
click.echo()
|
|
465
|
-
click.echo(
|
|
560
|
+
click.echo(
|
|
561
|
+
click.style(
|
|
562
|
+
f" ⚠️ This will reject {pending} pending chains!", fg="red", bold=True
|
|
563
|
+
)
|
|
564
|
+
)
|
|
466
565
|
click.echo()
|
|
467
566
|
|
|
468
|
-
if click.confirm(click.style(" Are you sure?", fg=
|
|
567
|
+
if click.confirm(click.style(" Are you sure?", fg="yellow"), default=False):
|
|
469
568
|
count = reject_all_pending(engagement_id)
|
|
470
|
-
click.echo(click.style(f"\n ✗ Rejected {count} chains", fg=
|
|
569
|
+
click.echo(click.style(f"\n ✗ Rejected {count} chains", fg="yellow"))
|
|
471
570
|
|
|
472
571
|
click.pause()
|
|
473
572
|
|
|
474
573
|
|
|
475
574
|
def _execute_approved_chains(chaining: ToolChaining, engagement_id: int = None):
|
|
476
575
|
"""Execute all approved chains.
|
|
477
|
-
|
|
576
|
+
|
|
478
577
|
Args:
|
|
479
578
|
chaining: ToolChaining instance
|
|
480
579
|
engagement_id: Filter by engagement (None = show all)
|
|
481
580
|
"""
|
|
482
581
|
stats = get_chain_stats(engagement_id)
|
|
483
|
-
approved = stats[
|
|
582
|
+
approved = stats["approved"]
|
|
484
583
|
|
|
485
584
|
if approved == 0:
|
|
486
|
-
click.echo(click.style("\n No approved chains to execute!", fg=
|
|
487
|
-
click.echo(
|
|
585
|
+
click.echo(click.style("\n No approved chains to execute!", fg="yellow"))
|
|
586
|
+
click.echo(
|
|
587
|
+
click.style(
|
|
588
|
+
" Use option [1] to review and approve pending chains first.",
|
|
589
|
+
fg="bright_black",
|
|
590
|
+
)
|
|
591
|
+
)
|
|
488
592
|
click.pause()
|
|
489
593
|
return
|
|
490
594
|
|
|
491
595
|
click.echo()
|
|
492
|
-
click.echo(
|
|
596
|
+
click.echo(
|
|
597
|
+
f" Ready to execute {click.style(str(approved), fg='green', bold=True)} approved chains."
|
|
598
|
+
)
|
|
493
599
|
click.echo()
|
|
494
600
|
|
|
495
601
|
if click.confirm(" Execute now?", default=True):
|
|
496
602
|
job_ids = chaining.execute_approved_chains(engagement_id)
|
|
497
|
-
click.echo(click.style(f"\n ✓ Created {len(job_ids)} jobs!", fg=
|
|
603
|
+
click.echo(click.style(f"\n ✓ Created {len(job_ids)} jobs!", fg="green"))
|
|
498
604
|
|
|
499
605
|
if job_ids:
|
|
500
606
|
click.echo(f" Job IDs: {', '.join(str(j) for j in job_ids[:10])}")
|
|
@@ -510,13 +616,13 @@ def _toggle_approval_mode(chaining: ToolChaining):
|
|
|
510
616
|
|
|
511
617
|
click.echo()
|
|
512
618
|
if current:
|
|
513
|
-
click.echo(" Current mode: " + click.style("APPROVAL", fg=
|
|
514
|
-
click.echo(" Switching to: " + click.style("AUTO", fg=
|
|
619
|
+
click.echo(" Current mode: " + click.style("APPROVAL", fg="yellow", bold=True))
|
|
620
|
+
click.echo(" Switching to: " + click.style("AUTO", fg="green", bold=True))
|
|
515
621
|
click.echo()
|
|
516
622
|
click.echo(" In AUTO mode, chains execute immediately without approval.")
|
|
517
623
|
else:
|
|
518
|
-
click.echo(" Current mode: " + click.style("AUTO", fg=
|
|
519
|
-
click.echo(" Switching to: " + click.style("APPROVAL", fg=
|
|
624
|
+
click.echo(" Current mode: " + click.style("AUTO", fg="green", bold=True))
|
|
625
|
+
click.echo(" Switching to: " + click.style("APPROVAL", fg="yellow", bold=True))
|
|
520
626
|
click.echo()
|
|
521
627
|
click.echo(" In APPROVAL mode, you'll review chains before they execute.")
|
|
522
628
|
|
|
@@ -525,18 +631,24 @@ def _toggle_approval_mode(chaining: ToolChaining):
|
|
|
525
631
|
if click.confirm(" Switch mode?", default=True):
|
|
526
632
|
new_mode = chaining.toggle_approval_mode()
|
|
527
633
|
if new_mode:
|
|
528
|
-
click.echo(click.style("\n ✓ APPROVAL MODE enabled", fg=
|
|
529
|
-
click.echo(
|
|
634
|
+
click.echo(click.style("\n ✓ APPROVAL MODE enabled", fg="yellow"))
|
|
635
|
+
click.echo(
|
|
636
|
+
click.style(
|
|
637
|
+
" Chains will now queue for your approval.", fg="bright_black"
|
|
638
|
+
)
|
|
639
|
+
)
|
|
530
640
|
else:
|
|
531
|
-
click.echo(click.style("\n ✓ AUTO MODE enabled", fg=
|
|
532
|
-
click.echo(
|
|
641
|
+
click.echo(click.style("\n ✓ AUTO MODE enabled", fg="green"))
|
|
642
|
+
click.echo(
|
|
643
|
+
click.style(" Chains will execute automatically.", fg="bright_black")
|
|
644
|
+
)
|
|
533
645
|
|
|
534
646
|
click.pause()
|
|
535
647
|
|
|
536
648
|
|
|
537
649
|
def _view_chain_history(engagement_id: int = None):
|
|
538
650
|
"""View executed and rejected chains.
|
|
539
|
-
|
|
651
|
+
|
|
540
652
|
Args:
|
|
541
653
|
engagement_id: Filter by engagement (None = show all)
|
|
542
654
|
"""
|
|
@@ -550,18 +662,22 @@ def _view_chain_history(engagement_id: int = None):
|
|
|
550
662
|
|
|
551
663
|
# Get all non-pending chains
|
|
552
664
|
all_chains = list_pending_chains(engagement_id=engagement_id, limit=500)
|
|
553
|
-
history_chains = [c for c in all_chains if c.get(
|
|
665
|
+
history_chains = [c for c in all_chains if c.get("status") != CHAIN_PENDING]
|
|
554
666
|
|
|
555
667
|
# Apply filter
|
|
556
668
|
if filter_status:
|
|
557
|
-
history_chains = [
|
|
669
|
+
history_chains = [
|
|
670
|
+
c for c in history_chains if c.get("status") == filter_status
|
|
671
|
+
]
|
|
558
672
|
|
|
559
673
|
total = len(history_chains)
|
|
560
674
|
|
|
561
675
|
if total == 0:
|
|
562
|
-
click.echo("\n" + click.style(" No chain history yet!", fg=
|
|
676
|
+
click.echo("\n" + click.style(" No chain history yet!", fg="bright_black"))
|
|
563
677
|
if filter_status:
|
|
564
|
-
click.echo(
|
|
678
|
+
click.echo(
|
|
679
|
+
click.style(f" (filtered by: {filter_status})", fg="bright_black")
|
|
680
|
+
)
|
|
565
681
|
click.echo()
|
|
566
682
|
click.pause()
|
|
567
683
|
return
|
|
@@ -571,25 +687,38 @@ def _view_chain_history(engagement_id: int = None):
|
|
|
571
687
|
|
|
572
688
|
# Header
|
|
573
689
|
click.echo("\n┌" + "─" * (width - 2) + "┐")
|
|
574
|
-
click.echo(
|
|
690
|
+
click.echo(
|
|
691
|
+
"│"
|
|
692
|
+
+ click.style(" CHAIN HISTORY ".center(width - 2), bold=True, fg="cyan")
|
|
693
|
+
+ "│"
|
|
694
|
+
)
|
|
575
695
|
click.echo("└" + "─" * (width - 2) + "┘")
|
|
576
696
|
click.echo()
|
|
577
697
|
|
|
578
698
|
# Filter indicator
|
|
579
699
|
filter_text = ""
|
|
580
700
|
if filter_status == CHAIN_APPROVED:
|
|
581
|
-
filter_text = click.style(" [Showing: Approved only]", fg=
|
|
701
|
+
filter_text = click.style(" [Showing: Approved only]", fg="green")
|
|
582
702
|
elif filter_status == CHAIN_REJECTED:
|
|
583
|
-
filter_text = click.style(" [Showing: Rejected only]", fg=
|
|
703
|
+
filter_text = click.style(" [Showing: Rejected only]", fg="red")
|
|
584
704
|
elif filter_status == CHAIN_EXECUTED:
|
|
585
|
-
filter_text = click.style(" [Showing: Executed only]", fg=
|
|
705
|
+
filter_text = click.style(" [Showing: Executed only]", fg="blue")
|
|
586
706
|
|
|
587
707
|
page_info = f"Page {current_page + 1}/{total_pages} ({total} chains)"
|
|
588
|
-
click.echo(
|
|
708
|
+
click.echo(
|
|
709
|
+
click.style(f"📜 HISTORY ", bold=True, fg="cyan")
|
|
710
|
+
+ click.style(page_info, fg="bright_black")
|
|
711
|
+
+ filter_text
|
|
712
|
+
)
|
|
589
713
|
click.echo("─" * width)
|
|
590
714
|
|
|
591
715
|
# Table header
|
|
592
|
-
click.echo(
|
|
716
|
+
click.echo(
|
|
717
|
+
click.style(
|
|
718
|
+
f" {'#':>4} │ {'Status':<10} │ {'Tool':<15} │ {'Target':<25} │ Decided",
|
|
719
|
+
bold=True,
|
|
720
|
+
)
|
|
721
|
+
)
|
|
593
722
|
click.echo("─" * width)
|
|
594
723
|
|
|
595
724
|
# Calculate slice
|
|
@@ -598,24 +727,28 @@ def _view_chain_history(engagement_id: int = None):
|
|
|
598
727
|
|
|
599
728
|
for idx in range(start_idx, end_idx):
|
|
600
729
|
chain = history_chains[idx]
|
|
601
|
-
chain_id = chain[
|
|
730
|
+
chain_id = chain["id"]
|
|
602
731
|
|
|
603
732
|
# Status with color
|
|
604
|
-
status = chain.get(
|
|
733
|
+
status = chain.get("status", "")
|
|
605
734
|
if status == CHAIN_APPROVED:
|
|
606
|
-
status_display = click.style("Approved", fg=
|
|
735
|
+
status_display = click.style("Approved", fg="green")
|
|
607
736
|
elif status == CHAIN_REJECTED:
|
|
608
|
-
status_display = click.style("Rejected", fg=
|
|
737
|
+
status_display = click.style("Rejected", fg="red")
|
|
609
738
|
elif status == CHAIN_EXECUTED:
|
|
610
|
-
status_display = click.style("Executed", fg=
|
|
739
|
+
status_display = click.style("Executed", fg="blue")
|
|
611
740
|
else:
|
|
612
741
|
status_display = status
|
|
613
742
|
|
|
614
|
-
tool = chain.get(
|
|
615
|
-
target = chain.get(
|
|
616
|
-
decided =
|
|
743
|
+
tool = chain.get("tool", "")[:15]
|
|
744
|
+
target = chain.get("target", "")[:25]
|
|
745
|
+
decided = (
|
|
746
|
+
chain.get("decided_at", "")[:10] if chain.get("decided_at") else "N/A"
|
|
747
|
+
)
|
|
617
748
|
|
|
618
|
-
click.echo(
|
|
749
|
+
click.echo(
|
|
750
|
+
f" {chain_id:>4} │ {status_display:<10} │ {tool:<15} │ {target:<25} │ {decided}"
|
|
751
|
+
)
|
|
619
752
|
|
|
620
753
|
click.echo("─" * width)
|
|
621
754
|
click.echo()
|
|
@@ -631,29 +764,38 @@ def _view_chain_history(engagement_id: int = None):
|
|
|
631
764
|
click.echo(" " + " ".join(nav_options))
|
|
632
765
|
|
|
633
766
|
# Filters
|
|
634
|
-
filter_options = [
|
|
767
|
+
filter_options = [
|
|
768
|
+
"[a] Approved",
|
|
769
|
+
"[r] Rejected",
|
|
770
|
+
"[e] Executed",
|
|
771
|
+
"[x] Clear filter",
|
|
772
|
+
]
|
|
635
773
|
click.echo(" " + " ".join(filter_options))
|
|
636
774
|
click.echo()
|
|
637
775
|
|
|
638
776
|
try:
|
|
639
|
-
choice =
|
|
777
|
+
choice = (
|
|
778
|
+
click.prompt("Select option", default="0", show_default=False)
|
|
779
|
+
.strip()
|
|
780
|
+
.lower()
|
|
781
|
+
)
|
|
640
782
|
|
|
641
|
-
if choice ==
|
|
783
|
+
if choice == "q":
|
|
642
784
|
return
|
|
643
|
-
elif choice ==
|
|
785
|
+
elif choice == "n" and current_page < total_pages - 1:
|
|
644
786
|
current_page += 1
|
|
645
|
-
elif choice ==
|
|
787
|
+
elif choice == "p" and current_page > 0:
|
|
646
788
|
current_page -= 1
|
|
647
|
-
elif choice ==
|
|
789
|
+
elif choice == "a":
|
|
648
790
|
filter_status = CHAIN_APPROVED
|
|
649
791
|
current_page = 0
|
|
650
|
-
elif choice ==
|
|
792
|
+
elif choice == "r":
|
|
651
793
|
filter_status = CHAIN_REJECTED
|
|
652
794
|
current_page = 0
|
|
653
|
-
elif choice ==
|
|
795
|
+
elif choice == "e":
|
|
654
796
|
filter_status = CHAIN_EXECUTED
|
|
655
797
|
current_page = 0
|
|
656
|
-
elif choice ==
|
|
798
|
+
elif choice == "x":
|
|
657
799
|
filter_status = None
|
|
658
800
|
current_page = 0
|
|
659
801
|
|
|
@@ -672,11 +814,13 @@ def show_active_orchestration_help():
|
|
|
672
814
|
|
|
673
815
|
# Header
|
|
674
816
|
console.print()
|
|
675
|
-
console.print(
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
817
|
+
console.print(
|
|
818
|
+
Panel(
|
|
819
|
+
"[bold cyan]Active Orchestration Guide[/bold cyan]",
|
|
820
|
+
box=box.DOUBLE,
|
|
821
|
+
padding=(0, 2),
|
|
822
|
+
)
|
|
823
|
+
)
|
|
680
824
|
console.print()
|
|
681
825
|
|
|
682
826
|
# Overview section
|
|
@@ -705,7 +849,9 @@ def show_active_orchestration_help():
|
|
|
705
849
|
console.print("[bold yellow]▸ Chain Statistics[/bold yellow]")
|
|
706
850
|
console.print(" " + "─" * 40)
|
|
707
851
|
console.print()
|
|
708
|
-
console.print(
|
|
852
|
+
console.print(
|
|
853
|
+
" [yellow]⏳ Pending[/yellow] - Chains waiting for your approval/rejection"
|
|
854
|
+
)
|
|
709
855
|
console.print(" [green]✓ Approved[/green] - Chains approved, ready to execute")
|
|
710
856
|
console.print(" [red]✗ Rejected[/red] - Chains you declined to run")
|
|
711
857
|
console.print(" [blue]▶ Executed[/blue] - Chains that have been run as jobs")
|
|
@@ -715,24 +861,42 @@ def show_active_orchestration_help():
|
|
|
715
861
|
console.print("[bold yellow]▸ Available Actions[/bold yellow]")
|
|
716
862
|
console.print(" " + "─" * 40)
|
|
717
863
|
console.print()
|
|
718
|
-
console.print(
|
|
719
|
-
|
|
864
|
+
console.print(
|
|
865
|
+
" [magenta][1][/magenta] Review & Approve - Select chains individually to approve/reject"
|
|
866
|
+
)
|
|
867
|
+
console.print(
|
|
868
|
+
" [magenta][2][/magenta] Approve All - Approve all pending chains at once"
|
|
869
|
+
)
|
|
720
870
|
console.print(" [magenta][3][/magenta] Reject All - Reject all pending chains")
|
|
721
|
-
console.print(
|
|
722
|
-
|
|
723
|
-
|
|
871
|
+
console.print(
|
|
872
|
+
" [magenta][4][/magenta] Execute Approved - Run all approved chains as jobs"
|
|
873
|
+
)
|
|
874
|
+
console.print(
|
|
875
|
+
" [magenta][5][/magenta] Toggle Mode - Switch between AUTO and APPROVAL modes"
|
|
876
|
+
)
|
|
877
|
+
console.print(
|
|
878
|
+
" [magenta][6][/magenta] View History - See previously executed/rejected chains"
|
|
879
|
+
)
|
|
724
880
|
console.print()
|
|
725
881
|
|
|
726
882
|
# Tips section
|
|
727
883
|
console.print("[bold yellow]▸ Tips[/bold yellow]")
|
|
728
884
|
console.print(" " + "─" * 40)
|
|
729
885
|
console.print()
|
|
730
|
-
console.print(
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
console.print(
|
|
886
|
+
console.print(
|
|
887
|
+
" • Review chain [bold]priority[/bold] - higher priority chains target more valuable findings"
|
|
888
|
+
)
|
|
889
|
+
console.print(
|
|
890
|
+
" • Check the [bold]parent job[/bold] to understand what triggered the chain"
|
|
891
|
+
)
|
|
892
|
+
console.print(
|
|
893
|
+
" • Use [bold]Reject All[/bold] to clear the queue if chains are stale"
|
|
894
|
+
)
|
|
895
|
+
console.print(
|
|
896
|
+
" • Toggle to [bold]APPROVAL MODE[/bold] before running sensitive scans"
|
|
897
|
+
)
|
|
734
898
|
console.print()
|
|
735
899
|
|
|
736
900
|
# Footer
|
|
737
901
|
console.print("[dim]Press any key to return...[/dim]")
|
|
738
|
-
click.pause(
|
|
902
|
+
click.pause("")
|