souleyez 2.43.29__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 +22827 -10678
  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.29.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.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -25,7 +25,13 @@ def manage_chain_rules():
25
25
 
26
26
  # Header
27
27
  click.echo("\nā”Œ" + "─" * (width - 2) + "┐")
28
- click.echo("│" + click.style(" CHAIN RULE MANAGEMENT ".center(width - 2), bold=True, fg='cyan') + "│")
28
+ click.echo(
29
+ "│"
30
+ + click.style(
31
+ " CHAIN RULE MANAGEMENT ".center(width - 2), bold=True, fg="cyan"
32
+ )
33
+ + "│"
34
+ )
29
35
  click.echo("ā””" + "─" * (width - 2) + "ā”˜")
30
36
  click.echo()
31
37
 
@@ -44,11 +50,18 @@ def manage_chain_rules():
44
50
  tool_list = sorted(rules_by_tool.keys())
45
51
 
46
52
  # Page info
47
- click.echo(f" Total Rules: {total_rules} │ āœ“ Enabled: {enabled_rules} │ āœ— Disabled: {disabled_rules}")
53
+ click.echo(
54
+ f" Total Rules: {total_rules} │ āœ“ Enabled: {enabled_rules} │ āœ— Disabled: {disabled_rules}"
55
+ )
48
56
  click.echo()
49
57
 
50
58
  # Table header with checkbox and category columns
51
- click.echo(click.style(f" ā—‹ {'Tool':<20} │ Enabled │ Disabled │ Total │ šŸŽÆ │ šŸ¢ │ āš™ļø │ Status", bold=True))
59
+ click.echo(
60
+ click.style(
61
+ f" ā—‹ {'Tool':<20} │ Enabled │ Disabled │ Total │ šŸŽÆ │ šŸ¢ │ āš™ļø │ Status",
62
+ bold=True,
63
+ )
64
+ )
52
65
  click.echo(" " + "─" * (width - 2))
53
66
 
54
67
  # Display tool groups
@@ -59,32 +72,50 @@ def manage_chain_rules():
59
72
  total = len(rules)
60
73
 
61
74
  # Category counts
62
- ctf_count = sum(1 for r in rules if r.category == 'ctf')
63
- enterprise_count = sum(1 for r in rules if r.category == 'enterprise')
64
- general_count = sum(1 for r in rules if r.category == 'general')
75
+ ctf_count = sum(1 for r in rules if r.category == "ctf")
76
+ enterprise_count = sum(1 for r in rules if r.category == "enterprise")
77
+ general_count = sum(1 for r in rules if r.category == "general")
65
78
 
66
79
  # Checkbox for multi-select
67
- checkbox = 'ā—' if tool in selected_tools else 'ā—‹'
80
+ checkbox = "ā—" if tool in selected_tools else "ā—‹"
68
81
 
69
82
  # Icon based on status
70
83
  if tool_disabled == 0:
71
- icon = click.style("āœ“", fg='green', bold=True)
84
+ icon = click.style("āœ“", fg="green", bold=True)
72
85
  elif tool_enabled == 0:
73
- icon = click.style("āœ—", fg='red')
86
+ icon = click.style("āœ—", fg="red")
74
87
  else:
75
- icon = click.style("◐", fg='yellow')
88
+ icon = click.style("◐", fg="yellow")
76
89
 
77
90
  # Tool name
78
91
  tool_display = f"{tool.upper():<20}"
79
- on_display = click.style(f"{tool_enabled:>7}", fg='green')
80
- off_display = click.style(f"{tool_disabled:>8}", fg='yellow') if tool_disabled > 0 else f"{tool_disabled:>8}"
92
+ on_display = click.style(f"{tool_enabled:>7}", fg="green")
93
+ off_display = (
94
+ click.style(f"{tool_disabled:>8}", fg="yellow")
95
+ if tool_disabled > 0
96
+ else f"{tool_disabled:>8}"
97
+ )
81
98
 
82
99
  # Category displays (dim if 0)
83
- ctf_display = click.style(f"{ctf_count:>4}", fg='bright_black') if ctf_count == 0 else f"{ctf_count:>4}"
84
- ent_display = click.style(f"{enterprise_count:>4}", fg='bright_black') if enterprise_count == 0 else f"{enterprise_count:>4}"
85
- gen_display = click.style(f"{general_count:>4}", fg='bright_black') if general_count == 0 else f"{general_count:>4}"
100
+ ctf_display = (
101
+ click.style(f"{ctf_count:>4}", fg="bright_black")
102
+ if ctf_count == 0
103
+ else f"{ctf_count:>4}"
104
+ )
105
+ ent_display = (
106
+ click.style(f"{enterprise_count:>4}", fg="bright_black")
107
+ if enterprise_count == 0
108
+ else f"{enterprise_count:>4}"
109
+ )
110
+ gen_display = (
111
+ click.style(f"{general_count:>4}", fg="bright_black")
112
+ if general_count == 0
113
+ else f"{general_count:>4}"
114
+ )
86
115
 
87
- click.echo(f" {checkbox} {click.style(tool_display, fg='cyan')} │ {on_display} │ {off_display} │ {total:>5} │ {ctf_display} │ {ent_display} │ {gen_display} │ {icon}")
116
+ click.echo(
117
+ f" {checkbox} {click.style(tool_display, fg='cyan')} │ {on_display} │ {off_display} │ {total:>5} │ {ctf_display} │ {ent_display} │ {gen_display} │ {icon}"
118
+ )
88
119
 
89
120
  click.echo()
90
121
 
@@ -93,9 +124,17 @@ def manage_chain_rules():
93
124
  click.echo()
94
125
 
95
126
  # Brute-force warning
96
- brute_force_enabled = sum(1 for r in chaining.rules if r.target_tool == 'hydra' and r.enabled)
127
+ brute_force_enabled = sum(
128
+ 1 for r in chaining.rules if r.target_tool == "hydra" and r.enabled
129
+ )
97
130
  if brute_force_enabled > 0:
98
- click.echo(click.style(f" āš ļø WARNING: {brute_force_enabled} brute-force rules are ACTIVE!", fg='red', bold=True))
131
+ click.echo(
132
+ click.style(
133
+ f" āš ļø WARNING: {brute_force_enabled} brute-force rules are ACTIVE!",
134
+ fg="red",
135
+ bold=True,
136
+ )
137
+ )
99
138
  click.echo()
100
139
 
101
140
  # Separator + inline menu
@@ -112,29 +151,34 @@ def manage_chain_rules():
112
151
  click.echo()
113
152
 
114
153
  try:
115
- choice = click.prompt("Select option", default="0", show_default=False).strip().lower()
154
+ choice = (
155
+ click.prompt("Select option", default="0", show_default=False)
156
+ .strip()
157
+ .lower()
158
+ )
116
159
 
117
- if choice == '?':
160
+ if choice == "?":
118
161
  # Show auto-chaining help guide
119
162
  from souleyez.ui.interactive import show_auto_chaining_help
163
+
120
164
  show_auto_chaining_help()
121
165
  continue
122
- elif choice == 'q':
166
+ elif choice == "q":
123
167
  return
124
- elif choice == 'i':
168
+ elif choice == "i":
125
169
  # Interactive multi-select mode for tool groups
126
170
  _run_tool_groups_interactive(chaining, rules_by_tool, selected_tools)
127
- elif choice == 't':
171
+ elif choice == "t":
128
172
  _toggle_rule_interactive(chaining)
129
- elif choice == 'v':
173
+ elif choice == "v":
130
174
  _view_rule_details(chaining)
131
- elif choice == 'f':
175
+ elif choice == "f":
132
176
  _filter_by_status(chaining)
133
- elif choice == 'e':
177
+ elif choice == "e":
134
178
  _enable_all_rules(chaining)
135
- elif choice == 'd':
179
+ elif choice == "d":
136
180
  _disable_all_rules(chaining)
137
- elif choice == 'r':
181
+ elif choice == "r":
138
182
  _reset_to_defaults(chaining)
139
183
 
140
184
  except (KeyboardInterrupt, EOFError):
@@ -146,17 +190,33 @@ def _toggle_single_rule(chaining, rule_idx: int):
146
190
  rule = chaining.rules[rule_idx]
147
191
 
148
192
  # Safety warning for enabling brute-force rules
149
- if not rule.enabled and rule.target_tool == 'hydra':
193
+ if not rule.enabled and rule.target_tool == "hydra":
150
194
  click.echo()
151
- click.echo(click.style("āš ļø WARNING: You are about to enable a BRUTE-FORCE rule!", fg='red', bold=True))
152
- click.echo(click.style(" This may cause account lockouts or trigger security alerts.", fg='red'))
195
+ click.echo(
196
+ click.style(
197
+ "āš ļø WARNING: You are about to enable a BRUTE-FORCE rule!",
198
+ fg="red",
199
+ bold=True,
200
+ )
201
+ )
202
+ click.echo(
203
+ click.style(
204
+ " This may cause account lockouts or trigger security alerts.",
205
+ fg="red",
206
+ )
207
+ )
153
208
  click.echo()
154
209
  click.echo(f" Rule: {rule.trigger_tool}→{rule.target_tool}")
155
210
  click.echo(f" Description: {rule.description}")
156
211
  click.echo()
157
212
 
158
- if not click.confirm(click.style("Are you sure you want to enable this rule?", fg='yellow', bold=True), default=False):
159
- click.echo(click.style("\nāœ“ Rule toggle cancelled", fg='green'))
213
+ if not click.confirm(
214
+ click.style(
215
+ "Are you sure you want to enable this rule?", fg="yellow", bold=True
216
+ ),
217
+ default=False,
218
+ ):
219
+ click.echo(click.style("\nāœ“ Rule toggle cancelled", fg="green"))
160
220
  click.pause()
161
221
  return
162
222
 
@@ -165,10 +225,16 @@ def _toggle_single_rule(chaining, rule_idx: int):
165
225
  chaining.save_rules()
166
226
 
167
227
  status = "ENABLED" if rule.enabled else "DISABLED"
168
- status_color = 'green' if rule.enabled else 'yellow'
228
+ status_color = "green" if rule.enabled else "yellow"
169
229
 
170
230
  click.echo()
171
- click.echo(click.style(f"āœ“ Rule {status}: {rule.trigger_tool}→{rule.target_tool}", fg=status_color, bold=True))
231
+ click.echo(
232
+ click.style(
233
+ f"āœ“ Rule {status}: {rule.trigger_tool}→{rule.target_tool}",
234
+ fg=status_color,
235
+ bold=True,
236
+ )
237
+ )
172
238
  click.pause()
173
239
 
174
240
 
