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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +23434 -10286
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.26.dist-info/RECORD +0 -379
  356. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/ui.py CHANGED
@@ -17,32 +17,44 @@ from .utils import nmap_installed, detect_local_subnet
17
17
  try:
18
18
  from .engine.background import enqueue_job, start_worker, stop_worker
19
19
  except Exception:
20
+
20
21
  def enqueue_job(tool, target, args=None, label=None):
21
- raise RuntimeError("enqueue_job not available in this environment (background engine missing).")
22
+ raise RuntimeError(
23
+ "enqueue_job not available in this environment (background engine missing)."
24
+ )
25
+
22
26
  def start_worker(detach=False):
23
- raise RuntimeError("start_worker not available in this environment (background engine missing).")
27
+ raise RuntimeError(
28
+ "start_worker not available in this environment (background engine missing)."
29
+ )
30
+
24
31
  def stop_worker():
25
- raise RuntimeError("stop_worker not available in this environment (background engine missing).")
32
+ raise RuntimeError(
33
+ "stop_worker not available in this environment (background engine missing)."
34
+ )
35
+
36
+
26
37
  from .engine.background import list_jobs, get_job
27
38
  from .engine.loader import discover_plugins
28
39
  from .engine.manager import run_scan_sync
29
40
  from .scanner import run_nmap
30
41
  from .storage.db import get_scans, get_scan
42
+
31
43
  # (history.py kept for export helpers if needed in future, not used for reads)
32
44
 
33
45
  VERSION = "0.5.0"
34
46
 
35
47
  # ANSI colors
36
- CSI = '\033['
37
- RESET = CSI + '0m'
38
- BOLD = CSI + '1m'
39
- GREEN = CSI + '32m'
40
- RED = CSI + '31m'
41
- CYAN = CSI + '36m'
42
- MAG = CSI + '35m'
48
+ CSI = "\033["
49
+ RESET = CSI + "0m"
50
+ BOLD = CSI + "1m"
51
+ GREEN = CSI + "32m"
52
+ RED = CSI + "31m"
53
+ CYAN = CSI + "36m"
54
+ MAG = CSI + "35m"
43
55
 
44
56
  # === Branding ===
45
- BANNER = r'''
57
+ BANNER = r"""
46
58
  ____ ____ _ _ _ ____ _ _ ____ _ _ ____ ____
47
59
  / ___| / ___| | | | | / \ / ___|| | | |/ ___|| | | |/ ___|| _ \
48
60
  \___ \| | | |_| | / _ \ \___ \| | | | | _ | | | | | _ | |_) |
@@ -50,65 +62,81 @@ BANNER = r'''
50
62
  |____/ \____| |_| |_/_/ \_\____/ \___/ \____| \___/ \____||_| \_\
51
63
 
52
64
  $0u! H@cK3R$
