vault_audit_tools/commands/
entity_gaps.rs1use 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 for (op, count) in other.operations_by_type {
69 *self.operations_by_type.entry(op).or_insert(0) += count;
70 }
71
72 for (path, count) in other.paths_accessed {
74 *self.paths_accessed.entry(path).or_insert(0) += count;
75 }
76
77 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 if entry.entity_id().is_some() {
95 return;
96 }
97
98 state.no_entity_operations += 1;
99
100 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}