vault_audit_tools/commands/
token_operations.rs

1//! Token lifecycle operations analysis.
2//!
3//! **⚠️ DEPRECATED**: Use `token-analysis` instead.
4//!
5//! This command has been consolidated into the unified `token-analysis` command
6//! which provides the same functionality plus abuse detection and CSV export.
7//!
8//! ```bash
9//! # Old command (deprecated)
10//! vault-audit token-operations vault_audit.log
11//!
12//! # New command (recommended)
13//! vault-audit token-analysis vault_audit.log
14//! ```
15//!
16//! See [`token_analysis`](crate::commands::token_analysis) module for full documentation.
17//!
18//! ---
19//!
20//! Tracks token-related operations to understand token usage patterns
21//! and identify entities performing high volumes of token operations.
22//! Supports multi-file analysis for long-term trending.
23//!
24//! # Usage
25//!
26//! ```bash
27//! # Single file analysis
28//! vault-audit token-operations vault_audit.log
29//!
30//! # Week-long analysis
31//! vault-audit token-operations logs/vault_audit.2025-10-*.log
32//! ```
33//!
34//! # Output
35//!
36//! Displays a summary table showing per-entity token operations:
37//! - **lookup-self**: Token self-inspection operations
38//! - **renew-self**: Token renewal operations
39//! - **revoke-self**: Token revocation operations
40//! - **create**: New token creation
41//! - **login**: Authentication operations that create tokens
42//! - **other**: Other token-related operations
43//!
44//! Results are sorted by total operations (descending) to identify
45//! the most active entities.
46
47use 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/// Token operation statistics for a single entity
55#[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    // Process each log file sequentially
84    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        // Get file size for progress tracking
93        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; // +1 for newline
111
112            // Update progress every 10k lines for smooth animation
113            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            // Get entity_id first
127            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            // Filter for token operations OR login operations
136            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            // Categorize operation
160            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            // Capture display name and metadata (first occurrence)
175            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        // Ensure 100% progress for this file
192        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    // Calculate totals per entity
205    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    // Sort by total operations
233    entity_totals.sort_by(|a, b| b.1.cmp(&a.1));
234
235    // Display results
236    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    // Summary by operation type
296    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    // TODO: CSV output if specified
344    if let Some(_output_path) = output {
345        eprintln!("Note: CSV output not yet implemented");
346    }
347
348    Ok(())
349}