53
- '''
54
- FOOTER = 'y0d8 & CyberSoul SecurITy'
65
+ """
66
+ FOOTER = "y0d8 & CyberSoul SecurITy"
67
+
55
68
 
56
69
  def print_banner():
57
- subprocess.run(['clear' if os.name == 'posix' else 'cls'], shell=False)
70
+ subprocess.run(["clear" if os.name == "posix" else "cls"], shell=False)
58
71
  print(MAG + BANNER + RESET)
59
72
  print(f"{BOLD}souleyez v{VERSION}{RESET}")
60
73
  print(FOOTER)
61
74
  print()
62
- print(RED + 'LEGAL:' + RESET + ' Use only on systems you own or have explicit permission to test.')
75
+ print(
76
+ RED
77
+ + "LEGAL:"
78
+ + RESET
79
+ + " Use only on systems you own or have explicit permission to test."
80
+ )
63
81
  print()
64
82
 
83
+
65
84
  def print_header():
66
- print('=' * 60)
67
- print(' souleyez — quick nmap launcher')
68
- print('=' * 60)
85
+ print("=" * 60)
86
+ print(" souleyez — quick nmap launcher")
87
+ print("=" * 60)
69
88
  if not nmap_installed():
70
- print('WARNING: nmap not found on PATH. Install nmap to run scans.')
89
+ print("WARNING: nmap not found on PATH. Install nmap to run scans.")
71
90
  print()
72
91
 
92
+
73
93
  def prompt(text: str) -> str:
74
94
  try:
75
95
  return input(text).strip()
76
96
  except KeyboardInterrupt:
77
- print('\nReturning to menu.')
78
- return ''
97
+ print("\nReturning to menu.")
98
+ return ""
79
99
  except EOFError:
80
- return ''
100
+ return ""
101
+
81
102
 
82
103
  # ---- Nmap run & helpers ----
83
104
 
105
+
84
106
  def _render_table(per_host: List[Dict[str, Any]]):
85
107
  """Pretty-print discovery results (optional table)."""
86
108
  rows = []
87
109
  for h in per_host or []:
88
- addr = h.get('addr') or ''
89
- state = 'Up' if h.get('up') else 'Down'
90
- openp = str(h.get('open') or 0)
110
+ addr = h.get("addr") or ""
111
+ state = "Up" if h.get("up") else "Down"
112
+ openp = str(h.get("open") or 0)
91
113
  rows.append((addr, state, openp))
92
114
  if not rows:
93
- print('No hosts to display.')
115
+ print("No hosts to display.")
94
116
  return
95
117
  col1 = max([len(r[0]) for r in rows] + [4])
96
118
  col2 = max([len(r[1]) for r in rows] + [5])
97
119
  col3 = max([len(r[2]) for r in rows] + [10])
98
- sep = '+' + '-'*(col1+2) + '+' + '-'*(col2+2) + '+' + '-'*(col3+2) + '+'
99
- header = f'| {"Host".ljust(col1)} | {"State".ljust(col2)} | {"Open Ports".ljust(col3)} |'
100
- print(sep); print(header); print(sep)
120
+ sep = "+" + "-" * (col1 + 2) + "+" + "-" * (col2 + 2) + "+" + "-" * (col3 + 2) + "+"
121
+ header = (
122
+ f'| {"Host".ljust(col1)} | {"State".ljust(col2)} | {"Open Ports".ljust(col3)} |'
123
+ )
124
+ print(sep)
125
+ print(header)
126
+ print(sep)
101
127
  for addr, state, openp in rows:
102
- color = GREEN if state == 'Up' else RED
103
- line = f'| {addr.ljust(col1)} | {state.ljust(col2)} | {openp.rjust(col3)} |'
128
+ color = GREEN if state == "Up" else RED
129
+ line = f"| {addr.ljust(col1)} | {state.ljust(col2)} | {openp.rjust(col3)} |"
104
130
  print(color + line + RESET)
105
131
  print(sep)
106
132
 
133
+
107
134
  def _ask_show_table(summary: Dict[str, Any]):
108
- if summary and isinstance(summary, dict) and 'per_host' in summary:
109
- ans = prompt('Show discovery table now? (Y/n) > ').lower()
110
- if ans in ('', 'y', 'yes'):
111
- _render_table(summary.get('per_host'))
135
+ if summary and isinstance(summary, dict) and "per_host" in summary:
136
+ ans = prompt("Show discovery table now? (Y/n) > ").lower()
137
+ if ans in ("", "y", "yes"):
138
+ _render_table(summary.get("per_host"))
139
+
112
140
 
113
141
  def _run_and_record_legacy(target: str, args: List[str], label: str, tool="nmap"):
114
142
  """
@@ -116,73 +144,83 @@ def _run_and_record_legacy(target: str, args: List[str], label: str, tool="nmap"
116
144
  Results are written to legacy history.json by scanner.add_history_entry (if used in scanner),
117
145
  but the UI reads history from SQLite (manager/db handles DB writes for plugin era).
118
146
  """
119
- xml_choice = prompt('Save XML output? (y/N) > ').lower() in ('y','yes')
120
- print('\nStarting scan — live output below (ctrl+C to cancel)\n')
147
+ xml_choice = prompt("Save XML output? (y/N) > ").lower() in ("y", "yes")
148
+ print("\nStarting scan — live output below (ctrl+C to cancel)\n")
121
149
  try:
122
- logpath, rc, xmlpath, summary = run_nmap(target, args, label, save_xml=xml_choice)
150
+ logpath, rc, xmlpath, summary = run_nmap(
151
+ target, args, label, save_xml=xml_choice
152
+ )
123
153
  except EnvironmentError as e:
124
- print(f'Error: {e}')
154
+ print(f"Error: {e}")
125
155
  return
126
- print(f'\nScan finished: log saved to {logpath} (rc={rc})')
156
+ print(f"\nScan finished: log saved to {logpath} (rc={rc})")
127
157
  if xml_choice and xmlpath:
128
- print(f'XML saved to: {xmlpath}')
158
+ print(f"XML saved to: {xmlpath}")
129
159
  if summary:
130
- print('XML Summary:')
160
+ print("XML Summary:")
131
161
  print(f' Hosts total: {summary.get("hosts_total")}')
132
162
  print(f' Hosts up: {summary.get("hosts_up")}')
133
163
  print(f' Open ports: {summary.get("open_ports")}')
134
164
  _ask_show_table(summary)
135
165
  else:
136
- print('No XML summary available (parsing failed).')
166
+ print("No XML summary available (parsing failed).")
137
167
  print()
138
168
 
169
+
139
170
  PRESETS = {
140
- '1': ('Discovery Scan (ping only)', ['-sn']),
141
- '2': ('Fast Scan (-F)', ['-v', '-PS', '-F']),
142
- '3': ('Service & OS (full)', ['-sV', '-O', '-p1-65535']),
171
+ "1": ("Discovery Scan (ping only)", ["-sn"]),
172
+ "2": ("Fast Scan (-F)", ["-v", "-PS", "-F"]),
173
+ "3": ("Service & OS (full)", ["-sV", "-O", "-p1-65535"]),
143
174
  }
144
175
 
176
+
145
177
  def handle_preset_choice(choice: str):
146
178
  preset = PRESETS.get(choice)
147
179
  if not preset:
148
- print('Invalid preset.')
180
+ print("Invalid preset.")
149
181
  return
150
182
  desc, args = preset
151
183
  print(f'\nPreset: {desc}\nArgs: {" ".join(args)}')
152
- target = prompt('Target (IP, host, CIDR) > ')
184
+ target = prompt("Target (IP, host, CIDR) > ")
153
185
  if not target:
154
- print('No target provided. Returning to menu.')
186
+ print("No target provided. Returning to menu.")
155
187
  return
156
- label = prompt('Label for this scan (optional) > ')
188
+ label = prompt("Label for this scan (optional) > ")
157
189
  _run_and_record_legacy(target, args, label, tool="nmap")
158
190
 
191
+
159
192
  def handle_custom():
160
- raw = prompt('Enter custom nmap args (e.g. -sV -p22-80) > ')
193
+ raw = prompt("Enter custom nmap args (e.g. -sV -p22-80) > ")
161
194
  if not raw:
162
- print('No args, returning.')
195
+ print("No args, returning.")
163
196
  return
164
197
  args = raw.split()
165
- target = prompt('Target (IP, host, CIDR) > ')
198
+ target = prompt("Target (IP, host, CIDR) > ")
166
199
  if not target:
167
- print('No target provided. Returning to menu.')
200
+ print("No target provided. Returning to menu.")
168
201
  return
169
- label = prompt('Label for this scan (optional) > ')
202
+ label = prompt("Label for this scan (optional) > ")
170
203
  _run_and_record_legacy(target, args, label, tool="nmap")
171
204
 
205
+
172
206
  def handle_scan_my_lan():
173
207
  subnet = detect_local_subnet()
174
208
  if not subnet:
175
- print('Could not auto-detect your local subnet. Enter it manually (e.g. 192.168.1.0/24).')
176
- subnet = prompt('Subnet > ')
209
+ print(
210
+ "Could not auto-detect your local subnet. Enter it manually (e.g. 192.168.1.0/24)."
211
+ )
212
+ subnet = prompt("Subnet > ")
177
213
  if not subnet:
178
- print('No subnet provided. Returning to menu.')
214
+ print("No subnet provided. Returning to menu.")
179
215
  return
180
- print(f'Using subnet: {subnet}')
181
- label = prompt('Label for this scan (optional) > ')
182
- _run_and_record_legacy(subnet, ['-sn'], label or 'lan', tool="nmap")
216
+ print(f"Using subnet: {subnet}")
217
+ label = prompt("Label for this scan (optional) > ")
218
+ _run_and_record_legacy(subnet, ["-sn"], label or "lan", tool="nmap")
219
+
183
220
 
184
221
  # ---- DB-backed History (H2) ----
185
222
 
223
+
186
224
  def _group_by_tool(scans: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
187
225
  groups = {}
188
226
  for s in scans:
@@ -193,66 +231,76 @@ def _group_by_tool(scans: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]
193
231
  groups[k].sort(key=lambda x: x.get("id", 0), reverse=True)
194
232
  return dict(sorted(groups.items(), key=lambda kv: kv[0])) # alphabetical tool order
195
233
 
234
+
196
235
  def _print_history_list(scans: List[Dict[str, Any]]):
197
236
  """Render a flat list of scans with 1-based indices."""
198
237
  if not scans:
199
- print('No history yet.')
238
+ print("No history yet.")
200
239
  return
201
240
  for i, e in enumerate(scans, start=1):
202
- args_str = ' '.join(e.get('args') or [])
203
- xml_mark = ' [xml]' if e.get('xml') else ''
204
- print(f"{i}) {e.get('ts')} | {e.get('tool')} | {e.get('target')} | {args_str} | label={e.get('label')} | log={e.get('log')}{xml_mark}")
205
- print(f"\nShowing {len(scans)} results | Storage: DB mode | souleyez v{VERSION}\n")
241
+ args_str = " ".join(e.get("args") or [])
242
+ xml_mark = " [xml]" if e.get("xml") else ""
243
+ print(
244
+ f"{i}) {e.get('ts')} | {e.get('tool')} | {e.get('target')} | {args_str} | label={e.get('label')} | log={e.get('log')}{xml_mark}"
245
+ )
246
+ print(
247
+ f"\nShowing {len(scans)} results | Storage: DB mode | souleyez v{VERSION}\n"
248
+ )
249
+
206
250
 
207
251
  def history_view_all() -> List[Dict[str, Any]]:
208
252
  scans = get_scans(limit=100000, tool=None) # H-on: show all
209
253
  _print_history_list(scans)
210
254
  return scans
211
255
 
256
+
212
257
  def history_view_by_tool():
213
258
  scans_all = get_scans(limit=100000)
214
259
  groups = _group_by_tool(scans_all)
215
260
  if not groups:
216
- print('No history yet.')
261
+ print("No history yet.")
217
262
  return
218
263
  print("\nTools in history:")
219
264
  tools = list(groups.keys())
220
265
  for i, t in enumerate(tools, start=1):
221
266
  print(f" {i}) {t} ({len(groups[t])})")
222
267
  print(" 0) Back")
223
- sel = prompt('Choose tool > ')
268
+ sel = prompt("Choose tool > ")
224
269
  if not sel.isdigit():
225
- print('Invalid choice.')
270
+ print("Invalid choice.")
226
271
  return
227
272
  idx = int(sel)
228
273
  if idx == 0 or not (1 <= idx <= len(tools)):
229
274
  return
230
- chosen = tools[idx-1]
275
+ chosen = tools[idx - 1]
231
276
  print(f"\n=== {chosen} history ===")
232
277
  _print_history_list(groups[chosen])
233
278
 
279
+
234
280
  def history_rerun(scans: List[Dict[str, Any]]):
235
281
  if not scans:
236
- print('No history to re-run.')
282
+ print("No history to re-run.")
237
283
  return
238
- sel = prompt('Entry number to re-run (1-based) > ')
284
+ sel = prompt("Entry number to re-run (1-based) > ")
239
285
  if not sel.isdigit():
240
- print('Invalid index.')
286
+ print("Invalid index.")
241
287
  return
242
288
  idx = int(sel)
243
289
  if idx < 1 or idx > len(scans):
244
- print('Out of range.')
245
- return
246
- e = scans[idx-1]
247
- target = e.get('target') or ''
248
- args = e.get('args') or []
249
- label = e.get('label')
250
- tool = (e.get('tool') or 'nmap').lower()
251
- print(f"\nSelected: target={target}, args={' '.join(args)}, label={label}, tool={tool}")
252
- action = prompt('Run now (r), Edit first (e), Back (b) > ').lower()
253
- if action == 'q':
254
- return
255
- if action == 'e':
290
+ print("Out of range.")
291
+ return
292
+ e = scans[idx - 1]
293
+ target = e.get("target") or ""
294
+ args = e.get("args") or []
295
+ label = e.get("label")
296
+ tool = (e.get("tool") or "nmap").lower()
297
+ print(
298
+ f"\nSelected: target={target}, args={' '.join(args)}, label={label}, tool={tool}"
299
+ )
300
+ action = prompt("Run now (r), Edit first (e), Back (b) > ").lower()
301
+ if action == "q":
302
+ return
303
+ if action == "e":
256
304
  new_args_raw = prompt(f"Args [{ ' '.join(args) or '-sn' } ] > ").strip()
257
305
  if new_args_raw:
258
306
  args = new_args_raw.split()
@@ -260,93 +308,122 @@ def history_rerun(scans: List[Dict[str, Any]]):
260
308
  if new_target:
261
309
  target = new_target
262
310
  label = prompt(f"Label [{ label or '' } ] > ").strip() or label
263
- elif action != 'r':
264
- print('Unknown choice.')
311
+ elif action != "r":
312
+ print("Unknown choice.")
265
313
  return
266
314
  # For now, we re-use legacy runner; plugin engine will hook here next
267
315
  _run_and_record_legacy(target, args, label, tool=tool)
268
316
 
317
+
269
318
  def history_export(scans: List[Dict[str, Any]]):
270
319
  if not scans:
271
- print('No history to export.')
320
+ print("No history to export.")
272
321
  return
273
- fmt = prompt('Format (json/csv) > ').lower()
274
- if fmt not in ('json','csv'):
275
- print('Unknown format.')
322
+ fmt = prompt("Format (json/csv) > ").lower()
323
+ if fmt not in ("json", "csv"):
324
+ print("Unknown format.")
276
325
  return
277
- sel = prompt('Entry number to export (1-based, ENTER for latest) > ')
326
+ sel = prompt("Entry number to export (1-based, ENTER for latest) > ")
278
327
  if not sel:
279
328
  entry = scans[0]
280
329
  elif sel.isdigit():
281
330
  idx = int(sel)
282
331
  if idx < 1 or idx > len(scans):
283
- print('Out of range.')
332
+ print("Out of range.")
284
333
  return
285
- entry = scans[idx-1]
334
+ entry = scans[idx - 1]
286
335
  else:
287
- print('Invalid index.')
336
+ print("Invalid index.")
288
337
  return
289
338
  # Export using a simple inline writer (avoid legacy)
290
339
  from pathlib import Path
291
340
  import json, csv
341
+
292
342
  export_dir = Path.home() / ".souleyez" / "exports"
293
343
  export_dir.mkdir(parents=True, exist_ok=True)
294
- def safe(s): return "".join(c if (c.isalnum() or c in '._-') else '_' for c in str(s or ''))
295
- base = f"{safe(entry.get('ts'))}_{safe(entry.get('tool'))}_{safe(entry.get('target'))}"
296
- if entry.get('label'):
344
+
345
+ def safe(s):
346
+ return "".join(c if (c.isalnum() or c in "._-") else "_" for c in str(s or ""))
347
+
348
+ base = (
349
+ f"{safe(entry.get('ts'))}_{safe(entry.get('tool'))}_{safe(entry.get('target'))}"
350
+ )
351
+ if entry.get("label"):
297
352
  base += f"_{safe(entry.get('label'))}"
298
- if fmt == 'json':
353
+ if fmt == "json":
299
354
  path = export_dir / f"{base}.json"
300
- with path.open('w', encoding='utf-8') as f:
355
+ with path.open("w", encoding="utf-8") as f:
301
356
  json.dump(entry, f, indent=2)
302
357
  else:
303
358
  path = export_dir / f"{base}.csv"
304
359
  per_host = entry.get("per_host") or []
305
360
  if not per_host:
306
361
  # minimal CSV row when no per_host breakdown
307
- per_host = [{"addr":"", "up": "", "open": (entry.get("summary") or {}).get("open_ports", 0)}]
308
- with path.open('w', newline='', encoding='utf-8') as f:
362
+ per_host = [
363
+ {
364
+ "addr": "",
365
+ "up": "",
366
+ "open": (entry.get("summary") or {}).get("open_ports", 0),
367
+ }
368
+ ]
369
+ with path.open("w", newline="", encoding="utf-8") as f:
309
370
  w = csv.writer(f)
310
- w.writerow(["timestamp","tool","target","label","host","up","open_ports","log","xml"])
371
+ w.writerow(
372
+ [
373
+ "timestamp",
374
+ "tool",
375
+ "target",
376
+ "label",
377
+ "host",
378
+ "up",
379
+ "open_ports",
380
+ "log",
381
+ "xml",
382
+ ]
383
+ )
311
384
  for h in per_host:
312
- w.writerow([
313
- entry.get("ts",""),
314
- entry.get("tool",""),
315
- entry.get("target",""),
316
- entry.get("label",""),
317
- h.get("addr",""),
318
- "" if h.get("up") is None else bool(h.get("up")),
319
- h.get("open",0),
320
- entry.get("log",""),
321
- entry.get("xml",""),
322
- ])
385
+ w.writerow(
386
+ [
387
+ entry.get("ts", ""),
388
+ entry.get("tool", ""),
389
+ entry.get("target", ""),
390
+ entry.get("label", ""),
391
+ h.get("addr", ""),
392
+ "" if h.get("up") is None else bool(h.get("up")),
393
+ h.get("open", 0),
394
+ entry.get("log", ""),
395
+ entry.get("xml", ""),
396
+ ]
397
+ )
323
398
  print(f"Exported {fmt.upper()} to: {path}")
324
399
 
400
+
325
401
  def history_menu():
326
402
  while True:
327
- print('\nHistory Menu:')
328
- print(' 1) View all history')
329
- print(' 2) View by tool')
330
- print(' 3) Re-run scan')
331
- print(' 4) Export scan')
332
- print(' 5) Back')
403
+ print("\nHistory Menu:")
404
+ print(" 1) View all history")
405
+ print(" 2) View by tool")
406
+ print(" 3) Re-run scan")
407
+ print(" 4) Export scan")
408
+ print(" 5) Back")
333
409
  print()
334
- choice = prompt('Choice > ')
335
- if choice == '1':
410
+ choice = prompt("Choice > ")
411
+ if choice == "1":
336
412
  scans = history_view_all()
337
- elif choice == '2':
413
+ elif choice == "2":
338
414
  history_view_by_tool()
339
415
  scans = None
340
- elif choice == '3':
416
+ elif choice == "3":
341
417
  scans = history_view_all()
342
418
  history_rerun(scans)
343
- elif choice == '4':
419
+ elif choice == "4":
344
420
  scans = history_view_all()
345
421
  history_export(scans)
346
- elif choice == '5' or choice.lower() == 'q':
422
+ elif choice == "5" or choice.lower() == "q":
347
423
  return
348
424
  else:
349
- print('Unknown choice.')
425
+ print("Unknown choice.")
426
+
350
427
 
351
428
  # ---- Main menu ----
352
429
 
@@ -357,75 +434,92 @@ def handle_gobuster():
357
434
  Prompts for target (URL), extra args (user-provided), and an optional label.
358
435
  Requires the user to supply -w <wordlist> in args for Gobuster to run.
359
436
  """
360
- print('\n--- Gobuster (Web Plugin) ---')
361
- target = prompt('Target (URL) > ')
437
+ print("\n--- Gobuster (Web Plugin) ---")
438
+ target = prompt("Target (URL) > ")
362
439
  if not target:
363
- print('No target provided; returning.')
440
+ print("No target provided; returning.")
364
441
  return
365
- print('\n' + '\033[31m\033[1m' + 'REMINDER: Gobuster will run next — make sure you provided -w <wordlist>. Example: /usr/share/wordlists/dirb/common.txt' + '\033[0m')
366
- args_raw = prompt('Gobuster args (e.g. dir -u http://example.com -w /path/wordlist -t 10) > ')
442
+ print(
443
+ "\n"
444
+ + "\033[31m\033[1m"
445
+ + "REMINDER: Gobuster will run next — make sure you provided -w <wordlist>. Example: data/wordlists/web_dirs_common.txt"
446
+ + "\033[0m"
447
+ )
448
+ args_raw = prompt(
449
+ "Gobuster args (e.g. dir -u http://example.com -w /path/wordlist -t 10) > "
450
+ )
367
451
  if not args_raw:
368
- print('No args provided. You must include \' -w <wordlist>\' for Gobuster to run.')
452
+ print(
453
+ "No args provided. You must include ' -w <wordlist>' for Gobuster to run."
454
+ )
369
455
  return
370
456
  args = args_raw.split()
371
457
  # Basic safety check: require -w (wordlist) in args
372
- if not any(a == '-w' for a in args):
373
- print('Gobuster requires a wordlist (-w). Please provide -w <path>. Aborting.')
458
+ if not any(a == "-w" for a in args):
459
+ print("Gobuster requires a wordlist (-w). Please provide -w <path>. Aborting.")
374
460
  return
375
- label = prompt('Label for this scan (optional) > ')
376
- print('\\nStarting Gobuster scan — this will run synchronously (wait until it completes).')
461
+ label = prompt("Label for this scan (optional) > ")
462
+ print(
463
+ "\\nStarting Gobuster scan — this will run synchronously (wait until it completes)."
464
+ )
377
465
  sid = None
378
466
  try:
379
- sid = run_scan_sync('gobuster', target, args, label, save_xml=False)
467
+ sid = run_scan_sync("gobuster", target, args, label, save_xml=False)
380
468
  except Exception as e:
381
- print(f'Error launching gobuster: {e}')
469
+ print(f"Error launching gobuster: {e}")
382
470
  return
383
- print(f'Scan scheduled/completed with id: {sid}')
471
+ print(f"Scan scheduled/completed with id: {sid}")
384
472
  print()
385
473
 
474
+
386
475
  def handle_web_plugins():
387
- print('\n' + '\033[31m\033[1m' + 'NOTE: Gobuster requires -w <wordlist> to run. Example: /usr/share/wordlists/dirb/common.txt' + '\033[0m')
476
+ print(
477
+ "\n"
478
+ + "\033[31m\033[1m"
479
+ + "NOTE: Gobuster requires -w <wordlist> to run. Example: data/wordlists/web_dirs_common.txt"
480
+ + "\033[0m"
481
+ )
388
482
  """
389
483
  Web Plugins submenu (TUI). Keep this minimal: user chooses a plugin to run or Back.
390
484
  """
391
485
  while True:
392
486
  print()
393
- print('--- Web Plugins ---')
394
- print(' 1) Gobuster')
395
- print(' b) Back')
487
+ print("--- Web Plugins ---")
488
+ print(" 1) Gobuster")
489
+ print(" b) Back")
396
490
  print()
397
- ch = prompt('Choice > ').strip().lower()
398
- if ch in ('1','gobuster'):
491
+ ch = prompt("Choice > ").strip().lower()
492
+ if ch in ("1", "gobuster"):
399
493
  handle_gobuster()
400
- elif ch == 'q':
494
+ elif ch == "q":
401
495
  return
402
496
  else:
403
- print('Unknown choice. Use 1 or b (back).')
497
+ print("Unknown choice. Use 1 or b (back).")
404
498
 
405
499
 
406
500
  def show_menu():
407
501
  detected = detect_local_subnet()
408
502
  print_header()
409
503
  if detected:
410
- print(f' 0) Scan my LAN (auto-detect) [{detected}]')
504
+ print(f" 0) Scan my LAN (auto-detect) [{detected}]")
411
505
  else:
412
- print(' 0) Scan my LAN (auto-detect) [no subnet detected]')
506
+ print(" 0) Scan my LAN (auto-detect) [no subnet detected]")
413
507
  print()
414
- print('Recon Plugins (Network)')
415
- print(' 1) Discovery Scan (ping only)')
416
- print(' 2) Fast Scan')
417
- print(' 3) Full Service/OS Scan')
508
+ print("Recon Plugins (Network)")
509
+ print(" 1) Discovery Scan (ping only)")
510
+ print(" 2) Fast Scan")
511
+ print(" 3) Full Service/OS Scan")
418
512
  print()
419
- print('Web Plugins')
420
- print(' 7) Web Plugins (Gobuster, etc.)')
513
+ print("Web Plugins")
514
+ print(" 7) Web Plugins (Gobuster, etc.)")
421
515
  print()
422
- print(' 4) Custom Scan')
423
- print(' 5) History')
424
- print(' 6) Exit')
516
+ print(" 4) Custom Scan")
517
+ print(" 5) History")
518
+ print(" 6) Exit")
425
519
  print()
426
- print('System')
427
- print(' 8) Background Jobs')
428
- print(' 9) Network Plugins')
520
+ print("System")
521
+ print(" 8) Background Jobs")
522
+ print(" 9) Network Plugins")
429
523
  print()
430
524
 
431
525
 
@@ -433,57 +527,71 @@ def run_menu_loop():
433
527
  print_banner()
434
528
  while True:
435
529
  show_menu()
436
- choice = prompt('Choice > ').strip().lower()
437
- if choice == '0':
530
+ choice = prompt("Choice > ").strip().lower()
531
+ if choice == "0":
438
532
  handle_scan_my_lan()
439
- elif choice in ('1', '2', '3'):
533
+ elif choice in ("1", "2", "3"):
440
534
  handle_preset_choice(choice)
441
- elif choice == '4':
535
+ elif choice == "4":
442
536
  handle_custom()
443
- elif choice == '5':
537
+ elif choice == "5":
444
538
  history_menu()
445
- elif choice == '8':
539
+ elif choice == "8":
446
540
  background_jobs_menu()
447
- elif choice == '6' or choice.lower() in ('q', 'quit', 'exit'):
448
- print('Goodbye!')
541
+ elif choice == "6" or choice.lower() in ("q", "quit", "exit"):
542
+ print("Goodbye!")
449
543
  sys.exit(0)
450
544
  # web plugins (if present) — typically printed as 7 in the menu
451
- elif choice == '7':
545
+ elif choice == "7":
452
546
  try:
453
547
  from .ui import handle_web_plugins
548
+
454
549
  handle_web_plugins()
455
550
  except Exception:
456
551
  # if not present, show message and continue
457
- print('Web Plugins are not available.')
552
+ print("Web Plugins are not available.")
458
553
  # background jobs (system) — printed as 8
459
- elif choice == '8':
554
+ elif choice == "8":
460
555
  try:
461
556
  # reuse CLI job menu if exposed; fallback to _cmd_jobs via main
462
557
  from .main import _cmd_jobs
463
- _cmd_jobs(['list'])
558
+
559
+ _cmd_jobs(["list"])
464
560
  except Exception:
465
561
  # if a background jobs TUI exists, call it; else notify
466
- print('Background jobs command not available from TUI.')
562
+ print("Background jobs command not available from TUI.")
467
563
  # network plugins (printed as 9)
468
- elif choice == '9':
564
+ elif choice == "9":
469
565
  try:
470
566
  handle_network_plugins()
471
567
  except NameError:
472
- print('Network plugins are not available.')
568
+ print("Network plugins are not available.")
473
569
  else:
474
- print('Invalid choice. Try again.\\n')
475
-
570
+ print("Invalid choice. Try again.\\n")
476
571
 
477
572
 
478
573
  def _status_icon_txt(s: str) -> str:
479
- m = {"queued":"● queued","running":"▶ running","done":"✔ done","failed":"✖ failed"}
574
+ m = {
575
+ "queued": "● queued",
576
+ "running": "▶ running",
577
+ "done": "✔ done",
578
+ "failed": "✖ failed",
579
+ }
480
580
  return m.get((s or "").lower(), s or "?")
481
581
 
582
+
482
583
  def _print_jobs_grouped():
483
584
  # Group order: running -> queued -> done -> failed
484
- groups = [("running","RUNNING"), ("queued","QUEUED"), ("done","DONE"), ("failed","FAILED")]
585
+ groups = [
586
+ ("running", "RUNNING"),
587
+ ("queued", "QUEUED"),
588
+ ("done", "DONE"),
589
+ ("failed", "FAILED"),
590
+ ]
485
591
  print(" BACKGROUND JOBS")
486
- print("──────────────────────────────────────────────────────────────────────────────")
592
+ print(
593
+ "──────────────────────────────────────────────────────────────────────────────"
594
+ )
487
595
  jobs = list_jobs(limit=200)
488
596
  if not jobs:
489
597
  print(" (no jobs)")
@@ -494,77 +602,86 @@ def _print_jobs_grouped():
494
602
  continue
495
603
  print(f" {title}")
496
604
  print(" ID TOOL STATUS TARGET CREATED")
497
- print(" ───────────────────────────────────────────────────────────────────────────")
605
+ print(
606
+ " ───────────────────────────────────────────────────────────────────────────"
607
+ )
498
608
  for j in subset:
499
609
  st = (j.get("status") or "").lower()
500
610
  icon = _status_icon_txt(st)
501
- print(f"{str(j['id']).ljust(4)} {j['tool'][:10].ljust(10)} {icon[:10].ljust(10)} {j['target'][:30].ljust(30)} {j.get('created_at','')}")
611
+ print(
612
+ f"{str(j['id']).ljust(4)} {j['tool'][:10].ljust(10)} {icon[:10].ljust(10)} {j['target'][:30].ljust(30)} {j.get('created_at','')}"
613
+ )
502
614
  print()
503
615
 
616
+
504
617
  def _jobs_tail_prompt():
505
- jid = prompt('Job id to tail > ')
618
+ jid = prompt("Job id to tail > ")
506
619
  if not jid or not jid.isdigit():
507
- print('Invalid job id.')
620
+ print("Invalid job id.")
508
621
  return
509
622
  j = get_job(int(jid))
510
623
  if not j:
511
- print('Job not found.')
624
+ print("Job not found.")
512
625
  return
513
- scan_id = j.get('result_scan_id')
626
+ scan_id = j.get("result_scan_id")
514
627
  if not scan_id:
515
- print('Job has no scan result yet.')
628
+ print("Job has no scan result yet.")
516
629
  return
517
630
  try:
518
631
  from .storage.db import get_scan
632
+
519
633
  rec = get_scan(scan_id)
520
- if rec and rec.get('log'):
634
+ if rec and rec.get("log"):
521
635
  try:
522
- with open(rec['log'], 'r', encoding='utf-8', errors='ignore') as fh:
636
+ with open(rec["log"], "r", encoding="utf-8", errors="ignore") as fh:
523
637
  lines = fh.readlines()
524
- print(''.join(lines[-200:]))
638
+ print("".join(lines[-200:]))
525
639
  except Exception as e:
526
- print('Could not read log file:', e)
640
+ print("Could not read log file:", e)
527
641
  else:
528
- print('No log path recorded for scan.')
642
+ print("No log path recorded for scan.")
529
643
  except Exception as e:
530
- print('Cannot load scan record:', e)
644
+ print("Cannot load scan record:", e)
645
+
531
646
 
532
647
  def handle_background_jobs():
533
648
  while True:
534
- print('\\n--- Background Jobs ---')
535
- print(' 1) View queued jobs')
536
- print(' 2) View running jobs')
537
- print(' 3) View completed jobs')
538
- print(' 4) Tail job log')
539
- print(' v) View live output')
540
- print(' 5) Start worker')
541
- print(' 6) Stop worker')
542
- print(' r) Refresh')
543
- print(' b) Back')
649
+ print("\\n--- Background Jobs ---")
650
+ print(" 1) View queued jobs")
651
+ print(" 2) View running jobs")
652
+ print(" 3) View completed jobs")
653
+ print(" 4) Tail job log")
654
+ print(" v) View live output")
655
+ print(" 5) Start worker")
656
+ print(" 6) Stop worker")
657
+ print(" r) Refresh")
658
+ print(" b) Back")
544
659
  print()
545
- ch = prompt('Choice > ').strip().lower()
546
- if ch in ('1','2','3','r'):
660
+ ch = prompt("Choice > ").strip().lower()
661
+ if ch in ("1", "2", "3", "r"):
547
662
  _print_jobs_grouped()
548
- elif ch == '4':
663
+ elif ch == "4":
549
664
  _jobs_tail_prompt()
550
- elif ch == 'v':
665
+ elif ch == "v":
551
666
  view_job_live_prompt()
552
- elif ch == '5':
553
- print('Run worker:')
554
- print(' 1) Foreground (monitor in this terminal)')
555
- print(' 2) Background (detach)')
556
- sel = prompt('Choice > ').strip()
557
- fg = (sel == '1')
667
+ elif ch == "5":
668
+ print("Run worker:")
669
+ print(" 1) Foreground (monitor in this terminal)")
670
+ print(" 2) Background (detach)")
671
+ sel = prompt("Choice > ").strip()
672
+ fg = sel == "1"
558
673
  start_worker(detach=(not fg))
559
- print('Worker started ' + ('(foreground thread).' if fg else '(daemon background thread).'))
560
- elif ch == '6':
674
+ print(
675
+ "Worker started "
676
+ + ("(foreground thread)." if fg else "(daemon background thread).")
677
+ )
678
+ elif ch == "6":
561
679
  stop_worker()
562
- print('Worker stop signal sent.')
563
- elif ch == 'q':
680
+ print("Worker stop signal sent.")
681
+ elif ch == "q":
564
682
  return
565
683
  else:
566
- print('Unknown choice.')
567
-
684
+ print("Unknown choice.")
568
685
 
569
686
 
570
687
  # ----------------------------
@@ -572,12 +689,13 @@ def handle_background_jobs():
572
689
  # ----------------------------
573
690
  def view_job_live_prompt():
574
691
  """Prompt for a job id and launch the live viewer."""
575
- jid = prompt('Job id to view live > ')
692
+ jid = prompt("Job id to view live > ")
576
693
  if not jid or not jid.isdigit():
577
- print('Invalid job id.')
694
+ print("Invalid job id.")
578
695
  return
579
696
  view_job_live(int(jid))
580
697
 
698
+
581
699
  def view_job_live(job_id: int, refresh_interval: float = 1.0, max_lines: int = 300):
582
700
  """
583
701
  Framed live tail viewer (T3).
@@ -590,85 +708,99 @@ def view_job_live(job_id: int, refresh_interval: float = 1.0, max_lines: int = 3
590
708
  - falls back to ~/.souleyez/artifacts/<job_id>.log when missing
591
709
  """
592
710
  import os, sys, time, json, select
711
+
593
712
  # lookup job record
594
713
  j = None
595
714
  try:
596
715
  j = get_job(job_id)
597
716
  except Exception as _e:
598
- print('Could not load job:', _e)
717
+ print("Could not load job:", _e)
599
718
  return
600
719
  if not j:
601
- print('Job not found.')
720
+ print("Job not found.")
602
721
  return
603
722
  # attempt to find log path
604
723
  log_path = None
605
- scan_id = j.get('result_scan_id')
724
+ scan_id = j.get("result_scan_id")
606
725
  if scan_id:
607
726
  try:
608
727
  from .storage.db import get_scan
728
+
609
729
  rec = get_scan(scan_id)
610
- if rec and rec.get('log'):
611
- log_path = rec.get('log')
730
+ if rec and rec.get("log"):
731
+ log_path = rec.get("log")
612
732
  except Exception:
613
733
  # ignore, fallback later
614
734
  pass
615
735
  if not log_path:
616
736
  # default fallback
617
- log_dir = os.path.join(os.path.expanduser('~'), '.souleyez', 'artifacts')
737
+ log_dir = os.path.join(os.path.expanduser("~"), ".souleyez", "artifacts")
618
738
  if not os.path.isdir(log_dir):
619
739
  os.makedirs(log_dir, exist_ok=True)
620
- log_path = os.path.join(log_dir, f'{job_id}.log')
740
+ log_path = os.path.join(log_dir, f"{job_id}.log")
621
741
  # viewer loop
622
- print(f'Opening live view for job {job_id} (log: {log_path}) — press q then ENTER to quit')
742
+ print(
743
+ f"Opening live view for job {job_id} (log: {log_path}) — press q then ENTER to quit"
744
+ )
623
745
  last_size = 0
624
746
  try:
625
747
  while True:
626
748
  # clear screen
627
- subprocess.run(['clear' if os.name == 'posix' else 'cls'], shell=False)
749
+ subprocess.run(["clear" if os.name == "posix" else "cls"], shell=False)
628
750
  # header frame
629
- print('' + ''*74 + '')
630
- title = f' Live Output — Job {job_id} '
631
- print('' + title.center(74) + '')
632
- print('' + ''*74 + '')
751
+ print("" + "" * 74 + "")
752
+ title = f" Live Output — Job {job_id} "
753
+ print("" + title.center(74) + "")
754
+ print("" + "" * 74 + "")
633
755
  # read last lines safely
634
756
  lines = []
635
757
  try:
636
758
  if os.path.exists(log_path):
637
- with open(log_path, 'r', encoding="utf-8", errors="replace") as fh:
759
+ with open(log_path, "r", encoding="utf-8", errors="replace") as fh:
638
760
  all_lines = fh.readlines()
639
761
  if len(all_lines) > max_lines:
640
762
  lines = all_lines[-max_lines:]
641
763
  else:
642
764
  lines = all_lines
643
765
  else:
644
- lines = ['(log file not found yet — waiting for output...)']
766
+ lines = ["(log file not found yet — waiting for output...)"]
645
767
  except Exception as e:
646
- lines = [f'(error reading log: {e})']
768
+ lines = [f"(error reading log: {e})"]
647
769
  # print content with side padding
648
770
  for L in lines:
649
771
  # ensure single-line prints (no embedded control sequences)
650
- L = L.rstrip('\n')
772
+ L = L.rstrip("\n")
651
773
  # clamp to width 74
652
774
  if len(L) > 74:
653
775
  L = L[-74:]
654
- print('' + L.ljust(74) + '')
776
+ print("" + L.ljust(74) + "")
655
777
  # footer
656
- print('' + ''*74 + '')
657
- print('│' + f' Press q + ENTER to quit | Refresh every {refresh_interval}s '.ljust(74) + '│')
658
- print('╰' + '─'*74 + '╯')
778
+ print("" + "" * 74 + "")
779
+ print(
780
+ "│"
781
+ + f" Press q + ENTER to quit | Refresh every {refresh_interval}s ".ljust(
782
+ 74
783
+ )
784
+ + "│"
785
+ )
786
+ print("╰" + "─" * 74 + "╯")
659
787
  # wait with non-blocking check for input
660
788
  # select.select works on POSIX; on Windows this might behave differently.
661
789
  sys.stdout.flush()
662
790
  rlist, _, _ = select.select([sys.stdin], [], [], refresh_interval)
663
791
  if rlist:
664
792
  inp = sys.stdin.readline().strip().lower()
665
- if inp == 'q':
793
+ if inp == "q":
666
794
  break
667
795
  # check job status and exit when done (optional)
668
796
  try:
669
797
  j = get_job(job_id)
670
- if j and (j.get('status') in ('done','failed')):
671
- print('\nJob finished (status: {}). Press ENTER to return.'.format(j.get('status')))
798
+ if j and (j.get("status") in ("done", "failed")):
799
+ print(
800
+ "\nJob finished (status: {}). Press ENTER to return.".format(
801
+ j.get("status")
802
+ )
803
+ )
672
804
  # wait for user to press enter
673
805
  _ = sys.stdin.readline()
674
806
  break
@@ -680,48 +812,45 @@ def view_job_live(job_id: int, refresh_interval: float = 1.0, max_lines: int = 3
680
812
  pass
681
813
  finally:
682
814
  # small tidy
683
- print('Exiting live view.')
815
+ print("Exiting live view.")
684
816
  print(" 8) Background Jobs")
685
817
 
686
818
 
687
-
688
-
689
-
690
-
691
819
  def _print_plugin_help_block(helpdata: dict):
692
820
  """Pretty-print H2 style help for plugin HELP dict."""
693
821
  if not helpdata:
694
822
  print("No help available for this plugin.")
695
823
  return
696
824
  print()
697
- print(helpdata.get("name", "Plugin") )
825
+ print(helpdata.get("name", "Plugin"))
698
826
  print("─" * max(10, len(helpdata.get("name", ""))))
699
827
  print("Description:")
700
- for line in helpdata.get("description","").splitlines():
828
+ for line in helpdata.get("description", "").splitlines():
701
829
  print(" " + line)
702
830
  print()
703
831
  print("Usage:")
704
- print(" " + helpdata.get("usage",""))
832
+ print(" " + helpdata.get("usage", ""))
705
833
  print()
706
834
  if helpdata.get("examples"):
707
835
  print("Examples:")
708
- for ex in helpdata.get("examples",[]):
836
+ for ex in helpdata.get("examples", []):
709
837
  print(" " + ex)
710
838
  print()
711
839
  if helpdata.get("flags"):
712
840
  print("Useful Flags:")
713
- for flag, desc in helpdata.get("flags",[]):
841
+ for flag, desc in helpdata.get("flags", []):
714
842
  print(f" {flag.ljust(18)} {desc}")
715
843
  print()
716
844
  if helpdata.get("presets"):
717
845
  print("Presets:")
718
- for i, p in enumerate(helpdata.get("presets",[]), start=1):
846
+ for i, p in enumerate(helpdata.get("presets", []), start=1):
719
847
  print(f" {i}) {p.get('name')} - {p.get('desc')}")
720
848
  print()
721
849
  print("Legal:")
722
850
  print(" Use only on systems you own or have explicit permission to test.")
723
851
  print()
724
852
 
853
+
725
854
  def handle_network_plugins():
726
855
  """
727
856
  TUI submenu with H2 help + presets + run/enqueue flow.
@@ -744,7 +873,7 @@ def handle_network_plugins():
744
873
  choice = prompt("Choice > ").strip().lower()
745
874
  if not choice:
746
875
  continue
747
- if choice in ("b","back"):
876
+ if choice in ("b", "back"):
748
877
  return
749
878
  if choice == "h":
750
879
  print("Choose a plugin number to show detailed help.")
@@ -771,7 +900,7 @@ def handle_network_plugins():
771
900
  ch = prompt("Choice > ").strip().lower()
772
901
  if not ch:
773
902
  continue
774
- if ch in ("b","back"):
903
+ if ch in ("b", "back"):
775
904
  break
776
905
  if ch == "h":
777
906
  _print_plugin_help_block(helpdata or {})
@@ -780,7 +909,7 @@ def handle_network_plugins():
780
909
  args_raw = prompt("Custom args (e.g. -Tuning 9 -ssl) > ").strip()
781
910
  args = args_raw.split() if args_raw else []
782
911
  elif ch.isdigit() and presets and (1 <= int(ch) <= len(presets)):
783
- args = presets[int(ch)-1].get("args",[])
912
+ args = presets[int(ch) - 1].get("args", [])
784
913
  print("Selected preset args:", " ".join(args))
785
914
  else:
786
915
  print("Invalid choice.")
@@ -796,7 +925,10 @@ def handle_network_plugins():
796
925
  print(f"Run completed rc={rc} log={logp}")
797
926
  try:
798
927
  from .history import add_history_entry
799
- add_history_entry(target, args, label or "", logp, "", tool=plugin.tool)
928
+
929
+ add_history_entry(
930
+ target, args, label or "", logp, "", tool=plugin.tool
931
+ )
800
932
  except Exception:
801
933
  pass
802
934
  except Exception as e:
@@ -814,111 +946,131 @@ def handle_network_plugins():
814
946
  # after run/enqueue return to plugin submenu
815
947
  # end while
816
948
 
949
+
817
950
  # ---- Background Jobs TUI (auto-added) ----
818
951
  # Provides: list jobs, view details, tail log, start worker
819
952
  def _format_status(s):
820
953
  try:
821
- if s == 'done':
822
- return GREEN + 'done' + RESET
823
- if s == 'running':
824
- return CYAN + 'running' + RESET
825
- if s == 'queued':
826
- return MAG + 'queued' + RESET
954
+ if s == "done":
955
+ return GREEN + "done" + RESET
956
+ if s == "running":
957
+ return CYAN + "running" + RESET
958
+ if s == "queued":
959
+ return MAG + "queued" + RESET
827
960
  return RED + str(s) + RESET
828
961
  except Exception:
829
962
  return str(s)
830
963
 
964
+
831
965
  def _print_job_row(j):
832
- jid = j.get('id')
833
- tool = j.get('tool') or ''
834
- target = j.get('target') or ''
835
- status = _format_status(j.get('status'))
836
- created = j.get('created_at') or ''
837
- print(f"{str(jid).ljust(4)} {tool.ljust(10)} {target.ljust(30)} {status.ljust(10)} {created}")
966
+ jid = j.get("id")
967
+ tool = j.get("tool") or ""
968
+ target = j.get("target") or ""
969
+ status = _format_status(j.get("status"))
970
+ created = j.get("created_at") or ""
971
+ print(
972
+ f"{str(jid).ljust(4)} {tool.ljust(10)} {target.ljust(30)} {status.ljust(10)} {created}"
973
+ )
974
+
838
975
 
839
976
  def background_jobs_menu():
840
977
  while True:
841
978
  print()
842
- print('='*60)
843
- print('--- Background Jobs ---')
844
- print(' 1) List jobs')
845
- print(' 2) View job details')
846
- print(' 3) Tail job log')
847
- print(' 4) Start worker (background)')
848
- print(' 5) Start worker (foreground)')
849
- print(' b) Back')
850
- ch = prompt('Choice > ').strip().lower()
851
- if ch in ('1','list'):
979
+ print("=" * 60)
980
+ print("--- Background Jobs ---")
981
+ print(" 1) List jobs")
982
+ print(" 2) View job details")
983
+ print(" 3) Tail job log")
984
+ print(" 4) Start worker (background)")
985
+ print(" 5) Start worker (foreground)")
986
+ print(" b) Back")
987
+ ch = prompt("Choice > ").strip().lower()
988
+ if ch in ("1", "list"):
852
989
  jobs = []
853
990
  try:
854
991
  jobs = list_jobs(limit=200)
855
992
  except Exception as e:
856
- print('Could not load jobs:', e)
993
+ print("Could not load jobs:", e)
857
994
  jobs = []
858
995
  if not jobs:
859
- print('No jobs.')
996
+ print("No jobs.")
860
997
  else:
861
998
  print()
862
- print('ID Tool Target Status Created')
863
- print('-'*80)
999
+ print(
1000
+ "ID Tool Target Status Created"
1001
+ )
1002
+ print("-" * 80)
864
1003
  for j in jobs:
865
1004
  _print_job_row(j)
866
- print('-'*80)
867
- elif ch in ('2','view'):
868
- jid = prompt('Job ID > ')
1005
+ print("-" * 80)
1006
+ elif ch in ("2", "view"):
1007
+ jid = prompt("Job ID > ")
869
1008
  if not jid:
870
1009
  continue
871
1010
  try:
872
1011
  jidn = int(jid)
873
1012
  except Exception:
874
- print('Invalid id.')
1013
+ print("Invalid id.")
875
1014
  continue
876
1015
  rec = get_job(jidn)
877
1016
  if not rec:
878
- print('Job not found.')
1017
+ print("Job not found.")
879
1018
  continue
880
1019
  # pretty print limited fields
881
- for k in ('id','tool','target','args','label','status','created_at','started_at','finished_at','error','log'):
1020
+ for k in (
1021
+ "id",
1022
+ "tool",
1023
+ "target",
1024
+ "args",
1025
+ "label",
1026
+ "status",
1027
+ "created_at",
1028
+ "started_at",
1029
+ "finished_at",
1030
+ "error",
1031
+ "log",
1032
+ ):
882
1033
  print(f"{k}: {rec.get(k)}")
883
- elif ch in ('3','tail'):
884
- jid = prompt('Job ID > ')
1034
+ elif ch in ("3", "tail"):
1035
+ jid = prompt("Job ID > ")
885
1036
  if not jid:
886
1037
  continue
887
1038
  try:
888
1039
  jidn = int(jid)
889
1040
  except Exception:
890
- print('Invalid id.')
1041
+ print("Invalid id.")
891
1042
  continue
892
1043
  rec = get_job(jidn)
893
1044
  if not rec:
894
- print('Job not found.')
1045
+ print("Job not found.")
895
1046
  continue
896
- logp = rec.get('log') or ''
1047
+ logp = rec.get("log") or ""
897
1048
  if not logp or not os.path.exists(logp):
898
- print('Log not found:', logp)
1049
+ print("Log not found:", logp)
899
1050
  continue
900
1051
  try:
901
- with open(logp, 'r', encoding='utf-8', errors='replace') as fh:
1052
+ with open(logp, "r", encoding="utf-8", errors="replace") as fh:
902
1053
  lines = fh.readlines()[-200:]
903
- print(''.join(lines))
1054
+ print("".join(lines))
904
1055
  except Exception as e:
905
- print('Could not read log:', e)
906
- elif ch == '4':
1056
+ print("Could not read log:", e)
1057
+ elif ch == "4":
907
1058
  try:
908
1059
  start_worker(detach=True)
909
- print('Worker started (background). Check data/logs/worker.log')
1060
+ print("Worker started (background). Check data/logs/worker.log")
910
1061
  except Exception as e:
911
- print('Could not start worker:', e)
912
- elif ch == '5':
1062
+ print("Could not start worker:", e)
1063
+ elif ch == "5":
913
1064
  try:
914
1065
  start_worker(detach=False, fg=True)
915
1066
  except KeyboardInterrupt:
916
- print('\nWorker stopped (foreground).')
1067
+ print("\nWorker stopped (foreground).")
917
1068
  except Exception as e:
918
- print('Could not start worker:', e)
919
- elif ch == 'q':
1069
+ print("Could not start worker:", e)
1070
+ elif ch == "q":
920
1071
  return
921
1072
  else:
922
- print('Unknown choice.')
923
- # ---- end Background Jobs TUI ----
1073
+ print("Unknown choice.")
1074
+
924
1075
 
1076
+ # ---- end Background Jobs TUI ----