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
@@ -1,4 +1,5 @@
1
1
  """Evidence linking interface for deliverables."""
2
+
2
3
  import click
3
4
  from souleyez.storage.deliverable_evidence import EvidenceManager
4
5
  from souleyez.storage.deliverables import DeliverableManager
@@ -8,7 +9,7 @@ from souleyez.ui.design_system import DesignSystem
8
9
  def show_evidence_linking_view(deliverable_id: int):
9
10
  """
10
11
  Show evidence linking interface for a deliverable.
11
-
12
+
12
13
  Options:
13
14
  - View linked evidence
14
15
  - Add evidence (manual selection)
@@ -17,87 +18,141 @@ def show_evidence_linking_view(deliverable_id: int):
17
18
  """
18
19
  em = EvidenceManager()
19
20
  dm = DeliverableManager()
20
-
21
+
21
22
  deliverable = dm.get_deliverable(deliverable_id)
22
23
  if not deliverable:
23
- click.echo(click.style(" Error: Deliverable not found", fg='red'))
24
+ click.echo(click.style(" Error: Deliverable not found", fg="red"))
24
25
  click.pause()
25
26
  return
26
-
27
+
27
28
  while True:
28
29
  DesignSystem.clear_screen()
29
-
30
+
30
31
  width = DesignSystem.get_terminal_width()
31
-
32
+
32
33
  # Header
33
34
  click.echo("\n┌" + "─" * (width - 2) + "┐")
34
- click.echo("│" + click.style(" 🔗 EVIDENCE LINKING ".center(width - 2), bold=True, fg='cyan') + "│")
35
+ click.echo(
36
+ "│"
37
+ + click.style(
38
+ " 🔗 EVIDENCE LINKING ".center(width - 2), bold=True, fg="cyan"
39
+ )
40
+ + "│"
41
+ )
35
42
  click.echo("└" + "─" * (width - 2) + "┘")
36
43
  click.echo()
37
-
44
+
38
45
  # Deliverable info
39
- click.echo(f" Deliverable: {click.style(deliverable['title'], bold=True, fg='cyan')}")
40
- if deliverable.get('description'):
46
+ click.echo(
47
+ f" Deliverable: {click.style(deliverable['title'], bold=True, fg='cyan')}"
48
+ )
49
+ if deliverable.get("description"):
41
50
  click.echo(f" Description: {deliverable['description'][:80]}...")
42
51
  click.echo()
43
-
52
+
44
53
  # Get linked evidence
45
54
  evidence = em.get_evidence(deliverable_id)
46
55
  evidence_count = em.get_evidence_count(deliverable_id)
47
-
56
+
48
57
  # Display linked evidence summary
49
- click.echo(click.style(f" 📋 LINKED EVIDENCE ({evidence_count} items)", bold=True, fg='cyan'))
58
+ click.echo(
59
+ click.style(
60
+ f" 📋 LINKED EVIDENCE ({evidence_count} items)", bold=True, fg="cyan"
61
+ )
62
+ )
50
63
  click.echo(" " + "─" * (width - 4))
51
64
  click.echo()
52
-
65
+
53
66
  if evidence_count == 0:
54
- click.echo(click.style(" No evidence linked yet", fg='yellow'))
67
+ click.echo(click.style(" No evidence linked yet", fg="yellow"))
55
68
  click.echo()
56
69
  else:
57
70
  # Show findings
