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        // Count lines in file first for accurate progress tracking
82        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            // Update progress every 10k lines for smooth animation
98            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            // Get entity_id first
108            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            // Filter for token operations OR login operations
117            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            // Categorize operation
141            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            // Capture display name and metadata (first occurrence)
156            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        // Ensure 100% progress for this file
173        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    // Calculate totals per entity
184    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    // Sort by total operations
212    entity_totals.sort_by(|a, b| b.1.cmp(&a.1));
213
214    // Display results
215    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    // Summary by operation type
275    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    // TODO: CSV output if specified
323    if let Some(_output_path) = output {
324        eprintln!("Note: CSV output not yet implemented");
325    }
326
327    Ok(())
328}