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