souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of souleyez might be problematic. Click here for more details.

Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  """Template selection interface for deliverables - Interactive table style."""
2
+
2
3
  import click
3
4
  from typing import Dict, List, Optional, Any
4
5
  from rich.console import Console
@@ -10,6 +11,7 @@ from souleyez.ui.design_system import DesignSystem
10
11
  # Key codes
11
12
  try:
12
13
  import readchar
14
+
13
15
  KEY_UP = readchar.key.UP
14
16
  KEY_DOWN = readchar.key.DOWN
15
17
  KEY_PAGE_UP = readchar.key.PAGE_UP
@@ -18,51 +20,52 @@ try:
18
20
  KEY_ENTER = readchar.key.ENTER
19
21
  KEY_SPACE = readchar.key.SPACE
20
22
  except (ImportError, AttributeError):
21
- KEY_UP = '\x1b[A'
22
- KEY_DOWN = '\x1b[B'
23
- KEY_PAGE_UP = '\x1b[5~'
24
- KEY_PAGE_DOWN = '\x1b[6~'
25
- KEY_ESCAPE = '\x1b'
26
- KEY_ENTER = '\r'
27
- KEY_SPACE = ' '
23
+ KEY_UP = "\x1b[A"
24
+ KEY_DOWN = "\x1b[B"
25
+ KEY_PAGE_UP = "\x1b[5~"
26
+ KEY_PAGE_DOWN = "\x1b[6~"
27
+ KEY_ESCAPE = "\x1b"
28
+ KEY_ENTER = "\r"
29
+ KEY_SPACE = " "
28
30
 
29
31
 
30
32
  def _get_key() -> str:
31
33
  """Read a single keypress."""
32
34
  try:
33
35
  import readchar
36
+
34
37
  return readchar.readkey()
35
38
  except ImportError:
36
39
  pass
37
40
 
38
41
  try:
39
42
  ch = click.getchar()
40
- if ch == '\x1b' or (len(ch) > 1 and ch.startswith('\x1b')):
43
+ if ch == "\x1b" or (len(ch) > 1 and ch.startswith("\x1b")):
41
44
  if len(ch) >= 3:
42
- if ch == '\x1b[A':
45
+ if ch == "\x1b[A":
43
46
  return KEY_UP
44
- elif ch == '\x1b[B':
47
+ elif ch == "\x1b[B":
45
48
  return KEY_DOWN
46
- elif ch in ('\x1b[5~', '\x1b[5'):
49
+ elif ch in ("\x1b[5~", "\x1b[5"):
47
50
  return KEY_PAGE_UP
48
- elif ch in ('\x1b[6~', '\x1b[6'):
51
+ elif ch in ("\x1b[6~", "\x1b[6"):
49
52
  return KEY_PAGE_DOWN
50
- elif ch == '\x1b':
53
+ elif ch == "\x1b":
51
54
  try:
52
55
  ch2 = click.getchar()
53
- if ch2 == '[':
56
+ if ch2 == "[":
54
57
  ch3 = click.getchar()
55
- if ch3 == 'A':
58
+ if ch3 == "A":
56
59
  return KEY_UP
57
- elif ch3 == 'B':
60
+ elif ch3 == "B":
58
61
  return KEY_DOWN
59
- elif ch3 in ('5', '6'):
62
+ elif ch3 in ("5", "6"):
60
63
  click.getchar()
61
- return KEY_PAGE_UP if ch3 == '5' else KEY_PAGE_DOWN
62
- return ''
64
+ return KEY_PAGE_UP if ch3 == "5" else KEY_PAGE_DOWN
65
+ return ""
63
66
  except Exception:
64
67
  return KEY_ESCAPE
65
- return ''
68
+ return ""
66
69
  return ch
67
70
  except Exception:
68
71
  return KEY_ESCAPE
@@ -71,17 +74,36 @@ def _get_key() -> str:
71
74
  class TemplateSelector:
72
75
  """Interactive template selector with keyboard navigation."""
73
76
 
