vault_audit_tools/commands/
token_lookup_abuse.rs1use crate::audit::types::AuditEntry;
51use crate::utils::format::format_number;
52use crate::utils::progress::ProgressBar;
53use crate::utils::reader::open_file;
54use crate::utils::time::parse_timestamp;
55use anyhow::{Context, Result};
56use std::collections::HashMap;
57use std::io::{BufRead, BufReader};
58
59#[derive(Debug)]
61struct TokenData {
62 lookups: usize,
63 first_seen: String,
64 last_seen: String,
65}
66
67impl TokenData {
68 fn new(timestamp: String) -> Self {
69 Self {
70 lookups: 1,
71 first_seen: timestamp.clone(),
72 last_seen: timestamp,
73 }
74 }
75}
76
77fn calculate_time_span_hours(first_seen: &str, last_seen: &str) -> Result<f64> {
78 let first = parse_timestamp(first_seen)
79 .with_context(|| format!("Failed to parse first timestamp: {}", first_seen))?;
80 let last = parse_timestamp(last_seen)
81 .with_context(|| format!("Failed to parse last timestamp: {}", last_seen))?;
82
83 let duration = last.signed_duration_since(first);
84 Ok(duration.num_seconds() as f64 / 3600.0)
85}
86
87pub fn run(log_files: &[String], threshold: usize) -> Result<()> {
88 let mut patterns: HashMap<String, HashMap<String, TokenData>> = HashMap::new();
90 let mut total_lines = 0;
91 let mut lookup_lines = 0;
92
93 for (file_idx, log_file) in log_files.iter().enumerate() {
95 eprintln!(
96 "[{}/{}] Processing: {}",
97 file_idx + 1,
98 log_files.len(),
99 log_file
100 );
101
102 let file_size = std::fs::metadata(log_file).ok().map(|m| m.len() as usize);
104 let mut progress = if let Some(size) = file_size {
105 ProgressBar::new(size, "Processing")
106 } else {
107 ProgressBar::new_spinner("Processing")
108 };
109
110 let file = open_file(log_file)?;
111 let reader = BufReader::new(file);
112
113 let mut file_lines = 0;
114 let mut bytes_read = 0;
115
116 for line in reader.lines() {
117 file_lines += 1;
118 total_lines += 1;
119 let line = line?;
120 bytes_read += line.len() + 1; if file_lines % 10_000 == 0 {
124 if let Some(size) = file_size {
125 progress.update(bytes_read.min(size)); } else {
127 progress.update(file_lines);
128 }
129 }
130
131 let entry: AuditEntry = match serde_json::from_str(&line) {
132 Ok(e) => e,
133 Err(_) => continue,
134 };
135
136 let Some(request) = &entry.request else {
138 continue;
139 };
140
141 let path = match &request.path {
142 Some(p) => p.as_str(),
143 None => continue,
144 };
145
146 if path != "auth/token/lookup-self" {
147 continue;
148 }
149
150 let Some(auth) = &entry.auth else { continue };
151
152 let entity_id = match &auth.entity_id {
153 Some(id) => id.as_str(),
154 None => continue,
155 };
156
157 let accessor = match &auth.accessor {
158 Some(a) => a.clone(),
159 None => continue,
160 };
161
162 lookup_lines += 1;
163
164 let entity_map = patterns.entry(entity_id.to_string()).or_default();
165
166 entity_map
167 .entry(accessor)
168 .and_modify(|data| {
169 data.lookups += 1;
170 data.last_seen.clone_from(&entry.time);
171 })
172 .or_insert_with(|| TokenData::new(entry.time.clone()));
173 }
174
175 if let Some(size) = file_size {
177 progress.update(size);
178 }
179
180 progress.finish_with_message(&format!(
181 "Processed {} lines from this file",
182 format_number(file_lines)
183 ));
184 }
185
186 eprintln!(
187 "\nTotal: Processed {} lines, found {} lookup-self operations",
188 format_number(total_lines),
189 format_number(lookup_lines)
190 );
191
192 let mut excessive_patterns = Vec::new();
194
195 for (entity_id, tokens) in &patterns {
196 for (accessor, data) in tokens {
197 if data.lookups >= threshold {
198 let time_span = calculate_time_span_hours(&data.first_seen, &data.last_seen)
199 .unwrap_or_else(|err| {
200 eprintln!(
201 "Warning: Failed to calculate time span for accessor {}: {}",
202 accessor, err
203 );
204 0.0
205 });
206 let lookups_per_hour = if time_span > 0.0 {
207 data.lookups as f64 / time_span
208 } else {
209 0.0
210 };
211
212 let accessor_display = if accessor.len() > 23 {
214 format!("{}...", &accessor[..20])
215 } else {
216 accessor.clone()
217 };
218
219 excessive_patterns.push((
220 entity_id.clone(),
221 accessor_display,
222 data.lookups,
223 time_span,
224 lookups_per_hour,
225 data.first_seen.clone(),
226 data.last_seen.clone(),
227 ));
228 }
229 }
230 }
231
232 excessive_patterns.sort_by(|a, b| b.2.cmp(&a.2));
234
235 println!("\n{}", "=".repeat(120));
237 println!("Token Lookup Pattern Analysis");
238 println!("{}", "=".repeat(120));
239 println!("\nTotal Entities: {}", format_number(patterns.len()));
240 println!(
241 "Entities with ≥{} lookups on same token: {}",
242 threshold,
243 format_number(excessive_patterns.len())
244 );
245
246 if !excessive_patterns.is_empty() {
247 let top = 20;
248 println!("\nTop {} Entities with Excessive Token Lookups:", top);
249 println!("{}", "-".repeat(120));
250 println!(
251 "{:<40} {:<25} {:>10} {:>12} {:>15}",
252 "Entity ID", "Token Accessor", "Lookups", "Time Span", "Rate"
253 );
254 println!(
255 "{:<40} {:<25} {:>10} {:>12} {:>15}",
256 "", "", "", "(hours)", "(lookups/hr)"
257 );
258 println!("{}", "-".repeat(120));
259
260 for (entity_id, accessor, lookups, time_span, rate, _first, _last) in
261 excessive_patterns.iter().take(top)
262 {
263 println!(
264 "{:<40} {:<25} {:>10} {:>12.1} {:>15.1}",
265 entity_id,
266 accessor,
267 format_number(*lookups),
268 time_span,
269 rate
270 );
271 }
272
273 let total_excessive_lookups: usize = excessive_patterns.iter().map(|p| p.2).sum();
275 let avg_lookups = total_excessive_lookups as f64 / excessive_patterns.len() as f64;
276 let max_lookups = excessive_patterns[0].2;
277
278 println!("\n{}", "-".repeat(120));
279 println!(
280 "Total Excessive Lookups: {}",
281 format_number(total_excessive_lookups)
282 );
283 println!("Average Lookups per Entity: {:.1}", avg_lookups);
284 println!(
285 "Maximum Lookups (single token): {}",
286 format_number(max_lookups)
287 );
288
289 let mut by_rate = excessive_patterns.clone();
291 by_rate.sort_by(|a, b| b.4.partial_cmp(&a.4).unwrap_or(std::cmp::Ordering::Equal));
292
293 if by_rate[0].4 > 0.0 {
294 println!("\nHighest Rate: {:.1} lookups/hour", by_rate[0].4);
295 println!(" Entity: {}", by_rate[0].0);
296 println!(" Lookups: {}", format_number(by_rate[0].2));
297 }
298 }
299
300 println!("{}", "=".repeat(120));
301
302 Ok(())
303}