vta_cli_common/commands/
audit.rs1use ratatui::layout::Constraint;
2use ratatui::style::{Color, Modifier, Style};
3use ratatui::text::Span;
4use ratatui::widgets::{Cell, Row, Table};
5use vta_sdk::prelude::*;
6
7use crate::display::{NameBook, book_from_acl, named_did_cell};
8use crate::render::{is_full_display, print_full_entry_owned, print_full_list_title, print_widget};
9
10pub async fn cmd_list_audit_logs(
12 client: &VtaClient,
13 params: &ListAuditLogsBody,
14) -> Result<(), Box<dyn std::error::Error>> {
15 let result = client.list_audit_logs(params).await?;
16
17 if result.entries.is_empty() {
18 println!(" No audit log entries found.");
19 return Ok(());
20 }
21
22 let mut book = NameBook::new();
27 if let Ok(acl) = client.list_acl(None).await {
28 book_from_acl(&mut book, &acl.entries);
29 }
30
31 if is_full_display() {
32 print_full_list_title(
33 &format!(
34 "Audit Log (page {}/{}, {} total)",
35 result.page, result.total_pages, result.total
36 ),
37 result.entries.len(),
38 );
39 for entry in &result.entries {
40 let ts = crate::duration::format_local_time(entry.timestamp);
41 let resource = entry.resource.as_deref().unwrap_or("—");
42 let channel = entry.channel.as_deref().unwrap_or("—");
43 let context = entry.context_id.as_deref().unwrap_or("—");
44 let mut fields = vec![
45 ("ID", entry.id.clone()),
46 ("Timestamp", ts),
47 ("Action", entry.action.clone()),
48 ];
49 if let Some(name) = book.name_of(&entry.actor) {
50 fields.push(("Actor", name));
51 }
52 fields.push(("Actor DID", entry.actor.clone()));
54 fields.push(("Resource", resource.to_string()));
55 fields.push(("Channel", channel.to_string()));
56 fields.push(("Context", context.to_string()));
57 fields.push(("Outcome", entry.outcome.clone()));
58 print_full_entry_owned(&fields);
59 }
60 return Ok(());
61 }
62
63 println!(
65 "\n \x1b[1mAudit Log\x1b[0m \x1b[2m(page {}/{}, {} total entries)\x1b[0m\n",
66 result.page, result.total_pages, result.total
67 );
68
69 let rows: Vec<Row> = result
71 .entries
72 .iter()
73 .map(|entry| {
74 let ts = crate::duration::format_local_time(entry.timestamp);
76
77 let outcome_style = if entry.outcome == "success" {
79 Style::default().fg(Color::Green)
80 } else if entry.outcome.starts_with("denied") {
81 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
82 } else {
83 Style::default().fg(Color::Yellow)
84 };
85
86 let action_style = if entry.action.starts_with("auth.") {
88 Style::default().fg(Color::Cyan)
89 } else if entry.action.starts_with("key.") || entry.action.starts_with("seed.") {
90 Style::default().fg(Color::Magenta)
91 } else if entry.action.starts_with("acl.") {
92 Style::default().fg(Color::Yellow)
93 } else if entry.action.starts_with("session.") {
94 Style::default().fg(Color::Blue)
95 } else {
96 Style::default()
97 };
98
99 let resource_display = entry.resource.as_deref().unwrap_or("\u{2014}");
103
104 Row::new(vec![
105 Cell::from(Span::styled(ts, Style::default().fg(Color::DarkGray))),
106 Cell::from(Span::styled(entry.action.clone(), action_style)),
107 named_did_cell(&book, &entry.actor),
108 Cell::from(resource_display.to_string()),
109 Cell::from(Span::styled(entry.outcome.clone(), outcome_style)),
110 ])
111 })
112 .collect();
113
114 let header = Row::new(vec![
115 Cell::from(Span::styled(
116 "Timestamp",
117 Style::default()
118 .fg(Color::White)
119 .add_modifier(Modifier::BOLD),
120 )),
121 Cell::from(Span::styled(
122 "Action",
123 Style::default()
124 .fg(Color::White)
125 .add_modifier(Modifier::BOLD),
126 )),
127 Cell::from(Span::styled(
128 "Actor",
129 Style::default()
130 .fg(Color::White)
131 .add_modifier(Modifier::BOLD),
132 )),
133 Cell::from(Span::styled(
134 "Resource",
135 Style::default()
136 .fg(Color::White)
137 .add_modifier(Modifier::BOLD),
138 )),
139 Cell::from(Span::styled(
140 "Outcome",
141 Style::default()
142 .fg(Color::White)
143 .add_modifier(Modifier::BOLD),
144 )),
145 ]);
146
147 let row_count = result.entries.len();
148
149 let table = Table::new(
154 rows,
155 [
156 Constraint::Length(25), Constraint::Length(22), Constraint::Min(30), Constraint::Min(16), Constraint::Length(20), ],
162 )
163 .header(header)
164 .column_spacing(2);
165
166 let height = row_count as u16 + 2; print_widget(table, height);
168
169 if result.total_pages > 1 {
171 println!(
172 "\n \x1b[2mPage {}/{} \u{2014} use --page N to navigate\x1b[0m",
173 result.page, result.total_pages
174 );
175 }
176
177 Ok(())
178}
179
180pub async fn cmd_get_retention(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
182 let result = client.get_audit_retention().await?;
183 println!("\n \x1b[1mAudit Retention\x1b[0m");
184 println!(
185 " Retention period: \x1b[36m{}\x1b[0m days",
186 result.retention_days
187 );
188 println!();
189 Ok(())
190}
191
192pub async fn cmd_update_retention(
194 client: &VtaClient,
195 days: u32,
196) -> Result<(), Box<dyn std::error::Error>> {
197 let result = client.update_audit_retention(days).await?;
198 println!(
199 "\n \x1b[32m\u{2713}\x1b[0m Audit retention updated to \x1b[36m{}\x1b[0m days",
200 result.retention_days
201 );
202 println!();
203 Ok(())
204}