souleyez 2.43.29__py3-none-any.whl → 2.43.32__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 (356) 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 +9592 -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 +1238 -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 +2198 -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 +288 -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/whois_handler.py +277 -0
  126. souleyez/handlers/wpscan_handler.py +554 -0
  127. souleyez/history.py +32 -16
  128. souleyez/importers/msf_importer.py +106 -75
  129. souleyez/importers/smart_importer.py +208 -147
  130. souleyez/integrations/siem/__init__.py +10 -10
  131. souleyez/integrations/siem/base.py +17 -18
  132. souleyez/integrations/siem/elastic.py +108 -122
  133. souleyez/integrations/siem/factory.py +207 -80
  134. souleyez/integrations/siem/googlesecops.py +146 -154
  135. souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
  136. souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
  137. souleyez/integrations/siem/sentinel.py +107 -109
  138. souleyez/integrations/siem/splunk.py +246 -212
  139. souleyez/integrations/siem/wazuh.py +65 -71
  140. souleyez/integrations/wazuh/__init__.py +5 -5
  141. souleyez/integrations/wazuh/client.py +70 -93
  142. souleyez/integrations/wazuh/config.py +85 -57
  143. souleyez/integrations/wazuh/host_mapper.py +28 -36
  144. souleyez/integrations/wazuh/sync.py +78 -68
  145. souleyez/intelligence/__init__.py +4 -5
  146. souleyez/intelligence/correlation_analyzer.py +309 -295
  147. souleyez/intelligence/exploit_knowledge.py +661 -623
  148. souleyez/intelligence/exploit_suggestions.py +159 -139
  149. souleyez/intelligence/gap_analyzer.py +132 -97
  150. souleyez/intelligence/gap_detector.py +251 -214
  151. souleyez/intelligence/sensitive_tables.py +266 -129
  152. souleyez/intelligence/service_parser.py +137 -123
  153. souleyez/intelligence/surface_analyzer.py +407 -268
  154. souleyez/intelligence/target_parser.py +159 -162
  155. souleyez/licensing/__init__.py +6 -6
  156. souleyez/licensing/validator.py +17 -19
  157. souleyez/log_config.py +79 -54
  158. souleyez/main.py +1505 -687
  159. souleyez/migrations/fix_job_counter.py +16 -14
  160. souleyez/parsers/bloodhound_parser.py +41 -39
  161. souleyez/parsers/crackmapexec_parser.py +178 -111
  162. souleyez/parsers/dalfox_parser.py +72 -77
  163. souleyez/parsers/dnsrecon_parser.py +103 -91
  164. souleyez/parsers/enum4linux_parser.py +183 -153
  165. souleyez/parsers/ffuf_parser.py +29 -25
  166. souleyez/parsers/gobuster_parser.py +301 -41
  167. souleyez/parsers/hashcat_parser.py +324 -79
  168. souleyez/parsers/http_fingerprint_parser.py +350 -103
  169. souleyez/parsers/hydra_parser.py +131 -111
  170. souleyez/parsers/impacket_parser.py +231 -178
  171. souleyez/parsers/john_parser.py +98 -86
  172. souleyez/parsers/katana_parser.py +316 -0
  173. souleyez/parsers/msf_parser.py +943 -498
  174. souleyez/parsers/nikto_parser.py +346 -65
  175. souleyez/parsers/nmap_parser.py +262 -174
  176. souleyez/parsers/nuclei_parser.py +40 -44
  177. souleyez/parsers/responder_parser.py +26 -26
  178. souleyez/parsers/searchsploit_parser.py +74 -74
  179. souleyez/parsers/service_explorer_parser.py +279 -0
  180. souleyez/parsers/smbmap_parser.py +180 -124
  181. souleyez/parsers/sqlmap_parser.py +434 -308
  182. souleyez/parsers/theharvester_parser.py +75 -57
  183. souleyez/parsers/whois_parser.py +135 -94
  184. souleyez/parsers/wpscan_parser.py +278 -190
  185. souleyez/plugins/afp.py +44 -36
  186. souleyez/plugins/afp_brute.py +114 -46
  187. souleyez/plugins/ard.py +48 -37
  188. souleyez/plugins/bloodhound.py +95 -61
  189. souleyez/plugins/certipy.py +303 -0
  190. souleyez/plugins/crackmapexec.py +186 -85
  191. souleyez/plugins/dalfox.py +120 -59
  192. souleyez/plugins/dns_hijack.py +146 -41
  193. souleyez/plugins/dnsrecon.py +97 -61
  194. souleyez/plugins/enum4linux.py +91 -66
  195. souleyez/plugins/evil_winrm.py +291 -0
  196. souleyez/plugins/ffuf.py +166 -90
  197. souleyez/plugins/firmware_extract.py +133 -29
  198. souleyez/plugins/gobuster.py +387 -190
  199. souleyez/plugins/gpp_extract.py +393 -0
  200. souleyez/plugins/hashcat.py +100 -73
  201. souleyez/plugins/http_fingerprint.py +854 -267
  202. souleyez/plugins/hydra.py +566 -200
  203. souleyez/plugins/impacket_getnpusers.py +117 -69
  204. souleyez/plugins/impacket_psexec.py +84 -64
  205. souleyez/plugins/impacket_secretsdump.py +103 -69
  206. souleyez/plugins/impacket_smbclient.py +89 -75
  207. souleyez/plugins/john.py +86 -69
  208. souleyez/plugins/katana.py +313 -0
  209. souleyez/plugins/kerbrute.py +237 -0
  210. souleyez/plugins/lfi_extract.py +541 -0
  211. souleyez/plugins/macos_ssh.py +117 -48
  212. souleyez/plugins/mdns.py +35 -30
  213. souleyez/plugins/msf_auxiliary.py +253 -130
  214. souleyez/plugins/msf_exploit.py +239 -161
  215. souleyez/plugins/nikto.py +134 -78
  216. souleyez/plugins/nmap.py +275 -91
  217. souleyez/plugins/nuclei.py +180 -89
  218. souleyez/plugins/nxc.py +285 -0
  219. souleyez/plugins/plugin_base.py +35 -36
  220. souleyez/plugins/plugin_template.py +13 -5
  221. souleyez/plugins/rdp_sec_check.py +130 -0
  222. souleyez/plugins/responder.py +112 -71
  223. souleyez/plugins/router_http_brute.py +76 -65
  224. souleyez/plugins/router_ssh_brute.py +118 -41
  225. souleyez/plugins/router_telnet_brute.py +124 -42
  226. souleyez/plugins/routersploit.py +91 -59
  227. souleyez/plugins/routersploit_exploit.py +77 -55
  228. souleyez/plugins/searchsploit.py +91 -77
  229. souleyez/plugins/service_explorer.py +1160 -0
  230. souleyez/plugins/smbmap.py +122 -72
  231. souleyez/plugins/smbpasswd.py +215 -0
  232. souleyez/plugins/sqlmap.py +301 -113
  233. souleyez/plugins/theharvester.py +127 -75
  234. souleyez/plugins/tr069.py +79 -57
  235. souleyez/plugins/upnp.py +65 -47
  236. souleyez/plugins/upnp_abuse.py +73 -55
  237. souleyez/plugins/vnc_access.py +129 -42
  238. souleyez/plugins/vnc_brute.py +109 -38
  239. souleyez/plugins/whois.py +77 -58
  240. souleyez/plugins/wpscan.py +173 -69
  241. souleyez/reporting/__init__.py +2 -1
  242. souleyez/reporting/attack_chain.py +411 -346
  243. souleyez/reporting/charts.py +436 -501
  244. souleyez/reporting/compliance_mappings.py +334 -201
  245. souleyez/reporting/detection_report.py +126 -125
  246. souleyez/reporting/formatters.py +828 -591
  247. souleyez/reporting/generator.py +386 -302
  248. souleyez/reporting/metrics.py +72 -75
  249. souleyez/scanner.py +35 -29
  250. souleyez/security/__init__.py +37 -11
  251. souleyez/security/scope_validator.py +175 -106
  252. souleyez/security/validation.py +223 -149
  253. souleyez/security.py +22 -6
  254. souleyez/storage/credentials.py +247 -186
  255. souleyez/storage/crypto.py +296 -129
  256. souleyez/storage/database.py +73 -50
  257. souleyez/storage/db.py +58 -36
  258. souleyez/storage/deliverable_evidence.py +177 -128
  259. souleyez/storage/deliverable_exporter.py +282 -246
  260. souleyez/storage/deliverable_templates.py +134 -116
  261. souleyez/storage/deliverables.py +135 -130
  262. souleyez/storage/engagements.py +109 -56
  263. souleyez/storage/evidence.py +181 -152
  264. souleyez/storage/execution_log.py +31 -17
  265. souleyez/storage/exploit_attempts.py +93 -57
  266. souleyez/storage/exploits.py +67 -36
  267. souleyez/storage/findings.py +48 -61
  268. souleyez/storage/hosts.py +176 -144
  269. souleyez/storage/migrate_to_engagements.py +43 -19
  270. souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
  271. souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
  272. souleyez/storage/migrations/_003_add_execution_log.py +14 -8
  273. souleyez/storage/migrations/_005_screenshots.py +13 -5
  274. souleyez/storage/migrations/_006_deliverables.py +13 -5
  275. souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
  276. souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
  277. souleyez/storage/migrations/_010_evidence_linking.py +17 -10
  278. souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
  279. souleyez/storage/migrations/_012_team_collaboration.py +34 -21
  280. souleyez/storage/migrations/_013_add_host_tags.py +12 -6
  281. souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
  282. souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
  283. souleyez/storage/migrations/_016_add_domain_field.py +10 -4
  284. souleyez/storage/migrations/_017_msf_sessions.py +16 -8
  285. souleyez/storage/migrations/_018_add_osint_target.py +10 -6
  286. souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
  287. souleyez/storage/migrations/_020_add_rbac.py +36 -15
  288. souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
  289. souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
  290. souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
  291. souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
  292. souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
  293. souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
  294. souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
  295. souleyez/storage/migrations/__init__.py +26 -26
  296. souleyez/storage/migrations/migration_manager.py +19 -19
  297. souleyez/storage/msf_sessions.py +100 -65
  298. souleyez/storage/osint.py +17 -24
  299. souleyez/storage/recommendation_engine.py +269 -235
  300. souleyez/storage/screenshots.py +33 -32
  301. souleyez/storage/smb_shares.py +136 -92
  302. souleyez/storage/sqlmap_data.py +183 -128
  303. souleyez/storage/team_collaboration.py +135 -141
  304. souleyez/storage/timeline_tracker.py +122 -94
  305. souleyez/storage/wazuh_vulns.py +64 -66
  306. souleyez/storage/web_paths.py +33 -37
  307. souleyez/testing/credential_tester.py +221 -205
  308. souleyez/ui/__init__.py +1 -1
  309. souleyez/ui/ai_quotes.py +12 -12
  310. souleyez/ui/attack_surface.py +2439 -1516
  311. souleyez/ui/chain_rules_view.py +914 -382
  312. souleyez/ui/correlation_view.py +312 -230
  313. souleyez/ui/dashboard.py +2382 -1130
  314. souleyez/ui/deliverables_view.py +148 -62
  315. souleyez/ui/design_system.py +13 -13
  316. souleyez/ui/errors.py +49 -49
  317. souleyez/ui/evidence_linking_view.py +284 -179
  318. souleyez/ui/evidence_vault.py +393 -285
  319. souleyez/ui/exploit_suggestions_view.py +555 -349
  320. souleyez/ui/export_view.py +100 -66
  321. souleyez/ui/gap_analysis_view.py +315 -171
  322. souleyez/ui/help_system.py +105 -97
  323. souleyez/ui/intelligence_view.py +436 -293
  324. souleyez/ui/interactive.py +22783 -10678
  325. souleyez/ui/interactive_selector.py +75 -68
  326. souleyez/ui/log_formatter.py +47 -39
  327. souleyez/ui/menu_components.py +22 -13
  328. souleyez/ui/msf_auxiliary_menu.py +184 -133
  329. souleyez/ui/pending_chains_view.py +336 -172
  330. souleyez/ui/progress_indicators.py +5 -3
  331. souleyez/ui/recommendations_view.py +195 -137
  332. souleyez/ui/rule_builder.py +343 -225
  333. souleyez/ui/setup_wizard.py +678 -284
  334. souleyez/ui/shortcuts.py +217 -165
  335. souleyez/ui/splunk_gap_analysis_view.py +452 -270
  336. souleyez/ui/splunk_vulns_view.py +139 -86
  337. souleyez/ui/team_dashboard.py +498 -335
  338. souleyez/ui/template_selector.py +196 -105
  339. souleyez/ui/terminal.py +6 -6
  340. souleyez/ui/timeline_view.py +198 -127
  341. souleyez/ui/tool_setup.py +264 -164
  342. souleyez/ui/tutorial.py +202 -72
  343. souleyez/ui/tutorial_state.py +40 -40
  344. souleyez/ui/wazuh_vulns_view.py +235 -141
  345. souleyez/ui/wordlist_browser.py +260 -107
  346. souleyez/ui.py +464 -312
  347. souleyez/utils/tool_checker.py +427 -367
  348. souleyez/utils.py +33 -29
  349. souleyez/wordlists.py +134 -167
  350. {souleyez-2.43.29.dist-info → souleyez-2.43.32.dist-info}/METADATA +1 -1
  351. souleyez-2.43.32.dist-info/RECORD +441 -0
  352. {souleyez-2.43.29.dist-info → souleyez-2.43.32.dist-info}/WHEEL +1 -1
  353. souleyez-2.43.29.dist-info/RECORD +0 -379
  354. {souleyez-2.43.29.dist-info → souleyez-2.43.32.dist-info}/entry_points.txt +0 -0
  355. {souleyez-2.43.29.dist-info → souleyez-2.43.32.dist-info}/licenses/LICENSE +0 -0
  356. {souleyez-2.43.29.dist-info → souleyez-2.43.32.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()