58
- if evidence['findings']:
59
- click.echo(click.style(f" 🔍 Findings ({len(evidence['findings'])})", bold=True))
60
- for f in evidence['findings'][:3]:
71
+ if evidence["findings"]:
72
+ click.echo(
73
+ click.style(
74
+ f" 🔍 Findings ({len(evidence['findings'])})", bold=True
75
+ )
76
+ )
77
+ for f in evidence["findings"][:3]:
61
78
  severity_color = {
62
- 'critical': 'red',
63
- 'high': 'yellow',
64
- 'medium': 'white',
65
- 'low': 'bright_black'
66
- }.get(f.get('severity', 'medium'), 'white')
67
- click.echo(f" • [{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] {f.get('title', 'Unknown')[:60]}")
68
- if len(evidence['findings']) > 3:
69
- click.echo(click.style(f" ... and {len(evidence['findings']) - 3} more", fg='bright_black'))
79
+ "critical": "red",
80
+ "high": "yellow",
81
+ "medium": "white",
82
+ "low": "bright_black",
83
+ }.get(f.get("severity", "medium"), "white")
84
+ click.echo(
85
+ f" • [{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] {f.get('title', 'Unknown')[:60]}"
86
+ )
87
+ if len(evidence["findings"]) > 3:
88
+ click.echo(
89
+ click.style(
90
+ f" ... and {len(evidence['findings']) - 3} more",
91
+ fg="bright_black",
92
+ )
93
+ )
70
94
  click.echo()
71
-
95
+
72
96
  # Show credentials
73
- if evidence['credentials']:
74
- click.echo(click.style(f" 🔑 Credentials ({len(evidence['credentials'])})", bold=True))
75
- for c in evidence['credentials'][:3]:
76
- click.echo(f" {c.get('username', 'N/A')}@{c.get('host', 'N/A')}")
77
- if len(evidence['credentials']) > 3:
78
- click.echo(click.style(f" ... and {len(evidence['credentials']) - 3} more", fg='bright_black'))
97
+ if evidence["credentials"]:
98
+ click.echo(
99
+ click.style(
100
+ f" 🔑 Credentials ({len(evidence['credentials'])})", bold=True
101
+ )
102
+ )
103
+ for c in evidence["credentials"][:3]:
104
+ click.echo(
105
+ f" • {c.get('username', 'N/A')}@{c.get('host', 'N/A')}"
106
+ )
107
+ if len(evidence["credentials"]) > 3:
108
+ click.echo(
109
+ click.style(
110
+ f" ... and {len(evidence['credentials']) - 3} more",
111
+ fg="bright_black",
112
+ )
113
+ )
79
114
  click.echo()
80
-
115
+
81
116
  # Show screenshots
82
- if evidence['screenshots']:
83
- click.echo(click.style(f" 📸 Screenshots ({len(evidence['screenshots'])})", bold=True))
84
- for s in evidence['screenshots'][:3]:
85
- click.echo(f" {s.get('description', s.get('filename', 'Unknown'))[:60]}")
86
- if len(evidence['screenshots']) > 3:
87
- click.echo(click.style(f" ... and {len(evidence['screenshots']) - 3} more", fg='bright_black'))
117
+ if evidence["screenshots"]:
118
+ click.echo(
119
+ click.style(
120
+ f" 📸 Screenshots ({len(evidence['screenshots'])})", bold=True
121
+ )
122
+ )
123
+ for s in evidence["screenshots"][:3]:
124
+ click.echo(
125
+ f" • {s.get('description', s.get('filename', 'Unknown'))[:60]}"
126
+ )
127
+ if len(evidence["screenshots"]) > 3:
128
+ click.echo(
129
+ click.style(
130
+ f" ... and {len(evidence['screenshots']) - 3} more",
131
+ fg="bright_black",
132
+ )
133
+ )
88
134
  click.echo()
89
-
135
+
90
136
  # Show jobs
91
- if evidence['jobs']:
92
- click.echo(click.style(f" ⚡ Jobs ({len(evidence['jobs'])})", bold=True))
93
- for j in evidence['jobs'][:3]:
94
- click.echo(f" • #{j.get('id')} - {j.get('tool', 'N/A')} @ {j.get('target', 'N/A')}")
95
- if len(evidence['jobs']) > 3:
96
- click.echo(click.style(f" ... and {len(evidence['jobs']) - 3} more", fg='bright_black'))
137
+ if evidence["jobs"]:
138
+ click.echo(
139
+ click.style(f" ⚡ Jobs ({len(evidence['jobs'])})", bold=True)
140
+ )
141
+ for j in evidence["jobs"][:3]:
142
+ click.echo(
143
+ f" • #{j.get('id')} - {j.get('tool', 'N/A')} @ {j.get('target', 'N/A')}"
144
+ )
145
+ if len(evidence["jobs"]) > 3:
146
+ click.echo(
147
+ click.style(
148
+ f" ... and {len(evidence['jobs']) - 3} more",
149
+ fg="bright_black",
150
+ )
151
+ )
97
152
  click.echo()
