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