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.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {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, get_pending_chain, approve_chain, reject_chain,
14
- approve_all_pending, reject_all_pending, get_chain_stats,
15
- CHAIN_PENDING, CHAIN_APPROVED, CHAIN_REJECTED, CHAIN_EXECUTED
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(chaining, preview_page, engagement_id)
40
+ preview_page, total_pages = _display_pending_dashboard(
41
+ chaining, preview_page, engagement_id
42
+ )
33
43
 
34
44
  options = [
35
- {'number': 1, 'label': 'Review & Approve Chains', 'description': 'View pending chains and approve/reject them'},
36
- {'number': 2, 'label': 'Approve All Pending', 'description': 'Approve all pending chains at once'},
37
- {'number': 3, 'label': 'Reject All Pending', 'description': 'Reject all pending chains'},
38
- {'number': 4, 'label': 'Execute Approved', 'description': 'Run all approved chains now'},
39
- {'number': 5, 'label': 'Toggle Approval Mode', 'description': 'Switch between auto/approval modes'},
40
- {'number': 6, 'label': 'View Chain History', 'description': 'See executed and rejected chains'},
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 = {'?': -3} # Help shortcut
78
+ shortcuts = {"?": -3} # Help shortcut
45
79
  if total_pages > 1:
46
80
  if preview_page > 0:
47
- shortcuts['p'] = -1 # Previous page
81
+ shortcuts["p"] = -1 # Previous page
48
82
  if preview_page < total_pages - 1:
49
- shortcuts['n'] = -2 # Next page
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(chaining: ToolChaining, preview_page: int = 0, engagement_id: int = None) -> Tuple[int, int]:
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("│" + click.style(" PENDING CHAINS - ACTIVE ORCHESTRATION ".center(width - 2), bold=True, fg='cyan') + "│")
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='yellow', bold=True)
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='green', bold=True)
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='bright_black'))
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['pending']
128
- approved = stats['approved']
129
- rejected = stats['rejected']
130
- executed = stats['executed']
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='cyan'))
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(f" ⏳ Pending: {click.style(str(pending), fg='yellow', bold=True)} chains awaiting your decision")
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(f" ✓ Approved: {click.style(str(approved), fg='green', bold=True)} ready to execute")
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(status=CHAIN_PENDING, engagement_id=engagement_id, limit=PAGE_SIZE, offset=offset)
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(click.style("📋 PENDING CHAINS PREVIEW ", bold=True, fg='cyan') +
162
- click.style(page_info, fg='bright_black'))
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('tool', 'unknown')
167
- target = chain.get('target', '')[:30]
168
- priority = chain.get('priority', 5)
221
+ tool = chain.get("tool", "unknown")
222
+ target = chain.get("target", "")[:30]
223
+ priority = chain.get("priority", 5)
169
224
 
170
- pri_color = 'green' if priority >= 8 else 'yellow' if priority >= 5 else 'white'
171
- click.echo(f" {click.style(tool.upper(), fg='magenta')} {target} "
172
- f"(Priority: {click.style(str(priority), fg=pri_color)})")
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='bright_black'))
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(status=CHAIN_PENDING, engagement_id=engagement_id, limit=200)
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='green'))
205
- click.echo(click.style(" All chains have been processed.", fg='bright_black'))
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
- 'id': chain['id'],
215
- 'tool': chain.get('tool', ''),
216
- 'target': chain.get('target', ''),
217
- 'priority': chain.get('priority', 5),
218
- 'parent_id': chain.get('parent_job_id', '?'),
219
- 'args': chain.get('args', [])
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
- {'name': '#', 'width': 5, 'key': 'id', 'justify': 'right'},
225
- {'name': 'Tool', 'width': 15, 'key': 'tool'},
226
- {'name': 'Target', 'width': 35, 'key': 'target'},
227
- {'name': 'Priority', 'width': 8, 'key': 'priority', 'justify': 'center'},
228
- {'name': 'From', 'width': 10, 'key': 'parent_id'},
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 == 'priority':
298
+ if key == "priority":
234
299
  pri = value or 5
235
300
  if pri >= 8:
236
- return f'[green]{pri}[/green]'
301
+ return f"[green]{pri}[/green]"
237
302
  elif pri >= 5:
238
- return f'[yellow]{pri}[/yellow]'
303
+ return f"[yellow]{pri}[/yellow]"
239
304
  return str(pri)
