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 eprintln!("Scanning file to determine total lines...");
83 let total_file_lines = crate::utils::parallel::count_file_lines(log_file)?;
84
85 let progress = ProgressBar::new(total_file_lines, "Processing");
86
87 let mut file_lines = 0;
88
89 let file = open_file(log_file)?;
90 let reader = BufReader::new(file);
91
92 for line in reader.lines() {
93 file_lines += 1;
94 total_lines += 1;
95 let line = line?;
96
97 if file_lines % 10_000 == 0 {
99 progress.update(file_lines);
100 }
101
102 let entry: AuditEntry = match serde_json::from_str(&line) {
103 Ok(e) => e,
104 Err(_) => continue,
105 };
106
107 let entity_id = match &entry.auth {
109 Some(auth) => match &auth.entity_id {
110 Some(id) => id.as_str(),
111 None => continue,
112 },
113 None => continue,
114 };
115
116 let path = match &entry.request {
118 Some(r) => match &r.path {
119 Some(p) => p.as_str(),
120 None => continue,
121 },
122 None => continue,
123 };
124
125 let is_token_op = path.starts_with("auth/token/");
126 let is_login = path.starts_with("auth/") && path.contains("/login");
127
128 if !is_token_op && !is_login {
129 continue;
130 }
131
132 let operation = entry
133 .request
134 .as_ref()
135 .and_then(|r| r.operation.as_deref())
136 .unwrap_or("");
137
138 let ops = token_ops.entry(entity_id.to_string()).or_default();
139
140 if is_login {
142 ops.login += 1;
143 } else if path.contains("lookup-self") {
144 ops.lookup_self += 1;
145 } else if path.contains("renew-self") {
146 ops.renew_self += 1;
147 } else if path.contains("revoke-self") {
148 ops.revoke_self += 1;
149 } else if path.contains("create") || operation == "create" {
150 ops.create += 1;
151 } else {
152 ops.other += 1;
153 }
154
155 if ops.display_name.is_none() {
157 ops.display_name = entry
158 .auth
159 .as_ref()
160 .and_then(|a| a.display_name.as_deref())
161 .map(std::string::ToString::to_string);
162 if let Some(auth) = &entry.auth {
163 if let Some(metadata) = &auth.metadata {
164 if let Some(username) = metadata.get("username") {
165 ops.username = username.as_str().map(std::string::ToString::to_string);
166 }
167 }
168 }
169 }
170 }
171
172 progress.update(total_file_lines);
174
175 progress.finish_with_message(&format!(
176 "Processed {} lines from this file",
177 format_number(file_lines)
178 ));
179 }
180
181 eprintln!("\nTotal: Processed {} lines", format_number(total_lines));
182
183 let mut entity_totals: Vec<_> = token_ops
185 .iter()
186 .map(|(entity_id, ops)| {
187 let total = ops.lookup_self
188 + ops.renew_self
189 + ops.revoke_self
190 + ops.create
191 + ops.login
192 + ops.other;
193 (
194 entity_id.clone(),
195 total,
196 ops.display_name
197 .clone()
198 .unwrap_or_else(|| "unknown".to_string()),
199 ops.lookup_self,
200 ops.renew_self,
201 ops.revoke_self,
202 ops.create,
203 ops.login,
204 ops.other,
205 ops.username.clone().unwrap_or_default(),
206 )
207 })
208 .filter(|x| x.1 > 0)
209 .collect();
210
211 entity_totals.sort_by(|a, b| b.1.cmp(&a.1));
213
214 let top = 50;
216 println!("\n{}", "=".repeat(150));
217 println!(
218 "{:<30} {:<25} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10}",
219 "Display Name",
220 "Username",
221 "Total",
222 "Lookup",
223 "Renew",
224 "Revoke",
225 "Create",
226 "Login",
227 "Other"
228 );
229 println!("{}", "=".repeat(150));
230
231 let mut grand_total = 0;
232 for (_, total, display_name, lookup, renew, revoke, create, login, other, username) in
233 entity_totals.iter().take(top)
234 {
235 let display_name_trunc = if display_name.len() > 29 {
236 &display_name[..29]
237 } else {
238 display_name
239 };
240 let username_trunc = if username.len() > 24 {
241 &username[..24]
242 } else {
243 username
244 };
245
246 println!(
247 "{:<30} {:<25} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10} {:<10}",
248 display_name_trunc,
249 username_trunc,
250 format_number(*total),
251 format_number(*lookup),
252 format_number(*renew),
253 format_number(*revoke),
254 format_number(*create),
255 format_number(*login),
256 format_number(*other)
257 );
258 grand_total += total;
259 }
260
261 println!("{}", "=".repeat(150));
262 println!(
263 "{:<55} {:<10}",
264 format!("TOTAL (top {})", entity_totals.len().min(top)),
265 format_number(grand_total)
266 );
267 println!(
268 "{:<55} {:<10}",
269 "TOTAL ENTITIES",
270 format_number(entity_totals.len())
271 );
272 println!("{}", "=".repeat(150));
273
274 let total_lookup: usize = entity_totals.iter().map(|x| x.3).sum();
276 let total_renew: usize = entity_totals.iter().map(|x| x.4).sum();
277 let total_revoke: usize = entity_totals.iter().map(|x| x.5).sum();
278 let total_create: usize = entity_totals.iter().map(|x| x.6).sum();
279 let total_login: usize = entity_totals.iter().map(|x| x.7).sum();
280 let total_other: usize = entity_totals.iter().map(|x| x.8).sum();
281 let overall_total =
282 total_lookup + total_renew + total_revoke + total_create + total_login + total_other;
283
284 println!("\nOperation Type Breakdown:");
285 println!("{}", "-".repeat(60));
286 println!(
287 "Lookup (lookup-self): {:>12} ({:>5.1}%)",
288 format_number(total_lookup),
289 (total_lookup as f64 / overall_total as f64) * 100.0
290 );
291 println!(
292 "Renew (renew-self): {:>12} ({:>5.1}%)",
293 format_number(total_renew),
294 (total_renew as f64 / overall_total as f64) * 100.0
295 );
296 println!(
297 "Revoke (revoke-self): {:>12} ({:>5.1}%)",
298 format_number(total_revoke),
299 (total_revoke as f64 / overall_total as f64) * 100.0
300 );
301 println!(
302 "Create (child token): {:>12} ({:>5.1}%)",
303 format_number(total_create),
304 (total_create as f64 / overall_total as f64) * 100.0
305 );
306 println!(
307 "Login (auth token): {:>12} ({:>5.1}%)",
308 format_number(total_login),
309 (total_login as f64 / overall_total as f64) * 100.0
310 );
311 println!(
312 "Other: {:>12} ({:>5.1}%)",
313 format_number(total_other),
314 (total_other as f64 / overall_total as f64) * 100.0
315 );
316 println!("{}", "-".repeat(60));
317 println!(
318 "TOTAL: {:>12}",
319 format_number(overall_total)
320 );
321
322 if let Some(_output_path) = output {
324 eprintln!("Note: CSV output not yet implemented");
325 }
326
327 Ok(())
328}