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