98
-
153
+
99
154
  # Menu
100
- click.echo(click.style(" ⚙️ ACTIONS", bold=True, fg='cyan'))
155
+ click.echo(click.style(" ⚙️ ACTIONS", bold=True, fg="cyan"))
101
156
  click.echo(" " + "─" * (width - 4))
102
157
  click.echo()
103
158
  click.echo(" [A] Add Evidence (manual selection)")
@@ -107,18 +162,22 @@ def show_evidence_linking_view(deliverable_id: int):
107
162
  click.echo()
108
163
  click.echo(" [q] ← Back")
109
164
  click.echo()
110
-
111
- choice = click.prompt("Select option", type=str, default='q', show_default=False).strip().lower()
112
-
113
- if choice == 'q':
165
+
166
+ choice = (
167
+ click.prompt("Select option", type=str, default="q", show_default=False)
168
+ .strip()
169
+ .lower()
170
+ )
171
+
172
+ if choice == "q":
114
173
  break
115
- elif choice == 'a':
116
- _add_evidence_manual(deliverable_id, deliverable['engagement_id'])
117
- elif choice == 's':
118
- _show_smart_suggestions(deliverable_id, deliverable['engagement_id'])
119
- elif choice == 'v':
174
+ elif choice == "a":
175
+ _add_evidence_manual(deliverable_id, deliverable["engagement_id"])
176
+ elif choice == "s":
177
+ _show_smart_suggestions(deliverable_id, deliverable["engagement_id"])
178
+ elif choice == "v":
120
179
  _view_evidence_detailed(deliverable_id)
121
- elif choice == 'r':
180
+ elif choice == "r":
122
181
  _remove_evidence(deliverable_id)
123
182
 
124
183
 
@@ -126,14 +185,14 @@ def _add_evidence_manual(deliverable_id: int, engagement_id: int):
126
185
  """Manually select and link evidence."""
127
186
  from souleyez.storage.findings import FindingsManager
128
187
  from souleyez.storage.credentials import CredentialsManager
129
-
188
+
130
189
  em = EvidenceManager()
131
190
  fm = FindingsManager()
132
191
  cm = CredentialsManager()
133
-
192
+
134
193
  DesignSystem.clear_screen()
135
194
  click.echo()
136
- click.echo(click.style(" 📎 ADD EVIDENCE", bold=True, fg='cyan'))
195
+ click.echo(click.style(" 📎 ADD EVIDENCE", bold=True, fg="cyan"))
137
196
  click.echo()
138
197
  click.echo(" Select evidence type:")
139
198
  click.echo()
@@ -143,146 +202,172 @@ def _add_evidence_manual(deliverable_id: int, engagement_id: int):
143
202
  click.echo(" [4] Job")
144
203
  click.echo(" [q] Cancel")
145
204
  click.echo()
146
-
147
- etype_choice = click.prompt("Evidence type", type=str, default='q').strip()
148
-
149
- if etype_choice == 'q':
205
+
206
+ etype_choice = click.prompt("Evidence type", type=str, default="q").strip()
207
+
208
+ if etype_choice == "q":
150
209
  return
151
-
210
+
152
211
  evidence_type_map = {
153
- '1': 'finding',
154
- '2': 'credential',
155
- '3': 'screenshot',
156
- '4': 'job'
212
+ "1": "finding",
213
+ "2": "credential",
214
+ "3": "screenshot",
215
+ "4": "job",
157
216
  }
158
-
217
+
159
218
  evidence_type = evidence_type_map.get(etype_choice)
160
219
  if not evidence_type:
161
- click.echo(click.style(" Invalid choice", fg='yellow'))
220
+ click.echo(click.style(" Invalid choice", fg="yellow"))
162
221
  click.pause()