74
- CURSOR = ''
75
- NO_CURSOR = ' '
76
- RADIO_EMPTY = ''
77
- RADIO_SELECTED = ''
77
+ CURSOR = ""
78
+ NO_CURSOR = " "
79
+ RADIO_EMPTY = ""
80
+ RADIO_SELECTED = ""
78
81
 
79
82
  # Category filters
80
83
  CATEGORIES = [
81
- ('all', 'All', None),
82
- ('compliance', '🏛️ Compliance', ['hipaa', 'pci-dss', 'nist', 'owasp', 'soc2', 'iso27001', 'cis', 'cmmc', 'gdpr', 'glba']),
83
- ('pentest', '🎯 Pentest', ['ptes', 'internal', 'external', 'webapp', 'redteam', 'cloud', 'ad']),
84
- ('industry', '🏭 Industry', ['nerc-cip', 'hitrust', 'ffiec']),
84
+ ("all", "All", None),
85
+ (
86
+ "compliance",
87
+ "🏛️ Compliance",
88
+ [
89
+ "hipaa",
90
+ "pci-dss",
91
+ "nist",
92
+ "owasp",
93
+ "soc2",
94
+ "iso27001",
95
+ "cis",
96
+ "cmmc",
97
+ "gdpr",
98
+ "glba",
99
+ ],
100
+ ),
101
+ (
102
+ "pentest",
103
+ "🎯 Pentest",
104
+ ["ptes", "internal", "external", "webapp", "redteam", "cloud", "ad"],
105
+ ),
106
+ ("industry", "🏭 Industry", ["nerc-cip", "hitrust", "ffiec"]),
85
107
  ]
86
108
 
87
109
  def __init__(self, templates: List[Dict], engagement_id: int):
@@ -96,12 +118,12 @@ class TemplateSelector:
96
118
  self.selected_template = None
97
119
  self.selected_idx = None # Currently selected row (for inline options)
98
120
  self.show_inline_options = False
99
- self.current_filter = 'all'
121
+ self.current_filter = "all"
100
122
 
101
123
  def run(self) -> Optional[Dict]:
102
124
  """Run the interactive selector. Returns selected template or None."""
103
125
  if not self.templates:
104
- click.echo(click.style(" No templates available.", fg='yellow'))
126
+ click.echo(click.style(" No templates available.", fg="yellow"))
105
127
  click.pause()
106
128
  return None
107
129
 
@@ -122,29 +144,44 @@ class TemplateSelector:
122
144
  self.selected_idx = None
123
145
  self.show_inline_options = False
124
146
 
125
- if filter_key == 'all':
147
+ if filter_key == "all":
126
148
  self.templates = self.all_templates
127
149
  else:
128
150
  # Find the filter
129
151
  for key, label, frameworks in self.CATEGORIES:
130
152
  if key == filter_key and frameworks:
131
- self.templates = [t for t in self.all_templates if t.get('framework') in frameworks]
153
+ self.templates = [
154
+ t
155
+ for t in self.all_templates
156
+ if t.get("framework") in frameworks
157
+ ]
132
158
  break
133
159
 
134
160
  def _get_category(self, template: Dict) -> str:
135
161
  """Get display category for template."""
136
- framework = template.get('framework', '')
137
- compliance = ['hipaa', 'pci-dss', 'nist', 'owasp', 'soc2', 'iso27001', 'cis', 'cmmc', 'gdpr', 'glba']
138
- pentest = ['ptes', 'internal', 'external', 'webapp', 'redteam', 'cloud', 'ad']
139
- industry = ['nerc-cip', 'hitrust', 'ffiec']
162
+ framework = template.get("framework", "")
163
+ compliance = [
164
+ "hipaa",
165
+ "pci-dss",
166
+ "nist",
167
+ "owasp",
168
+ "soc2",
169
+ "iso27001",
170
+ "cis",
171
+ "cmmc",
172
+ "gdpr",
173
+ "glba",
174
+ ]
175
+ pentest = ["ptes", "internal", "external", "webapp", "redteam", "cloud", "ad"]
176
+ industry = ["nerc-cip", "hitrust", "ffiec"]
140
177
 
