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