163
222
  return
164
-
223
+
165
224
  # Fetch available evidence
166
- if evidence_type == 'finding':
225
+ if evidence_type == "finding":
167
226
  findings = fm.list_findings(engagement_id)
168
227
  if not findings:
169
- click.echo(click.style(" No findings available", fg='yellow'))
228
+ click.echo(click.style(" No findings available", fg="yellow"))
170
229
  click.pause()
171
230
  return
172
-
231
+
173
232
  click.echo()
174
233
  click.echo(click.style(" Available Findings:", bold=True))
175
234
  for idx, f in enumerate(findings[:20], 1):
176
235
  severity_color = {
177
- 'critical': 'red',
178
- 'high': 'yellow',
179
- 'medium': 'white',
180
- 'low': 'bright_black'
181
- }.get(f.get('severity', 'medium'), 'white')
182
- click.echo(f" [{idx}] [{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] {f.get('title', 'Unknown')[:70]}")
183
-
236
+ "critical": "red",
237
+ "high": "yellow",
238
+ "medium": "white",
239
+ "low": "bright_black",
240
+ }.get(f.get("severity", "medium"), "white")
241
+ click.echo(
242
+ f" [{idx}] [{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] {f.get('title', 'Unknown')[:70]}"
243
+ )
244
+
184
245
  click.echo()
185
246
  choice = click.prompt("Select finding # (0 to cancel)", type=int, default=0)
186
247
  if choice == 0 or choice > len(findings):
187
248
  return
188
-
249
+
189
250
  finding = findings[choice - 1]
190
- notes = click.prompt("Optional notes", type=str, default='')
191
-
192
- em.link_evidence(deliverable_id, 'finding', finding['id'], notes=notes or None)
193
- click.echo(click.style(" ✅ Finding linked successfully", fg='green'))
251
+ notes = click.prompt("Optional notes", type=str, default="")
252
+
253
+ em.link_evidence(deliverable_id, "finding", finding["id"], notes=notes or None)
254
+ click.echo(click.style(" ✅ Finding linked successfully", fg="green"))
194
255
  click.pause()
195
-
196
- elif evidence_type == 'credential':
256
+
257
+ elif evidence_type == "credential":
197
258
  credentials = cm.list_credentials(engagement_id)
198
259
  if not credentials:
199
- click.echo(click.style(" No credentials available", fg='yellow'))
260
+ click.echo(click.style(" No credentials available", fg="yellow"))
200
261
  click.pause()
201
262
  return
202
-
263
+
203
264
  click.echo()
204
265
  click.echo(click.style(" Available Credentials:", bold=True))
205
266
  for idx, c in enumerate(credentials[:20], 1):
206
- click.echo(f" [{idx}] {c.get('username', 'N/A')}@{c.get('host', 'N/A')} ({c.get('credential_type', 'N/A')})")
207
-
267
+ click.echo(
268
+ f" [{idx}] {c.get('username', 'N/A')}@{c.get('host', 'N/A')} ({c.get('credential_type', 'N/A')})"
269
+ )
270
+
208
271
  click.echo()
209
272
  choice = click.prompt("Select credential # (0 to cancel)", type=int, default=0)
210
273
  if choice == 0 or choice > len(credentials):
211
274
  return
212
-
275
+
213
276
  credential = credentials[choice - 1]
214
- notes = click.prompt("Optional notes", type=str, default='')
215
-
216
- em.link_evidence(deliverable_id, 'credential', credential['id'], notes=notes or None)
217
- click.echo(click.style(" Credential linked successfully", fg='green'))
277
+ notes = click.prompt("Optional notes", type=str, default="")
278
+
279
+ em.link_evidence(
280
+ deliverable_id, "credential", credential["id"], notes=notes or None
281
+ )
282
+ click.echo(click.style(" ✅ Credential linked successfully", fg="green"))
218
283
  click.pause()
219
-
284
+
220
285
  else:
221
- click.echo(click.style(" Feature coming soon", fg='yellow'))
286
+ click.echo(click.style(" Feature coming soon", fg="yellow"))
222
287
  click.pause()