240
- if key == 'parent_id':
241
- return f'Job #{value}'
242
- if key == 'target':
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('id'),
253
- title=f'SELECT PENDING CHAINS ({total_pending} total)',
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("│" + click.style(" CHAIN ACTIONS ".center(width - 2), bold=True, fg='cyan') + "│")
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(f" Selected: {click.style(str(len(selected_ids)), fg='cyan', bold=True)} chain(s)")
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['id'] == chain_id), None)
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(" " + click.style("[a]", fg='green', bold=True) + " Approve selected")
283
- click.echo(" " + click.style("[r]", fg='red', bold=True) + " Reject selected")
284
- click.echo(" " + click.style("[d]", fg='cyan', bold=True) + " View details (first selected)")
285
- click.echo(" " + click.style("[q]", fg='white', bold=True) + " Back to selection")
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 = click.prompt("Select option", default="0", show_default=False).strip().lower()
368
+ choice = (
369
+ click.prompt("Select option", default="0", show_default=False)
370
+ .strip()
371
+ .lower()
372
+ )
290
373
 
291
- if choice == 'q':
374
+ if choice == "q":
292
375
  continue # Go back to selection
293
- elif choice == 'a':
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(click.style(f"\n ✓ Approved {approved} chain(s)", fg='green'))
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 == 'r':
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(click.style(f"\n ✗ Rejected {rejected} chain(s)", fg='yellow'))
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 == 'd':
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='red'))
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='cyan'))
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('status', 'unknown')
425
+ status = chain.get("status", "unknown")
339
426
  if status == CHAIN_PENDING:
340
- status_display = click.style("⏳ PENDING", fg='yellow', bold=True)
427
+ status_display = click.style("⏳ PENDING", fg="yellow", bold=True)
341
428
  elif status == CHAIN_APPROVED:
342
- status_display = click.style("✓ APPROVED", fg='green', bold=True)
429
+ status_display = click.style("✓ APPROVED", fg="green", bold=True)
343
430
  elif status == CHAIN_REJECTED:
344
- status_display = click.style("✗ REJECTED", fg='red', bold=True)
431
+ status_display = click.style("✗ REJECTED", fg="red", bold=True)
345
432
  elif status == CHAIN_EXECUTED:
346
- status_display = click.style("▶ EXECUTED", fg='blue', bold=True)
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(f" Tool: {click.style(chain.get('tool', 'unknown').upper(), fg='magenta')}")
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('args', [])
450
+ args = chain.get("args", [])
362
451
  if args:
363
452
  click.echo(click.style(" Arguments:", bold=True))
364
- args_str = ' '.join(str(a) for a in args)
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('rule_description', '')
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('decided_at'):
469
+ if chain.get("decided_at"):
381
470
  click.echo(f" Decided: {chain.get('decided_at')}")
382
- if chain.get('executed_at'):
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('job_id'):
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 = click.prompt("Select option", default="0", show_default=False).strip().lower()
401
- if action == 'a':
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='green'))
496
+ click.echo(click.style("\n ✓ Chain approved!", fg="green"))
404
497
  else:
405
- click.echo(click.style("\n Failed to approve chain", fg='red'))
498
+ click.echo(click.style("\n Failed to approve chain", fg="red"))
406
499
  click.pause()
407
- elif action == 'r':
500
+ elif action == "r":
408
501
  if reject_chain(chain_id):
409
- click.echo(click.style("\n ✗ Chain rejected", fg='yellow'))
502
+ click.echo(click.style("\n ✗ Chain rejected", fg="yellow"))
410
503
  else:
411
- click.echo(click.style("\n Failed to reject chain", fg='red'))
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['pending']
520
+ pending = stats["pending"]
428
521
 
429
522
  if pending == 0:
430
- click.echo(click.style("\n No pending chains to approve!", fg='green'))
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(f" This will approve {click.style(str(pending), fg='yellow', bold=True)} pending chains.")
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='green'))
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='green'))
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['pending']
552
+ pending = stats["pending"]
458
553
 
459
554
  if pending == 0:
460
- click.echo(click.style("\n No pending chains to reject!", fg='green'))
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(click.style(f" ⚠️ This will reject {pending} pending chains!", fg='red', bold=True))
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='yellow'), default=False):
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='yellow'))
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['approved']
582
+ approved = stats["approved"]
484
583
 
485
584
  if approved == 0:
