vault_audit_tools/commands/
token_operations.rs1use crate::audit::types::AuditEntry;
31use crate::utils::progress::ProgressBar;
32use anyhow::Result;
33use std::collections::HashMap;
34use std::fs::File;
35use std::io::{BufRead, BufReader};
36
37#[derive(Debug, Default)]
39struct TokenOps {
40 lookup_self: usize,
41 renew_self: usize,
42 revoke_self: usize,
43 create: usize,
44 login: usize,
45 other: usize,
46 display_name: Option<String>,
47 username: Option<String>,
48}
49
50fn format_number(n: usize) -> String {
51 let s = n.to_string();
52 let mut result = String::new();
53 for (i, c) in s.chars().rev().enumerate() {
54 if i > 0 && i % 3 == 0 {
55 result.push(',');
56 }
57 result.push(c);
58 }
59 result.chars().rev().collect()
60}
61
62pub fn run(log_files: &[String], output: Option<&str>) -> Result<()> {
63 let mut token_ops: HashMap<String, TokenOps> = HashMap::new();
64 let mut total_lines = 0;
65
66 for (file_idx, log_file) in log_files.iter().enumerate() {
68 eprintln!(
69 "[{}/{}] Processing: {}",
70 file_idx + 1,
71 log_files.len(),
72 log_file
73 );
74
75 let file_size = std::fs::metadata(log_file).ok().map(|m| m.len() as usize);
77 let mut progress = if let Some(size) = file_size {
78 ProgressBar::new(size, "Processing")
79 } else {
80 ProgressBar::new_spinner("Processing")
81 };
82
83 let mut file_lines = 0;
84 let mut bytes_read = 0;
85
86 let file = File::open(log_file)?;
87 let reader = BufReader::new(file);
88
89 for line in reader.lines() {
90 file_lines += 1;
91 total_lines += 1;
92 let line = line?;
93 bytes_read += line.len() + 1; if file_lines % 10_000 == 0 {
97 if let Some(size) = file_size {
98 progress.update(bytes_read.min(size));
99 } else {
100 progress.update(file_lines);
101 }
102 }
103
104 let entry: AuditEntry = match serde_json::from_str(&line) {
105 Ok(e) => e,
106 Err(_) => continue,
107 };
108
109 let entity_id = match &entry.auth {
111 Some(auth) => match &auth.entity_id {
112 Some(id) => id.as_str(),
113 None => continue,
114 },
115 None => continue,
116 };
117
118 let path = match &entry.request {
120 Some(r) => match &r.path {
121 Some(p) => p.as_str(),
122 None => continue,
123 },
124 None => continue,
125 };
126
127 let is_token_op = path.starts_with("auth/token/");
128 let is_login = path.starts_with("auth/") && path.contains("/login");
129
130 if !is_token_op && !is_login {
131 continue;
132 }
133
134 let operation = entry
135 .request
136 .as_ref()
137 .and_then(|r| r.operation.as_deref())
138 .unwrap_or("");
139
140 let ops = token_ops.entry(entity_id.to_string()).or_default();
141
142 if is_login {
144 ops.login += 1;
145 } else if path.contains("lookup-self") {
146 ops.lookup_self += 1;
147 } else if path.contains("renew-self") {
148 ops.renew_self += 1;
149 } else if path.contains("revoke-self") {
150 ops.revoke_self += 1;
151 } else if path.contains("create") || operation == "create" {
152 ops.create += 1;
153 } else {
154 ops.other += 1;
155 }
156
157 if ops.display_name.is_none() {
159 ops.display_name = entry
160 .auth
161 .as_ref()
162 .and_then(|a| a.display_name.as_deref())
163 .map(|s| s.to_string());
164 if let Some(auth) = &entry.auth {
165 if let Some(metadata) = &auth.metadata {
166 if let Some(username) = metadata.get("username") {
167 ops.username = username.as_str().map(|s| s.to_string());
168 }
169 }
170 }
171 }
172 }
173
174 if let Some(size) = file_size {
176 progress.update(size);
177 }
178
179 progress.finish_with_message(&format!(
180 "Processed {} lines from this file",
181 format_number(file_lines)
182 ));
183 }
184
185 eprintln!("\nTotal: Processed {} lines", format_number(total_lines));
186
187 let mut entity_totals: Vec<_> = token_ops
189 .iter()
190 .map(|(entity_id, ops)| {
191 let total = ops.lookup_self
192 + ops.renew_self
193 + ops.revoke_self
194 + ops.create
195 + ops.login
196 + ops.other;
197 (
198 entity_id.clone(),
199 total,
200 ops.display_name
201 .clone()
202 .unwrap_or_else(|| "unknown".to_string()),
203 ops.lookup_self,
204 ops.renew_self,
205 ops.revoke_self,
206 ops.create,
207 ops.login,
208 ops.other,
209 ops.username.clone().unwrap_or_default(),
210 )
211 })
212 .filter(|x| x.1 > 0)
213 .collect();
214
215 entity_totals.sort_by(|a, b| b.1.cmp(&a.1));
217
218 let top = 50;
220 println!("\n{}", "=".repeat(150));
221 println!(
222 "{:<30} {:<25} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10}",
223 "Display Name",
224 "Username",
225 "Total",
226 "Lookup",
227 "Renew",
228 "Revoke",
229 "Create",
230 "Login",
231 "Other"
232 );
233 println!("{}", "=".repeat(150));
234
235 let mut grand_total = 0;
236 for (_, total, display_name, lookup, renew, revoke, create, login, other, username) in
237 entity_totals.iter().take(top)
238 {
239 let display_name_trunc = if display_name.len() > 29 {
240 &display_name[..29]
241 } else {
242 display_name
243 };
244 let username_trunc = if username.len() > 24 {
245 &username[..24]
246 } else {
247 username
248 };
249
250 println!(
251 "{:<30} {:<25} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10}",
252 display_name_trunc,
253 username_trunc,
254 format_number(*total),
255 format_number(*lookup),
256 format_number(*renew),
257 format_number(*revoke),
258 format_number(*create),
259 format_number(*login),
260 format_number(*other)
261 );
262 grand_total += total;
263 }
264
265 println!("{}", "=".repeat(150));
266 println!(
267 "{:<55} {:<10}",
268 format!("TOTAL (top {})", entity_totals.len().min(top)),
269 format_number(grand_total)
270 );
271 println!(
272 "{:<55} {:<10}",
273 "TOTAL ENTITIES",
274 format_number(entity_totals.len())
275 );
276 println!("{}", "=".repeat(150));
277
278 let total_lookup: usize = entity_totals.iter().map(|x| x.3).sum();
280 let total_renew: usize = entity_totals.iter().map(|x| x.4).sum();
281 let total_revoke: usize = entity_totals.iter().map(|x| x.5).sum();
282 let total_create: usize = entity_totals.iter().map(|x| x.6).sum();
283 let total_login: usize = entity_totals.iter().map(|x| x.7).sum();
284 let total_other: usize = entity_totals.iter().map(|x| x.8).sum();
285 let overall_total =
286 total_lookup + total_renew + total_revoke + total_create + total_login + total_other;
287
288 println!("\nOperation Type Breakdown:");
289 println!("{}", "-".repeat(60));
290 println!(
291 "Lookup (lookup-self): {:>12} ({:>5.1}%)",
292 format_number(total_lookup),
293 (total_lookup as f64 / overall_total as f64) * 100.0
294 );
295 println!(
296 "Renew (renew-self): {:>12} ({:>5.1}%)",
297 format_number(total_renew),
298 (total_renew as f64 / overall_total as f64) * 100.0
299 );
300 println!(
301 "Revoke (revoke-self): {:>12} ({:>5.1}%)",
302 format_number(total_revoke),
303 (total_revoke as f64 / overall_total as f64) * 100.0
304 );
305 println!(
306 "Create (child token): {:>12} ({:>5.1}%)",
307 format_number(total_create),
308 (total_create as f64 / overall_total as f64) * 100.0
309 );
310 println!(
311 "Login (auth token): {:>12} ({:>5.1}%)",
312 format_number(total_login),
313 (total_login as f64 / overall_total as f64) * 100.0
314 );
315 println!(
316 "Other: {:>12} ({:>5.1}%)",
317 format_number(total_other),
318 (total_other as f64 / overall_total as f64) * 100.0
319 );
320 println!("{}", "-".repeat(60));
321 println!(
322 "TOTAL: {:>12}",
323 format_number(overall_total)
324 );
325
326 if let Some(_output_path) = output {
328 eprintln!("Note: CSV output not yet implemented");
329 }
330
331 Ok(())
332}