223
288
 
224
289
 
225
290
  def _show_smart_suggestions(deliverable_id: int, engagement_id: int):
226
291
  """Show AI-powered evidence suggestions."""
227
292
  em = EvidenceManager()
228
-
293
+
229
294
  DesignSystem.clear_screen()
230
295
  click.echo()
231
- click.echo(click.style(" 🤖 SMART EVIDENCE SUGGESTIONS", bold=True, fg='cyan'))
296
+ click.echo(click.style(" 🤖 SMART EVIDENCE SUGGESTIONS", bold=True, fg="cyan"))
232
297
  click.echo()
233
298
  click.echo(" Analyzing deliverable and finding matches...")
234
299
  click.echo()
235
-
300
+
236
301
  suggestions = em.suggest_evidence(deliverable_id, engagement_id)
237
-
302
+
238
303
  if not any(suggestions.values()):
239
- click.echo(click.style(" No suggestions found", fg='yellow'))
304
+ click.echo(click.style(" No suggestions found", fg="yellow"))
240
305
  click.echo()
241
306
  click.echo(" Try adding evidence manually or run more scans first.")
242
307
  click.pause()
243
308
  return
244
-
309
+
245
310
  # Show findings suggestions
246
- if suggestions['findings']:
247
- click.echo(click.style(f" 🔍 SUGGESTED FINDINGS ({len(suggestions['findings'])} matches)", bold=True))
311
+ if suggestions["findings"]:
312
+ click.echo(
313
+ click.style(
314
+ f" 🔍 SUGGESTED FINDINGS ({len(suggestions['findings'])} matches)",
315
+ bold=True,
316
+ )
317
+ )
248
318
  click.echo()
249
- for idx, f in enumerate(suggestions['findings'][:10], 1):
250
- confidence = f.get('_confidence', 0)
251
- keyword = f.get('_match_keyword', 'N/A')
319
+ for idx, f in enumerate(suggestions["findings"][:10], 1):
320
+ confidence = f.get("_confidence", 0)
321
+ keyword = f.get("_match_keyword", "N/A")
252
322
  severity_color = {
253
- 'critical': 'red',
254
- 'high': 'yellow',
255
- 'medium': 'white',
256
- 'low': 'bright_black'
257
- }.get(f.get('severity', 'medium'), 'white')
258
-
259
- confidence_color = 'green' if confidence >= 70 else ('yellow' if confidence >= 50 else 'bright_black')
260
-
261
- click.echo(f" [{idx}] [{click.style(f'{confidence}%', fg=confidence_color)}] "
262
- f"[{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] "
263
- f"{f.get('title', 'Unknown')[:60]}")
264
- click.echo(click.style(f" Match: '{keyword}'", fg='bright_black'))
323
+ "critical": "red",
324
+ "high": "yellow",
325
+ "medium": "white",
326
+ "low": "bright_black",
327
+ }.get(f.get("severity", "medium"), "white")
328
+
329
+ confidence_color = (
330
+ "green"
331
+ if confidence >= 70
332
+ else ("yellow" if confidence >= 50 else "bright_black")
333
+ )
334
+
335
+ click.echo(
336
+ f" [{idx}] [{click.style(f'{confidence}%', fg=confidence_color)}] "
337
+ f"[{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] "
338
+ f"{f.get('title', 'Unknown')[:60]}"
339
+ )
340
+ click.echo(click.style(f" Match: '{keyword}'", fg="bright_black"))
265
341
  click.echo()
266
-
342
+
267
343
  # Show credentials suggestions
268
- if suggestions['credentials']:
269
- click.echo(click.style(f" 🔑 SUGGESTED CREDENTIALS ({len(suggestions['credentials'])} matches)", bold=True))
344
+ if suggestions["credentials"]:
345
+ click.echo(
346
+ click.style(
347
+ f" 🔑 SUGGESTED CREDENTIALS ({len(suggestions['credentials'])} matches)",
348
+ bold=True,
349
+ )
350
+ )
270
351
  click.echo()
