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