141
178
  if framework in compliance:
142
- return '🏛️'
179
+ return "🏛️"
143
180
  elif framework in pentest:
144
- return '🎯'
181
+ return "🎯"
145
182
  elif framework in industry:
146
- return '🏭'
147
- return '📋'
183
+ return "🏭"
184
+ return "📋"
148
185
 
149
186
  def _render(self):
150
187
  """Render the table."""
@@ -154,7 +191,15 @@ class TemplateSelector:
154
191
  # Title
155
192
  click.echo()
156
193
  click.echo("┌" + "─" * (width - 2) + "┐")
157
- click.echo("│" + click.style(" 🎯 INITIALIZE DELIVERABLES FROM TEMPLATE ".center(width - 2), bold=True, fg='cyan') + "│")
194
+ click.echo(
195
+ "│"
196
+ + click.style(
197
+ " 🎯 INITIALIZE DELIVERABLES FROM TEMPLATE ".center(width - 2),
198
+ bold=True,
199
+ fg="cyan",
200
+ )
201
+ + "│"
202
+ )
158
203
  click.echo("└" + "─" * (width - 2) + "┘")
159
204
  click.echo()
160
205
 
@@ -162,7 +207,9 @@ class TemplateSelector:
162
207
  filter_parts = []
163
208
  for idx, (key, label, _) in enumerate(self.CATEGORIES):
164
209
  if key == self.current_filter:
165
- filter_parts.append(click.style(f"[{idx+1}] {label}", fg='cyan', bold=True))
210
+ filter_parts.append(
211
+ click.style(f"[{idx+1}] {label}", fg="cyan", bold=True)
212
+ )
166
213
  else:
167
214
  filter_parts.append(f"[{idx+1}] {label}")
168
215
  click.echo(" " + " | ".join(filter_parts))
@@ -173,12 +220,14 @@ class TemplateSelector:
173
220
  selected_info = ""
174
221
  if self.selected_idx is not None:
175
222
  selected_info = f" | {click.style('Selected:', bold=True, fg='green')} 1"
176
- click.echo(f" {click.style('Total:', bold=True)} {total} templates{selected_info}")
223
+ click.echo(
224
+ f" {click.style('Total:', bold=True)} {total} templates{selected_info}"
225
+ )
177
226
  click.echo()
178
227
 
179
228
  # Calculate visible items
180
229
  page_end = min(self.page_start + self.page_size, len(self.templates))
181
- visible_templates = self.templates[self.page_start:page_end]
230
+ visible_templates = self.templates[self.page_start : page_end]
182
231
 
183
232
  # Create table
184
233
  table = Table(
@@ -186,7 +235,7 @@ class TemplateSelector:
186
235
  header_style="bold cyan",
187
236
  box=DesignSystem.TABLE_BOX,
188
237
  padding=(0, 1),
189
- expand=True
238
+ expand=True,
190
239
  )
191
240
 
192
241
  table.add_column(" ", width=2, justify="center", no_wrap=True)
@@ -199,21 +248,21 @@ class TemplateSelector:
199
248
 
200
249
  for idx, template in enumerate(visible_templates):
201
250
  absolute_idx = self.page_start + idx
202
- is_cursor = (absolute_idx == self.cursor_pos)
203
- is_selected = (absolute_idx == self.selected_idx)
251
+ is_cursor = absolute_idx == self.cursor_pos
252
+ is_selected = absolute_idx == self.selected_idx
204
253
 
205
254
  cursor = self.CURSOR if is_cursor else self.NO_CURSOR
206
255
  radio = self.RADIO_SELECTED if is_selected else self.RADIO_EMPTY
207
256
 
208
257
  category_icon = self._get_category(template)
209
- deliverable_count = len(template.get('deliverables', []))
210
- framework = (template.get('framework') or '').upper()
258
+ deliverable_count = len(template.get("deliverables", []))
259
+ framework = (template.get("framework") or "").upper()
211
260
 