271
- for idx, c in enumerate(suggestions['credentials'][:5], 1):
352
+ for idx, c in enumerate(suggestions["credentials"][:5], 1):
272
353
  click.echo(f" [{idx}] {c.get('username', 'N/A')}@{c.get('host', 'N/A')}")
273
354
  click.echo()
274
-
355
+
275
356
  # Offer to link
276
357
  click.echo()
277
358
  if click.confirm(" Link suggested findings to this deliverable?", default=True):
278
359
  count = 0
279
- for f in suggestions['findings'][:5]: # Link top 5
280
- em.link_evidence(deliverable_id, 'finding', f['id'],
281
- notes=f"Auto-linked (confidence: {f.get('_confidence', 0)}%)")
360
+ for f in suggestions["findings"][:5]: # Link top 5
361
+ em.link_evidence(
362
+ deliverable_id,
363
+ "finding",
364
+ f["id"],
365
+ notes=f"Auto-linked (confidence: {f.get('_confidence', 0)}%)",
366
+ )
282
367
  count += 1
283
-
284
- click.echo(click.style(f" ✅ Linked {count} findings", fg='green'))
285
-
368
+
369
+ click.echo(click.style(f" ✅ Linked {count} findings", fg="green"))
370
+
286
371
  click.pause()
287
372
 
288
373
 
@@ -290,44 +375,50 @@ def _view_evidence_detailed(deliverable_id: int):
290
375
  """Show detailed view of all linked evidence."""
291
376
  em = EvidenceManager()
292
377
  evidence = em.get_evidence(deliverable_id)
293
-
378
+
294
379
  DesignSystem.clear_screen()
295
380
  click.echo()
296
- click.echo(click.style(" 📋 EVIDENCE DETAILS", bold=True, fg='cyan'))
381
+ click.echo(click.style(" 📋 EVIDENCE DETAILS", bold=True, fg="cyan"))
297
382
  click.echo()
298
-
383
+
299
384
  # Detailed findings
300
- if evidence['findings']:
301
- click.echo(click.style(f" 🔍 FINDINGS ({len(evidence['findings'])})", bold=True))
385
+ if evidence["findings"]:
386
+ click.echo(
387
+ click.style(f" 🔍 FINDINGS ({len(evidence['findings'])})", bold=True)
388
+ )
302
389
  click.echo(" " + "─" * 80)
303
- for f in evidence['findings']:
390
+ for f in evidence["findings"]:
304
391
  severity_color = {
305
- 'critical': 'red',
306
- 'high': 'yellow',
307
- 'medium': 'white',
308
- 'low': 'bright_black'
309
- }.get(f.get('severity', 'medium'), 'white')
310
-
311
- click.echo(f" • [{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] {f.get('title', 'Unknown')}")
392
+ "critical": "red",
393
+ "high": "yellow",
394
+ "medium": "white",
395
+ "low": "bright_black",
396
+ }.get(f.get("severity", "medium"), "white")
397
+
398
+ click.echo(
399
+ f" • [{click.style(f.get('severity', 'N/A').upper(), fg=severity_color)}] {f.get('title', 'Unknown')}"
400
+ )
312
401
  click.echo(f" Host: {f.get('host', 'N/A')}")
313
- if f.get('_link_notes'):
402
+ if f.get("_link_notes"):
314
403
  click.echo(f" Notes: {f['_link_notes']}")
315
404
  click.echo(f" Linked: {f.get('_linked_at', 'N/A')}")
316
405
  click.echo()
317
406
  click.echo()
318
-
407
+
319
408
  # Detailed credentials
320
- if evidence['credentials']:
321
- click.echo(click.style(f" 🔑 CREDENTIALS ({len(evidence['credentials'])})", bold=True))
409
+ if evidence["credentials"]:
410
+ click.echo(
411
+ click.style(f" 🔑 CREDENTIALS ({len(evidence['credentials'])})", bold=True)
412
+ )
322
413
  click.echo(" " + "─" * 80)
