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::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/// Token operation statistics for a single entity
56#[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    // Process each log file sequentially
73    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        // Get file size for progress tracking
82        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; // +1 for newline
100
101            // Update progress every 10k lines for smooth animation
102            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            // Get entity_id first
116            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            // Filter for token operations OR login operations
125            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            // Categorize operation
149            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            // Capture display name and metadata (first occurrence)
164            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        // Ensure 100% progress for this file
181        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    // Calculate totals per entity
194    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    // Sort by total operations
222    entity_totals.sort_by(|a, b| b.1.cmp(&a.1));
223
224    // Display results
225    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    // Summary by operation type
285    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    // TODO: CSV output if specified
333    if let Some(_output_path) = output {
334        eprintln!("Note: CSV output not yet implemented");
335    }
336
337    Ok(())
338}