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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. souleyez/__init__.py +1 -2
  2. souleyez/ai/__init__.py +21 -15
  3. souleyez/ai/action_mapper.py +249 -150
  4. souleyez/ai/chain_advisor.py +116 -100
  5. souleyez/ai/claude_provider.py +29 -28
  6. souleyez/ai/context_builder.py +80 -62
  7. souleyez/ai/executor.py +158 -117
  8. souleyez/ai/feedback_handler.py +136 -121
  9. souleyez/ai/llm_factory.py +27 -20
  10. souleyez/ai/llm_provider.py +4 -2
  11. souleyez/ai/ollama_provider.py +6 -9
  12. souleyez/ai/ollama_service.py +44 -37
  13. souleyez/ai/path_scorer.py +91 -76
  14. souleyez/ai/recommender.py +176 -144
  15. souleyez/ai/report_context.py +74 -73
  16. souleyez/ai/report_service.py +84 -66
  17. souleyez/ai/result_parser.py +222 -229
  18. souleyez/ai/safety.py +67 -44
  19. souleyez/auth/__init__.py +23 -22
  20. souleyez/auth/audit.py +36 -26
  21. souleyez/auth/engagement_access.py +65 -48
  22. souleyez/auth/permissions.py +14 -3
  23. souleyez/auth/session_manager.py +54 -37
  24. souleyez/auth/user_manager.py +109 -64
  25. souleyez/commands/audit.py +40 -43
  26. souleyez/commands/auth.py +35 -15
  27. souleyez/commands/deliverables.py +55 -50
  28. souleyez/commands/engagement.py +47 -28
  29. souleyez/commands/license.py +32 -23
  30. souleyez/commands/screenshots.py +36 -32
  31. souleyez/commands/user.py +82 -36
  32. souleyez/config.py +52 -44
  33. souleyez/core/credential_tester.py +87 -81
  34. souleyez/core/cve_mappings.py +179 -192
  35. souleyez/core/cve_matcher.py +162 -148
  36. souleyez/core/msf_auto_mapper.py +100 -83
  37. souleyez/core/msf_chain_engine.py +294 -256
  38. souleyez/core/msf_database.py +153 -70
  39. souleyez/core/msf_integration.py +679 -673
  40. souleyez/core/msf_rpc_client.py +40 -42
  41. souleyez/core/msf_rpc_manager.py +77 -79
  42. souleyez/core/msf_sync_manager.py +241 -181
  43. souleyez/core/network_utils.py +22 -15
  44. souleyez/core/parser_handler.py +34 -25
  45. souleyez/core/pending_chains.py +114 -63
  46. souleyez/core/templates.py +158 -107
  47. souleyez/core/tool_chaining.py +9526 -2879
  48. souleyez/core/version_utils.py +79 -94
  49. souleyez/core/vuln_correlation.py +136 -89
  50. souleyez/core/web_utils.py +33 -32
  51. souleyez/data/wordlists/ad_users.txt +378 -0
  52. souleyez/data/wordlists/api_endpoints_large.txt +769 -0
  53. souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
  54. souleyez/data/wordlists/lfi_payloads.txt +82 -0
  55. souleyez/data/wordlists/passwords_brute.txt +1548 -0
  56. souleyez/data/wordlists/passwords_crack.txt +2479 -0
  57. souleyez/data/wordlists/passwords_spray.txt +386 -0
  58. souleyez/data/wordlists/subdomains_large.txt +5057 -0
  59. souleyez/data/wordlists/usernames_common.txt +694 -0
  60. souleyez/data/wordlists/web_dirs_large.txt +4769 -0
  61. souleyez/detection/__init__.py +1 -1
  62. souleyez/detection/attack_signatures.py +12 -17
  63. souleyez/detection/mitre_mappings.py +61 -55
  64. souleyez/detection/validator.py +97 -86
  65. souleyez/devtools.py +23 -10
  66. souleyez/docs/README.md +4 -4
  67. souleyez/docs/api-reference/cli-commands.md +2 -2
  68. souleyez/docs/developer-guide/adding-new-tools.md +562 -0
  69. souleyez/docs/user-guide/auto-chaining.md +30 -8
  70. souleyez/docs/user-guide/getting-started.md +1 -1
  71. souleyez/docs/user-guide/installation.md +26 -3
  72. souleyez/docs/user-guide/metasploit-integration.md +2 -2
  73. souleyez/docs/user-guide/rbac.md +1 -1
  74. souleyez/docs/user-guide/scope-management.md +1 -1
  75. souleyez/docs/user-guide/siem-integration.md +1 -1
  76. souleyez/docs/user-guide/tools-reference.md +1 -8
  77. souleyez/docs/user-guide/worker-management.md +1 -1
  78. souleyez/engine/background.py +1239 -535
  79. souleyez/engine/base.py +4 -1
  80. souleyez/engine/job_status.py +17 -49
  81. souleyez/engine/log_sanitizer.py +103 -77
  82. souleyez/engine/manager.py +38 -7
  83. souleyez/engine/result_handler.py +2200 -1550
  84. souleyez/engine/worker_manager.py +50 -41
  85. souleyez/export/evidence_bundle.py +72 -62
  86. souleyez/feature_flags/features.py +16 -20
  87. souleyez/feature_flags.py +5 -9
  88. souleyez/handlers/__init__.py +11 -0
  89. souleyez/handlers/base.py +188 -0
  90. souleyez/handlers/bash_handler.py +277 -0
  91. souleyez/handlers/bloodhound_handler.py +243 -0
  92. souleyez/handlers/certipy_handler.py +311 -0
  93. souleyez/handlers/crackmapexec_handler.py +486 -0
  94. souleyez/handlers/dnsrecon_handler.py +344 -0
  95. souleyez/handlers/enum4linux_handler.py +400 -0
  96. souleyez/handlers/evil_winrm_handler.py +493 -0
  97. souleyez/handlers/ffuf_handler.py +815 -0
  98. souleyez/handlers/gobuster_handler.py +1114 -0
  99. souleyez/handlers/gpp_extract_handler.py +334 -0
  100. souleyez/handlers/hashcat_handler.py +444 -0
  101. souleyez/handlers/hydra_handler.py +563 -0
  102. souleyez/handlers/impacket_getuserspns_handler.py +343 -0
  103. souleyez/handlers/impacket_psexec_handler.py +222 -0
  104. souleyez/handlers/impacket_secretsdump_handler.py +426 -0
  105. souleyez/handlers/john_handler.py +286 -0
  106. souleyez/handlers/katana_handler.py +425 -0
  107. souleyez/handlers/kerbrute_handler.py +298 -0
  108. souleyez/handlers/ldapsearch_handler.py +636 -0
  109. souleyez/handlers/lfi_extract_handler.py +464 -0
  110. souleyez/handlers/msf_auxiliary_handler.py +408 -0
  111. souleyez/handlers/msf_exploit_handler.py +380 -0
  112. souleyez/handlers/nikto_handler.py +413 -0
  113. souleyez/handlers/nmap_handler.py +821 -0
  114. souleyez/handlers/nuclei_handler.py +359 -0
  115. souleyez/handlers/nxc_handler.py +371 -0
  116. souleyez/handlers/rdp_sec_check_handler.py +353 -0
  117. souleyez/handlers/registry.py +292 -0
  118. souleyez/handlers/responder_handler.py +232 -0
  119. souleyez/handlers/service_explorer_handler.py +434 -0
  120. souleyez/handlers/smbclient_handler.py +344 -0
  121. souleyez/handlers/smbmap_handler.py +510 -0
  122. souleyez/handlers/smbpasswd_handler.py +296 -0
  123. souleyez/handlers/sqlmap_handler.py +1116 -0
  124. souleyez/handlers/theharvester_handler.py +601 -0
  125. souleyez/handlers/web_login_test_handler.py +327 -0
  126. souleyez/handlers/whois_handler.py +277 -0
  127. souleyez/handlers/wpscan_handler.py +554 -0
  128. souleyez/history.py +32 -16
  129. souleyez/importers/msf_importer.py +106 -75
  130. souleyez/importers/smart_importer.py +208 -147
  131. souleyez/integrations/siem/__init__.py +10 -10
  132. souleyez/integrations/siem/base.py +17 -18
  133. souleyez/integrations/siem/elastic.py +108 -122
  134. souleyez/integrations/siem/factory.py +207 -80
  135. souleyez/integrations/siem/googlesecops.py +146 -154
  136. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  137. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  138. souleyez/integrations/siem/sentinel.py +107 -109
  139. souleyez/integrations/siem/splunk.py +246 -212
  140. souleyez/integrations/siem/wazuh.py +65 -71
  141. souleyez/integrations/wazuh/__init__.py +5 -5
  142. souleyez/integrations/wazuh/client.py +70 -93
  143. souleyez/integrations/wazuh/config.py +85 -57
  144. souleyez/integrations/wazuh/host_mapper.py +28 -36
  145. souleyez/integrations/wazuh/sync.py +78 -68
  146. souleyez/intelligence/__init__.py +4 -5
  147. souleyez/intelligence/correlation_analyzer.py +309 -295
  148. souleyez/intelligence/exploit_knowledge.py +661 -623
  149. souleyez/intelligence/exploit_suggestions.py +159 -139
  150. souleyez/intelligence/gap_analyzer.py +132 -97
  151. souleyez/intelligence/gap_detector.py +251 -214
  152. souleyez/intelligence/sensitive_tables.py +266 -129
  153. souleyez/intelligence/service_parser.py +137 -123
  154. souleyez/intelligence/surface_analyzer.py +407 -268
  155. souleyez/intelligence/target_parser.py +159 -162
  156. souleyez/licensing/__init__.py +6 -6
  157. souleyez/licensing/validator.py +17 -19
  158. souleyez/log_config.py +79 -54
  159. souleyez/main.py +1505 -687
  160. souleyez/migrations/fix_job_counter.py +16 -14
  161. souleyez/parsers/bloodhound_parser.py +41 -39
  162. souleyez/parsers/crackmapexec_parser.py +178 -111
  163. souleyez/parsers/dalfox_parser.py +72 -77
  164. souleyez/parsers/dnsrecon_parser.py +103 -91
  165. souleyez/parsers/enum4linux_parser.py +183 -153
  166. souleyez/parsers/ffuf_parser.py +29 -25
  167. souleyez/parsers/gobuster_parser.py +301 -41
  168. souleyez/parsers/hashcat_parser.py +324 -79
  169. souleyez/parsers/http_fingerprint_parser.py +350 -103
  170. souleyez/parsers/hydra_parser.py +131 -111
  171. souleyez/parsers/impacket_parser.py +231 -178
  172. souleyez/parsers/john_parser.py +98 -86
  173. souleyez/parsers/katana_parser.py +316 -0
  174. souleyez/parsers/msf_parser.py +943 -498
  175. souleyez/parsers/nikto_parser.py +346 -65
  176. souleyez/parsers/nmap_parser.py +262 -174
  177. souleyez/parsers/nuclei_parser.py +40 -44
  178. souleyez/parsers/responder_parser.py +26 -26
  179. souleyez/parsers/searchsploit_parser.py +74 -74
  180. souleyez/parsers/service_explorer_parser.py +279 -0
  181. souleyez/parsers/smbmap_parser.py +180 -124
  182. souleyez/parsers/sqlmap_parser.py +434 -308
  183. souleyez/parsers/theharvester_parser.py +75 -57
  184. souleyez/parsers/whois_parser.py +135 -94
  185. souleyez/parsers/wpscan_parser.py +278 -190
  186. souleyez/plugins/afp.py +44 -36
  187. souleyez/plugins/afp_brute.py +114 -46
  188. souleyez/plugins/ard.py +48 -37
  189. souleyez/plugins/bloodhound.py +95 -61
  190. souleyez/plugins/certipy.py +303 -0
  191. souleyez/plugins/crackmapexec.py +186 -85
  192. souleyez/plugins/dalfox.py +120 -59
  193. souleyez/plugins/dns_hijack.py +146 -41
  194. souleyez/plugins/dnsrecon.py +97 -61
  195. souleyez/plugins/enum4linux.py +91 -66
  196. souleyez/plugins/evil_winrm.py +291 -0
  197. souleyez/plugins/ffuf.py +166 -90
  198. souleyez/plugins/firmware_extract.py +133 -29
  199. souleyez/plugins/gobuster.py +387 -190
  200. souleyez/plugins/gpp_extract.py +393 -0
  201. souleyez/plugins/hashcat.py +100 -73
  202. souleyez/plugins/http_fingerprint.py +854 -267
  203. souleyez/plugins/hydra.py +566 -200
  204. souleyez/plugins/impacket_getnpusers.py +117 -69
  205. souleyez/plugins/impacket_psexec.py +84 -64
  206. souleyez/plugins/impacket_secretsdump.py +103 -69
  207. souleyez/plugins/impacket_smbclient.py +89 -75
  208. souleyez/plugins/john.py +86 -69
  209. souleyez/plugins/katana.py +313 -0
  210. souleyez/plugins/kerbrute.py +237 -0
  211. souleyez/plugins/lfi_extract.py +541 -0
  212. souleyez/plugins/macos_ssh.py +117 -48
  213. souleyez/plugins/mdns.py +35 -30
  214. souleyez/plugins/msf_auxiliary.py +253 -130
  215. souleyez/plugins/msf_exploit.py +239 -161
  216. souleyez/plugins/nikto.py +134 -78
  217. souleyez/plugins/nmap.py +275 -91
  218. souleyez/plugins/nuclei.py +180 -89
  219. souleyez/plugins/nxc.py +285 -0
  220. souleyez/plugins/plugin_base.py +35 -36
  221. souleyez/plugins/plugin_template.py +13 -5
  222. souleyez/plugins/rdp_sec_check.py +130 -0
  223. souleyez/plugins/responder.py +112 -71
  224. souleyez/plugins/router_http_brute.py +76 -65
  225. souleyez/plugins/router_ssh_brute.py +118 -41
  226. souleyez/plugins/router_telnet_brute.py +124 -42
  227. souleyez/plugins/routersploit.py +91 -59
  228. souleyez/plugins/routersploit_exploit.py +77 -55
  229. souleyez/plugins/searchsploit.py +91 -77
  230. souleyez/plugins/service_explorer.py +1160 -0
  231. souleyez/plugins/smbmap.py +122 -72
  232. souleyez/plugins/smbpasswd.py +215 -0
  233. souleyez/plugins/sqlmap.py +301 -113
  234. souleyez/plugins/theharvester.py +127 -75
  235. souleyez/plugins/tr069.py +79 -57
  236. souleyez/plugins/upnp.py +65 -47
  237. souleyez/plugins/upnp_abuse.py +73 -55
  238. souleyez/plugins/vnc_access.py +129 -42
  239. souleyez/plugins/vnc_brute.py +109 -38
  240. souleyez/plugins/web_login_test.py +417 -0
  241. souleyez/plugins/whois.py +77 -58
  242. souleyez/plugins/wpscan.py +173 -69
  243. souleyez/reporting/__init__.py +2 -1
  244. souleyez/reporting/attack_chain.py +411 -346
  245. souleyez/reporting/charts.py +436 -501
  246. souleyez/reporting/compliance_mappings.py +334 -201
  247. souleyez/reporting/detection_report.py +126 -125
  248. souleyez/reporting/formatters.py +828 -591
  249. souleyez/reporting/generator.py +386 -302
  250. souleyez/reporting/metrics.py +72 -75
  251. souleyez/scanner.py +35 -29
  252. souleyez/security/__init__.py +37 -11
  253. souleyez/security/scope_validator.py +175 -106
  254. souleyez/security/validation.py +223 -149
  255. souleyez/security.py +22 -6
  256. souleyez/storage/credentials.py +247 -186
  257. souleyez/storage/crypto.py +296 -129
  258. souleyez/storage/database.py +73 -50
  259. souleyez/storage/db.py +58 -36
  260. souleyez/storage/deliverable_evidence.py +177 -128
  261. souleyez/storage/deliverable_exporter.py +282 -246
  262. souleyez/storage/deliverable_templates.py +134 -116
  263. souleyez/storage/deliverables.py +135 -130
  264. souleyez/storage/engagements.py +109 -56
  265. souleyez/storage/evidence.py +181 -152
  266. souleyez/storage/execution_log.py +31 -17
  267. souleyez/storage/exploit_attempts.py +93 -57
  268. souleyez/storage/exploits.py +67 -36
  269. souleyez/storage/findings.py +48 -61
  270. souleyez/storage/hosts.py +176 -144
  271. souleyez/storage/migrate_to_engagements.py +43 -19
  272. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  273. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  274. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  275. souleyez/storage/migrations/_005_screenshots.py +13 -5
  276. souleyez/storage/migrations/_006_deliverables.py +13 -5
  277. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  278. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  279. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  280. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  281. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  282. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  283. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  284. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  285. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  286. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  287. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  288. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  289. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  290. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  291. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  292. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  293. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  294. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  295. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  296. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  297. souleyez/storage/migrations/__init__.py +26 -26
  298. souleyez/storage/migrations/migration_manager.py +19 -19
  299. souleyez/storage/msf_sessions.py +100 -65
  300. souleyez/storage/osint.py +17 -24
  301. souleyez/storage/recommendation_engine.py +269 -235
  302. souleyez/storage/screenshots.py +33 -32
  303. souleyez/storage/smb_shares.py +136 -92
  304. souleyez/storage/sqlmap_data.py +183 -128
  305. souleyez/storage/team_collaboration.py +135 -141
  306. souleyez/storage/timeline_tracker.py +122 -94
  307. souleyez/storage/wazuh_vulns.py +64 -66
  308. souleyez/storage/web_paths.py +33 -37
  309. souleyez/testing/credential_tester.py +221 -205
  310. souleyez/ui/__init__.py +1 -1
  311. souleyez/ui/ai_quotes.py +12 -12
  312. souleyez/ui/attack_surface.py +2439 -1516
  313. souleyez/ui/chain_rules_view.py +914 -382
  314. souleyez/ui/correlation_view.py +312 -230
  315. souleyez/ui/dashboard.py +2382 -1130
  316. souleyez/ui/deliverables_view.py +148 -62
  317. souleyez/ui/design_system.py +13 -13
  318. souleyez/ui/errors.py +49 -49
  319. souleyez/ui/evidence_linking_view.py +284 -179
  320. souleyez/ui/evidence_vault.py +393 -285
  321. souleyez/ui/exploit_suggestions_view.py +555 -349
  322. souleyez/ui/export_view.py +100 -66
  323. souleyez/ui/gap_analysis_view.py +315 -171
  324. souleyez/ui/help_system.py +105 -97
  325. souleyez/ui/intelligence_view.py +436 -293
  326. souleyez/ui/interactive.py +22827 -10678
  327. souleyez/ui/interactive_selector.py +75 -68
  328. souleyez/ui/log_formatter.py +47 -39
  329. souleyez/ui/menu_components.py +22 -13
  330. souleyez/ui/msf_auxiliary_menu.py +184 -133
  331. souleyez/ui/pending_chains_view.py +336 -172
  332. souleyez/ui/progress_indicators.py +5 -3
  333. souleyez/ui/recommendations_view.py +195 -137
  334. souleyez/ui/rule_builder.py +343 -225
  335. souleyez/ui/setup_wizard.py +678 -284
  336. souleyez/ui/shortcuts.py +217 -165
  337. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  338. souleyez/ui/splunk_vulns_view.py +139 -86
  339. souleyez/ui/team_dashboard.py +498 -335
  340. souleyez/ui/template_selector.py +196 -105
  341. souleyez/ui/terminal.py +6 -6
  342. souleyez/ui/timeline_view.py +198 -127
  343. souleyez/ui/tool_setup.py +264 -164
  344. souleyez/ui/tutorial.py +202 -72
  345. souleyez/ui/tutorial_state.py +40 -40
  346. souleyez/ui/wazuh_vulns_view.py +235 -141
  347. souleyez/ui/wordlist_browser.py +260 -107
  348. souleyez/ui.py +464 -312
  349. souleyez/utils/tool_checker.py +427 -367
  350. souleyez/utils.py +33 -29
  351. souleyez/wordlists.py +134 -167
  352. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
  353. souleyez-2.43.34.dist-info/RECORD +443 -0
  354. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
  355. souleyez-2.43.29.dist-info/RECORD +0 -379
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
  357. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
  358. {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
@@ -17,16 +17,18 @@ console = Console()
17
17
 
18
18
  # Phase display info
19
19
  PHASE_DISPLAY = {
20
- 'reconnaissance': ('📡 RECON', 'cyan'),
21
- 'enumeration': ('🔍 ENUM', 'blue'),
22
- 'exploitation': ('💥 EXPLOIT', 'red'),
23
- 'post_exploitation': ('🎯 POST', 'magenta')
20
+ "reconnaissance": ("📡 RECON", "cyan"),
21
+ "enumeration": ("🔍 ENUM", "blue"),
22
+ "exploitation": ("💥 EXPLOIT", "red"),
23
+ "post_exploitation": ("🎯 POST", "magenta"),
24
24
  }
25
25
 
26
- PHASE_ORDER = ['reconnaissance', 'enumeration', 'exploitation', 'post_exploitation']
26
+ PHASE_ORDER = ["reconnaissance", "enumeration", "exploitation", "post_exploitation"]
27
27
 
28
28
 
29
- def _flatten_evidence(evidence: Dict[str, List[Dict]], phase_filter: Optional[str] = None) -> List[Dict]:
29
+ def _flatten_evidence(
30
+ evidence: Dict[str, List[Dict]], phase_filter: Optional[str] = None
31
+ ) -> List[Dict]:
30
32
  """Flatten evidence dict into a single list with phase info."""
31
33
  flat = []
32
34
  for phase_key in PHASE_ORDER:
@@ -34,17 +36,13 @@ def _flatten_evidence(evidence: Dict[str, List[Dict]], phase_filter: Optional[st
34
36
  continue
35
37
  for item in evidence.get(phase_key, []):
36
38
  item_copy = item.copy()
37
- item_copy['phase'] = phase_key
39
+ item_copy["phase"] = phase_key
38
40
  flat.append(item_copy)
39
41
  return flat
40
42
 
41
43
 
42
44
  def _build_evidence_table(
43
- items: List[Dict],
44
- selected_ids: Set[int],
45
- page: int,
46
- page_size: int,
47
- view_all: bool
45
+ items: List[Dict], selected_ids: Set[int], page: int, page_size: int, view_all: bool
48
46
  ) -> tuple:
49
47
  """Build Rich table for evidence items.
50
48
 
@@ -67,7 +65,7 @@ def _build_evidence_table(
67
65
  header_style="bold cyan",
68
66
  box=DesignSystem.TABLE_BOX,
69
67
  padding=(0, 1),
70
- expand=True
68
+ expand=True,
71
69
  )
72
70
 
73
71
  table.add_column("○", width=3, justify="center")
@@ -87,40 +85,40 @@ def _build_evidence_table(
87
85
  row_num = (page * page_size) + idx + 1
88
86
 
89
87
  # Checkbox
90
- item_id = item.get('id', idx)
91
- checkbox = '' if item_id in selected_ids else ''
88
+ item_id = item.get("id", idx)
89
+ checkbox = "" if item_id in selected_ids else ""
92
90
 
93
91
  # Phase with color
94
- phase_key = item.get('phase', 'reconnaissance')
95
- phase_label, phase_color = PHASE_DISPLAY.get(phase_key, ('?', 'white'))
92
+ phase_key = item.get("phase", "reconnaissance")
93
+ phase_label, phase_color = PHASE_DISPLAY.get(phase_key, ("?", "white"))
96
94
  phase_display = f"[{phase_color}]{phase_label}[/{phase_color}]"
97
95
 
98
96
  # Type
99
- item_type = item.get('type', 'job').capitalize()
97
+ item_type = item.get("type", "job").capitalize()
100
98
 
101
99
  # Tool
102
- tool = item.get('tool', '-').upper()
100
+ tool = item.get("tool", "-").upper()
103
101
 
104
102
  # Title - use label for jobs, title for findings
105
- if item_type.lower() == 'job':
106
- title = item.get('label') or item.get('title', '-')
103
+ if item_type.lower() == "job":
104
+ title = item.get("label") or item.get("title", "-")
107
105
  else:
108
- title = item.get('title', '-')
106
+ title = item.get("title", "-")
109
107
 
110
108
  # Description - actual description field
111
- desc = item.get('description', '-')
112
- if not desc or desc == 'None':
113
- desc = '-'
109
+ desc = item.get("description", "-")
110
+ if not desc or desc == "None":
111
+ desc = "-"
114
112
  if len(str(desc)) > 28:
115
- desc = str(desc)[:28] + ''
113
+ desc = str(desc)[:28] + ""
116
114
 
117
115
  # Severity with color
118
- severity = item.get('severity', '')
119
- if severity in ['critical', 'high']:
116
+ severity = item.get("severity", "")
117
+ if severity in ["critical", "high"]:
120
118
  sev_display = f"[red]🔴 HIGH[/red]"
121
- elif severity == 'medium':
119
+ elif severity == "medium":
122
120
  sev_display = f"[yellow]🟡 MED[/yellow]"
123
- elif severity in ['low', 'info']:
121
+ elif severity in ["low", "info"]:
124
122
  sev_display = f"[green]🟢 LOW[/green]"
125
123
  else:
126
124
  sev_display = "[dim]--[/dim]"
@@ -133,7 +131,7 @@ def _build_evidence_table(
133
131
  tool,
134
132
  title,
135
133
  desc,
136
- sev_display
134
+ sev_display,
137
135
  )
138
136
 
139
137
  return table, total_pages, displayed
@@ -177,7 +175,13 @@ def view_evidence_vault(engagement_id: int):
177
175
 
178
176
  # Header
179
177
  click.echo("\n┌" + "─" * (width - 2) + "┐")
180
- click.echo("│" + click.style(" EVIDENCE & ARTIFACTS ".center(width - 2), bold=True, fg='cyan') + "│")
178
+ click.echo(
179
+ "│"
180
+ + click.style(
181
+ " EVIDENCE & ARTIFACTS ".center(width - 2), bold=True, fg="cyan"
182
+ )
183
+ + "│"
184
+ )
181
185
  click.echo("└" + "─" * (width - 2) + "┘")
182
186
  click.echo()
183
187
 
@@ -188,30 +192,43 @@ def view_evidence_vault(engagement_id: int):
188
192
 
189
193
  # Calculate stats
190
194
  total_count = sum(len(items) for items in evidence.values())
191
- credentials_count = sum(1 for phase_items in evidence.values()
192
- for item in phase_items if item.get('type') == 'credential')
193
- findings_count = sum(1 for phase_items in evidence.values()
194
- for item in phase_items if item.get('type') == 'finding')
195
- high_value = sum(1 for phase_items in evidence.values()
196
- for item in phase_items
197
- if item.get('severity') in ['critical', 'high'])
195
+ credentials_count = sum(
196
+ 1
197
+ for phase_items in evidence.values()
198
+ for item in phase_items
199
+ if item.get("type") == "credential"
200
+ )
201
+ findings_count = sum(
202
+ 1
203
+ for phase_items in evidence.values()
204
+ for item in phase_items
205
+ if item.get("type") == "finding"
206
+ )
207
+ high_value = sum(
208
+ 1
209
+ for phase_items in evidence.values()
210
+ for item in phase_items
211
+ if item.get("severity") in ["critical", "high"]
212
+ )
198
213
 
199
214
  # Summary line
200
215
  click.echo(click.style("📊 SUMMARY", bold=True))
201
- click.echo(f" Total: {total_count} │ 🔑 Credentials: {credentials_count} │ 🔍 Findings: {findings_count} │ 🔴 High-Value: {high_value} │ 📸 Screenshots: {screenshot_count}")
216
+ click.echo(
217
+ f" Total: {total_count} │ 🔑 Credentials: {credentials_count} │ 🔍 Findings: {findings_count} │ 🔴 High-Value: {high_value} │ 📸 Screenshots: {screenshot_count}"
218
+ )
202
219
  click.echo()
203
220
 
204
221
  # Engagement and filter info
205
222
  filter_parts = [f"Engagement: {engagement['name']}"]
206
223
  if phase_filter:
207
- phase_label, _ = PHASE_DISPLAY.get(phase_filter, (phase_filter, 'white'))
224
+ phase_label, _ = PHASE_DISPLAY.get(phase_filter, (phase_filter, "white"))
208
225
  filter_parts.append(f"Phase: {phase_label}")
209
226
  if filters:
210
- if 'tool' in filters:
227
+ if "tool" in filters:
211
228
  filter_parts.append(f"Tool: {filters['tool']}")
212
- if 'host' in filters:
229
+ if "host" in filters:
213
230
  filter_parts.append(f"Host: {filters['host']}")
214
- if 'days' in filters:
231
+ if "days" in filters:
215
232
  filter_parts.append(f"Last {filters['days']} days")
216
233
 
217
234
  # Flatten evidence for table display
@@ -229,7 +246,7 @@ def view_evidence_vault(engagement_id: int):
229
246
 
230
247
  # Build and display table
231
248
  if total_items == 0:
232
- click.echo(click.style(" No evidence collected yet", fg='bright_black'))
249
+ click.echo(click.style(" No evidence collected yet", fg="bright_black"))
233
250
  else:
234
251
  table, _, displayed_items = _build_evidence_table(
235
252
  flat_evidence, selected_ids, page, page_size, view_all
@@ -242,7 +259,11 @@ def view_evidence_vault(engagement_id: int):
242
259
 
243
260
  # Selection count
244
261
  if selected_ids:
245
- click.echo(click.style(f" Selected: {len(selected_ids)} item(s)", fg='cyan', bold=True))
262
+ click.echo(
263
+ click.style(
264
+ f" Selected: {len(selected_ids)} item(s)", fg="cyan", bold=True
265
+ )
266
+ )
246
267
 
247
268
  # Menu
248
269
  click.echo()
@@ -250,11 +271,21 @@ def view_evidence_vault(engagement_id: int):
250
271
  click.echo()
251
272
  click.echo(" [#] View evidence details")
252
273
  click.echo(" [t] Toggle pagination" + (" (showing all)" if view_all else ""))
253
- click.echo(" [g] Filter by phase" + (f" ({PHASE_DISPLAY.get(phase_filter, ('All', ''))[0]})" if phase_filter else " (All)"))
274
+ click.echo(
275
+ " [g] Filter by phase"
276
+ + (
277
+ f" ({PHASE_DISPLAY.get(phase_filter, ('All', ''))[0]})"
278
+ if phase_filter
279
+ else " (All)"
280
+ )
281
+ )
254
282
  click.echo(" [c] Screenshots" + f" ({screenshot_count})")
255
283
  click.echo(" [s] Search evidence")
256
284
  click.echo(" [f] Filter by tool/host/date")
257
- click.echo(" [x] Export" + (f" ({len(selected_ids)} selected)" if selected_ids else " all"))
285
+ click.echo(
286
+ " [x] Export"
287
+ + (f" ({len(selected_ids)} selected)" if selected_ids else " all")
288
+ )
258
289
  click.echo(" [?] Help")
259
290
  click.echo(" [q] Back")
260
291
  click.echo()
@@ -262,18 +293,18 @@ def view_evidence_vault(engagement_id: int):
262
293
  try:
263
294
  choice = input(" Select option: ").strip().lower()
264
295
 
265
- if choice == 'q':
296
+ if choice == "q":
266
297
  return
267
- elif choice == 't':
298
+ elif choice == "t":
268
299
  view_all = not view_all
269
300
  page = 0
270
- elif choice == 'n' and not view_all:
301
+ elif choice == "n" and not view_all:
271
302
  if page < total_pages - 1:
272
303
  page += 1
273
- elif choice == 'p' and not view_all:
304
+ elif choice == "p" and not view_all:
274
305
  if page > 0:
275
306
  page -= 1
276
- elif choice == 'g':
307
+ elif choice == "g":
277
308
  # Inline phase filter submenu
278
309
  click.echo("\n Filter by phase:")
279
310
  click.echo(" [0] All phases")
@@ -284,32 +315,37 @@ def view_evidence_vault(engagement_id: int):
284
315
  click.echo()
285
316
  phase_choice = input(" Select option: ").strip()
286
317
  phase_map = {
287
- '0': None,
288
- '1': 'reconnaissance',
289
- '2': 'enumeration',
290
- '3': 'exploitation',
291
- '4': 'post_exploitation'
318
+ "0": None,
319
+ "1": "reconnaissance",
320
+ "2": "enumeration",
321
+ "3": "exploitation",
322
+ "4": "post_exploitation",
292
323
  }
293
324
  if phase_choice in phase_map:
294
325
  phase_filter = phase_map[phase_choice]
295
326
  page = 0
296
- elif choice == 'c':
327
+ elif choice == "c":
297
328
  view_screenshots(engagement_id, screenshots, sm)
298
- elif choice == 's':
329
+ elif choice == "s":
299
330
  search_evidence(engagement_id, evidence)
300
- elif choice == 'f':
331
+ elif choice == "f":
301
332
  filters = apply_filters()
302
333
  page = 0
303
- elif choice == 'x':
334
+ elif choice == "x":
304
335
  if selected_ids:
305
336
  # Export selected items
306
- click.echo(click.style(f"\nExporting {len(selected_ids)} selected items...", fg='yellow'))
337
+ click.echo(
338
+ click.style(
339
+ f"\nExporting {len(selected_ids)} selected items...",
340
+ fg="yellow",
341
+ )
342
+ )
307
343
  export_evidence_bundle(engagement_id, engagement, evidence)
308
344
  else:
309
345
  export_evidence_bundle(engagement_id, engagement, evidence)
310
- elif choice == '?':
346
+ elif choice == "?":
311
347
  _show_evidence_help()
312
- elif choice == 'i':
348
+ elif choice == "i":
313
349
  # Interactive mode
314
350
  _interactive_evidence_select(flat_evidence, selected_ids, engagement_id)
315
351
  elif choice.isdigit():
@@ -319,79 +355,85 @@ def view_evidence_vault(engagement_id: int):
319
355
  item = flat_evidence[row_num - 1]
320
356
  _view_evidence_detail(item)
321
357
  else:
322
- click.echo(click.style(f"Invalid row number (1-{total_items})", fg='red'))
358
+ click.echo(
359
+ click.style(f"Invalid row number (1-{total_items})", fg="red")
360
+ )
323
361
  click.pause()
324
- elif choice.startswith(' ') and choice.strip().isdigit():
362
+ elif choice.startswith(" ") and choice.strip().isdigit():
325
363
  # Toggle selection with space+number
326
364
  row_num = int(choice.strip())
327
365
  if 1 <= row_num <= total_items:
328
366
  item = flat_evidence[row_num - 1]
329
- item_id = item.get('id', row_num - 1)
367
+ item_id = item.get("id", row_num - 1)
330
368
  if item_id in selected_ids:
331
369
  selected_ids.discard(item_id)
332
370
  else:
333
371
  selected_ids.add(item_id)
334
- elif choice == '':
372
+ elif choice == "":
335
373
  pass # Just refresh
336
374
  else:
337
- click.echo(click.style("Invalid option. Press ? for help.", fg='red'))
375
+ click.echo(click.style("Invalid option. Press ? for help.", fg="red"))
338
376
  click.pause()
339
377
 
340
378
  except (KeyboardInterrupt, EOFError):
341
379
  return
342
380
  except ValueError:
343
- click.echo(click.style("Invalid input", fg='red'))
381
+ click.echo(click.style("Invalid input", fg="red"))
344
382
  click.pause()
345
383
 
346
384
 
347
- def _interactive_evidence_select(items: List[Dict], selected_ids: Set[int], engagement_id: int):
385
+ def _interactive_evidence_select(
386
+ items: List[Dict], selected_ids: Set[int], engagement_id: int
387
+ ):
348
388
  """Interactive mode for evidence selection with arrow keys."""
349
389
  from souleyez.ui.interactive_selector import interactive_select
350
390
 
351
391
  if not items:
352
- click.echo(click.style("\nNo evidence to display", fg='yellow'))
392
+ click.echo(click.style("\nNo evidence to display", fg="yellow"))
353
393
  click.pause()
354
394
  return
355
395
 
356
396
  # Build item dicts for selector
357
397
  item_dicts = []
358
398
  for item in items:
359
- item_type = item.get('type', 'job')
360
- if item_type == 'job':
361
- title = item.get('label') or item.get('title', '-')
399
+ item_type = item.get("type", "job")
400
+ if item_type == "job":
401
+ title = item.get("label") or item.get("title", "-")
362
402
  else:
363
- title = item.get('title', '-')
403
+ title = item.get("title", "-")
364
404
  if len(str(title)) > 50:
365
- title = str(title)[:50] + ''
366
-
367
- phase_key = item.get('phase', 'reconnaissance')
368
- phase_label, _ = PHASE_DISPLAY.get(phase_key, ('?', 'white'))
369
-
370
- severity = item.get('severity', '')
371
- if severity in ['critical', 'high']:
372
- sev_display = '🔴 HIGH'
373
- elif severity == 'medium':
374
- sev_display = '🟡 MED'
375
- elif severity in ['low', 'info']:
376
- sev_display = '🟢 LOW'
405
+ title = str(title)[:50] + ""
406
+
407
+ phase_key = item.get("phase", "reconnaissance")
408
+ phase_label, _ = PHASE_DISPLAY.get(phase_key, ("?", "white"))
409
+
410
+ severity = item.get("severity", "")
411
+ if severity in ["critical", "high"]:
412
+ sev_display = "🔴 HIGH"
413
+ elif severity == "medium":
414
+ sev_display = "🟡 MED"
415
+ elif severity in ["low", "info"]:
416
+ sev_display = "🟢 LOW"
377
417
  else:
378
- sev_display = '--'
379
-
380
- item_dicts.append({
381
- 'id': item.get('id', 0),
382
- 'phase': phase_label,
383
- 'type': item.get('type', 'job').capitalize(),
384
- 'tool': item.get('tool', '-').upper(),
385
- 'title': title,
386
- 'severity': sev_display
387
- })
418
+ sev_display = "--"
419
+
420
+ item_dicts.append(
421
+ {
422
+ "id": item.get("id", 0),
423
+ "phase": phase_label,
424
+ "type": item.get("type", "job").capitalize(),
425
+ "tool": item.get("tool", "-").upper(),
426
+ "title": title,
427
+ "severity": sev_display,
428
+ }
429
+ )
388
430
 
389
431
  def on_action(action: str, selected: set, current_item: dict):
390
- if action == 'v' and current_item:
432
+ if action == "v" and current_item:
391
433
  # View details
392
- item_id = current_item.get('id')
434
+ item_id = current_item.get("id")
393
435
  for item in items:
394
- if item.get('id') == item_id:
436
+ if item.get("id") == item_id:
395
437
  _view_evidence_detail(item)
396
438
  break
397
439
 
@@ -399,31 +441,33 @@ def _interactive_evidence_select(items: List[Dict], selected_ids: Set[int], enga
399
441
  interactive_select(
400
442
  items=item_dicts,
401
443
  columns=[
402
- {'name': 'Phase', 'width': 12, 'key': 'phase'},
403
- {'name': 'Type', 'width': 10, 'key': 'type'},
404
- {'name': 'Tool', 'width': 10, 'key': 'tool'},
405
- {'name': 'Title', 'key': 'title'},
406
- {'name': 'Severity', 'width': 10, 'key': 'severity'},
444
+ {"name": "Phase", "width": 12, "key": "phase"},
445
+ {"name": "Type", "width": 10, "key": "type"},
446
+ {"name": "Tool", "width": 10, "key": "tool"},
447
+ {"name": "Title", "key": "title"},
448
+ {"name": "Severity", "width": 10, "key": "severity"},
407
449
  ],
408
450
  selected_ids=selected_ids,
409
- get_id=lambda x: x.get('id'),
410
- title="SELECT EVIDENCE"
451
+ get_id=lambda x: x.get("id"),
452
+ title="SELECT EVIDENCE",
411
453
  )
412
454
 
413
455
  if not selected_ids:
414
456
  return
415
457
 
416
458
  result = _evidence_bulk_action_menu(items, selected_ids, engagement_id)
417
- if result == 'back':
459
+ if result == "back":
418
460
  return
419
461
 
420
462
 
421
- def _evidence_bulk_action_menu(items: List[Dict], selected_ids: Set[int], engagement_id: int) -> str:
463
+ def _evidence_bulk_action_menu(
464
+ items: List[Dict], selected_ids: Set[int], engagement_id: int
465
+ ) -> str:
422
466
  """Show action menu for selected evidence items."""
423
- selected_items = [item for item in items if item.get('id') in selected_ids]
467
+ selected_items = [item for item in items if item.get("id") in selected_ids]
424
468
 
425
469
  if not selected_items:
426
- return 'continue'
470
+ return "continue"
427
471
 
428
472
  click.echo()
429
473
  click.echo(f" Selected: {len(selected_items)} item(s)")
@@ -434,25 +478,29 @@ def _evidence_bulk_action_menu(items: List[Dict], selected_ids: Set[int], engage
434
478
  click.echo()
435
479
 
436
480
  try:
437
- choice = click.prompt(" Select option", default='q', show_default=False).strip().lower()
481
+ choice = (
482
+ click.prompt(" Select option", default="q", show_default=False)
483
+ .strip()
484
+ .lower()
485
+ )
438
486
 
439
- if choice == 'q':
440
- return 'back'
441
- elif choice == 'v' and selected_items:
487
+ if choice == "q":
488
+ return "back"
489
+ elif choice == "v" and selected_items:
442
490
  _view_evidence_detail(selected_items[0])
443
- return 'continue'
444
- elif choice == 'x':
491
+ return "continue"
492
+ elif choice == "x":
445
493
  _export_selected_evidence(selected_items, engagement_id)
446
- return 'continue'
447
- elif choice == 'c':
494
+ return "continue"
495
+ elif choice == "c":
448
496
  selected_ids.clear()
449
- click.echo(click.style(" ✓ Selection cleared", fg='green'))
450
- return 'continue'
497
+ click.echo(click.style(" ✓ Selection cleared", fg="green"))
498
+ return "continue"
451
499
 
452
500
  except (KeyboardInterrupt, EOFError):
453
501
  pass
454
502
 
455
- return 'continue'
503
+ return "continue"
456
504
 
457
505
 
458
506
  def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
@@ -462,13 +510,15 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
462
510
  from datetime import datetime
463
511
  from souleyez.storage.engagement import EngagementManager
464
512
 
465
- click.echo(click.style(f"\n Exporting {len(selected_items)} item(s)...", fg='yellow'))
513
+ click.echo(
514
+ click.style(f"\n Exporting {len(selected_items)} item(s)...", fg="yellow")
515
+ )
466
516
 
467
517
  try:
468
518
  # Get engagement info
469
519
  em = EngagementManager()
470
520
  engagement = em.get_by_id(engagement_id)
471
- eng_name = engagement['name'] if engagement else f"engagement_{engagement_id}"
521
+ eng_name = engagement["name"] if engagement else f"engagement_{engagement_id}"
472
522
 
473
523
  # Create output directory
474
524
  output_dir = os.path.expanduser("~/.souleyez/exports")
@@ -476,12 +526,12 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
476
526
 
477
527
  # Generate filename
478
528
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
479
- safe_name = eng_name.replace(' ', '_').replace('/', '_').replace('\\', '_')
529
+ safe_name = eng_name.replace(" ", "_").replace("/", "_").replace("\\", "_")
480
530
  zip_filename = f"{safe_name}_selected_evidence_{timestamp}.zip"
481
531
  zip_path = os.path.join(output_dir, zip_filename)
482
532
 
483
533
  # Create ZIP
484
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
534
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
485
535
  # Add README
486
536
  readme_lines = [
487
537
  "=" * 70,
@@ -496,18 +546,20 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
496
546
  ]
497
547
 
498
548
  for idx, item in enumerate(selected_items, 1):
499
- item_type = item.get('type', 'unknown')
500
- tool = item.get('tool', 'N/A')
501
- target = item.get('target', 'N/A')
549
+ item_type = item.get("type", "unknown")
550
+ tool = item.get("tool", "N/A")
551
+ target = item.get("target", "N/A")
502
552
  readme_lines.append(f" {idx}. [{item_type}] {tool} -> {target}")
503
553
 
504
554
  # Export job logs
505
- if item_type == 'job':
506
- log_path = item.get('log_path')
555
+ if item_type == "job":
556
+ log_path = item.get("log_path")
507
557
  if log_path and os.path.exists(log_path):
508
- safe_tool = tool.replace('/', '_').replace('\\', '_')
509
- safe_target = str(target).replace('/', '_').replace(':', '_')[:50]
510
- phase = item.get('phase', 'general').replace('_', '-')
558
+ safe_tool = tool.replace("/", "_").replace("\\", "_")
559
+ safe_target = (
560
+ str(target).replace("/", "_").replace(":", "_")[:50]
561
+ )
562
+ phase = item.get("phase", "general").replace("_", "-")
511
563
  arcname = f"{phase}/{idx:03d}_{safe_tool}_{safe_target}.log"
512
564
  zipf.write(log_path, arcname)
513
565
  readme_lines.append(f" -> {arcname}")
@@ -515,11 +567,11 @@ def _export_selected_evidence(selected_items: List[Dict], engagement_id: int):
515
567
  readme_lines.extend(["", "=" * 70])
516
568
  zipf.writestr("README.txt", "\n".join(readme_lines))
517
569
 
518
- click.echo(click.style(f"\n ✓ Exported to:", fg='green'))
570
+ click.echo(click.style(f"\n ✓ Exported to:", fg="green"))
519
571
  click.echo(f" {zip_path}")
520
572
 
521
573
  except Exception as e:
522
- click.echo(click.style(f"\n ✗ Export failed: {e}", fg='red'))
574
+ click.echo(click.style(f"\n ✗ Export failed: {e}", fg="red"))
523
575
 
524
576
  click.pause()
525
577
 
@@ -530,32 +582,38 @@ def _show_evidence_help():
530
582
  width = get_terminal_width()
531
583
 
532
584
  click.echo("\n┌" + "─" * (width - 2) + "┐")
533
- click.echo("│" + click.style(" EVIDENCE & ARTIFACTS - HELP ".center(width - 2), bold=True, fg='cyan') + "│")
585
+ click.echo(
586
+ "│"
587
+ + click.style(
588
+ " EVIDENCE & ARTIFACTS - HELP ".center(width - 2), bold=True, fg="cyan"
589
+ )
590
+ + "│"
591
+ )
534
592
  click.echo("└" + "─" * (width - 2) + "┘")
535
593
  click.echo()
536
594
 
537
- click.echo(click.style(" NAVIGATION", bold=True, fg='yellow'))
595
+ click.echo(click.style(" NAVIGATION", bold=True, fg="yellow"))
538
596
  click.echo(" ───────────────────────────────────────")
539
597
  click.echo(" [#] Enter a row number to view details")
540
598
  click.echo(" [n/p] Next/Previous page (when paginated)")
541
599
  click.echo(" [t] Toggle between paginated and full view")
542
600
  click.echo()
543
601
 
544
- click.echo(click.style(" FILTERING", bold=True, fg='yellow'))
602
+ click.echo(click.style(" FILTERING", bold=True, fg="yellow"))
545
603
  click.echo(" ───────────────────────────────────────")
546
604
  click.echo(" [g] Filter by phase (shows submenu)")
547
605
  click.echo(" [f] Apply filters by tool, host, or date range")
548
606
  click.echo(" [s] Full-text search across all evidence")
549
607
  click.echo()
550
608
 
551
- click.echo(click.style(" ACTIONS", bold=True, fg='yellow'))
609
+ click.echo(click.style(" ACTIONS", bold=True, fg="yellow"))
552
610
  click.echo(" ───────────────────────────────────────")
553
611
  click.echo(" [space #] Toggle selection for row # (e.g., ' 3' selects row 3)")
554
612
  click.echo(" [x] Export evidence (selected items or all)")
555
613
  click.echo(" [c] View and manage screenshots")
556
614
  click.echo()
557
615
 
558
- click.echo(click.style(" OTHER", bold=True, fg='yellow'))
616
+ click.echo(click.style(" OTHER", bold=True, fg="yellow"))
559
617
  click.echo(" ───────────────────────────────────────")
560
618
  click.echo(" [?] Show this help")
561
619
  click.echo(" [q] Return to main menu")
@@ -570,15 +628,21 @@ def _view_evidence_detail(item: Dict):
570
628
  width = get_terminal_width()
571
629
 
572
630
  # Header
573
- item_type = item.get('type', 'item').upper()
631
+ item_type = item.get("type", "item").upper()
574
632
  click.echo("\n┌" + "─" * (width - 2) + "┐")
575
- click.echo("│" + click.style(f" EVIDENCE DETAIL - {item_type} ".center(width - 2), bold=True, fg='cyan') + "│")
633
+ click.echo(
634
+ "│"
635
+ + click.style(
636
+ f" EVIDENCE DETAIL - {item_type} ".center(width - 2), bold=True, fg="cyan"
637
+ )
638
+ + "│"
639
+ )
576
640
  click.echo("└" + "─" * (width - 2) + "┘")
577
641
  click.echo()
578
642
 
579
643
  # Phase badge
580
- phase_key = item.get('phase', 'reconnaissance')
581
- phase_label, phase_color = PHASE_DISPLAY.get(phase_key, ('?', 'white'))
644
+ phase_key = item.get("phase", "reconnaissance")
645
+ phase_label, phase_color = PHASE_DISPLAY.get(phase_key, ("?", "white"))
582
646
  click.echo(f" {click.style(phase_label, fg=phase_color, bold=True)}")
583
647
  click.echo()
584
648
 
@@ -588,52 +652,60 @@ def _view_evidence_detail(item: Dict):
588
652
  click.echo()
589
653
 
590
654
  click.echo(click.style(" DESCRIPTION", bold=True))
591
- desc = item.get('description', 'No description')
655
+ desc = item.get("description", "No description")
592
656
  # Word wrap description
593
- for line in desc.split('\n'):
657
+ for line in desc.split("\n"):
594
658
  click.echo(f" {line}")
595
659
  click.echo()
596
660
 
597
661
  # Tool and target
598
662
  click.echo(click.style(" DETAILS", bold=True))
599
663
  click.echo(f" Tool: {item.get('tool', '-').upper()}")
600
- target = item.get('target', item.get('host', '-'))
664
+ target = item.get("target", item.get("host", "-"))
601
665
  if isinstance(target, list):
602
- target = ', '.join(target) if target else '-'
666
+ target = ", ".join(target) if target else "-"
603
667
  click.echo(f" Target: {target}")
604
668
 
605
669
  # Severity for findings
606
- if item.get('severity'):
607
- severity = item['severity'].upper()
608
- if severity in ['CRITICAL', 'HIGH']:
609
- sev_str = click.style(f"🔴 {severity}", fg='red', bold=True)
610
- elif severity == 'MEDIUM':
611
- sev_str = click.style(f"🟡 {severity}", fg='yellow')
670
+ if item.get("severity"):
671
+ severity = item["severity"].upper()
672
+ if severity in ["CRITICAL", "HIGH"]:
673
+ sev_str = click.style(f"🔴 {severity}", fg="red", bold=True)
674
+ elif severity == "MEDIUM":
675
+ sev_str = click.style(f"🟡 {severity}", fg="yellow")
612
676
  else:
613
- sev_str = click.style(f"🟢 {severity}", fg='green')
677
+ sev_str = click.style(f"🟢 {severity}", fg="green")
614
678
  click.echo(f" Severity: {sev_str}")
615
679
 
616
680
  # Status for jobs
617
- if item.get('status'):
618
- status = item['status']
619
- if status == 'done':
620
- status_str = click.style("✓ Completed", fg='green')
621
- elif status == 'error':
622
- status_str = click.style("✗ Error", fg='red')
623
- elif status == 'running':
624
- status_str = click.style("⟳ Running", fg='yellow')
681
+ if item.get("status"):
682
+ status = item["status"]
683
+ if status == "done":
684
+ status_str = click.style("✓ Completed", fg="green")
685
+ elif status == "error":
686
+ status_str = click.style("✗ Error", fg="red")
687
+ elif status == "running":
688
+ status_str = click.style("⟳ Running", fg="yellow")
625
689
  else:
626
690
  status_str = status
627
691
  click.echo(f" Status: {status_str}")
628
692
 
629
693
  # Date
630
- if item.get('created_at'):
694
+ if item.get("created_at"):
631
695
  click.echo(f" Date: {item['created_at'][:19].replace('T', ' ')}")
632
696
 
633
697
  click.echo()
634
698
 
635
699
  # Additional metadata if present
636
- metadata_keys = ['port', 'service', 'version', 'cve', 'cvss', 'username', 'raw_output']
700
+ metadata_keys = [
701
+ "port",
702
+ "service",
703
+ "version",
704
+ "cve",
705
+ "cvss",
706
+ "username",
707
+ "raw_output",
708
+ ]
637
709
  has_metadata = any(item.get(k) for k in metadata_keys)
638
710
 
639
711
  if has_metadata:
@@ -641,9 +713,9 @@ def _view_evidence_detail(item: Dict):
641
713
  for key in metadata_keys:
642
714
  if item.get(key):
643
715
  value = item[key]
644
- if key == 'raw_output':
716
+ if key == "raw_output":
645
717
  click.echo(f" {key}:")
646
- for line in str(value)[:500].split('\n')[:10]:
718
+ for line in str(value)[:500].split("\n")[:10]:
647
719
  click.echo(f" {line}")
648
720
  if len(str(value)) > 500:
649
721
  click.echo(" ... (truncated)")
@@ -662,46 +734,47 @@ def _view_evidence_detail(item: Dict):
662
734
  def display_evidence_item(item: Dict):
663
735
  """Display a single evidence item."""
664
736
  # Icon based on type
665
- icons = {
666
- 'job': '📄',
667
- 'finding': '🔍',
668
- 'credential': '🔑',
669
- 'file': '📁'
670
- }
671
- icon = icons.get(item['type'], '•')
672
-
737
+ icons = {"job": "📄", "finding": "🔍", "credential": "🔑", "file": "📁"}
738
+ icon = icons.get(item["type"], "•")
739
+
673
740
  # Format date
674
741
  try:
675
- date_str = item['created_at'][:16].replace('T', ' ')
742
+ date_str = item["created_at"][:16].replace("T", " ")
676
743
  except:
677
- date_str = 'Unknown date'
678
-
744
+ date_str = "Unknown date"
745
+
679
746
  # Add severity icon for findings
680
747
  severity_icon = ""
681
- if item.get('severity') in ['critical', 'high']:
748
+ if item.get("severity") in ["critical", "high"]:
682
749
  severity_icon = " 🔴"
683
- elif item.get('severity') == 'medium':
750
+ elif item.get("severity") == "medium":
684
751
  severity_icon = " 🟡"
685
-
752
+
686
753
  # Main line
687
- tool_upper = item['tool'].upper()
688
- click.echo(f" {icon} [{click.style(tool_upper, fg='cyan')}]{severity_icon} {item['title']}")
754
+ tool_upper = item["tool"].upper()
755
+ click.echo(
756
+ f" {icon} [{click.style(tool_upper, fg='cyan')}]{severity_icon} {item['title']}"
757
+ )
689
758
  click.echo(f" → {item['description']}")
690
759
  click.echo(f" {click.style(date_str, fg='bright_black')}", nl=False)
691
-
760
+
692
761
  # Type-specific info
693
- if item['type'] == 'job':
694
- if item.get('status') == 'done':
762
+ if item["type"] == "job":
763
+ if item.get("status") == "done":
695
764
  click.echo(f" | {click.style('Completed', fg='green')}", nl=False)
696
- elif item.get('status') == 'error':
765
+ elif item.get("status") == "error":
697
766
  click.echo(f" | {click.style('Error', fg='red')}", nl=False)
698
- elif item['type'] == 'finding':
699
- severity = item.get('severity', 'info').upper()
700
- sev_color = 'red' if severity in ['CRITICAL', 'HIGH'] else 'yellow' if severity == 'MEDIUM' else 'blue'
767
+ elif item["type"] == "finding":
768
+ severity = item.get("severity", "info").upper()
769
+ sev_color = (
770
+ "red"
771
+ if severity in ["CRITICAL", "HIGH"]
772
+ else "yellow" if severity == "MEDIUM" else "blue"
773
+ )
701
774
  click.echo(f" | {click.style(f'Severity: {severity}', fg=sev_color)}", nl=False)
702
- elif item['type'] == 'credential':
775
+ elif item["type"] == "credential":
703
776
  click.echo(f" | {click.style('Credential Found', fg='green')}", nl=False)
704
-
777
+
705
778
  click.echo() # New line
706
779
  click.echo() # Spacing
707
780
 
@@ -715,90 +788,90 @@ def view_phase_details(engagement_id: int, evidence: Dict[str, List[Dict]]):
715
788
  click.echo(" [4] Post-Exploitation")
716
789
  click.echo(" [q] Cancel")
717
790
  click.echo()
718
-
791
+
719
792
  choice = input(" Select option: ").strip()
720
-
793
+
721
794
  phase_map = {
722
- '1': 'reconnaissance',
723
- '2': 'enumeration',
724
- '3': 'exploitation',
725
- '4': 'post_exploitation'
795
+ "1": "reconnaissance",
796
+ "2": "enumeration",
797
+ "3": "exploitation",
798
+ "4": "post_exploitation",
726
799
  }
727
-
728
- if choice == 'q':
800
+
801
+ if choice == "q":
729
802
  return
730
-
803
+
731
804
  phase_key = phase_map.get(choice)
732
805
  if not phase_key:
733
- click.echo(click.style("Invalid selection", fg='red'))
806
+ click.echo(click.style("Invalid selection", fg="red"))
734
807
  click.pause()
735
808
  return
736
-
809
+
737
810
  # Show all items in this phase
738
811
  DesignSystem.clear_screen()
739
812
  width = get_terminal_width()
740
-
813
+
741
814
  phase_names = {
742
- 'reconnaissance': 'RECONNAISSANCE',
743
- 'enumeration': 'ENUMERATION',
744
- 'exploitation': 'EXPLOITATION',
745
- 'post_exploitation': 'POST-EXPLOITATION'
815
+ "reconnaissance": "RECONNAISSANCE",
816
+ "enumeration": "ENUMERATION",
817
+ "exploitation": "EXPLOITATION",
818
+ "post_exploitation": "POST-EXPLOITATION",
746
819
  }
747
-
820
+
748
821
  click.echo("\n" + "=" * width)
749
- click.echo(click.style(phase_names[phase_key], bold=True, fg='cyan').center(width))
822
+ click.echo(click.style(phase_names[phase_key], bold=True, fg="cyan").center(width))
750
823
  click.echo("=" * width + "\n")
751
-
824
+
752
825
  items = evidence.get(phase_key, [])
753
-
826
+
754
827
  if not items:
755
- click.echo(click.style("No evidence in this phase", fg='yellow'))
828
+ click.echo(click.style("No evidence in this phase", fg="yellow"))
756
829
  else:
757
830
  for idx, item in enumerate(items, 1):
758
831
  click.echo(f"\n{click.style(f'[{idx}]', fg='cyan', bold=True)} ", nl=False)
759
832
  display_evidence_item(item)
760
-
833
+
761
834
  click.pause("\nPress any key to return...")
762
835
 
763
836
 
764
837
  def apply_filters() -> Dict:
765
838
  """Apply filters to evidence view."""
766
- click.echo("\n" + click.style("Filter Evidence", bold=True, fg='cyan'))
839
+ click.echo("\n" + click.style("Filter Evidence", bold=True, fg="cyan"))
767
840
  click.echo("─" * 40)
768
841
  click.echo()
769
-
842
+
770
843
  filters = {}
771
-
844
+
772
845
  # Tool filter
773
846
  tool = input("Filter by tool (or 'all'): ").strip()
774
- if tool and tool.lower() != 'all':
775
- filters['tool'] = tool
776
-
847
+ if tool and tool.lower() != "all":
848
+ filters["tool"] = tool
849
+
777
850
  # Host filter
778
851
  host = input("Filter by host/target (or 'all'): ").strip()
779
- if host and host.lower() != 'all':
780
- filters['host'] = host
781
-
852
+ if host and host.lower() != "all":
853
+ filters["host"] = host
854
+
782
855
  # Date filter
783
856
  click.echo("\nFilter by date:")
784
857
  click.echo(" [1] Last 24 hours")
785
858
  click.echo(" [2] Last 7 days")
786
859
  click.echo(" [3] Last 30 days")
787
860
  click.echo(" [4] All time")
788
-
861
+
789
862
  date_choice = input(" Select option: ").strip()
790
-
791
- days_map = {'1': 1, '2': 7, '3': 30, '4': None}
863
+
864
+ days_map = {"1": 1, "2": 7, "3": 30, "4": None}
792
865
  days = days_map.get(date_choice)
793
-
866
+
794
867
  if days:
795
- filters['days'] = days
796
-
868
+ filters["days"] = days
869
+
797
870
  if filters:
798
- click.echo("\n" + click.style("✓ Filters applied", fg='green'))
871
+ click.echo("\n" + click.style("✓ Filters applied", fg="green"))
799
872
  else:
800
- click.echo("\n" + click.style("No filters applied", fg='yellow'))
801
-
873
+ click.echo("\n" + click.style("No filters applied", fg="yellow"))
874
+
802
875
  click.pause()
803
876
  return filters
804
877
 
@@ -807,13 +880,13 @@ def search_evidence(engagement_id: int, evidence: Dict[str, List[Dict]]):
807
880
  """Search evidence with full-text search."""
808
881
  click.echo()
809
882
  search_term = click.prompt("Enter search term", default="").strip()
810
-
883
+
811
884
  if not search_term:
812
885
  return
813
-
886
+
814
887
  search_lower = search_term.lower()
815
888
  results = []
816
-
889
+
817
890
  # Search across all evidence
818
891
  for phase, items in evidence.items():
819
892
  for item in items:
@@ -821,50 +894,56 @@ def search_evidence(engagement_id: int, evidence: Dict[str, List[Dict]]):
821
894
  searchable = f"{item.get('title', '')} {item.get('description', '')} {item.get('tool', '')}".lower()
822
895
  if search_lower in searchable:
823
896
  results.append((phase, item))
824
-
897
+
825
898
  # Display results
826
899
  DesignSystem.clear_screen()
827
900
  click.echo()
828
- click.echo(click.style(f"🔍 SEARCH RESULTS FOR: {search_term}", bold=True, fg='cyan'))
901
+ click.echo(
902
+ click.style(f"🔍 SEARCH RESULTS FOR: {search_term}", bold=True, fg="cyan")
903
+ )
829
904
  click.echo("=" * 80)
830
905
  click.echo()
831
-
906
+
832
907
  if not results:
833
- click.echo(click.style(" No matches found", fg='yellow'))
908
+ click.echo(click.style(" No matches found", fg="yellow"))
834
909
  else:
835
910
  click.echo(f"Found {len(results)} match(es):\n")
836
911
  for phase, item in results[:20]: # Show first 20
837
912
  severity_icon = ""
838
- if item.get('severity') in ['critical', 'high']:
913
+ if item.get("severity") in ["critical", "high"]:
839
914
  severity_icon = " 🔴"
840
- elif item.get('severity') == 'medium':
915
+ elif item.get("severity") == "medium":
841
916
  severity_icon = " 🟡"
842
-
843
- icon = {'job': '📄', 'finding': '🔍', 'credential': '🔑', 'file': '📁'}.get(item['type'], '•')
917
+
918
+ icon = {"job": "📄", "finding": "🔍", "credential": "🔑", "file": "📁"}.get(
919
+ item["type"], "•"
920
+ )
844
921
  click.echo(f" {icon} [{phase.upper()}]{severity_icon} {item['title']}")
845
922
  click.echo(f" → {item['description'][:80]}")
846
923
  click.echo()
847
-
924
+
848
925
  if len(results) > 20:
849
926
  click.echo(f" ... and {len(results) - 20} more matches")
850
-
927
+
851
928
  click.echo()
852
929
  click.pause()
853
930
 
854
931
 
855
- def export_evidence_bundle(engagement_id: int, engagement: Dict, evidence: Dict[str, List[Dict]]):
932
+ def export_evidence_bundle(
933
+ engagement_id: int, engagement: Dict, evidence: Dict[str, List[Dict]]
934
+ ):
856
935
  """Export all evidence as ZIP bundle."""
857
- click.echo("\n" + click.style("Exporting evidence bundle...", fg='yellow'))
936
+ click.echo("\n" + click.style("Exporting evidence bundle...", fg="yellow"))
858
937
 
859
938
  try:
860
939
  from souleyez.export.evidence_bundle import create_evidence_bundle
861
940
 
862
941
  zip_path = create_evidence_bundle(engagement_id, engagement, evidence)
863
- click.echo(click.style(f"\n✓ Evidence bundle created:", fg='green'))
942
+ click.echo(click.style(f"\n✓ Evidence bundle created:", fg="green"))
864
943
  click.echo(f" {zip_path}")
865
944
 
866
945
  except Exception as e:
867
- click.echo(click.style(f"\n✗ Export failed: {e}", fg='red'))
946
+ click.echo(click.style(f"\n✗ Export failed: {e}", fg="red"))
868
947
 
869
948
  click.pause()
870
949
 
@@ -885,7 +964,11 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
885
964
  width = get_terminal_width()
886
965
 
887
966
  click.echo("\n┌" + "─" * (width - 2) + "┐")
888
- click.echo("│" + click.style(" SCREENSHOTS ".center(width - 2), bold=True, fg='cyan') + "│")
967
+ click.echo(
968
+ "│"
969
+ + click.style(" SCREENSHOTS ".center(width - 2), bold=True, fg="cyan")
970
+ + "│"
971
+ )
889
972
  click.echo("└" + "─" * (width - 2) + "┘")
890
973
  click.echo()
891
974
 
@@ -893,15 +976,19 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
893
976
  screenshots = sm.list_screenshots(engagement_id)
894
977
 
895
978
  if not screenshots:
896
- click.echo(click.style(" No screenshots found", fg='yellow'))
979
+ click.echo(click.style(" No screenshots found", fg="yellow"))
897
980
  click.echo()
898
- click.echo(" 💡 Add screenshots with: souleyez screenshots add /path/to/image.png")
981
+ click.echo(
982
+ " 💡 Add screenshots with: souleyez screenshots add /path/to/image.png"
983
+ )
899
984
  click.echo()
900
985
  click.echo(" [q] ← Back")
901
986
  click.echo()
902
987
 
903
- choice = click.prompt("Select option", type=str, default='q', show_default=False)
904
- if choice == 'q':
988
+ choice = click.prompt(
989
+ "Select option", type=str, default="q", show_default=False
990
+ )
991
+ if choice == "q":
905
992
  return
906
993
  continue
907
994
 
@@ -910,7 +997,12 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
910
997
 
911
998
  # Display table
912
999
  console = Console()
913
- table = Table(show_header=True, header_style="bold", box=DesignSystem.TABLE_BOX, expand=True)
1000
+ table = Table(
1001
+ show_header=True,
1002
+ header_style="bold",
1003
+ box=DesignSystem.TABLE_BOX,
1004
+ expand=True,
1005
+ )
914
1006
  table.add_column("#", width=4)
915
1007
  table.add_column("ID", width=6)
916
1008
  table.add_column("Title", width=30)
@@ -920,7 +1012,7 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
920
1012
 
921
1013
  for idx, s in enumerate(screenshots, 1):
922
1014
  # Format size
923
- size = s['file_size']
1015
+ size = s["file_size"]
924
1016
  if size < 1024:
925
1017
  size_str = f"{size} B"
926
1018
  elif size < 1024 * 1024:
@@ -930,25 +1022,25 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
930
1022
 
931
1023
  # Format links
932
1024
  links = []
933
- if s['host_id']:
1025
+ if s["host_id"]:
934
1026
  links.append(f"Host:{s['host_id']}")
935
- if s['finding_id']:
1027
+ if s["finding_id"]:
936
1028
  links.append(f"Finding:{s['finding_id']}")
937
- if s['job_id']:
1029
+ if s["job_id"]:
938
1030
  links.append(f"Job:{s['job_id']}")
939
1031
  links_str = ", ".join(links) if links else "-"
940
1032
 
941
- title = s['title'] or s['filename']
1033
+ title = s["title"] or s["filename"]
942
1034
  if len(title) > 30:
943
1035
  title = title[:27] + "..."
944
1036
 
945
1037
  table.add_row(
946
1038
  str(idx),
947
- str(s['id']),
1039
+ str(s["id"]),
948
1040
  title,
949
1041
  size_str,
950
1042
  links_str,
951
- s['created_at'][:10] if s['created_at'] else "N/A"
1043
+ s["created_at"][:10] if s["created_at"] else "N/A",
952
1044
  )
953
1045
 
954
1046
  console.print(table)
@@ -959,54 +1051,70 @@ def view_screenshots(engagement_id: int, screenshots: List[Dict], sm):
959
1051
  click.echo(" [q] ← Back")
960
1052
  click.echo()
961
1053
 
962
- choice = click.prompt("Select option", type=str, default='q', show_default=False).strip().lower()
1054
+ choice = (
1055
+ click.prompt("Select option", type=str, default="q", show_default=False)
1056
+ .strip()
1057
+ .lower()
1058
+ )
963
1059
 
964
- if choice == 'q':
1060
+ if choice == "q":
965
1061
  return
966
- elif choice == 'd':
1062
+ elif choice == "d":
967
1063
  # Delete screenshot
968
1064
  try:
969
- screenshot_id = click.prompt(" Enter screenshot ID to delete", type=int)
1065
+ screenshot_id = click.prompt(
1066
+ " Enter screenshot ID to delete", type=int
1067
+ )
970
1068
  screenshot = sm.get_screenshot(screenshot_id)
971
1069
  if screenshot:
972
- if click.confirm(f" Delete screenshot '{screenshot['title']}'?", default=False):
1070
+ if click.confirm(
1071
+ f" Delete screenshot '{screenshot['title']}'?", default=False
1072
+ ):
973
1073
  sm.delete_screenshot(screenshot_id)
974
- click.echo(click.style(" ✓ Screenshot deleted", fg='green'))
1074
+ click.echo(click.style(" ✓ Screenshot deleted", fg="green"))
975
1075
  else:
976
- click.echo(click.style(f" Screenshot {screenshot_id} not found", fg='red'))
1076
+ click.echo(
1077
+ click.style(f" Screenshot {screenshot_id} not found", fg="red")
1078
+ )
977
1079
  except (ValueError, KeyboardInterrupt):
978
1080
  pass
979
1081
  click.pause()
980
- elif choice == 'v':
1082
+ elif choice == "v":
981
1083
  # View screenshot by number
982
1084
  try:
983
1085
  idx = click.prompt(" Enter screenshot # to view", type=int) - 1
984
1086
  if 0 <= idx < len(screenshots):
985
1087
  s = screenshots[idx]
986
- filepath = Path(s['filepath'])
1088
+ filepath = Path(s["filepath"])
987
1089
  if filepath.exists():
988
1090
  click.echo()
989
- click.echo(click.style(f" Opening: {s['title']}", fg='cyan'))
1091
+ click.echo(click.style(f" Opening: {s['title']}", fg="cyan"))
990
1092
  click.echo(f" Location: {filepath}")
991
1093
  # Try to open with default viewer
992
1094
  try:
993
- if sys.platform == 'darwin': # macOS
994
- subprocess.run(['open', str(filepath)])
995
- elif sys.platform.startswith('linux'): # Linux
996
- subprocess.run(['xdg-open', str(filepath)])
1095
+ if sys.platform == "darwin": # macOS
1096
+ subprocess.run(["open", str(filepath)])
1097
+ elif sys.platform.startswith("linux"): # Linux
1098
+ subprocess.run(["xdg-open", str(filepath)])
997
1099
  else: # Windows
998
- subprocess.run(['start', str(filepath)], shell=True)
999
- click.echo(click.style(" ✓ Screenshot opened", fg='green'))
1100
+ subprocess.run(["start", str(filepath)], shell=True)
1101
+ click.echo(click.style(" ✓ Screenshot opened", fg="green"))
1000
1102
  except Exception as e:
1001
- click.echo(click.style(f" Could not open screenshot: {e}", fg='yellow'))
1103
+ click.echo(
1104
+ click.style(
1105
+ f" Could not open screenshot: {e}", fg="yellow"
1106
+ )
1107
+ )
1002
1108
  click.echo(f" Manual path: {filepath}")
1003
1109
  else:
1004
- click.echo(click.style(" Screenshot file not found!", fg='red'))
1110
+ click.echo(
1111
+ click.style(" Screenshot file not found!", fg="red")
1112
+ )
1005
1113
  else:
1006
- click.echo(click.style(" Invalid screenshot number", fg='red'))
1114
+ click.echo(click.style(" Invalid screenshot number", fg="red"))
1007
1115
  except (ValueError, click.Abort):
1008
1116
  pass
1009
1117
  click.pause()
1010
1118
  else:
1011
- click.echo(click.style(" Invalid option", fg='red'))
1119
+ click.echo(click.style(" Invalid option", fg="red"))
1012
1120
  click.pause()