323
- for c in evidence['credentials']:
414
+ for c in evidence["credentials"]:
324
415
  click.echo(f" • {c.get('username', 'N/A')}@{c.get('host', 'N/A')}")
325
416
  click.echo(f" Type: {c.get('credential_type', 'N/A')}")
326
- if c.get('_link_notes'):
417
+ if c.get("_link_notes"):
327
418
  click.echo(f" Notes: {c['_link_notes']}")
328
419
  click.echo()
329
420
  click.echo()
330
-
421
+
331
422
  click.pause()
332
423
 
333
424
 
@@ -335,32 +426,46 @@ def _remove_evidence(deliverable_id: int):
335
426
  """Remove evidence links."""
336
427
  em = EvidenceManager()
337
428
  evidence = em.get_evidence(deliverable_id)
338
-
429
+
339
430
  if not any(evidence.values()):
340
- click.echo(click.style(" No evidence to remove", fg='yellow'))
431
+ click.echo(click.style(" No evidence to remove", fg="yellow"))
341
432
  click.pause()
342
433
  return
343
-
434
+
344
435
  DesignSystem.clear_screen()
345
436
  click.echo()
346
- click.echo(click.style(" 🗑️ REMOVE EVIDENCE", bold=True, fg='red'))
437
+ click.echo(click.style(" 🗑️ REMOVE EVIDENCE", bold=True, fg="red"))
347
438
  click.echo()
348
-
439
+
349
440
  # Build removal menu
350
441
  items = []
351
-
352
- for f in evidence['findings']:
353
- items.append(('finding', f['id'], f"[Finding] {f.get('title', 'Unknown')[:60]}"))
354
-
355
- for c in evidence['credentials']:
356
- items.append(('credential', c['id'], f"[Credential] {c.get('username', 'N/A')}@{c.get('host', 'N/A')}"))
357
-
358
- for s in evidence['screenshots']:
359
- items.append(('screenshot', s['id'], f"[Screenshot] {s.get('description', s.get('filename', 'Unknown'))[:60]}"))
360
-
361
- for j in evidence['jobs']:
362
- items.append(('job', j['id'], f"[Job] #{j.get('id')} - {j.get('tool', 'N/A')}"))
363
-
442
+
443
+ for f in evidence["findings"]:
444
+ items.append(
445
+ ("finding", f["id"], f"[Finding] {f.get('title', 'Unknown')[:60]}")
446
+ )
447
+
448
+ for c in evidence["credentials"]:
449
+ items.append(
450
+ (
451
+ "credential",
452
+ c["id"],
453
+ f"[Credential] {c.get('username', 'N/A')}@{c.get('host', 'N/A')}",
454
+ )
455
+ )
456
+
457
+ for s in evidence["screenshots"]:
458
+ items.append(
459
+ (
460
+ "screenshot",
461
+ s["id"],
462
+ f"[Screenshot] {s.get('description', s.get('filename', 'Unknown'))[:60]}",
463
+ )
464
+ )
465
+
466
+ for j in evidence["jobs"]:
467
+ items.append(("job", j["id"], f"[Job] #{j.get('id')} - {j.get('tool', 'N/A')}"))
468
+
364
469
  click.echo(" Select evidence to remove:")
365
470
  click.echo()
366
471
  for idx, item in enumerate(items, 1):
@@ -368,15 +473,15 @@ def _remove_evidence(deliverable_id: int):
368
473
  click.echo()
369
474
  click.echo(" [q] Cancel")
370
475
  click.echo()
371
-
476
+
372
477
  choice = click.prompt("Select option", type=int, default=0, show_default=False)
373
478
  if choice == 0 or choice > len(items):
374
479
  return
375
-
480
+
376
481
  evidence_type, evidence_id, _ = items[choice - 1]
377
-
482
+
378
483
  if click.confirm(" Are you sure?", default=False):
379
484
  em.unlink_evidence(deliverable_id, evidence_type, evidence_id)
380
- click.echo(click.style(" ✅ Evidence unlinked", fg='green'))
381
-
485
+ click.echo(click.style(" ✅ Evidence unlinked", fg="green"))
486
+
382
487
  click.pause()