vault_audit_tools/commands/
entity_gaps.rs

1//! Entity gaps analysis command.
2//!
3//! ⚠️ **DEPRECATED**: Use `entity-analysis gaps` instead.
4//!
5//! ```bash
6//! # Old (deprecated):
7//! vault-audit entity-gaps logs/*.log
8//!
9//! # New (recommended):
10//! vault-audit entity-analysis gaps logs/*.log
11//! ```
12//!
13//! See [`entity_analysis`](crate::commands::entity_analysis) for the unified command.
14//!
15//! ---
16//!
17//! Identifies operations that occur without an associated entity ID,
18//! which can indicate unauthenticated requests or system operations.
19//! Supports multi-file analysis for comprehensive coverage.
20//!
21//! # Usage
22//!
23//! ```bash
24//! # Single file
25//! vault-audit entity-gaps audit.log
26//!
27//! # Multi-day analysis
28//! vault-audit entity-gaps logs/vault_audit.*.log
29//! ```
30//!
31//! # Output
32//!
33//! Displays operations grouped by path that have no entity ID:
34//! - Request path
35//! - Total operations count
36//! - Common operations (read, write, list, etc.)
37//!
38//! Helps identify:
39//! - Public endpoints (health checks, metrics)
40//! - System operations
41//! - Potential authentication issues
42//! - Unauthenticated access patterns
43
44use crate::audit::types::AuditEntry;
45use crate::utils::progress::ProgressBar;
46use crate::utils::reader::open_file;
47use anyhow::Result;
48use std::collections::HashMap;
49use std::io::{BufRead, BufReader};
50
51fn format_number(n: usize) -> String {
52    let s = n.to_string();
53    let mut result = String::new();
54    for (i, c) in s.chars().rev().enumerate() {
55        if i > 0 && i % 3 == 0 {
56            result.push(',');
57        }
58        result.push(c);
59    }
60    result.chars().rev().collect()
61}
62
63pub fn run(log_files: &[String], _window_seconds: u64) -> Result<()> {
64    let mut operations_by_type: HashMap<String, usize> = HashMap::new();
65    let mut paths_accessed: HashMap<String, usize> = HashMap::new();
66    let mut total_lines = 0;
67    let mut no_entity_operations = 0;
68
69    // Process each log file sequentially
70    for (file_idx, log_file) in log_files.iter().enumerate() {
71        eprintln!(
72            "[{}/{}] Processing: {}",
73            file_idx + 1,
74            log_files.len(),
75            log_file
76        );
77
78        // Get file size for progress tracking
79        let file_size = std::fs::metadata(log_file).ok().map(|m| m.len() as usize);
80        let mut progress = if let Some(size) = file_size {
81            ProgressBar::new(size, "Processing")
82        } else {
83            ProgressBar::new_spinner("Processing")
84        };
85
86        let file = open_file(log_file)?;
87        let reader = BufReader::new(file);
88
89        let mut file_lines = 0;
90        let mut bytes_read = 0;
91
92        for line in reader.lines() {
93            file_lines += 1;
94            total_lines += 1;
95            let line = line?;
96            bytes_read += line.len() + 1; // +1 for newline
97
98            // Update progress every 10k lines for smooth animation
99            if file_lines % 10_000 == 0 {
100                if let Some(size) = file_size {
101                    progress.update(bytes_read.min(size));
102                } else {
103                    progress.update(file_lines);
104                }
105            }
106
107            let entry: AuditEntry = match serde_json::from_str(&line) {
108                Ok(e) => e,
109                Err(_) => continue,
110            };
111
112            // Check for no entity
113            if entry.entity_id().is_some() {
114                continue;
115            }
116
117            no_entity_operations += 1;
118
119            // Track data
120            if let Some(op) = entry.operation() {
121                *operations_by_type.entry(op.to_string()).or_insert(0) += 1;
122            }
123
124            if let Some(path) = entry.path() {
125                *paths_accessed.entry(path.to_string()).or_insert(0) += 1;
126            }
127        }
128
129        progress.finish_with_message(&format!(
130            "Processed {} lines from this file",
131            format_number(file_lines)
132        ));
133    }
134
135    eprintln!("\nTotal: Processed {} lines", format_number(total_lines));
136    eprintln!(
137        "Found {} operations with no entity ID",
138        format_number(no_entity_operations)
139    );
140
141    if no_entity_operations == 0 {
142        println!("\nNo operations without entity ID found!");
143        return Ok(());
144    }
145
146    println!("\n{}", "=".repeat(100));
147    println!("NO-ENTITY OPERATIONS ANALYSIS");
148    println!("{}", "=".repeat(100));
149
150    println!("\n1. SUMMARY");
151    println!("{}", "-".repeat(100));
152    println!(
153        "Total no-entity operations: {}",
154        format_number(no_entity_operations)
155    );
156    println!(
157        "Percentage of all operations: {:.2}%",
158        (no_entity_operations as f64 / total_lines as f64) * 100.0
159    );
160
161    println!("\n2. OPERATION TYPE DISTRIBUTION");
162    println!("{}", "-".repeat(100));
163    println!("{:<30} {:<15} {:<15}", "Operation", "Count", "Percentage");
164    println!("{}", "-".repeat(100));
165
166    let mut sorted_ops: Vec<_> = operations_by_type.iter().collect();
167    sorted_ops.sort_by(|a, b| b.1.cmp(a.1));
168
169    for (op, count) in sorted_ops.iter().take(20) {
170        let percentage = (**count as f64 / no_entity_operations as f64) * 100.0;
171        println!(
172            "{:<30} {:<15} {:<15.2}%",
173            op,
174            format_number(**count),
175            percentage
176        );
177    }
178
179    println!("\n3. TOP 30 PATHS ACCESSED");
180    println!("{}", "-".repeat(100));
181    println!("{:<70} {:>15} {:>15}", "Path", "Count", "% of No-Entity");
182    println!("{}", "-".repeat(100));
183
184    let mut sorted_paths: Vec<_> = paths_accessed.iter().collect();
185    sorted_paths.sort_by(|a, b| b.1.cmp(a.1));
186
187    for (path, count) in sorted_paths.iter().take(30) {
188        let percentage = (**count as f64 / no_entity_operations as f64) * 100.0;
189        let display_path = if path.len() > 68 {
190            format!("{}...", &path[..65])
191        } else {
192            path.to_string()
193        };
194        println!(
195            "{:<70} {:>15} {:>14.2}%",
196            display_path,
197            format_number(**count),
198            percentage
199        );
200    }
201
202    println!("\n{}", "=".repeat(100));
203
204    Ok(())
205}