vault_audit_tools/commands/
entity_list.rs1use crate::vault_api::{extract_data, should_skip_verify, VaultClient};
36use anyhow::{Context, Result};
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39use std::fs::File;
40
41#[derive(Debug, Deserialize)]
43struct AuthMount {
44 #[serde(rename = "type")]
45 mount_type: Option<String>,
46 accessor: Option<String>,
47}
48
49#[derive(Debug, Deserialize)]
51struct EntityListResponse {
52 keys: Vec<String>,
53}
54
55#[derive(Debug, Deserialize)]
57struct EntityData {
58 id: String,
59 name: Option<String>,
60 disabled: bool,
61 creation_time: Option<String>,
62 last_update_time: Option<String>,
63 aliases: Option<Vec<AliasData>>,
64}
65
66#[derive(Debug, Deserialize, Clone)]
67struct AliasData {
68 id: String,
69 name: String,
70 mount_accessor: String,
71 creation_time: Option<String>,
72 last_update_time: Option<String>,
73 metadata: Option<HashMap<String, String>>,
74}
75
76#[derive(Debug, Serialize)]
77struct EntityOutput {
78 entity_id: String,
79 entity_name: String,
80 entity_disabled: bool,
81 entity_created: String,
82 entity_updated: String,
83 alias_id: String,
84 alias_name: String,
85 mount_path: String,
86 mount_type: String,
87 mount_accessor: String,
88 alias_created: String,
89 alias_updated: String,
90 alias_metadata: String,
91}
92
93pub fn format_number(n: usize) -> String {
94 let s = n.to_string();
95 let mut result = String::new();
96 for (i, c) in s.chars().rev().enumerate() {
97 if i > 0 && i % 3 == 0 {
98 result.push(',');
99 }
100 result.push(c);
101 }
102 result.chars().rev().collect()
103}
104
105pub async fn run(
106 vault_addr: Option<&str>,
107 vault_token: Option<&str>,
108 insecure: bool,
109 output: Option<&str>,
110 format: &str,
111 filter_mount: Option<&str>,
112) -> Result<()> {
113 let skip_verify = should_skip_verify(insecure);
114 let client = VaultClient::from_options(vault_addr, vault_token, skip_verify)?;
115
116 eprintln!("=== Vault Entity Analysis ===");
117 eprintln!("Vault Address: {}", client.addr());
118 if let Some(mount) = filter_mount {
119 eprintln!("Filtering by mount: {}", mount);
120 }
121 if skip_verify {
122 eprintln!("⚠️ TLS certificate verification is DISABLED");
123 }
124 eprintln!();
125
126 eprintln!("Building mount map...");
128 let mount_map = fetch_auth_mount_map(&client).await?;
129 eprintln!("Found {} auth mounts", mount_map.len());
130
131 eprintln!("Fetching entity list...");
133 let entity_list: EntityListResponse =
134 extract_data(client.get_json("/v1/identity/entity/id?list=true").await?)?;
135
136 let entity_count = entity_list.keys.len();
137 eprintln!("Found {} entities", format_number(entity_count));
138 eprintln!();
139
140 eprintln!("Fetching entity details...");
142 let mut entities_data = Vec::new();
143 let mut processed = 0;
144
145 for entity_id in &entity_list.keys {
146 processed += 1;
147 if processed % 100 == 0 || processed == entity_count {
148 eprint!("\rProcessing entity {}/{}...", processed, entity_count);
149 }
150
151 let entity_path = format!("/v1/identity/entity/id/{}", entity_id);
152 if let Ok(entity_json) = client.get_json(&entity_path).await {
153 if let Ok(entity) = extract_data::<EntityData>(entity_json) {
154 entities_data.push(entity);
155 }
156 }
157 }
158 eprintln!("\n");
159
160 let mut output_rows = Vec::new();
162
163 for entity in &entities_data {
164 let entity_name = entity.name.clone().unwrap_or_default();
165 let entity_created = entity.creation_time.clone().unwrap_or_default();
166 let entity_updated = entity.last_update_time.clone().unwrap_or_default();
167
168 if let Some(aliases) = &entity.aliases {
169 let mut filtered_aliases: Vec<&AliasData> = aliases.iter().collect();
170
171 if let Some(filter) = filter_mount {
173 filtered_aliases.retain(|alias| {
174 if let Some((path, _)) = mount_map.get(&alias.mount_accessor) {
175 path == filter
176 } else {
177 false
178 }
179 });
180 }
181
182 if filtered_aliases.is_empty() && filter_mount.is_some() {
183 continue; }
185
186 for alias in filtered_aliases {
187 let (mount_path, mount_type) = mount_map
188 .get(&alias.mount_accessor)
189 .cloned()
190 .unwrap_or_else(|| ("unknown".to_string(), "unknown".to_string()));
191
192 let metadata_str = alias
193 .metadata
194 .as_ref()
195 .map(|m| {
196 m.iter()
197 .map(|(k, v)| format!("{}={}", k, v))
198 .collect::<Vec<_>>()
199 .join("; ")
200 })
201 .unwrap_or_default();
202
203 output_rows.push(EntityOutput {
204 entity_id: entity.id.clone(),
205 entity_name: entity_name.clone(),
206 entity_disabled: entity.disabled,
207 entity_created: entity_created.clone(),
208 entity_updated: entity_updated.clone(),
209 alias_id: alias.id.clone(),
210 alias_name: alias.name.clone(),
211 mount_path,
212 mount_type,
213 mount_accessor: alias.mount_accessor.clone(),
214 alias_created: alias.creation_time.clone().unwrap_or_default(),
215 alias_updated: alias.last_update_time.clone().unwrap_or_default(),
216 alias_metadata: metadata_str,
217 });
218 }
219 } else if filter_mount.is_none() {
220 output_rows.push(EntityOutput {
222 entity_id: entity.id.clone(),
223 entity_name,
224 entity_disabled: entity.disabled,
225 entity_created,
226 entity_updated,
227 alias_id: String::new(),
228 alias_name: String::new(),
229 mount_path: String::new(),
230 mount_type: String::new(),
231 mount_accessor: String::new(),
232 alias_created: String::new(),
233 alias_updated: String::new(),
234 alias_metadata: String::new(),
235 });
236 }
237 }
238
239 eprintln!("=== Summary ===");
241 eprintln!("Total entities: {}", format_number(entities_data.len()));
242 eprintln!("Total aliases: {}", format_number(output_rows.len()));
243 eprintln!();
244
245 let mut mount_counts: HashMap<String, usize> = HashMap::new();
247 for row in &output_rows {
248 if !row.mount_path.is_empty() {
249 *mount_counts.entry(row.mount_path.clone()).or_insert(0) += 1;
250 }
251 }
252
253 if !mount_counts.is_empty() {
254 eprintln!("Aliases by mount:");
255 let mut counts: Vec<_> = mount_counts.into_iter().collect();
256 counts.sort_by(|a, b| b.1.cmp(&a.1));
257 for (mount, count) in counts {
258 eprintln!(" {}: {}", mount, format_number(count));
259 }
260 eprintln!();
261 }
262
263 if let Some(output_path) = output {
265 let file = File::create(output_path)
266 .with_context(|| format!("Failed to create output file: {}", output_path))?;
267
268 match format.to_lowercase().as_str() {
269 "json" => {
270 serde_json::to_writer_pretty(file, &output_rows)
271 .with_context(|| format!("Failed to write JSON to: {}", output_path))?;
272 eprintln!("JSON written to: {}", output_path);
273 }
274 "csv" => {
275 let mut writer = csv::Writer::from_writer(file);
276
277 writer.write_record([
278 "entity_id",
279 "entity_name",
280 "entity_disabled",
281 "entity_created",
282 "entity_updated",
283 "alias_id",
284 "alias_name",
285 "mount_path",
286 "mount_type",
287 "mount_accessor",
288 "alias_created",
289 "alias_updated",
290 "alias_metadata",
291 ])?;
292
293 for row in &output_rows {
294 writer.write_record([
295 &row.entity_id,
296 &row.entity_name,
297 &row.entity_disabled.to_string(),
298 &row.entity_created,
299 &row.entity_updated,
300 &row.alias_id,
301 &row.alias_name,
302 &row.mount_path,
303 &row.mount_type,
304 &row.mount_accessor,
305 &row.alias_created,
306 &row.alias_updated,
307 &row.alias_metadata,
308 ])?;
309 }
310
311 writer.flush()?;
312 eprintln!("CSV written to: {}", output_path);
313 }
314 _ => {
315 anyhow::bail!("Invalid format '{}'. Use 'csv' or 'json'", format);
316 }
317 }
318 } else {
319 match format.to_lowercase().as_str() {
321 "json" => {
322 println!("{}", serde_json::to_string_pretty(&output_rows)?);
323 }
324 "csv" => {
325 let mut writer = csv::Writer::from_writer(std::io::stdout());
326 writer.write_record([
327 "entity_id",
328 "entity_name",
329 "entity_disabled",
330 "entity_created",
331 "entity_updated",
332 "alias_id",
333 "alias_name",
334 "mount_path",
335 "mount_type",
336 "mount_accessor",
337 "alias_created",
338 "alias_updated",
339 "alias_metadata",
340 ])?;
341
342 for row in &output_rows {
343 writer.write_record([
344 &row.entity_id,
345 &row.entity_name,
346 &row.entity_disabled.to_string(),
347 &row.entity_created,
348 &row.entity_updated,
349 &row.alias_id,
350 &row.alias_name,
351 &row.mount_path,
352 &row.mount_type,
353 &row.mount_accessor,
354 &row.alias_created,
355 &row.alias_updated,
356 &row.alias_metadata,
357 ])?;
358 }
359
360 writer.flush()?;
361 }
362 _ => {
363 anyhow::bail!("Invalid format '{}'. Use 'csv' or 'json'", format);
364 }
365 }
366 }
367
368 Ok(())
369}
370
371async fn fetch_auth_mount_map(client: &VaultClient) -> Result<HashMap<String, (String, String)>> {
372 let mut map = HashMap::new();
373
374 if let Ok(auth_data) = client.get_json("/v1/sys/auth").await {
375 if let Ok(auths) = extract_data::<HashMap<String, AuthMount>>(auth_data) {
376 for (path, info) in auths {
377 if let Some(accessor) = info.accessor {
378 map.insert(
379 accessor,
380 (
381 path,
382 info.mount_type.unwrap_or_else(|| "unknown".to_string()),
383 ),
384 );
385 }
386 }
387 }
388 }
389
390 Ok(map)
391}