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::format::format_number;
46use crate::utils::processor::{ProcessingMode, ProcessorBuilder};
47use anyhow::Result;
48use std::collections::HashMap;
49
50#[derive(Debug, Clone)]
51struct GapsState {
52    operations_by_type: HashMap<String, usize>,
53    paths_accessed: HashMap<String, usize>,
54    no_entity_operations: usize,
55}
56
57impl GapsState {
58    fn new() -> Self {
59        Self {
60            operations_by_type: HashMap::new(),
61            paths_accessed: HashMap::new(),
62            no_entity_operations: 0,
63        }
64    }
65
66    fn merge(mut self, other: Self) -> Self {
67        // Merge operations_by_type
68        for (op, count) in other.operations_by_type {
69            *self.operations_by_type.entry(op).or_insert(0) += count;
70        }
71
72        // Merge paths_accessed
73        for (path, count) in other.paths_accessed {
74            *self.paths_accessed.entry(path).or_insert(0) += count;
75        }
76
77        // Merge counters
78        self.no_entity_operations += other.no_entity_operations;
79
80        self
81    }
82}
83
84pub fn run(log_files: &[String], _window_seconds: u64) -> Result<()> {
85    let processor = ProcessorBuilder::new()
86        .mode(ProcessingMode::Auto)
87        .progress_label("Processing".to_string())
88        .build();
89
90    let (result, stats) = processor.process_files_streaming(
91        log_files,
92        |entry: &AuditEntry, state: &mut GapsState| {
93            // Check for no entity
94            if entry.entity_id().is_some() {
95                return;
96            }
97
98            state.no_entity_operations += 1;
99
100            // Track data
101            if let Some(op) = entry.operation() {
102                *state.operations_by_type.entry(op.to_string()).or_insert(0) += 1;
103            }
104
105            if let Some(path) = entry.path() {
106                *state.paths_accessed.entry(path.to_string()).or_insert(0) += 1;
107            }
108        },
109        GapsState::merge,
110        GapsState::new(),
111    )?;
112
113    let total_lines = stats.total_lines;
114    let no_entity_operations = result.no_entity_operations;
115    let operations_by_type = result.operations_by_type;
116    let paths_accessed = result.paths_accessed;
117
118    eprintln!("\nTotal: Processed {} lines", format_number(total_lines));
119    eprintln!(
120        "Found {} operations with no entity ID",
121        format_number(no_entity_operations)
122    );
123
124    if no_entity_operations == 0 {
125        println!("\nNo operations without entity ID found!");
126        return Ok(());
127    }
128
129    println!("\n{}", "=".repeat(100));
130    println!("NO-ENTITY OPERATIONS ANALYSIS");
131    println!("{}", "=".repeat(100));
132
133    println!("\n1. SUMMARY");
134    println!("{}", "-".repeat(100));
135    println!(
136        "Total no-entity operations: {}",
137        format_number(no_entity_operations)
138    );
139    println!(
140        "Percentage of all operations: {:.2}%",
141        (no_entity_operations as f64 / total_lines as f64) * 100.0
142    );
143
144    println!("\n2. OPERATION TYPE DISTRIBUTION");
145    println!("{}", "-".repeat(100));
146    println!("{:<30} {:<15} {:<15}", "Operation", "Count", "Percentage");
147    println!("{}", "-".repeat(100));
148
149    let mut sorted_ops: Vec<_> = operations_by_type.iter().collect();
150    sorted_ops.sort_by(|a, b| b.1.cmp(a.1));
151
152    for (op, count) in sorted_ops.iter().take(20) {
153        let percentage = (**count as f64 / no_entity_operations as f64) * 100.0;
154        println!(
155            "{:<30} {:<15} {:<15.2}%",
156            op,
157            format_number(**count),
158            percentage
159        );
160    }
161
162    println!("\n3. TOP 30 PATHS ACCESSED");
163    println!("{}", "-".repeat(100));
164    println!("{:<70} {:>15} {:>15}", "Path", "Count", "% of No-Entity");
165    println!("{}", "-".repeat(100));
166
167    let mut sorted_paths: Vec<_> = paths_accessed.iter().collect();
168    sorted_paths.sort_by(|a, b| b.1.cmp(a.1));
169
170    for (path, count) in sorted_paths.iter().take(30) {
171        let percentage = (**count as f64 / no_entity_operations as f64) * 100.0;
172        let display_path = if path.len() > 68 {
173            format!("{}...", &path[..65])
174        } else {
175            (*path).to_string()
176        };
177        println!(
178            "{:<70} {:>15} {:>14.2}%",
179            display_path,
180            format_number(**count),
181            percentage
182        );
183    }
184
185    println!("\n{}", "=".repeat(100));
186
187    Ok(())
188}