212
261
  row = [
213
262
  cursor,
214
263
  radio,
215
264
  str(absolute_idx + 1),
216
- template['name'],
265
+ template["name"],
217
266
  str(deliverable_count),
218
267
  category_icon,
219
268
  framework,
@@ -244,7 +293,9 @@ class TemplateSelector:
244
293
  if self.show_inline_options and self.selected_idx is not None:
245
294
  # Inline options when template is selected
246
295
  template = self.templates[self.selected_idx]
247
- click.echo(f" {click.style('Selected:', bold=True, fg='green')} {template['name']} ({len(template.get('deliverables', []))} deliverables)")
296
+ click.echo(
297
+ f" {click.style('Selected:', bold=True, fg='green')} {template['name']} ({len(template.get('deliverables', []))} deliverables)"
298
+ )
248
299
  click.echo()
249
300
  click.echo(f" [v] Preview - View template details")
250
301
  click.echo(f" [l] Load - Load this template")
@@ -266,16 +317,16 @@ class TemplateSelector:
266
317
  """Handle keypress."""
267
318
  # Handle inline options mode first
268
319
  if self.show_inline_options and self.selected_idx is not None:
269
- if key == 'v':
320
+ if key == "v":
270
321
  # Preview selected template
271
322
  self._preview_template(self.selected_idx)
272
323
  return
273
- elif key == 'l':
324
+ elif key == "l":
274
325
  # Load selected template
275
326
  self.selected_template = self.templates[self.selected_idx]
276
327
  self.running = False
277
328
  return
278
- elif key in ('b', KEY_ESCAPE):
329
+ elif key in ("b", KEY_ESCAPE):
279
330
  # Back - deselect
280
331
  self.selected_idx = None
281
332
  self.show_inline_options = False
@@ -285,42 +336,42 @@ class TemplateSelector:
285
336
  self.show_inline_options = False
286
337
 
287
338
  # Filter keys (1-4)
288
- if key == '1':
289
- self._apply_filter('all')
339
+ if key == "1":
340
+ self._apply_filter("all")
290
341
  return
291
- elif key == '2':
292
- self._apply_filter('compliance')
342
+ elif key == "2":
343
+ self._apply_filter("compliance")
293
344
  return
294
- elif key == '3':
295
- self._apply_filter('pentest')
345
+ elif key == "3":
346
+ self._apply_filter("pentest")
296
347
  return
297
- elif key == '4':
298
- self._apply_filter('industry')
348
+ elif key == "4":
349
+ self._apply_filter("industry")
299
350
  return
300
351
 
301
352
  # Navigation - Up
302
- if key in (KEY_UP, 'k'):
353
+ if key in (KEY_UP, "k"):
303
354
  if self.cursor_pos > 0:
304
355
  self.cursor_pos -= 1
305
356
  if self.cursor_pos < self.page_start:
306
357
  self.page_start = max(0, self.page_start - self.page_size)
307
358
 
308
359
  # Navigation - Down
309
- elif key in (KEY_DOWN, 'j'):
360
+ elif key in (KEY_DOWN, "j"):
310
361
  if self.cursor_pos < len(self.templates) - 1:
311
362
  self.cursor_pos += 1
312
363
  if self.cursor_pos >= self.page_start + self.page_size:
313
364
  self.page_start += self.page_size
314
365
 
315
366
  # Page Up / Previous Page
316
- elif key in (KEY_PAGE_UP, 'p', '[', '<'):
367
+ elif key in (KEY_PAGE_UP, "p", "[", "<"):
317
368
  current_page = self.page_start // self.page_size
318
369
  if current_page > 0:
319
370
  self.page_start = (current_page - 1) * self.page_size
320
371
  self.cursor_pos = self.page_start
321
372
 
322
373
  # Page Down / Next Page
323
- elif key in (KEY_PAGE_DOWN, 'n', ']', '>'):
374
+ elif key in (KEY_PAGE_DOWN, "n", "]", ">"):
324
375
  total_pages = (len(self.templates) + self.page_size - 1) // self.page_size
325
376
  current_page = self.page_start // self.page_size
326
377
  if current_page < total_pages - 1:
@@ -328,15 +379,15 @@ class TemplateSelector:
328
379
  self.cursor_pos = self.page_start
329
380
 
330
381
  # Preview (v key)
331
- elif key == 'v':
382
+ elif key == "v":
332
383
  self._preview_template()
333
384
 
334
385
  # Import
335
- elif key == 'i':
386
+ elif key == "i":
336
387
  self._import_template()
337
388
 
338
389
  # Space - Select/toggle template and show inline options
339
- elif key in (KEY_SPACE, ' '):
390
+ elif key in (KEY_SPACE, " "):
340
391
  if self.selected_idx == self.cursor_pos:
341
392
  # Deselect if same item
342
393
  self.selected_idx = None
@@ -347,12 +398,12 @@ class TemplateSelector:
347
398
  self.show_inline_options = True
348
399
 
349
400
  # Enter - Quick load (bypass inline options)
350
- elif key in (KEY_ENTER, '\r', '\n'):
401
+ elif key in (KEY_ENTER, "\r", "\n"):
351
402
  self.selected_template = self.templates[self.cursor_pos]
352
403
  self.running = False
353
404
 
354
405
  # Cancel - q or Escape
355
- elif key in (KEY_ESCAPE, 'q', '\x03'):
406
+ elif key in (KEY_ESCAPE, "q", "\x03"):
356
407
  self.selected_template = None
357
408
  self.running = False
358
409
 
@@ -368,26 +419,28 @@ class TemplateSelector:
368
419
  """Import a template from JSON file."""
369
420
  DesignSystem.clear_screen()
370
421
  click.echo()
371
- click.echo(click.style(" 📦 IMPORT TEMPLATE", bold=True, fg='cyan'))
422
+ click.echo(click.style(" 📦 IMPORT TEMPLATE", bold=True, fg="cyan"))
372
423
  click.echo()
373
424
 
374
- file_path = click.prompt(" Enter path to template JSON file", type=str, default='')
425
+ file_path = click.prompt(
426
+ " Enter path to template JSON file", type=str, default=""
427
+ )
375
428
  if not file_path:
376
429
  return
377
430
 
378
431
  try:
379
432
  tm = TemplateManager()
380
- with open(file_path, 'r') as f:
433
+ with open(file_path, "r") as f:
381
434
  json_data = f.read()
382
435
  template_id = tm.import_template(json_data)
383
436
  template = tm.get_template(template_id)
384
437
  click.echo()
385
- click.echo(click.style(f" ✅ Imported: {template['name']}", fg='green'))
438
+ click.echo(click.style(f" ✅ Imported: {template['name']}", fg="green"))
386
439
  click.pause()
387
440
  # Refresh templates list
388
441
  self.templates = tm.list_templates()
389
442
  except Exception as e:
390
- click.echo(click.style(f" ❌ Import failed: {e}", fg='red'))
443
+ click.echo(click.style(f" ❌ Import failed: {e}", fg="red"))
391
444
  click.pause()
392
445
 
393
446
 
@@ -405,10 +458,21 @@ def show_template_selector(engagement_id: int) -> bool:
405
458
 
406
459
  # Sort by category then name
407
460
  def sort_key(t):
408
- framework = t.get('framework', '') or ''
409
- compliance = ['hipaa', 'pci-dss', 'nist', 'owasp', 'soc2', 'iso27001', 'cis', 'cmmc', 'gdpr', 'glba']
410
- pentest = ['ptes', 'internal', 'external', 'webapp', 'redteam', 'cloud', 'ad']
411
- industry = ['nerc-cip', 'hitrust', 'ffiec']
461
+ framework = t.get("framework", "") or ""
462
+ compliance = [
463
+ "hipaa",
464
+ "pci-dss",
465
+ "nist",
466
+ "owasp",
467
+ "soc2",
468
+ "iso27001",
469
+ "cis",
470
+ "cmmc",
471
+ "gdpr",
472
+ "glba",
473
+ ]
474
+ pentest = ["ptes", "internal", "external", "webapp", "redteam", "cloud", "ad"]
475
+ industry = ["nerc-cip", "hitrust", "ffiec"]
412
476
 
413
477
  if framework in compliance:
414
478
  cat = 0
@@ -418,7 +482,7 @@ def show_template_selector(engagement_id: int) -> bool:
418
482
  cat = 2
419
483
  else:
420
484
  cat = 3
421
- return (cat, t['name'])
485
+ return (cat, t["name"])
422
486
 
423
487
  all_templates.sort(key=sort_key)
424
488
 
@@ -444,23 +508,34 @@ def show_template_selector(engagement_id: int) -> bool:
444
508
  continue
445
509
 
446
510
  try:
447
- count = tm.apply_template(selected['id'], engagement_id)
511
+ count = tm.apply_template(selected["id"], engagement_id)
448
512
  click.echo()
449
- click.echo(click.style(f" ✅ Loaded {count} deliverables from template", fg='green'))
450
-
451
- auto_val_count = sum(1 for d in selected['deliverables'] if d.get('auto_validate'))
513
+ click.echo(
514
+ click.style(
515
+ f" ✅ Loaded {count} deliverables from template", fg="green"
516
+ )
517
+ )
518
+
519
+ auto_val_count = sum(
520
+ 1 for d in selected["deliverables"] if d.get("auto_validate")
521
+ )
452
522
  if auto_val_count > 0:
453
- click.echo(click.style(f" ✅ {auto_val_count} deliverables have auto-validation enabled", fg='green'))
523
+ click.echo(
524
+ click.style(
525
+ f" ✅ {auto_val_count} deliverables have auto-validation enabled",
526
+ fg="green",
527
+ )
528
+ )
454
529
 
455
530
  click.echo()
456
- click.echo(click.style(" 💡 Next steps:", fg='cyan'))
531
+ click.echo(click.style(" 💡 Next steps:", fg="cyan"))
457
532
  click.echo(" • Press [V] to validate and see current progress")
458
533
  click.echo(" • Start testing and link evidence as you go")
459
534
  click.echo(" • Export report when complete")
460
535
  click.pause()
461
536
  return True
462
537
  except Exception as e:
463
- click.echo(click.style(f" ❌ Failed to load template: {e}", fg='red'))
538
+ click.echo(click.style(f" ❌ Failed to load template: {e}", fg="red"))
464
539
  click.pause()
465
540
  return False
466
541
 
@@ -471,46 +546,62 @@ def _show_template_preview(template: dict):
471
546
  width = DesignSystem.get_terminal_width()
472
547
 
473
548
  click.echo("\n┌" + "─" * (width - 2) + "┐")
474
- click.echo("│" + click.style(" 📋 TEMPLATE PREVIEW ".center(width - 2), bold=True, fg='cyan') + "│")
549
+ click.echo(
550
+ "│"
551
+ + click.style(" 📋 TEMPLATE PREVIEW ".center(width - 2), bold=True, fg="cyan")
552
+ + "│"
553
+ )
475
554
  click.echo("└" + "─" * (width - 2) + "┘")
476
555
  click.echo()
477
556
 
478
557
  click.echo(f" Name: {click.style(template['name'], bold=True)}")
479
- if template.get('description'):
558
+ if template.get("description"):
480
559
  click.echo(f" Description: {template['description']}")
481
- if template.get('framework'):
560
+ if template.get("framework"):
482
561
  click.echo(f" Framework: {template['framework'].upper()}")
483
562
  click.echo(f" Total Deliverables: {len(template.get('deliverables', []))}")
484
563
 
485
- auto_val_count = sum(1 for d in template.get('deliverables', []) if d.get('auto_validate'))
486
- click.echo(f" Auto-validation: {auto_val_count}/{len(template.get('deliverables', []))} deliverables")
564
+ auto_val_count = sum(
565
+ 1 for d in template.get("deliverables", []) if d.get("auto_validate")
566
+ )
567
+ click.echo(
568
+ f" Auto-validation: {auto_val_count}/{len(template.get('deliverables', []))} deliverables"
569
+ )
487
570
 
488
571
  click.echo()
489
- click.echo(click.style(" DELIVERABLES", bold=True, fg='cyan'))
572
+ click.echo(click.style(" DELIVERABLES", bold=True, fg="cyan"))
490
573
  click.echo(" " + "═" * (width - 4))
491
574
  click.echo()
492
575
 
493
576
  # Group by category
494
577
  categories = {}
495
- for d in template.get('deliverables', []):
496
- cat = d.get('category', 'other')
578
+ for d in template.get("deliverables", []):
579
+ cat = d.get("category", "other")
497
580
  if cat not in categories:
498
581
  categories[cat] = []
499
582
  categories[cat].append(d)
500
583
 
501
584
  for category, deliverables in categories.items():
502
- click.echo(click.style(f" {category.upper().replace('_', ' ')}", bold=True, fg='cyan'))
585
+ click.echo(
586
+ click.style(f" {category.upper().replace('_', ' ')}", bold=True, fg="cyan")
587
+ )
503
588
  click.echo(" " + "─" * (width - 4))
504
589
 
505
590
  for d in deliverables:
506
591
  priority_color = {
507
- 'critical': 'red',
508
- 'high': 'yellow',
509
- 'medium': 'white',
510
- 'low': 'bright_black'
511
- }.get(d.get('priority', 'medium'), 'white')
512
-
513
- auto_val_icon = '' if d.get('auto_validate') else ' '
514
- click.echo(f" [{auto_val_icon}] " + click.style(f"[{d.get('priority', 'medium').upper()}]", fg=priority_color) + f" {d['title']}")
592
+ "critical": "red",
593
+ "high": "yellow",
594
+ "medium": "white",
595
+ "low": "bright_black",
596
+ }.get(d.get("priority", "medium"), "white")
597
+
598
+ auto_val_icon = "" if d.get("auto_validate") else " "
599
+ click.echo(
600
+ f" [{auto_val_icon}] "
601
+ + click.style(
602
+ f"[{d.get('priority', 'medium').upper()}]", fg=priority_color
603
+ )
604
+ + f" {d['title']}"
605
+ )
515
606
 
