vault_audit_tools/commands/
entity_gaps.rs

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