486
- click.echo(click.style("\n No approved chains to execute!", fg='yellow'))
487
- click.echo(click.style(" Use option [1] to review and approve pending chains first.", fg='bright_black'))
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(f" Ready to execute {click.style(str(approved), fg='green', bold=True)} approved chains.")
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='green'))
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='yellow', bold=True))
514
- click.echo(" Switching to: " + click.style("AUTO", fg='green', bold=True))
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='green', bold=True))
519
- click.echo(" Switching to: " + click.style("APPROVAL", fg='yellow', bold=True))
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='yellow'))
529
- click.echo(click.style(" Chains will now queue for your approval.", fg='bright_black'))
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='green'))
532
- click.echo(click.style(" Chains will execute automatically.", fg='bright_black'))
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('status') != CHAIN_PENDING]
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 = [c for c in history_chains if c.get('status') == filter_status]
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='bright_black'))
676
+ click.echo("\n" + click.style(" No chain history yet!", fg="bright_black"))
563
677
  if filter_status:
564
- click.echo(click.style(f" (filtered by: {filter_status})", fg='bright_black'))
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("│" + click.style(" CHAIN HISTORY ".center(width - 2), bold=True, fg='cyan') + "│")
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='green')
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='red')
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='blue')
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(click.style(f"📜 HISTORY ", bold=True, fg='cyan') + click.style(page_info, fg='bright_black') + filter_text)
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(click.style(f" {'#':>4} │ {'Status':<10} │ {'Tool':<15} │ {'Target':<25} │ Decided", bold=True))
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['id']
730
+ chain_id = chain["id"]
602
731
 
603
732
  # Status with color
604
- status = chain.get('status', '')
733
+ status = chain.get("status", "")
605
734
  if status == CHAIN_APPROVED:
606
- status_display = click.style("Approved", fg='green')
735
+ status_display = click.style("Approved", fg="green")
607
736
  elif status == CHAIN_REJECTED:
608
- status_display = click.style("Rejected", fg='red')
737
+ status_display = click.style("Rejected", fg="red")
609
738
  elif status == CHAIN_EXECUTED:
610
- status_display = click.style("Executed", fg='blue')
739
+ status_display = click.style("Executed", fg="blue")
611
740
  else:
612
741
  status_display = status
613
742
 
614
- tool = chain.get('tool', '')[:15]
615
- target = chain.get('target', '')[:25]
616
- decided = chain.get('decided_at', '')[:10] if chain.get('decided_at') else 'N/A'
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(f" {chain_id:>4} │ {status_display:<10} │ {tool:<15} │ {target:<25} │ {decided}")
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 = ["[a] Approved", "[r] Rejected", "[e] Executed", "[x] Clear filter"]
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 = click.prompt("Select option", default="0", show_default=False).strip().lower()
777
+ choice = (
778
+ click.prompt("Select option", default="0", show_default=False)
779
+ .strip()
780
+ .lower()
781
+ )
640
782
 
641
- if choice == 'q':
783
+ if choice == "q":
642
784
  return
643
- elif choice == 'n' and current_page < total_pages - 1:
785
+ elif choice == "n" and current_page < total_pages - 1:
644
786
  current_page += 1
645
- elif choice == 'p' and current_page > 0:
787
+ elif choice == "p" and current_page > 0:
646
788
  current_page -= 1
647
- elif choice == 'a':
789
+ elif choice == "a":
648
790
  filter_status = CHAIN_APPROVED
649
791
  current_page = 0
650
- elif choice == 'r':
792
+ elif choice == "r":
651
793
  filter_status = CHAIN_REJECTED
652
794
  current_page = 0
653
- elif choice == 'e':
795
+ elif choice == "e":
654
796
  filter_status = CHAIN_EXECUTED
655
797
  current_page = 0
656
- elif choice == 'x':
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(Panel(
676
- "[bold cyan]Active Orchestration Guide[/bold cyan]",
677
- box=box.DOUBLE,
678
- padding=(0, 2)
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(" [yellow]⏳ Pending[/yellow] - Chains waiting for your approval/rejection")
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(" [magenta][1][/magenta] Review & Approve - Select chains individually to approve/reject")
719
- console.print(" [magenta][2][/magenta] Approve All - Approve all pending chains at once")
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(" [magenta][4][/magenta] Execute Approved - Run all approved chains as jobs")
722
- console.print(" [magenta][5][/magenta] Toggle Mode - Switch between AUTO and APPROVAL modes")
723
- console.print(" [magenta][6][/magenta] View History - See previously executed/rejected chains")
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(" • Review chain [bold]priority[/bold] - higher priority chains target more valuable findings")
731
- console.print(" • Check the [bold]parent job[/bold] to understand what triggered the chain")
732
- console.print(" • Use [bold]Reject All[/bold] to clear the queue if chains are stale")
733
- console.print(" • Toggle to [bold]APPROVAL MODE[/bold] before running sensitive scans")
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("")