516
607
  click.echo()
souleyez/ui/terminal.py CHANGED
@@ -31,16 +31,16 @@ def init_readline():
31
31
  atexit.register(readline.write_history_file, histfile)
32
32
 
33
33
  # Enable tab completion (can be customized later)
34
- readline.parse_and_bind('tab: complete')
34
+ readline.parse_and_bind("tab: complete")
35
35
 
36
36
  # Set up proper keybindings for editing
37
37
  # These handle backspace, delete, arrow keys properly
38
- readline.parse_and_bind('set editing-mode emacs')
38
+ readline.parse_and_bind("set editing-mode emacs")
39
39
 
40
40
  # Ensure proper character handling
41
- if hasattr(readline, 'set_completer_delims'):
41
+ if hasattr(readline, "set_completer_delims"):
42
42
  # Set delimiters for word completion
43
- readline.set_completer_delims(' \t\n;')
43
+ readline.set_completer_delims(" \t\n;")
44
44
 
45
45
  # Try to fix terminal settings
46
46
  try:
@@ -55,7 +55,7 @@ def init_readline():
55
55
 
56
56
  # Set ERASE character to handle backspace
57
57
  # VERASE is typically index 2 in the cc array
58
- new_settings[6][termios.VERASE] = b'\x7f' # DEL character
58
+ new_settings[6][termios.VERASE] = b"\x7f" # DEL character
59
59
 
60
60
  # Apply the settings
61
61
  termios.tcsetattr(fd, termios.TCSANOW, new_settings)
@@ -77,7 +77,7 @@ def setup_terminal():
77
77
  init_readline()
78
78
 
79
79
  # Ensure stdout is unbuffered for immediate output
80
- if hasattr(sys.stdout, 'reconfigure'):
80
+ if hasattr(sys.stdout, "reconfigure"):
81
81
  try:
82
82
  sys.stdout.reconfigure(line_buffering=True)
83
83
  except (AttributeError, ValueError):