@@ -191,72 +257,79 @@ def _run_tool_groups_interactive(chaining, rules_by_tool: dict, selected_tools:
191
257
  else:
192
258
  status = "◐"
193
259
 
194
- tool_items.append({
195
- 'tool': tool,
196
- 'enabled': tool_enabled,
197
- 'disabled': tool_disabled,
198
- 'total': len(rules),
199
- 'status': status
200
- })
260
+ tool_items.append(
261
+ {
262
+ "tool": tool,
263
+ "enabled": tool_enabled,
264
+ "disabled": tool_disabled,
265
+ "total": len(rules),
266
+ "status": status,
267
+ }
268
+ )
201
269
 
202
270
  columns = [
203
- {'name': 'Tool', 'width': 20, 'key': 'tool'},
204
- {'name': 'Enabled', 'width': 8, 'key': 'enabled', 'justify': 'right'},
205
- {'name': 'Disabled', 'width': 9, 'key': 'disabled', 'justify': 'right'},
206
- {'name': 'Total', 'width': 6, 'key': 'total', 'justify': 'right'},
207
- {'name': 'Status', 'width': 8, 'key': 'status'},
271
+ {"name": "Tool", "width": 20, "key": "tool"},
272
+ {"name": "Enabled", "width": 8, "key": "enabled", "justify": "right"},
273
+ {"name": "Disabled", "width": 9, "key": "disabled", "justify": "right"},
274
+ {"name": "Total", "width": 6, "key": "total", "justify": "right"},
275
+ {"name": "Status", "width": 8, "key": "status"},
208
276
  ]
209
277
 
210
278
  def format_tool_cell(item: dict, key: str) -> str:
211
279
  value = item.get(key)
212
- if key == 'tool':
280
+ if key == "tool":
213
281
  return value.upper()
214
- if key == 'enabled':
215
- return f'[green]{value}[/green]'
216
- if key == 'disabled':
217
- return f'[yellow]{value}[/yellow]' if value > 0 else str(value)
218
- if key == 'status':
219
- if value == 'āœ“':
220
- return '[green]āœ“[/green]'
221
- elif value == 'āœ—':
222
- return '[red]āœ—[/red]'
282
+ if key == "enabled":
283
+ return f"[green]{value}[/green]"
284
+ if key == "disabled":
285
+ return f"[yellow]{value}[/yellow]" if value > 0 else str(value)
286
+ if key == "status":
287
+ if value == "āœ“":
288
+ return "[green]āœ“[/green]"
289
+ elif value == "āœ—":
290
+ return "[red]āœ—[/red]"
223
291
  else:
224
- return '[yellow]◐[/yellow]'
225
- return str(value) if value is not None else '-'
292
+ return "[yellow]◐[/yellow]"
293
+ return str(value) if value is not None else "-"
226
294
 
227
295
  while True:
228
296
  interactive_select(
229
297
  items=tool_items,
230
298
  columns=columns,
231
299
  selected_ids=selected_tools,
232
- get_id=lambda t: t.get('tool'),
233
- title='SELECT TOOL GROUPS',
234
- format_cell=format_tool_cell
300
+ get_id=lambda t: t.get("tool"),
301
+ title="SELECT TOOL GROUPS",
302
+ format_cell=format_tool_cell,
235
303
  )
236
304
 
237
305
  if not selected_tools:
238
306
  return
239
307
 
240
308
  result = _tool_groups_bulk_action_menu(chaining, rules_by_tool, selected_tools)
241
- if result == 'back':
309
+ if result == "back":
242
310
  return
243
- elif result == 'clear':
311
+ elif result == "clear":
244
312
  selected_tools.clear()
245
313
 
246
314
 
247
- def _tool_groups_bulk_action_menu(chaining, rules_by_tool: dict, selected_tools: set) -> str:
315
+ def _tool_groups_bulk_action_menu(
316
+ chaining, rules_by_tool: dict, selected_tools: set
317
+ ) -> str:
248
318
  """Show bulk action menu for selected tool groups."""
249
319
  from rich.console import Console
320
+
250
321
  console = Console()
251
322
 
252
323
  if not selected_tools:
253
- return 'continue'
324
+ return "continue"
254
325
 
255
326
  # Count rules in selected groups
256
327
  total_rules = sum(len(rules_by_tool.get(tool, [])) for tool in selected_tools)
257
328
 
258
329
  console.print()
259
- console.print(f" [bold]Selected: {len(selected_tools)} tool group(s) ({total_rules} rules)[/bold]")
330
+ console.print(
331
+ f" [bold]Selected: {len(selected_tools)} tool group(s) ({total_rules} rules)[/bold]"
332
+ )
260
333
  console.print(" \\[v] View All Rules")
261
334
  console.print(" \\[e] Enable All Rules")
262
335
  console.print(" \\[d] Disable All Rules")
@@ -264,34 +337,58 @@ def _tool_groups_bulk_action_menu(chaining, rules_by_tool: dict, selected_tools:
264
337
  console.print()
265
338
 
266
339
  try:
267
- action = click.prompt(" Select option", default="0", show_default=False).strip().lower()
340
+ action = (
341
+ click.prompt(" Select option", default="0", show_default=False)
342
+ .strip()
343
+ .lower()
344
+ )
268
345
 
269
- if action == 'q':
270
- return 'back'
271
- elif action == 'v':
346
+ if action == "q":
347
+ return "back"
348
+ elif action == "v":
272
349
  # View all rules in selected groups
273
350
  DesignSystem.clear_screen()
274
351
  width = DesignSystem.get_terminal_width()
275
352
 
276
353
  click.echo("\nā”Œ" + "─" * (width - 2) + "┐")
277
- click.echo("│" + click.style(" SELECTED TOOL GROUPS ".center(width - 2), bold=True, fg='cyan') + "│")
354
+ click.echo(
355
+ "│"
356
+ + click.style(
357
+ " SELECTED TOOL GROUPS ".center(width - 2), bold=True, fg="cyan"
358
+ )
359
+ + "│"
360
+ )
278
361
  click.echo("ā””" + "─" * (width - 2) + "ā”˜")
279
362
  click.echo()
280
363
 
281
364
  for tool in sorted(selected_tools):
282
365
  rules = rules_by_tool.get(tool, [])
283
- click.echo(click.style(f" {tool.upper()} ({len(rules)} rules)", bold=True, fg='cyan'))
366
+ click.echo(
367
+ click.style(
368
+ f" {tool.upper()} ({len(rules)} rules)", bold=True, fg="cyan"
369
+ )
370
+ )
284
371
 
285
372
  for rule in rules:
286
- status = click.style("ON", fg='green') if rule.enabled else click.style("OFF", fg='red')
287
- warning = click.style(" āš ļø", fg='red') if rule.target_tool == 'hydra' else ""
288
- click.echo(f" → {rule.target_tool}{warning} | {rule.trigger_condition} | {status}")
373
+ status = (
374
+ click.style("ON", fg="green")
375
+ if rule.enabled
376
+ else click.style("OFF", fg="red")
377
+ )
378
+ warning = (
379
+ click.style(" āš ļø", fg="red")
380
+ if rule.target_tool == "hydra"
381
+ else ""
382
+ )
383
+ click.echo(
384
+ f" → {rule.target_tool}{warning} | {rule.trigger_condition} | {status}"
385
+ )
289
386
 
290
387
  click.echo()
291
388
 
292
389
  click.pause()
293
- return 'continue'
294
- elif action == 'e':
390
+ return "continue"
391
+ elif action == "e":
295
392
  # Enable all rules in selected groups
296
393
  enabled = 0
297
394
  for tool in selected_tools:
@@ -301,10 +398,15 @@ def _tool_groups_bulk_action_menu(chaining, rules_by_tool: dict, selected_tools:
301
398
  enabled += 1
302
399
 
303
400
  chaining.save_rules()
304
- click.echo(click.style(f"\nāœ“ Enabled {enabled} rule(s) in {len(selected_tools)} group(s)", fg='green'))
401
+ click.echo(
402
+ click.style(
403
+ f"\nāœ“ Enabled {enabled} rule(s) in {len(selected_tools)} group(s)",
404
+ fg="green",
405
+ )
406
+ )
305
407
  click.pause()
306
- return 'clear'
307
- elif action == 'd':
408
+ return "clear"
409
+ elif action == "d":
308
410
  # Disable all rules in selected groups
309
411
  disabled = 0
310
412
  for tool in selected_tools:
@@ -314,14 +416,19 @@ def _tool_groups_bulk_action_menu(chaining, rules_by_tool: dict, selected_tools:
314
416
  disabled += 1
315
417
 
316
418
  chaining.save_rules()
317
- click.echo(click.style(f"\nāœ“ Disabled {disabled} rule(s) in {len(selected_tools)} group(s)", fg='yellow'))
419
+ click.echo(
420
+ click.style(
421
+ f"\nāœ“ Disabled {disabled} rule(s) in {len(selected_tools)} group(s)",
422
+ fg="yellow",
423
+ )
424
+ )
318
425
  click.pause()
319
- return 'clear'
426
+ return "clear"
320
427
 
321
428
  except (KeyboardInterrupt, EOFError):
322
429
  pass
323
430
 
324
- return 'continue'
431
+ return "continue"
325
432
 
326
433
 
327
434
  def _display_rules_dashboard(chaining: ToolChaining):
@@ -332,7 +439,11 @@ def _display_rules_dashboard(chaining: ToolChaining):
332
439
 
333
440
  # Header
334
441
  click.echo("\nā”Œ" + "─" * (width - 2) + "┐")
335
- click.echo("│" + click.style(" CHAIN RULE MANAGEMENT ".center(width - 2), bold=True, fg='cyan') + "│")
442
+ click.echo(
443
+ "│"
444
+ + click.style(" CHAIN RULE MANAGEMENT ".center(width - 2), bold=True, fg="cyan")
445
+ + "│"
446
+ )
336
447
  click.echo("ā””" + "─" * (width - 2) + "ā”˜")
337
448
  click.echo()
338
449
 
@@ -342,14 +453,16 @@ def _display_rules_dashboard(chaining: ToolChaining):
342
453
  disabled_rules = total_rules - enabled_rules
343
454
 
344
455
  # Categorize rules
345
- brute_force_rules = [r for r in chaining.rules if r.target_tool == 'hydra']
456
+ brute_force_rules = [r for r in chaining.rules if r.target_tool == "hydra"]
346
457
  brute_force_disabled = [r for r in brute_force_rules if not r.enabled]
347
458
  brute_force_enabled = len(brute_force_rules) - len(brute_force_disabled)
348
459
 
349
- click.echo(click.style("šŸ“Š OVERVIEW", bold=True, fg='cyan'))
460
+ click.echo(click.style("šŸ“Š OVERVIEW", bold=True, fg="cyan"))
350
461
  click.echo("─" * width)
351
462
  click.echo(f" Total Rules: {total_rules}")
352
- click.echo(f" āœ“ Enabled: {click.style(str(enabled_rules), fg='green', bold=True)}")
463
+ click.echo(
464
+ f" āœ“ Enabled: {click.style(str(enabled_rules), fg='green', bold=True)}"
465
+ )
353
466
  click.echo(f" āœ— Disabled: {click.style(str(disabled_rules), fg='yellow')}")
354
467
  click.echo()
355
468
 
@@ -361,9 +474,17 @@ def _display_rules_dashboard(chaining: ToolChaining):
361
474
  rules_by_tool[rule.trigger_tool].append(rule)
362
475
 
363
476
  # Display collapsed tool summary as table (FIRST)
364
- click.echo(click.style("⚔ TOOL GROUPS", bold=True, fg='cyan') + click.style(" Use option [3] to expand", fg='bright_black'))
477
+ click.echo(
478
+ click.style("⚔ TOOL GROUPS", bold=True, fg="cyan")
479
+ + click.style(" Use option [3] to expand", fg="bright_black")
480
+ )
365
481
  click.echo("─" * width)
366
- click.echo(click.style(f" {'Tool':<20} │ Enabled │ Disabled │ Total │ šŸŽÆ │ šŸ¢ │ āš™ļø │ Status", bold=True))
482
+ click.echo(
483
+ click.style(
484
+ f" {'Tool':<20} │ Enabled │ Disabled │ Total │ šŸŽÆ │ šŸ¢ │ āš™ļø │ Status",
485
+ bold=True,
486
+ )
487
+ )
367
488
  click.echo("─" * width)
368
489
 
369
490
  for tool in sorted(rules_by_tool.keys()):
@@ -373,66 +494,107 @@ def _display_rules_dashboard(chaining: ToolChaining):
373
494
  total = len(rules)
374
495
 
375
496
  # Category counts
376
- ctf_count = sum(1 for r in rules if r.category == 'ctf')
377
- enterprise_count = sum(1 for r in rules if r.category == 'enterprise')
378
- general_count = sum(1 for r in rules if r.category == 'general')
497
+ ctf_count = sum(1 for r in rules if r.category == "ctf")
498
+ enterprise_count = sum(1 for r in rules if r.category == "enterprise")
499
+ general_count = sum(1 for r in rules if r.category == "general")
379
500
 
380
501
  # Icon based on status
381
502
  if tool_disabled == 0:
382
- icon = click.style("āœ“", fg='green', bold=True)
503
+ icon = click.style("āœ“", fg="green", bold=True)
383
504
  elif tool_enabled == 0:
384
- icon = click.style("āœ—", fg='red')
505
+ icon = click.style("āœ—", fg="red")
385
506
  else:
386
- icon = click.style("◐", fg='yellow') # Partial
507
+ icon = click.style("◐", fg="yellow") # Partial
387
508
 
388
509
  # Tool name
389
510
  tool_display = f"{tool.upper():<20}"
390
- on_display = click.style(f"{tool_enabled:>7}", fg='green')
391
- off_display = click.style(f"{tool_disabled:>8}", fg='yellow') if tool_disabled > 0 else f"{tool_disabled:>8}"
511
+ on_display = click.style(f"{tool_enabled:>7}", fg="green")
512
+ off_display = (
513
+ click.style(f"{tool_disabled:>8}", fg="yellow")
514
+ if tool_disabled > 0
515
+ else f"{tool_disabled:>8}"
516
+ )
392
517
 
393
518
  # Category displays (dim if 0)
394
- ctf_display = click.style(f"{ctf_count:>4}", fg='bright_black') if ctf_count == 0 else f"{ctf_count:>4}"
395
- ent_display = click.style(f"{enterprise_count:>4}", fg='bright_black') if enterprise_count == 0 else f"{enterprise_count:>4}"
396
- gen_display = click.style(f"{general_count:>4}", fg='bright_black') if general_count == 0 else f"{general_count:>4}"
519
+ ctf_display = (
520
+ click.style(f"{ctf_count:>4}", fg="bright_black")
521
+ if ctf_count == 0
522
+ else f"{ctf_count:>4}"
523
+ )
524
+ ent_display = (
525
+ click.style(f"{enterprise_count:>4}", fg="bright_black")
526
+ if enterprise_count == 0
527
+ else f"{enterprise_count:>4}"
528
+ )
529
+ gen_display = (
530
+ click.style(f"{general_count:>4}", fg="bright_black")
531
+ if general_count == 0
532
+ else f"{general_count:>4}"
533
+ )
397
534
 
398
- click.echo(f" {click.style(tool_display, fg='cyan')} │ {on_display} │ {off_display} │ {total:>5} │ {ctf_display} │ {ent_display} │ {gen_display} │ {icon}")
535
+ click.echo(
536
+ f" {click.style(tool_display, fg='cyan')} │ {on_display} │ {off_display} │ {total:>5} │ {ctf_display} │ {ent_display} │ {gen_display} │ {icon}"
537
+ )
399
538
 
400
539
  click.echo("─" * width)
401
540
  click.echo()
402
541
 
403
542
  # Highlight disabled security-sensitive rules (AFTER Tool Groups)
404
543
  if brute_force_disabled:
405
- click.echo(click.style("šŸ” BRUTE-FORCE RULES (Disabled by default for safety)", bold=True, fg='yellow'))
544
+ click.echo(
545
+ click.style(
546
+ "šŸ” BRUTE-FORCE RULES (Disabled by default for safety)",
547
+ bold=True,
548
+ fg="yellow",
549
+ )
550
+ )
406
551
  click.echo("─" * width)
407
552
  for rule in brute_force_disabled:
408
- click.echo(f" āœ— {click.style(rule.trigger_tool.upper(), fg='cyan')} → "
409
- f"{click.style('Hydra', fg='magenta')} | {rule.trigger_condition}")
410
- click.echo(click.style(f" {rule.description}", fg='bright_black'))
553
+ click.echo(
554
+ f" āœ— {click.style(rule.trigger_tool.upper(), fg='cyan')} → "
555
+ f"{click.style('Hydra', fg='magenta')} | {rule.trigger_condition}"
556
+ )
557
+ click.echo(click.style(f" {rule.description}", fg="bright_black"))
411
558
  click.echo()
412
559
 
413
560
  if brute_force_enabled > 0:
414
- click.echo(click.style(f"āš ļø WARNING: {brute_force_enabled} brute-force rules are ACTIVE!",
415
- fg='red', bold=True))
416
- click.echo(click.style(" These may trigger account lockouts. Review option [1] to disable.", fg='red'))
561
+ click.echo(
562
+ click.style(
563
+ f"āš ļø WARNING: {brute_force_enabled} brute-force rules are ACTIVE!",
564
+ fg="red",
565
+ bold=True,
566
+ )
567
+ )
568
+ click.echo(
569
+ click.style(
570
+ " These may trigger account lockouts. Review option [1] to disable.",
571
+ fg="red",
572
+ )
573
+ )
417
574
  click.echo()
418
575
 
419
576
 
420
577
  def _rules_bulk_action_menu(chaining, selected_ids: set) -> str:
421
578
  """Show bulk action menu for selected rules."""
422
579
  from rich.console import Console
580
+
423
581
  console = Console()
424
582
 
425
- selected_rules = [(idx, chaining.rules[idx]) for idx in selected_ids if idx < len(chaining.rules)]
583
+ selected_rules = [
584
+ (idx, chaining.rules[idx]) for idx in selected_ids if idx < len(chaining.rules)
585
+ ]
426
586
 
427
587
  if not selected_rules:
428
- return 'continue'
588
+ return "continue"
429
589
 
430
590
  # Check for brute-force rules in selection
431
- has_hydra = any(rule.target_tool == 'hydra' for _, rule in selected_rules)
591
+ has_hydra = any(rule.target_tool == "hydra" for _, rule in selected_rules)
432
592
  hydra_warning = " āš ļø" if has_hydra else ""
433
593
 
434
594
  console.print()
435
- console.print(f" [bold]Selected: {len(selected_rules)} rule(s){hydra_warning}[/bold]")
595
+ console.print(
596
+ f" [bold]Selected: {len(selected_rules)} rule(s){hydra_warning}[/bold]"
597
+ )
436
598
  console.print(" \\[v] View All Rules")
437
599
  console.print(" \\[e] Enable All Rules")
438
600
  console.print(" \\[d] Disable All Rules")
@@ -440,48 +602,79 @@ def _rules_bulk_action_menu(chaining, selected_ids: set) -> str:
440
602
  console.print()
441
603
 
442
604
  try:
443
- action = click.prompt(" Select option", default="0", show_default=False).strip().lower()
605
+ action = (
606
+ click.prompt(" Select option", default="0", show_default=False)
607
+ .strip()
608
+ .lower()
609
+ )
444
610
 
445
- if action == 'q':
446
- return 'back'
447
- elif action == 'v':
611
+ if action == "q":
612
+ return "back"
613
+ elif action == "v":
448
614
  # View all selected rules (show details)
449
615
  DesignSystem.clear_screen()
450
616
  width = DesignSystem.get_terminal_width()
451
617
 
452
618
  click.echo("\nā”Œ" + "─" * (width - 2) + "┐")
453
- click.echo("│" + click.style(" SELECTED RULES ".center(width - 2), bold=True, fg='cyan') + "│")
619
+ click.echo(
620
+ "│"
621
+ + click.style(
622
+ " SELECTED RULES ".center(width - 2), bold=True, fg="cyan"
623
+ )
624
+ + "│"
625
+ )
454
626
  click.echo("ā””" + "─" * (width - 2) + "ā”˜")
455
627
  click.echo()
456
628
 
457
629
  for idx, rule in selected_rules:
458
- status = click.style("ON", fg='green') if rule.enabled else click.style("OFF", fg='red')
459
- warning = click.style(" āš ļø", fg='red') if rule.target_tool == 'hydra' else ""
630
+ status = (
631
+ click.style("ON", fg="green")
632
+ if rule.enabled
633
+ else click.style("OFF", fg="red")
634
+ )
635
+ warning = (
636
+ click.style(" āš ļø", fg="red") if rule.target_tool == "hydra" else ""
637
+ )
460
638
  cat_icon = CATEGORY_ICONS.get(rule.category, "āš™ļø")
461
639
 
462
- click.echo(f" [{idx + 1}] {rule.trigger_tool} → {rule.target_tool}{warning}")
640
+ click.echo(
641
+ f" [{idx + 1}] {rule.trigger_tool} → {rule.target_tool}{warning}"
642
+ )
463
643
  click.echo(f" Condition: {rule.trigger_condition}")
464
- click.echo(f" Priority: {rule.priority} Category: {cat_icon} Status: {status}")
644
+ click.echo(
645
+ f" Priority: {rule.priority} Category: {cat_icon} Status: {status}"
646
+ )
465
647
  if rule.description:
466
- click.echo(click.style(f" {rule.description}", fg='bright_black'))
648
+ click.echo(
649
+ click.style(f" {rule.description}", fg="bright_black")
650
+ )
467
651
  click.echo()
468
652
 
469
653
  click.pause()
470
- return 'continue'
471
- elif action == 'e':
654
+ return "continue"
655
+ elif action == "e":
472
656
  # Enable selected - warn about brute-force
473
657
  enabling_hydra = any(
474
- not chaining.rules[idx].enabled and chaining.rules[idx].target_tool == 'hydra'
658
+ not chaining.rules[idx].enabled
659
+ and chaining.rules[idx].target_tool == "hydra"
475
660
  for idx in selected_ids
476
661
  )
477
662
 
478
663
  if enabling_hydra:
479
664
  click.echo()
480
- click.echo(click.style("āš ļø WARNING: Selection includes BRUTE-FORCE rules!", fg='red', bold=True))
481
- if not click.confirm(click.style("Proceed with enabling?", fg='yellow'), default=False):
482
- click.echo(click.style("\nāœ“ Enable cancelled", fg='green'))
665
+ click.echo(
666
+ click.style(
667
+ "āš ļø WARNING: Selection includes BRUTE-FORCE rules!",
668
+ fg="red",
669
+ bold=True,
670
+ )
671
+ )
672
+ if not click.confirm(
673
+ click.style("Proceed with enabling?", fg="yellow"), default=False
674
+ ):
675
+ click.echo(click.style("\nāœ“ Enable cancelled", fg="green"))
483
676
  click.pause()
484
- return 'continue'
677
+ return "continue"
485
678
 
486
679
  enabled = 0
487
680
  for idx in selected_ids:
@@ -490,10 +683,10 @@ def _rules_bulk_action_menu(chaining, selected_ids: set) -> str:
490
683
  enabled += 1
491
684
 
492
685
  chaining.save_rules()
493
- click.echo(click.style(f"\nāœ“ Enabled {enabled} rule(s)", fg='green'))
686
+ click.echo(click.style(f"\nāœ“ Enabled {enabled} rule(s)", fg="green"))
494
687
  click.pause()
495
- return 'clear'
496
- elif action == 'd':
688
+ return "clear"
689
+ elif action == "d":
497
690
  # Disable selected
498
691
  disabled = 0
499
692
  for idx in selected_ids:
@@ -502,19 +695,23 @@ def _rules_bulk_action_menu(chaining, selected_ids: set) -> str:
502
695
  disabled += 1
503
696
 
504
697
  chaining.save_rules()
505
- click.echo(click.style(f"\nāœ“ Disabled {disabled} rule(s)", fg='yellow'))
698
+ click.echo(click.style(f"\nāœ“ Disabled {disabled} rule(s)", fg="yellow"))
506
699
  click.pause()
507
- return 'clear'
700
+ return "clear"
508
701
 
509
702
  except (KeyboardInterrupt, EOFError):
510
703
  pass
511
704
 
512
- return 'continue'
705
+ return "continue"
513
706
 
514
707
 
515
708
  def _toggle_rule_interactive(chaining: ToolChaining):
516
709
  """Interactive rule toggle with paginated table view."""
517
- from souleyez.core.tool_chaining import CATEGORY_CTF, CATEGORY_ENTERPRISE, CATEGORY_GENERAL
710
+ from souleyez.core.tool_chaining import (
711
+ CATEGORY_CTF,
712
+ CATEGORY_ENTERPRISE,
713
+ CATEGORY_GENERAL,
714
+ )
518
715
 
519
716
  page_size = 20
520
717
  current_page = 0
@@ -534,15 +731,23 @@ def _toggle_rule_interactive(chaining: ToolChaining):
534
731
  else:
535
732
  filtered_rules = [(i, r) for i, r in filtered_rules if not r.enabled]
536
733
  if filter_category is not None:
537
- filtered_rules = [(i, r) for i, r in filtered_rules if r.category == filter_category]
734
+ filtered_rules = [
735
+ (i, r) for i, r in filtered_rules if r.category == filter_category
736
+ ]
538
737
 
539
738
  total_filtered = len(filtered_rules)
540
739
  total_pages = max(1, math.ceil(total_filtered / page_size))
541
- current_page = min(current_page, total_pages - 1) # Adjust if filter reduced pages
740
+ current_page = min(
741
+ current_page, total_pages - 1
742
+ ) # Adjust if filter reduced pages
542
743
 
543
744
  # Header box
544
745
  click.echo("\nā”Œ" + "─" * (width - 2) + "┐")
545
- click.echo("│" + click.style(" TOGGLE CHAIN RULE ".center(width - 2), bold=True, fg='cyan') + "│")
746
+ click.echo(
747
+ "│"
748
+ + click.style(" TOGGLE CHAIN RULE ".center(width - 2), bold=True, fg="cyan")
749
+ + "│"
750
+ )
546
751
  click.echo("ā””" + "─" * (width - 2) + "ā”˜")
547
752
  click.echo()
548
753
 
@@ -550,33 +755,54 @@ def _toggle_rule_interactive(chaining: ToolChaining):
550
755
  total_rules = len(chaining.rules)
551
756
  enabled_rules = sum(1 for r in chaining.rules if r.enabled)
552
757
  disabled_rules = total_rules - enabled_rules
553
- brute_force_rules = sum(1 for r in chaining.rules if r.target_tool == 'hydra')
758
+ brute_force_rules = sum(1 for r in chaining.rules if r.target_tool == "hydra")
554
759
  ctf_rules = sum(1 for r in chaining.rules if r.category == CATEGORY_CTF)
555
- enterprise_rules = sum(1 for r in chaining.rules if r.category == CATEGORY_ENTERPRISE)
760
+ enterprise_rules = sum(
761
+ 1 for r in chaining.rules if r.category == CATEGORY_ENTERPRISE
762
+ )
556
763
  general_rules = sum(1 for r in chaining.rules if r.category == CATEGORY_GENERAL)
557
764
 
558
- click.echo(click.style("šŸ“Š OVERVIEW", bold=True, fg='cyan'))
765
+ click.echo(click.style("šŸ“Š OVERVIEW", bold=True, fg="cyan"))
559
766
  click.echo("─" * width)
560
- click.echo(f" Total: {total_rules} │ {click.style(str(enabled_rules), fg='green')} enabled │ {click.style(str(disabled_rules), fg='yellow')} disabled │ {click.style(str(brute_force_rules), fg='red')} brute-force │ šŸŽÆ {ctf_rules} šŸ¢ {enterprise_rules} āš™ļø {general_rules}")
767
+ click.echo(
768
+ f" Total: {total_rules} │ {click.style(str(enabled_rules), fg='green')} enabled │ {click.style(str(disabled_rules), fg='yellow')} disabled │ {click.style(str(brute_force_rules), fg='red')} brute-force │ šŸŽÆ {ctf_rules} šŸ¢ {enterprise_rules} āš™ļø {general_rules}"
769
+ )
561
770
  click.echo()
562
771
 
563
772
  # Page info with filter indicator
564
773
  filter_parts = []
565
774
  if filter_status is True:
566
- filter_parts.append(click.style("Enabled", fg='green'))
775
+ filter_parts.append(click.style("Enabled", fg="green"))
567
776
  elif filter_status is False:
568
- filter_parts.append(click.style("Disabled", fg='yellow'))
777
+ filter_parts.append(click.style("Disabled", fg="yellow"))
569
778
  if filter_category is not None:
570
779
  cat_icon = CATEGORY_ICONS.get(filter_category, "āš™ļø")
571
- cat_name = {"ctf": "CTF", "enterprise": "Enterprise", "general": "General"}.get(filter_category, filter_category)
780
+ cat_name = {
781
+ "ctf": "CTF",
782
+ "enterprise": "Enterprise",
783
+ "general": "General",
784
+ }.get(filter_category, filter_category)
572
785
  filter_parts.append(f"{cat_icon} {cat_name}")
573
- filter_text = click.style(f" [Filter: {', '.join(filter_parts)}]", fg='bright_black') if filter_parts else ""
786
+ filter_text = (
787
+ click.style(f" [Filter: {', '.join(filter_parts)}]", fg="bright_black")
788
+ if filter_parts
789
+ else ""
790
+ )
574
791
  page_info = f"Page {current_page + 1}/{total_pages}"
575
- click.echo(click.style(f"šŸ“‹ RULES ", bold=True, fg='cyan') + click.style(page_info, fg='bright_black') + filter_text)
792
+ click.echo(
793
+ click.style(f"šŸ“‹ RULES ", bold=True, fg="cyan")
794
+ + click.style(page_info, fg="bright_black")
795
+ + filter_text
796
+ )
576
797
  click.echo("─" * width)
577
798
 
578
799
  # Table header - with checkbox column and Fired column
579
- click.echo(click.style(f" ā—‹ │ # │ {'Trigger':<22} │ {'Target':<22} │ {'Condition':<30} │ Priority │ Category │ Status │ Fired", bold=True))
800
+ click.echo(
801
+ click.style(
802
+ f" ā—‹ │ # │ {'Trigger':<22} │ {'Target':<22} │ {'Condition':<30} │ Priority │ Category │ Status │ Fired",
803
+ bold=True,
804
+ )
805
+ )
580
806
  click.echo("─" * width)
581
807
 
582
808
  # Calculate slice for current page
@@ -589,31 +815,61 @@ def _toggle_rule_interactive(chaining: ToolChaining):
589
815
  rule_num = original_idx + 1 # Show original rule number for toggling
590
816
 
591
817
  # Checkbox for multi-select
592
- checkbox = 'ā—' if original_idx in selected_rule_ids else 'ā—‹'
818
+ checkbox = "ā—" if original_idx in selected_rule_ids else "ā—‹"
593
819
 
594
820
  # Use full names (no truncation)
595
- trigger = rule.trigger_tool[:22] if len(rule.trigger_tool) <= 22 else rule.trigger_tool[:19] + "..."
596
- target = rule.target_tool[:22] if len(rule.target_tool) <= 22 else rule.target_tool[:19] + "..."
597
- condition = rule.trigger_condition[:30] if len(rule.trigger_condition) <= 30 else rule.trigger_condition[:27] + "..."
821
+ trigger = (
822
+ rule.trigger_tool[:22]
823
+ if len(rule.trigger_tool) <= 22
824
+ else rule.trigger_tool[:19] + "..."
825
+ )
826
+ target = (
827
+ rule.target_tool[:22]
828
+ if len(rule.target_tool) <= 22
829
+ else rule.target_tool[:19] + "..."
830
+ )
831
+ condition = (
832
+ rule.trigger_condition[:30]
833
+ if len(rule.trigger_condition) <= 30
834
+ else rule.trigger_condition[:27] + "..."
835
+ )
598
836
 
599
837
  # Status with color and brute-force warning
600
- if rule.target_tool == 'hydra':
601
- status = click.style(" ON", fg='green') + click.style(" āš ļø", fg='red') if rule.enabled else click.style("OFF", fg='red') + click.style(" āš ļø", fg='red')
838
+ if rule.target_tool == "hydra":
839
+ status = (
840
+ click.style(" ON", fg="green") + click.style(" āš ļø", fg="red")
841
+ if rule.enabled
842
+ else click.style("OFF", fg="red") + click.style(" āš ļø", fg="red")
843
+ )
602
844
  else:
603
- status = click.style(" ON", fg='green') if rule.enabled else click.style("OFF", fg='red')
845
+ status = (
846
+ click.style(" ON", fg="green")
847
+ if rule.enabled
848
+ else click.style("OFF", fg="red")
849
+ )
604
850
 
605
851
  # Priority with color (8-char width to match "Priority" header)
606
- pri_color = 'green' if rule.priority >= 8 else 'yellow' if rule.priority >= 5 else 'white'
852
+ pri_color = (
853
+ "green"
854
+ if rule.priority >= 8
855
+ else "yellow" if rule.priority >= 5 else "white"
856
+ )
607
857
  priority = click.style(f"{rule.priority:>8}", fg=pri_color)
608
858
 
609
859
  # Category icon (8-char width to match "Category" header)
610
860
  cat_icon = CATEGORY_ICONS.get(rule.category, "āš™ļø")
611
861
 
612
862
  # Trigger count with color
613
- fired_count = rule.trigger_count if hasattr(rule, 'trigger_count') else 0
614
- fired_display = click.style(f"{fired_count:>5}", fg='green') if fired_count > 0 else click.style(f"{fired_count:>5}", fg='bright_black')
863
+ fired_count = rule.trigger_count if hasattr(rule, "trigger_count") else 0
864
+ fired_display = (
865
+ click.style(f"{fired_count:>5}", fg="green")
866
+ if fired_count > 0
867
+ else click.style(f"{fired_count:>5}", fg="bright_black")
868
+ )
615
869
 
616
- click.echo(f" {checkbox} │ {rule_num:>4} │ {trigger:<22} │ {target:<22} │ {condition:<30} │ {priority} │ {cat_icon} │ {status} │ {fired_display}")
870
+ click.echo(
871
+ f" {checkbox} │ {rule_num:>4} │ {trigger:<22} │ {target:<22} │ {condition:<30} │ {priority} │ {cat_icon} │ {status} │ {fired_display}"
872
+ )
617
873
 
618
874
  click.echo("─" * width)
619
875
  click.echo()
@@ -637,25 +893,29 @@ def _toggle_rule_interactive(chaining: ToolChaining):
637
893
  click.echo()
638
894
 
639
895
  try:
640
- choice = click.prompt("Select option", default="0", show_default=False).strip().lower()
896
+ choice = (
897
+ click.prompt("Select option", default="0", show_default=False)
898
+ .strip()
899
+ .lower()
900
+ )
641
901
 
642
- if choice == 'q':
902
+ if choice == "q":
643
903
  return
644
- elif choice == 'n' and current_page < total_pages - 1:
904
+ elif choice == "n" and current_page < total_pages - 1:
645
905
  current_page += 1
646
- elif choice == 'p' and current_page > 0:
906
+ elif choice == "p" and current_page > 0:
647
907
  current_page -= 1
648
- elif choice == 'e':
908
+ elif choice == "e":
649
909
  filter_status = True
650
910
  current_page = 0
651
- elif choice == 'd':
911
+ elif choice == "d":
652
912
  filter_status = False
653
913
  current_page = 0
654
- elif choice == 'a':
914
+ elif choice == "a":
655
915
  filter_status = None
656
916
  filter_category = None
657
917
  current_page = 0
658
- elif choice == 'c':
918
+ elif choice == "c":
659
919
  # Category filter submenu
660
920
  click.echo()
661
921
  click.echo(click.style(" Select category:", bold=True))
@@ -663,14 +923,16 @@ def _toggle_rule_interactive(chaining: ToolChaining):
663
923
  click.echo(f" [2] šŸ¢ Enterprise - Real-world testing")
664
924
  click.echo(f" [3] āš™ļø General - Standard recon")
665
925
  click.echo(f" [q] Cancel")
666
- cat_choice = click.prompt(" Select option", default="0", show_default=False).strip()
667
- if cat_choice == '1':
926
+ cat_choice = click.prompt(
927
+ " Select option", default="0", show_default=False
928
+ ).strip()
929
+ if cat_choice == "1":
668
930
  filter_category = CATEGORY_CTF
669
931
  current_page = 0
670
- elif cat_choice == '2':
932
+ elif cat_choice == "2":
671
933
  filter_category = CATEGORY_ENTERPRISE
672
934
  current_page = 0
673
- elif cat_choice == '3':
935
+ elif cat_choice == "3":
674
936
  filter_category = CATEGORY_GENERAL
675
937
  current_page = 0
676
938
  elif choice.isdigit():
@@ -679,17 +941,37 @@ def _toggle_rule_interactive(chaining: ToolChaining):
679
941
  rule = chaining.rules[rule_idx]
680
942
 
681
943
  # Safety warning for enabling brute-force rules
682
- if not rule.enabled and rule.target_tool == 'hydra':
944
+ if not rule.enabled and rule.target_tool == "hydra":
683
945
  click.echo()
684
- click.echo(click.style("āš ļø WARNING: You are about to enable a BRUTE-FORCE rule!", fg='red', bold=True))
685
- click.echo(click.style(" This may cause account lockouts or trigger security alerts.", fg='red'))
946
+ click.echo(
947
+ click.style(
948
+ "āš ļø WARNING: You are about to enable a BRUTE-FORCE rule!",
949
+ fg="red",
950
+ bold=True,
951
+ )
952
+ )
953
+ click.echo(
954
+ click.style(
955
+ " This may cause account lockouts or trigger security alerts.",
956
+ fg="red",
957
+ )
958
+ )
686
959
  click.echo()
687
960
  click.echo(f" Rule: {rule.trigger_tool}→{rule.target_tool}")
688
961
  click.echo(f" Description: {rule.description}")
689
962
  click.echo()
690
963
 
691
- if not click.confirm(click.style("Are you sure you want to enable this rule?", fg='yellow', bold=True), default=False):
692
- click.echo(click.style("\nāœ“ Rule toggle cancelled", fg='green'))
964
+ if not click.confirm(
965
+ click.style(
966
+ "Are you sure you want to enable this rule?",
967
+ fg="yellow",
968
+ bold=True,
969
+ ),
970
+ default=False,
971
+ ):
972
+ click.echo(
973
+ click.style("\nāœ“ Rule toggle cancelled", fg="green")
974
+ )
693
975
  click.pause()
694
976
  continue
695
977
 
@@ -698,19 +980,30 @@ def _toggle_rule_interactive(chaining: ToolChaining):
698
980
  chaining.save_rules() # Persist the change
699
981
 
700
982
  status = "ENABLED" if rule.enabled else "DISABLED"
701
- status_color = 'green' if rule.enabled else 'yellow'
983
+ status_color = "green" if rule.enabled else "yellow"
702
984
 
703
985
  click.echo()
704
- click.echo(click.style(f"āœ“ Rule {status}: {rule.trigger_tool}→{rule.target_tool}", fg=status_color, bold=True))
986
+ click.echo(
987
+ click.style(
988
+ f"āœ“ Rule {status}: {rule.trigger_tool}→{rule.target_tool}",
989
+ fg=status_color,
990
+ bold=True,
991
+ )
992
+ )
705
993
 
706
994
  if rule.enabled:
707
- click.echo(click.style(" Note: Worker will apply changes on next job completion", fg='bright_black'))
995
+ click.echo(
996
+ click.style(
997
+ " Note: Worker will apply changes on next job completion",
998
+ fg="bright_black",
999
+ )
1000
+ )
708
1001
 
709
1002
  click.pause()
710
1003
  else:
711
- click.echo(click.style("\nāœ— Invalid rule number", fg='red'))
1004
+ click.echo(click.style("\nāœ— Invalid rule number", fg="red"))
712
1005
  click.pause()
713
- elif choice == 'i':
1006
+ elif choice == "i":
714
1007
  # Interactive multi-select mode (respects current filters)
715
1008
  from souleyez.ui.interactive_selector import interactive_select
716
1009
 
@@ -718,42 +1011,50 @@ def _toggle_rule_interactive(chaining: ToolChaining):
718
1011
  # Convert FILTERED rules to dicts for selector
719
1012
  rule_items = []
720
1013
  for original_idx, rule in filtered_rules:
721
- rule_items.append({
722
- 'idx': original_idx,
723
- 'trigger_tool': rule.trigger_tool,
724
- 'target_tool': rule.target_tool,
725
- 'condition': rule.trigger_condition,
726
- 'enabled': rule.enabled,
727
- 'priority': rule.priority,
728
- 'category': rule.category
729
- })
1014
+ rule_items.append(
1015
+ {
1016
+ "idx": original_idx,
1017
+ "trigger_tool": rule.trigger_tool,
1018
+ "target_tool": rule.target_tool,
1019
+ "condition": rule.trigger_condition,
1020
+ "enabled": rule.enabled,
1021
+ "priority": rule.priority,
1022
+ "category": rule.category,
1023
+ }
1024
+ )
730
1025
 
731
1026
  columns = [
732
- {'name': '#', 'width': 5, 'key': 'idx', 'justify': 'right'},
733
- {'name': 'Trigger', 'width': 18, 'key': 'trigger_tool'},
734
- {'name': 'Target', 'width': 18, 'key': 'target_tool'},
735
- {'name': 'Condition', 'width': 25, 'key': 'condition'},
736
- {'name': 'Cat', 'width': 5, 'key': 'category'},
737
- {'name': 'Status', 'width': 8, 'key': 'enabled'},
1027
+ {"name": "#", "width": 5, "key": "idx", "justify": "right"},
1028
+ {"name": "Trigger", "width": 18, "key": "trigger_tool"},
1029
+ {"name": "Target", "width": 18, "key": "target_tool"},
1030
+ {"name": "Condition", "width": 25, "key": "condition"},
1031
+ {"name": "Cat", "width": 5, "key": "category"},
1032
+ {"name": "Status", "width": 8, "key": "enabled"},
738
1033
  ]
739
1034
 
740
1035
  def format_rule_cell(item: dict, key: str) -> str:
741
1036
  value = item.get(key)
742
- if key == 'enabled':
743
- return '[green]ON[/green]' if value else '[red]OFF[/red]'
744
- if key == 'idx':
1037
+ if key == "enabled":
1038
+ return "[green]ON[/green]" if value else "[red]OFF[/red]"
1039
+ if key == "idx":
745
1040
  return str(value + 1) # 1-indexed for display
746
- if key == 'category':
747
- return CATEGORY_ICONS.get(value, 'āš™ļø')
748
- return str(value) if value else '-'
1041
+ if key == "category":
1042
+ return CATEGORY_ICONS.get(value, "āš™ļø")
1043
+ return str(value) if value else "-"
749
1044
 
750
1045
  # Build title with filter info
751
- title = 'SELECT RULES'
1046
+ title = "SELECT RULES"
752
1047
  if filter_category or filter_status is not None:
753
1048
  filter_info = []
754
1049
  if filter_category:
755
- cat_name = {"ctf": "CTF", "enterprise": "Enterprise", "general": "General"}.get(filter_category, "")
756
- filter_info.append(f"{CATEGORY_ICONS.get(filter_category, '')} {cat_name}")
1050
+ cat_name = {
1051
+ "ctf": "CTF",
1052
+ "enterprise": "Enterprise",
1053
+ "general": "General",
1054
+ }.get(filter_category, "")
1055
+ filter_info.append(
1056
+ f"{CATEGORY_ICONS.get(filter_category, '')} {cat_name}"
1057
+ )
757
1058
  if filter_status is True:
758
1059
  filter_info.append("Enabled")
759
1060
  elif filter_status is False:
@@ -764,18 +1065,18 @@ def _toggle_rule_interactive(chaining: ToolChaining):
764
1065
  items=rule_items,
765
1066
  columns=columns,
766
1067
  selected_ids=selected_rule_ids,
767
- get_id=lambda r: r.get('idx'),
1068
+ get_id=lambda r: r.get("idx"),
768
1069
  title=title,
769
- format_cell=format_rule_cell
1070
+ format_cell=format_rule_cell,
770
1071
  )
771
1072
 
772
1073
  if not selected_rule_ids:
773
1074
  break
774
1075
 
775
1076
  result = _rules_bulk_action_menu(chaining, selected_rule_ids)
776
- if result == 'back':
1077
+ if result == "back":
777
1078
  break
778
- elif result == 'clear':
1079
+ elif result == "clear":
779
1080
  selected_rule_ids.clear()
780
1081
  except (KeyboardInterrupt, EOFError):
781
1082
  return
@@ -783,7 +1084,11 @@ def _toggle_rule_interactive(chaining: ToolChaining):
783
1084
 
784
1085
  def _view_rule_details(chaining: ToolChaining):
785
1086
  """Display detailed information about a rule with paginated table view."""
786
- from souleyez.core.tool_chaining import CATEGORY_CTF, CATEGORY_ENTERPRISE, CATEGORY_GENERAL
1087
+ from souleyez.core.tool_chaining import (
1088
+ CATEGORY_CTF,
1089
+ CATEGORY_ENTERPRISE,
1090
+ CATEGORY_GENERAL,
1091
+ )
787
1092
 
788
1093
  page_size = 20
789
1094
  current_page = 0
@@ -803,15 +1108,23 @@ def _view_rule_details(chaining: ToolChaining):
803
1108
  else:
804
1109
  filtered_rules = [(i, r) for i, r in filtered_rules if not r.enabled]
805
1110
  if filter_category is not None:
806
- filtered_rules = [(i, r) for i, r in filtered_rules if r.category == filter_category]
1111
+ filtered_rules = [
1112
+ (i, r) for i, r in filtered_rules if r.category == filter_category
1113
+ ]
807
1114
 
808
1115
  total_filtered = len(filtered_rules)
809
1116
  total_pages = max(1, math.ceil(total_filtered / page_size))
810
- current_page = min(current_page, total_pages - 1) # Adjust if filter reduced pages
1117
+ current_page = min(
1118
+ current_page, total_pages - 1
1119
+ ) # Adjust if filter reduced pages
811
1120
 
812
1121
  # Header box
813
1122
  click.echo("\nā”Œ" + "─" * (width - 2) + "┐")
814
- click.echo("│" + click.style(" VIEW RULE DETAILS ".center(width - 2), bold=True, fg='cyan') + "│")
1123
+ click.echo(
1124
+ "│"
1125
+ + click.style(" VIEW RULE DETAILS ".center(width - 2), bold=True, fg="cyan")
1126
+ + "│"
1127
+ )
815
1128
  click.echo("ā””" + "─" * (width - 2) + "ā”˜")
816
1129
  click.echo()
817
1130
 
@@ -819,33 +1132,54 @@ def _view_rule_details(chaining: ToolChaining):
819
1132
  total_rules = len(chaining.rules)
820
1133
  enabled_rules = sum(1 for r in chaining.rules if r.enabled)
821
1134
  disabled_rules = total_rules - enabled_rules
822
- brute_force_rules = sum(1 for r in chaining.rules if r.target_tool == 'hydra')
1135
+ brute_force_rules = sum(1 for r in chaining.rules if r.target_tool == "hydra")
823
1136
  ctf_rules = sum(1 for r in chaining.rules if r.category == CATEGORY_CTF)
824
- enterprise_rules = sum(1 for r in chaining.rules if r.category == CATEGORY_ENTERPRISE)
1137
+ enterprise_rules = sum(
1138
+ 1 for r in chaining.rules if r.category == CATEGORY_ENTERPRISE
1139
+ )
825
1140
  general_rules = sum(1 for r in chaining.rules if r.category == CATEGORY_GENERAL)
826
1141
 
827
- click.echo(click.style("šŸ“Š OVERVIEW", bold=True, fg='cyan'))
1142
+ click.echo(click.style("šŸ“Š OVERVIEW", bold=True, fg="cyan"))
828
1143
  click.echo("─" * width)
829
- click.echo(f" Total: {total_rules} │ {click.style(str(enabled_rules), fg='green')} enabled │ {click.style(str(disabled_rules), fg='yellow')} disabled │ {click.style(str(brute_force_rules), fg='red')} brute-force │ šŸŽÆ {ctf_rules} šŸ¢ {enterprise_rules} āš™ļø {general_rules}")
1144
+ click.echo(
1145
+ f" Total: {total_rules} │ {click.style(str(enabled_rules), fg='green')} enabled │ {click.style(str(disabled_rules), fg='yellow')} disabled │ {click.style(str(brute_force_rules), fg='red')} brute-force │ šŸŽÆ {ctf_rules} šŸ¢ {enterprise_rules} āš™ļø {general_rules}"
1146
+ )
830
1147
  click.echo()
831
1148
 
832
1149
  # Page info with filter indicator
833
1150
  filter_parts = []
834
1151
  if filter_status is True:
835
- filter_parts.append(click.style("Enabled", fg='green'))
1152
+ filter_parts.append(click.style("Enabled", fg="green"))
836
1153
  elif filter_status is False:
837
- filter_parts.append(click.style("Disabled", fg='yellow'))
1154
+ filter_parts.append(click.style("Disabled", fg="yellow"))
838
1155
  if filter_category is not None:
839
1156
  cat_icon = CATEGORY_ICONS.get(filter_category, "āš™ļø")
840
- cat_name = {"ctf": "CTF", "enterprise": "Enterprise", "general": "General"}.get(filter_category, filter_category)
1157
+ cat_name = {
1158
+ "ctf": "CTF",
1159
+ "enterprise": "Enterprise",
1160
+ "general": "General",
1161
+ }.get(filter_category, filter_category)
841
1162
  filter_parts.append(f"{cat_icon} {cat_name}")
842
- filter_text = click.style(f" [Filter: {', '.join(filter_parts)}]", fg='bright_black') if filter_parts else ""
1163
+ filter_text = (
1164
+ click.style(f" [Filter: {', '.join(filter_parts)}]", fg="bright_black")
1165
+ if filter_parts
1166
+ else ""
1167
+ )
843
1168
  page_info = f"Page {current_page + 1}/{total_pages}"
844
- click.echo(click.style(f"šŸ“‹ RULES ", bold=True, fg='cyan') + click.style(page_info, fg='bright_black') + filter_text)
1169
+ click.echo(
1170
+ click.style(f"šŸ“‹ RULES ", bold=True, fg="cyan")
1171
+ + click.style(page_info, fg="bright_black")
1172
+ + filter_text
1173
+ )
845
1174
  click.echo("─" * width)
846
1175
 
847
1176
  # Table header - with checkbox column and Fired column
848
- click.echo(click.style(f" ā—‹ │ # │ {'Trigger':<22} │ {'Target':<22} │ {'Condition':<30} │ Priority │ Category │ Status │ Fired", bold=True))
1177
+ click.echo(
1178
+ click.style(
1179
+ f" ā—‹ │ # │ {'Trigger':<22} │ {'Target':<22} │ {'Condition':<30} │ Priority │ Category │ Status │ Fired",
1180
+ bold=True,
1181
+ )
1182
+ )
849
1183
  click.echo("─" * width)
850
1184
 
851
1185
  # Calculate slice for current page
@@ -858,28 +1192,54 @@ def _view_rule_details(chaining: ToolChaining):
858
1192
  rule_num = original_idx + 1 # Show original rule number
859
1193
 
860
1194
  # Checkbox for multi-select
861
- checkbox = 'ā—' if original_idx in selected_rule_ids else 'ā—‹'
1195
+ checkbox = "ā—" if original_idx in selected_rule_ids else "ā—‹"
862
1196
 
863
1197
  # Use full names (no truncation)
864
- trigger = rule.trigger_tool[:22] if len(rule.trigger_tool) <= 22 else rule.trigger_tool[:19] + "..."
865
- target = rule.target_tool[:22] if len(rule.target_tool) <= 22 else rule.target_tool[:19] + "..."
866
- condition = rule.trigger_condition[:30] if len(rule.trigger_condition) <= 30 else rule.trigger_condition[:27] + "..."
1198
+ trigger = (
1199
+ rule.trigger_tool[:22]
1200
+ if len(rule.trigger_tool) <= 22
1201
+ else rule.trigger_tool[:19] + "..."
1202
+ )
1203
+ target = (
1204
+ rule.target_tool[:22]
1205
+ if len(rule.target_tool) <= 22
1206
+ else rule.target_tool[:19] + "..."
1207
+ )
1208
+ condition = (
1209
+ rule.trigger_condition[:30]
1210
+ if len(rule.trigger_condition) <= 30
1211
+ else rule.trigger_condition[:27] + "..."
1212
+ )
867
1213
 
868
1214
  # Status with color
869
- status = click.style(" ON", fg='green') if rule.enabled else click.style("OFF", fg='red')
1215
+ status = (
1216
+ click.style(" ON", fg="green")
1217
+ if rule.enabled
1218
+ else click.style("OFF", fg="red")
1219
+ )
870
1220
 
871
1221
  # Priority with color (8-char width to match "Priority" header)
872
- pri_color = 'green' if rule.priority >= 8 else 'yellow' if rule.priority >= 5 else 'white'
1222
+ pri_color = (
1223
+ "green"
1224
+ if rule.priority >= 8
1225
+ else "yellow" if rule.priority >= 5 else "white"
1226
+ )
873
1227
  priority = click.style(f"{rule.priority:>8}", fg=pri_color)
874
1228
 
875
1229
  # Category icon (8-char width to match "Category" header)
876
1230
  cat_icon = CATEGORY_ICONS.get(rule.category, "āš™ļø")
877
1231
 
878
1232
  # Trigger count with color
879
- fired_count = rule.trigger_count if hasattr(rule, 'trigger_count') else 0
880
- fired_display = click.style(f"{fired_count:>5}", fg='green') if fired_count > 0 else click.style(f"{fired_count:>5}", fg='bright_black')
1233
+ fired_count = rule.trigger_count if hasattr(rule, "trigger_count") else 0
1234
+ fired_display = (
1235
+ click.style(f"{fired_count:>5}", fg="green")
1236
+ if fired_count > 0
1237
+ else click.style(f"{fired_count:>5}", fg="bright_black")
1238
+ )
881
1239
 
882
- click.echo(f" {checkbox} │ {rule_num:>4} │ {trigger:<22} │ {target:<22} │ {condition:<30} │ {priority} │ {cat_icon} │ {status} │ {fired_display}")
1240
+ click.echo(
1241
+ f" {checkbox} │ {rule_num:>4} │ {trigger:<22} │ {target:<22} │ {condition:<30} │ {priority} │ {cat_icon} │ {status} │ {fired_display}"
1242
+ )
883
1243
 
884
1244
  click.echo("─" * width)
885
1245
  click.echo()
@@ -903,25 +1263,29 @@ def _view_rule_details(chaining: ToolChaining):
903
1263
  click.echo()
904
1264
 
905
1265
  try:
906
- choice = click.prompt("Select option", default="0", show_default=False).strip().lower()
1266
+ choice = (
1267
+ click.prompt("Select option", default="0", show_default=False)
1268
+ .strip()
1269
+ .lower()
1270
+ )
907
1271
 
908
- if choice == 'q':
1272
+ if choice == "q":
909
1273
  return
910
- elif choice == 'n' and current_page < total_pages - 1:
1274
+ elif choice == "n" and current_page < total_pages - 1:
911
1275
  current_page += 1
912
- elif choice == 'p' and current_page > 0:
1276
+ elif choice == "p" and current_page > 0:
913
1277
  current_page -= 1
914
- elif choice == 'e':
1278
+ elif choice == "e":
915
1279
  filter_status = True
916
1280
  current_page = 0
917
- elif choice == 'd':
1281
+ elif choice == "d":
918
1282
  filter_status = False
919
1283
  current_page = 0
920
- elif choice == 'a':
1284
+ elif choice == "a":
921
1285
  filter_status = None
922
1286
  filter_category = None
923
1287
  current_page = 0
924
- elif choice == 'c':
1288
+ elif choice == "c":
925
1289
  # Category filter submenu
926
1290
  click.echo()
927
1291
  click.echo(click.style(" Select category:", bold=True))
@@ -929,14 +1293,16 @@ def _view_rule_details(chaining: ToolChaining):
929
1293
  click.echo(f" [2] šŸ¢ Enterprise - Real-world testing")
930
1294
  click.echo(f" [3] āš™ļø General - Standard recon")
931
1295
  click.echo(f" [q] Cancel")
932
- cat_choice = click.prompt(" Select option", default="0", show_default=False).strip()
933
- if cat_choice == '1':
1296
+ cat_choice = click.prompt(
1297
+ " Select option", default="0", show_default=False
1298
+ ).strip()
1299
+ if cat_choice == "1":
934
1300
  filter_category = CATEGORY_CTF
935
1301
  current_page = 0
936
- elif cat_choice == '2':
1302
+ elif cat_choice == "2":
937
1303
  filter_category = CATEGORY_ENTERPRISE
938
1304
  current_page = 0
939
- elif cat_choice == '3':
1305
+ elif cat_choice == "3":
940
1306
  filter_category = CATEGORY_GENERAL
941
1307
  current_page = 0
942
1308
  elif choice.isdigit():
@@ -945,9 +1311,9 @@ def _view_rule_details(chaining: ToolChaining):
945
1311
  if 0 <= rule_idx < len(chaining.rules):
946
1312
  _show_single_rule_details(chaining.rules[rule_idx], width, chaining)
947
1313
  else:
948
- click.echo(click.style("\nāœ— Invalid rule number", fg='red'))
1314
+ click.echo(click.style("\nāœ— Invalid rule number", fg="red"))
949
1315
  click.pause()
950
- elif choice == 'i':
1316
+ elif choice == "i":
951
1317
  # Interactive multi-select mode (respects current filters)
952
1318
  from souleyez.ui.interactive_selector import interactive_select
953
1319
 
@@ -955,42 +1321,50 @@ def _view_rule_details(chaining: ToolChaining):
955
1321
  # Convert FILTERED rules to dicts for selector
956
1322
  rule_items = []
957
1323
  for original_idx, rule in filtered_rules:
958
- rule_items.append({
959
- 'idx': original_idx,
960
- 'trigger_tool': rule.trigger_tool,
961
- 'target_tool': rule.target_tool,
962
- 'condition': rule.trigger_condition,
963
- 'enabled': rule.enabled,
964
- 'priority': rule.priority,
965
- 'category': rule.category
966
- })
1324
+ rule_items.append(
1325
+ {
1326
+ "idx": original_idx,
1327
+ "trigger_tool": rule.trigger_tool,
1328
+ "target_tool": rule.target_tool,
1329
+ "condition": rule.trigger_condition,
1330
+ "enabled": rule.enabled,
1331
+ "priority": rule.priority,
1332
+ "category": rule.category,
1333
+ }
1334
+ )
967
1335
 
968
1336
  columns = [
969
- {'name': '#', 'width': 5, 'key': 'idx', 'justify': 'right'},
970
- {'name': 'Trigger', 'width': 18, 'key': 'trigger_tool'},
971
- {'name': 'Target', 'width': 18, 'key': 'target_tool'},
972
- {'name': 'Condition', 'width': 25, 'key': 'condition'},
973
- {'name': 'Cat', 'width': 5, 'key': 'category'},
974
- {'name': 'Status', 'width': 8, 'key': 'enabled'},
1337
+ {"name": "#", "width": 5, "key": "idx", "justify": "right"},
1338
+ {"name": "Trigger", "width": 18, "key": "trigger_tool"},
1339
+ {"name": "Target", "width": 18, "key": "target_tool"},
1340
+ {"name": "Condition", "width": 25, "key": "condition"},
1341
+ {"name": "Cat", "width": 5, "key": "category"},
1342
+ {"name": "Status", "width": 8, "key": "enabled"},
975
1343
  ]
976
1344
 
977
1345
  def format_rule_cell(item: dict, key: str) -> str:
978
1346
  value = item.get(key)
979
- if key == 'enabled':
980
- return '[green]ON[/green]' if value else '[red]OFF[/red]'
981
- if key == 'idx':
1347
+ if key == "enabled":
1348
+ return "[green]ON[/green]" if value else "[red]OFF[/red]"
1349
+ if key == "idx":
982
1350
  return str(value + 1) # 1-indexed for display
983
- if key == 'category':
984
- return CATEGORY_ICONS.get(value, 'āš™ļø')
985
- return str(value) if value else '-'
1351
+ if key == "category":
1352
+ return CATEGORY_ICONS.get(value, "āš™ļø")
1353
+ return str(value) if value else "-"
986
1354
 
987
1355
  # Build title with filter info
988
- title = 'SELECT RULE TO VIEW'
1356
+ title = "SELECT RULE TO VIEW"
989
1357
  if filter_category or filter_status is not None:
990
1358
  filter_info = []
991
1359
  if filter_category:
992
- cat_name = {"ctf": "CTF", "enterprise": "Enterprise", "general": "General"}.get(filter_category, "")
993
- filter_info.append(f"{CATEGORY_ICONS.get(filter_category, '')} {cat_name}")
1360
+ cat_name = {
1361
+ "ctf": "CTF",
1362
+ "enterprise": "Enterprise",
1363
+ "general": "General",
1364
+ }.get(filter_category, "")
1365
+ filter_info.append(
1366
+ f"{CATEGORY_ICONS.get(filter_category, '')} {cat_name}"
1367
+ )
994
1368
  if filter_status is True:
995
1369
  filter_info.append("Enabled")
996
1370
  elif filter_status is False:
@@ -1001,9 +1375,9 @@ def _view_rule_details(chaining: ToolChaining):
1001
1375
  items=rule_items,
1002
1376
  columns=columns,
1003
1377
  selected_ids=selected_rule_ids,
1004
- get_id=lambda r: r.get('idx'),
1378
+ get_id=lambda r: r.get("idx"),
1005
1379
  title=title,
1006
- format_cell=format_rule_cell
1380
+ format_cell=format_rule_cell,
1007
1381
  )
1008
1382
 
1009
1383
  if not selected_rule_ids:
@@ -1012,13 +1386,17 @@ def _view_rule_details(chaining: ToolChaining):
1012
1386
  # View the first selected rule
1013
1387
  rule_idx = next(iter(selected_rule_ids))
1014
1388
  if 0 <= rule_idx < len(chaining.rules):
1015
- _show_single_rule_details(chaining.rules[rule_idx], width, chaining)
1389
+ _show_single_rule_details(
1390
+ chaining.rules[rule_idx], width, chaining
1391
+ )
1016
1392
  selected_rule_ids.clear()
1017
1393
  except (KeyboardInterrupt, EOFError):
1018
1394
  return
1019
1395
 
1020
1396
 
1021
- def _show_single_rule_details(rule: ChainRule, width: int, chaining: ToolChaining = None):
1397
+ def _show_single_rule_details(
1398
+ rule: ChainRule, width: int, chaining: ToolChaining = None
1399
+ ):
1022
1400
  """Display detailed view for a single rule with toggle for raw definition."""
1023
1401
  show_raw = False
1024
1402
 
@@ -1027,34 +1405,50 @@ def _show_single_rule_details(rule: ChainRule, width: int, chaining: ToolChainin
1027
1405
 
1028
1406
  # Header
1029
1407
  click.echo("\n" + "═" * width)
1030
- click.echo(click.style(f" {rule.trigger_tool.upper()} → {rule.target_tool.upper()}", bold=True, fg='cyan'))
1408
+ click.echo(
1409
+ click.style(
1410
+ f" {rule.trigger_tool.upper()} → {rule.target_tool.upper()}",
1411
+ bold=True,
1412
+ fg="cyan",
1413
+ )
1414
+ )
1031
1415
  click.echo("═" * width)
1032
1416
  click.echo()
1033
1417
 
1034
1418
  # Status
1035
1419
  if rule.enabled:
1036
- status_display = click.style("āœ“ ENABLED", fg='green', bold=True)
1420
+ status_display = click.style("āœ“ ENABLED", fg="green", bold=True)
1037
1421
  else:
1038
- status_display = click.style("āœ— DISABLED", fg='red', bold=True)
1422
+ status_display = click.style("āœ— DISABLED", fg="red", bold=True)
1039
1423
 
1040
1424
  click.echo(f"Status: {status_display}")
1041
1425
 
1042
1426
  # Priority
1043
- priority_color = 'green' if rule.priority >= 8 else 'yellow' if rule.priority >= 5 else 'white'
1044
- click.echo(f"Priority: {click.style(str(rule.priority), fg=priority_color, bold=True)}/10")
1045
-
1427
+ priority_color = (
1428
+ "green"
1429
+ if rule.priority >= 8
1430
+ else "yellow" if rule.priority >= 5 else "white"
1431
+ )
1432
+ click.echo(
1433
+ f"Priority: {click.style(str(rule.priority), fg=priority_color, bold=True)}/10"
1434
+ )
1435
+
1046
1436
  # Trigger count
1047
- fired_count = rule.trigger_count if hasattr(rule, 'trigger_count') else 0
1437
+ fired_count = rule.trigger_count if hasattr(rule, "trigger_count") else 0
1048
1438
  if fired_count > 0:
1049
- click.echo(f"Triggered: {click.style(str(fired_count), fg='green', bold=True)} times")
1439
+ click.echo(
1440
+ f"Triggered: {click.style(str(fired_count), fg='green', bold=True)} times"
1441
+ )
1050
1442
  else:
1051
1443
  click.echo(f"Triggered: {click.style('Never', fg='bright_black')}")
1052
-
1444
+
1053
1445
  click.echo()
1054
1446
 
1055
1447
  # Rule Logic (human-readable)
1056
1448
  click.echo(click.style("Rule Logic:", bold=True))
1057
- click.echo(f" IF {rule.trigger_tool} finds {rule.trigger_condition} → THEN run {rule.target_tool}")
1449
+ click.echo(
1450
+ f" IF {rule.trigger_tool} finds {rule.trigger_condition} → THEN run {rule.target_tool}"
1451
+ )
1058
1452
  click.echo()
1059
1453
 
1060
1454
  # Description
@@ -1066,33 +1460,44 @@ def _show_single_rule_details(rule: ChainRule, width: int, chaining: ToolChainin
1066
1460
  # Command Template
1067
1461
  click.echo(click.style("Command Template:", bold=True))
1068
1462
  if rule.args_template:
1069
- click.echo(f" {rule.target_tool} " + " ".join(str(arg) for arg in rule.args_template))
1463
+ click.echo(
1464
+ f" {rule.target_tool} "
1465
+ + " ".join(str(arg) for arg in rule.args_template)
1466
+ )
1070
1467
  else:
1071
- click.echo(click.style(" (uses default tool arguments)", fg='bright_black'))
1468
+ click.echo(
1469
+ click.style(" (uses default tool arguments)", fg="bright_black")
1470
+ )
1072
1471
  click.echo()
1073
1472
 
1074
1473
  # Safety warnings for hydra
1075
- if rule.target_tool == 'hydra':
1076
- click.echo(click.style("āš ļø SAFETY INFORMATION:", fg='red', bold=True))
1077
- click.echo(click.style(" • This is a BRUTE-FORCE rule", fg='red'))
1078
- click.echo(click.style(" • May trigger account lockouts", fg='red'))
1079
- click.echo(click.style(" • May trigger security alerts", fg='red'))
1080
- click.echo(click.style(" • Enabled by default: NO (safety)", fg='green'))
1474
+ if rule.target_tool == "hydra":
1475
+ click.echo(click.style("āš ļø SAFETY INFORMATION:", fg="red", bold=True))
1476
+ click.echo(click.style(" • This is a BRUTE-FORCE rule", fg="red"))
1477
+ click.echo(click.style(" • May trigger account lockouts", fg="red"))
1478
+ click.echo(click.style(" • May trigger security alerts", fg="red"))
1479
+ click.echo(click.style(" • Enabled by default: NO (safety)", fg="green"))
1081
1480
  click.echo()
1082
1481
 
1083
1482
  # Raw rule definition (toggleable)
1084
1483
  if show_raw:
1085
1484
  click.echo("─── Raw Rule Definition " + "─" * (width - 24))
1086
- click.echo(click.style("ChainRule(", fg='cyan'))
1087
- click.echo(click.style(f" trigger_tool='{rule.trigger_tool}',", fg='cyan'))
1088
- click.echo(click.style(f" trigger_condition='{rule.trigger_condition}',", fg='cyan'))
1089
- click.echo(click.style(f" target_tool='{rule.target_tool}',", fg='cyan'))
1090
- click.echo(click.style(f" priority={rule.priority},", fg='cyan'))
1485
+ click.echo(click.style("ChainRule(", fg="cyan"))
1486
+ click.echo(
1487
+ click.style(f" trigger_tool='{rule.trigger_tool}',", fg="cyan")
1488
+ )
1489
+ click.echo(
1490
+ click.style(
1491
+ f" trigger_condition='{rule.trigger_condition}',", fg="cyan"
1492
+ )
1493
+ )
1494
+ click.echo(click.style(f" target_tool='{rule.target_tool}',", fg="cyan"))
1495
+ click.echo(click.style(f" priority={rule.priority},", fg="cyan"))
1091
1496
  if rule.args_template:
1092
1497
  args_str = str(rule.args_template)
1093
- click.echo(click.style(f" args_template={args_str},", fg='cyan'))
1094
- click.echo(click.style(f" enabled={rule.enabled}", fg='cyan'))
1095
- click.echo(click.style(")", fg='cyan'))
1498
+ click.echo(click.style(f" args_template={args_str},", fg="cyan"))
1499
+ click.echo(click.style(f" enabled={rule.enabled}", fg="cyan"))
1500
+ click.echo(click.style(")", fg="cyan"))
1096
1501
  click.echo("─" * width)
1097
1502
  click.echo()
1098
1503
 
@@ -1102,13 +1507,17 @@ def _show_single_rule_details(rule: ChainRule, width: int, chaining: ToolChainin
1102
1507
  click.echo()
1103
1508
 
1104
1509
  try:
1105
- choice = click.prompt("Select option", default="0", show_default=False).strip().lower()
1510
+ choice = (
1511
+ click.prompt("Select option", default="0", show_default=False)
1512
+ .strip()
1513
+ .lower()
1514
+ )
1106
1515
 
1107
- if choice == 'q':
1516
+ if choice == "q":
1108
1517
  return
1109
- elif choice == 'r':
1518
+ elif choice == "r":
1110
1519
  show_raw = not show_raw
1111
- elif choice == 'e':
1520
+ elif choice == "e":
1112
1521
  _edit_rule(rule, chaining)
1113
1522
  except (KeyboardInterrupt, EOFError):
1114
1523
  return
@@ -1123,88 +1532,147 @@ def _edit_rule(rule: ChainRule, chaining: ToolChaining):
1123
1532
 
1124
1533
  # Header
1125
1534
  click.echo("\n" + "═" * width)
1126
- click.echo(click.style(f" EDIT RULE: {rule.trigger_tool.upper()} → {rule.target_tool.upper()}", bold=True, fg='yellow'))
1535
+ click.echo(
1536
+ click.style(
1537
+ f" EDIT RULE: {rule.trigger_tool.upper()} → {rule.target_tool.upper()}",
1538
+ bold=True,
1539
+ fg="yellow",
1540
+ )
1541
+ )
1127
1542
  click.echo("═" * width)
1128
1543
  click.echo()
1129
1544
 
1130
1545
  # Show raw rule definition with editable fields highlighted
1131
1546
  click.echo("─── Rule Definition " + "─" * (width - 20))
1132
- click.echo(click.style("ChainRule(", fg='cyan'))
1133
- click.echo(click.style(f" trigger_tool='{rule.trigger_tool}',", fg='bright_black'))
1134
- click.echo(click.style(f" trigger_condition='{rule.trigger_condition}',", fg='bright_black'))
1135
- click.echo(click.style(f" target_tool='{rule.target_tool}',", fg='bright_black'))
1547
+ click.echo(click.style("ChainRule(", fg="cyan"))
1548
+ click.echo(
1549
+ click.style(f" trigger_tool='{rule.trigger_tool}',", fg="bright_black")
1550
+ )
1551
+ click.echo(
1552
+ click.style(
1553
+ f" trigger_condition='{rule.trigger_condition}',", fg="bright_black"
1554
+ )
1555
+ )
1556
+ click.echo(
1557
+ click.style(f" target_tool='{rule.target_tool}',", fg="bright_black")
1558
+ )
1136
1559
 
1137
1560
  # Editable: priority (highlighted)
1138
- click.echo(click.style(" priority=", fg='cyan') + click.style(f"{rule.priority}", fg='yellow', bold=True) + click.style(",", fg='cyan') + click.style(" ← [p] edit", fg='green'))
1561
+ click.echo(
1562
+ click.style(" priority=", fg="cyan")
1563
+ + click.style(f"{rule.priority}", fg="yellow", bold=True)
1564
+ + click.style(",", fg="cyan")
1565
+ + click.style(" ← [p] edit", fg="green")
1566
+ )
1139
1567
 
1140
1568
  # Editable: args_template (highlighted)
1141
1569
  if rule.args_template:
1142
1570
  args_str = str(rule.args_template)
1143
- click.echo(click.style(" args_template=", fg='cyan') + click.style(f"{args_str}", fg='yellow', bold=True) + click.style(",", fg='cyan') + click.style(" ← [a] edit", fg='green'))
1571
+ click.echo(
1572
+ click.style(" args_template=", fg="cyan")
1573
+ + click.style(f"{args_str}", fg="yellow", bold=True)
1574
+ + click.style(",", fg="cyan")
1575
+ + click.style(" ← [a] edit", fg="green")
1576
+ )
1144
1577
  else:
1145
- click.echo(click.style(" args_template=", fg='cyan') + click.style("[]", fg='yellow', bold=True) + click.style(",", fg='cyan') + click.style(" ← [a] edit", fg='green'))
1578
+ click.echo(
1579
+ click.style(" args_template=", fg="cyan")
1580
+ + click.style("[]", fg="yellow", bold=True)
1581
+ + click.style(",", fg="cyan")
1582
+ + click.style(" ← [a] edit", fg="green")
1583
+ )
1146
1584
 
1147
- click.echo(click.style(f" enabled={rule.enabled},", fg='bright_black'))
1148
- click.echo(click.style(f" description='{rule.description}',", fg='bright_black'))
1149
- click.echo(click.style(f" category='{rule.category}'", fg='bright_black'))
1150
- click.echo(click.style(")", fg='cyan'))
1585
+ click.echo(click.style(f" enabled={rule.enabled},", fg="bright_black"))
1586
+ click.echo(
1587
+ click.style(f" description='{rule.description}',", fg="bright_black")
1588
+ )
1589
+ click.echo(click.style(f" category='{rule.category}'", fg="bright_black"))
1590
+ click.echo(click.style(")", fg="cyan"))
1151
1591
  click.echo("─" * width)
1152
1592
  click.echo()
1153
1593
 
1154
1594
  # Menu
1155
- click.echo(click.style(" [p]", fg='green', bold=True) + " Edit priority " +
1156
- click.style("[a]", fg='green', bold=True) + " Edit args " +
1157
- click.style("[c]", fg='yellow', bold=True) + " Clear args " +
1158
- click.style("[s]", fg='cyan', bold=True) + " Save " +
1159
- click.style("[q]", fg='red', bold=True) + " Cancel")
1595
+ click.echo(
1596
+ click.style(" [p]", fg="green", bold=True)
1597
+ + " Edit priority "
1598
+ + click.style("[a]", fg="green", bold=True)
1599
+ + " Edit args "
1600
+ + click.style("[c]", fg="yellow", bold=True)
1601
+ + " Clear args "
1602
+ + click.style("[s]", fg="cyan", bold=True)
1603
+ + " Save "
1604
+ + click.style("[q]", fg="red", bold=True)
1605
+ + " Cancel"
1606
+ )
1160
1607
  click.echo()
1161
1608
 
1162
1609
  try:
1163
- choice = click.prompt("Select option", default="s", show_default=False).strip().lower()
1610
+ choice = (
1611
+ click.prompt("Select option", default="s", show_default=False)
1612
+ .strip()
1613
+ .lower()
1614
+ )
1164
1615
 
1165
- if choice == 'q':
1616
+ if choice == "q":
1166
1617
  return
1167
- elif choice == 's':
1618
+ elif choice == "s":
1168
1619
  # Save changes
1169
1620
  chaining.save_rules()
1170
- click.echo(click.style("\nāœ“ Rule saved!", fg='green'))
1621
+ click.echo(click.style("\nāœ“ Rule saved!", fg="green"))
1171
1622
  click.pause()
1172
1623
  return
1173
- elif choice == 'p':
1624
+ elif choice == "p":
1174
1625
  # Edit priority
1175
1626
  click.echo()
1176
1627
  try:
1177
- new_priority = click.prompt(" New priority (1-10)", type=int, default=rule.priority)
1628
+ new_priority = click.prompt(
1629
+ " New priority (1-10)", type=int, default=rule.priority
1630
+ )
1178
1631
  if 1 <= new_priority <= 10:
1179
1632
  rule.priority = new_priority
1180
- click.echo(click.style(f" āœ“ Priority set to {new_priority}", fg='green'))
1633
+ click.echo(
1634
+ click.style(
1635
+ f" āœ“ Priority set to {new_priority}", fg="green"
1636
+ )
1637
+ )
1181
1638
  else:
1182
- click.echo(click.style(" āœ— Priority must be between 1 and 10", fg='red'))
1639
+ click.echo(
1640
+ click.style(
1641
+ " āœ— Priority must be between 1 and 10", fg="red"
1642
+ )
1643
+ )
1183
1644
  except (ValueError, click.Abort):
1184
1645
  pass
1185
- elif choice == 'a':
1646
+ elif choice == "a":
1186
1647
  # Edit args template
1187
1648
  click.echo()
1188
- current_args = ' '.join(rule.args_template) if rule.args_template else ''
1649
+ current_args = (
1650
+ " ".join(rule.args_template) if rule.args_template else ""
1651
+ )
1189
1652
  try:
1190
- new_args = click.prompt(" Args (space-separated)", default=current_args).strip()
1653
+ new_args = click.prompt(
1654
+ " Args (space-separated)", default=current_args
1655
+ ).strip()
1191
1656
  if new_args:
1192
1657
  # Parse args - handle quoted strings
1193
1658
  import shlex
1659
+
1194
1660
  try:
1195
1661
  rule.args_template = shlex.split(new_args)
1196
- click.echo(click.style(f" āœ“ Args updated", fg='green'))
1662
+ click.echo(click.style(f" āœ“ Args updated", fg="green"))
1197
1663
  except ValueError as e:
1198
- click.echo(click.style(f" āœ— Invalid args format: {e}", fg='red'))
1664
+ click.echo(
1665
+ click.style(f" āœ— Invalid args format: {e}", fg="red")
1666
+ )
1199
1667
  else:
1200
1668
  rule.args_template = []
1201
- click.echo(click.style(" āœ“ Args cleared", fg='green'))
1669
+ click.echo(click.style(" āœ“ Args cleared", fg="green"))
1202
1670
  except click.Abort:
1203
1671
  pass
1204
- elif choice == 'c':
1672
+ elif choice == "c":
1205
1673
  # Clear args
1206
1674
  rule.args_template = []
1207
- click.echo(click.style("\nāœ“ Args cleared", fg='green'))
1675
+ click.echo(click.style("\nāœ“ Args cleared", fg="green"))
1208
1676
  except (KeyboardInterrupt, EOFError):
1209
1677
  return
1210
1678
 
@@ -1218,9 +1686,13 @@ def _filter_by_tool(chaining: ToolChaining):
1218
1686
  # Get unique trigger tools with stats
1219
1687
  tools = sorted(set(r.trigger_tool for r in chaining.rules))
1220
1688
 
1221
- click.echo("\n" + click.style("EXPAND TOOL GROUP", bold=True, fg='cyan'))
1689
+ click.echo("\n" + click.style("EXPAND TOOL GROUP", bold=True, fg="cyan"))
1222
1690
  click.echo("─" * width)
1223
- click.echo(click.style(" Select a tool to see all its chain rules in detail", fg='bright_black'))
1691
+ click.echo(
1692
+ click.style(
1693
+ " Select a tool to see all its chain rules in detail", fg="bright_black"
1694
+ )
1695
+ )
1224
1696
  click.echo()
1225
1697
 
1226
1698
  for idx, tool in enumerate(tools, 1):
@@ -1230,19 +1702,21 @@ def _filter_by_tool(chaining: ToolChaining):
1230
1702
 
1231
1703
  # Status icon
1232
1704
  if enabled_count == rule_count:
1233
- icon = click.style("āœ“", fg='green', bold=True)
1705
+ icon = click.style("āœ“", fg="green", bold=True)
1234
1706
  elif enabled_count == 0:
1235
- icon = click.style("āœ—", fg='red')
1707
+ icon = click.style("āœ—", fg="red")
1236
1708
  else:
1237
- icon = click.style("◐", fg='yellow')
1709
+ icon = click.style("◐", fg="yellow")
1238
1710
 
1239
1711
  # Highlight brute-force
1240
- has_hydra = any(r.target_tool == 'hydra' for r in rules_for_tool)
1241
- warning = click.style(" āš ļø ", fg='red') if has_hydra else " "
1712
+ has_hydra = any(r.target_tool == "hydra" for r in rules_for_tool)
1713
+ warning = click.style(" āš ļø ", fg="red") if has_hydra else " "
1242
1714
 
1243
1715
  tool_padded = f"{tool.upper():<20}"
1244
- click.echo(f"{warning}[{idx:2}] {icon} {click.style(tool_padded, fg='cyan')} "
1245
- f"[{click.style(str(enabled_count), fg='green')}/{rule_count}]")
1716
+ click.echo(
1717
+ f"{warning}[{idx:2}] {icon} {click.style(tool_padded, fg='cyan')} "
1718
+ f"[{click.style(str(enabled_count), fg='green')}/{rule_count}]"
1719
+ )
1246
1720
 
1247
1721
  click.echo()
1248
1722
  click.echo(" [q] ← Back")
@@ -1261,50 +1735,81 @@ def _filter_by_tool(chaining: ToolChaining):
1261
1735
 
1262
1736
  # Header
1263
1737
  click.echo("\nā”Œ" + "─" * (width - 2) + "┐")
1264
- click.echo("│" + click.style(f" {selected_tool.upper()} CHAIN RULES ".center(width - 2), bold=True, fg='cyan') + "│")
1738
+ click.echo(
1739
+ "│"
1740
+ + click.style(
1741
+ f" {selected_tool.upper()} CHAIN RULES ".center(width - 2),
1742
+ bold=True,
1743
+ fg="cyan",
1744
+ )
1745
+ + "│"
1746
+ )
1265
1747
  click.echo("ā””" + "─" * (width - 2) + "ā”˜")
1266
1748
  click.echo()
1267
1749
 
1268
1750
  # Summary
1269
1751
  enabled_count = sum(1 for r in filtered_rules if r.enabled)
1270
- click.echo(f"Total Rules: {len(filtered_rules)} | "
1271
- f"{click.style(str(enabled_count), fg='green')} enabled, "
1272
- f"{click.style(str(len(filtered_rules) - enabled_count), fg='yellow')} disabled")
1752
+ click.echo(
1753
+ f"Total Rules: {len(filtered_rules)} | "
1754
+ f"{click.style(str(enabled_count), fg='green')} enabled, "
1755
+ f"{click.style(str(len(filtered_rules) - enabled_count), fg='yellow')} disabled"
1756
+ )
1273
1757
  click.echo()
1274
1758
 
1275
1759
  # Display detailed rules
1276
- for idx, rule in enumerate(sorted(filtered_rules, key=lambda r: (-r.priority, r.target_tool)), 1):
1760
+ for idx, rule in enumerate(
1761
+ sorted(filtered_rules, key=lambda r: (-r.priority, r.target_tool)), 1
1762
+ ):
1277
1763
  # Status
1278
- status_icon = click.style("āœ“", fg='green', bold=True) if rule.enabled else click.style("āœ—", fg='red')
1764
+ status_icon = (
1765
+ click.style("āœ“", fg="green", bold=True)
1766
+ if rule.enabled
1767
+ else click.style("āœ—", fg="red")
1768
+ )
1279
1769
 
1280
1770
  # Priority color
1281
1771
  if rule.priority >= 8:
1282
- priority_color = 'green'
1772
+ priority_color = "green"
1283
1773
  elif rule.priority >= 5:
1284
- priority_color = 'yellow'
1774
+ priority_color = "yellow"
1285
1775
  else:
1286
- priority_color = 'white'
1776
+ priority_color = "white"
1287
1777
 
1288
1778
  # Target tool with warning
1289
- target = click.style(rule.target_tool, fg='magenta', bold=True)
1290
- warning = click.style(" āš ļø BRUTE-FORCE", fg='red') if rule.target_tool == 'hydra' else ""
1779
+ target = click.style(rule.target_tool, fg="magenta", bold=True)
1780
+ warning = (
1781
+ click.style(" āš ļø BRUTE-FORCE", fg="red")
1782
+ if rule.target_tool == "hydra"
1783
+ else ""
1784
+ )
1291
1785
 
1292
1786
  # Rule header
1293
- click.echo(f" {status_icon} [{idx}] {selected_tool.upper()} → {target} "
1294
- f"Priority: {click.style(str(rule.priority), fg=priority_color)}{warning}")
1787
+ click.echo(
1788
+ f" {status_icon} [{idx}] {selected_tool.upper()} → {target} "
1789
+ f"Priority: {click.style(str(rule.priority), fg=priority_color)}{warning}"
1790
+ )
1295
1791
 
1296
1792
  # Condition
1297
- click.echo(click.style(f" Trigger: {rule.trigger_condition}", fg='bright_black'))
1793
+ click.echo(
1794
+ click.style(
1795
+ f" Trigger: {rule.trigger_condition}", fg="bright_black"
1796
+ )
1797
+ )
1298
1798
 
1299
1799
  # Description
1300
1800
  if rule.description:
1301
- desc_color = 'red' if rule.target_tool == 'hydra' else 'bright_black'
1801
+ desc_color = "red" if rule.target_tool == "hydra" else "bright_black"
1302
1802
  click.echo(click.style(f" {rule.description}", fg=desc_color))
1303
1803
 
1304
1804
  click.echo()
1305
1805
 
1306
1806
  click.echo("─" * width)
1307
- click.echo(click.style("šŸ’” Use option [1] from main menu to toggle individual rules", fg='bright_black'))
1807
+ click.echo(
1808
+ click.style(
1809
+ "šŸ’” Use option [1] from main menu to toggle individual rules",
1810
+ fg="bright_black",
1811
+ )
1812
+ )
1308
1813
  click.pause()
1309
1814
 
1310
1815
  except (ValueError, KeyboardInterrupt, EOFError):
@@ -1315,7 +1820,7 @@ def _filter_by_status(chaining: ToolChaining):
1315
1820
  """Filter rules by enabled/disabled status."""
1316
1821
  DesignSystem.clear_screen()
1317
1822
 
1318
- click.echo("\n" + click.style("FILTER BY STATUS", bold=True, fg='cyan'))
1823
+ click.echo("\n" + click.style("FILTER BY STATUS", bold=True, fg="cyan"))
1319
1824
  click.echo("─" * 70)
1320
1825
  click.echo()
1321
1826
  click.echo(" [1] Show ENABLED rules only")
@@ -1329,20 +1834,24 @@ def _filter_by_status(chaining: ToolChaining):
1329
1834
  if choice == 0:
1330
1835
  return
1331
1836
 
1332
- show_enabled = (choice == 1)
1837
+ show_enabled = choice == 1
1333
1838
  filtered_rules = [r for r in chaining.rules if r.enabled == show_enabled]
1334
1839
 
1335
1840
  status_text = "ENABLED" if show_enabled else "DISABLED"
1336
1841
 
1337
1842
  DesignSystem.clear_screen()
1338
- click.echo("\n" + click.style(f"{status_text} RULES", bold=True, fg='cyan'))
1843
+ click.echo("\n" + click.style(f"{status_text} RULES", bold=True, fg="cyan"))
1339
1844
  click.echo("─" * 70)
1340
1845
  click.echo(f"\nShowing {len(filtered_rules)} rules\n")
1341
1846
 
1342
1847
  for rule in sorted(filtered_rules, key=lambda r: (-r.priority, r.trigger_tool)):
1343
- warning = click.style(" āš ļø BRUTE-FORCE", fg='red') if rule.target_tool == 'hydra' else ""
1848
+ warning = (
1849
+ click.style(" āš ļø BRUTE-FORCE", fg="red")
1850
+ if rule.target_tool == "hydra"
1851
+ else ""
1852
+ )
1344
1853
  click.echo(f" {rule.trigger_tool}→{rule.target_tool}{warning}")
1345
- click.echo(click.style(f" {rule.description}", fg='bright_black'))
1854
+ click.echo(click.style(f" {rule.description}", fg="bright_black"))
1346
1855
  click.echo()
1347
1856
 
1348
1857
  click.pause()
@@ -1354,22 +1863,37 @@ def _filter_by_status(chaining: ToolChaining):
1354
1863
  def _enable_all_rules(chaining: ToolChaining):
1355
1864
  """Enable all chain rules with safety confirmation."""
1356
1865
  click.echo()
1357
- click.echo(click.style("āš ļø WARNING: Enable ALL Chain Rules", fg='red', bold=True))
1358
- click.echo(click.style("─" * 70, fg='red'))
1866
+ click.echo(click.style("āš ļø WARNING: Enable ALL Chain Rules", fg="red", bold=True))
1867
+ click.echo(click.style("─" * 70, fg="red"))
1359
1868
  click.echo()
1360
1869
 
1361
- brute_force_count = sum(1 for r in chaining.rules if r.target_tool == 'hydra')
1870
+ brute_force_count = sum(1 for r in chaining.rules if r.target_tool == "hydra")
1362
1871
 
1363
1872
  if brute_force_count > 0:
1364
- click.echo(click.style(f" This will enable {brute_force_count} BRUTE-FORCE rules!", fg='red', bold=True))
1365
- click.echo(click.style(" These rules may cause account lockouts and security alerts.", fg='red'))
1873
+ click.echo(
1874
+ click.style(
1875
+ f" This will enable {brute_force_count} BRUTE-FORCE rules!",
1876
+ fg="red",
1877
+ bold=True,
1878
+ )
1879
+ )
1880
+ click.echo(
1881
+ click.style(
1882
+ " These rules may cause account lockouts and security alerts.",
1883
+ fg="red",
1884
+ )
1885
+ )
1366
1886
  click.echo()
1367
1887
 
1368
- click.echo(f" Total rules to enable: {sum(1 for r in chaining.rules if not r.enabled)}")
1888
+ click.echo(
1889
+ f" Total rules to enable: {sum(1 for r in chaining.rules if not r.enabled)}"
1890
+ )
1369
1891
  click.echo()
1370
1892
 
1371
- if not click.confirm(click.style("Are you ABSOLUTELY SURE?", fg='yellow', bold=True), default=False):
1372
- click.echo(click.style("\nāœ“ Operation cancelled", fg='green'))
1893
+ if not click.confirm(
1894
+ click.style("Are you ABSOLUTELY SURE?", fg="yellow", bold=True), default=False
1895
+ ):
1896
+ click.echo(click.style("\nāœ“ Operation cancelled", fg="green"))
1373
1897
  click.pause()
1374
1898
  return
1375
1899
 
@@ -1380,7 +1904,9 @@ def _enable_all_rules(chaining: ToolChaining):
1380
1904
  chaining.save_rules()
1381
1905
 
1382
1906
  click.echo()
1383
- click.echo(click.style(f"āœ“ All {len(chaining.rules)} rules ENABLED", fg='green', bold=True))
1907
+ click.echo(
1908
+ click.style(f"āœ“ All {len(chaining.rules)} rules ENABLED", fg="green", bold=True)
1909
+ )
1384
1910
  click.pause()
1385
1911
 
1386
1912
 
@@ -1397,7 +1923,11 @@ def _disable_all_rules(chaining: ToolChaining):
1397
1923
  chaining.save_rules()
1398
1924
 
1399
1925
  click.echo()
1400
- click.echo(click.style(f"āœ“ All {len(chaining.rules)} rules DISABLED", fg='yellow', bold=True))
1926
+ click.echo(
1927
+ click.style(
1928
+ f"āœ“ All {len(chaining.rules)} rules DISABLED", fg="yellow", bold=True
1929
+ )
1930
+ )
1401
1931
  click.pause()
1402
1932
 
1403
1933
 
@@ -1412,7 +1942,7 @@ def _reset_to_defaults(chaining: ToolChaining):
1412
1942
  return
1413
1943
 
1414
1944
  # Delete the saved state file
1415
- rules_file = Path.home() / '.souleyez' / 'chain_rules_state.json'
1945
+ rules_file = Path.home() / ".souleyez" / "chain_rules_state.json"
1416
1946
  if rules_file.exists():
1417
1947
  os.remove(rules_file)
1418
1948
 
@@ -1421,6 +1951,8 @@ def _reset_to_defaults(chaining: ToolChaining):
1421
1951
  chaining._init_default_rules()
1422
1952
 
1423
1953
  click.echo()
1424
- click.echo(click.style("āœ“ Rules reset to defaults", fg='green', bold=True))
1425
- click.echo(click.style(" Note: Brute-force rules are DISABLED by default", fg='yellow'))
1954
+ click.echo(click.style("āœ“ Rules reset to defaults", fg="green", bold=True))
1955
+ click.echo(
1956
+ click.style(" Note: Brute-force rules are DISABLED by default", fg="yellow")
1957
+ )
1426
1958
  click.pause()