vault_audit_tools/commands/
auth_mounts.rs1use anyhow::{Context, Result};
56use serde::{Deserialize, Serialize};
57use serde_json::Value;
58use std::collections::HashMap;
59use std::fs::File;
60use std::io::Write;
61
62use crate::vault_api::VaultClient;
63
64#[derive(Debug, Serialize, Deserialize)]
65struct AuthMountInfo {
66 #[serde(rename = "type")]
67 auth_type: String,
68 #[serde(default)]
69 description: String,
70 #[serde(default)]
71 accessor: String,
72 #[serde(default, deserialize_with = "deserialize_null_default")]
73 config: HashMap<String, Value>,
74 #[serde(default, deserialize_with = "deserialize_null_default")]
75 options: HashMap<String, Value>,
76 #[serde(default)]
77 local: bool,
78 #[serde(default)]
79 seal_wrap: bool,
80}
81
82fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
83where
84 T: Default + Deserialize<'de>,
85 D: serde::Deserializer<'de>,
86{
87 let opt = Option::deserialize(deserializer)?;
88 Ok(opt.unwrap_or_default())
89}
90
91#[derive(Debug, Serialize, Clone)]
92struct RoleEntry {
93 name: String,
94 #[serde(skip_serializing_if = "Vec::is_empty", default)]
95 children: Vec<RoleEntry>,
96}
97
98#[derive(Debug, Serialize)]
99struct AuthMountOutput {
100 path: String,
101 auth_type: String,
102 description: String,
103 accessor: String,
104 local: bool,
105 seal_wrap: bool,
106 default_lease_ttl: String,
107 max_lease_ttl: String,
108 #[serde(skip_serializing_if = "Vec::is_empty", default)]
109 roles: Vec<RoleEntry>,
110}
111
112async fn list_k8s_roles(client: &VaultClient, mount_path: &str) -> Result<Vec<RoleEntry>> {
114 let list_path = format!("/v1/auth/{}/role", mount_path.trim_end_matches('/'));
115
116 match client.list_json(&list_path).await {
117 Ok(response) => {
118 if let Some(keys) = response
119 .get("data")
120 .and_then(|d| d.get("keys"))
121 .and_then(|k| k.as_array())
122 {
123 let mut roles = Vec::new();
124 for key in keys {
125 if let Some(role_name) = key.as_str() {
126 roles.push(RoleEntry {
127 name: role_name.to_string(),
128 children: vec![],
129 });
130 }
131 }
132 Ok(roles)
133 } else {
134 Ok(vec![])
135 }
136 }
137 Err(_) => Ok(vec![]), }
139}
140
141async fn list_approle_roles(client: &VaultClient, mount_path: &str) -> Result<Vec<RoleEntry>> {
143 let list_path = format!("/v1/auth/{}/role", mount_path.trim_end_matches('/'));
144
145 match client.list_json(&list_path).await {
146 Ok(response) => {
147 if let Some(keys) = response
148 .get("data")
149 .and_then(|d| d.get("keys"))
150 .and_then(|k| k.as_array())
151 {
152 let mut roles = Vec::new();
153 for key in keys {
154 if let Some(role_name) = key.as_str() {
155 roles.push(RoleEntry {
156 name: role_name.to_string(),
157 children: vec![],
158 });
159 }
160 }
161 Ok(roles)
162 } else {
163 Ok(vec![])
164 }
165 }
166 Err(_) => Ok(vec![]), }
168}
169
170async fn list_userpass_users(client: &VaultClient, mount_path: &str) -> Result<Vec<RoleEntry>> {
172 let list_path = format!("/v1/auth/{}/users", mount_path.trim_end_matches('/'));
173
174 match client.list_json(&list_path).await {
175 Ok(response) => {
176 if let Some(keys) = response
177 .get("data")
178 .and_then(|d| d.get("keys"))
179 .and_then(|k| k.as_array())
180 {
181 let mut users = Vec::new();
182 for key in keys {
183 if let Some(user_name) = key.as_str() {
184 users.push(RoleEntry {
185 name: user_name.to_string(),
186 children: vec![],
187 });
188 }
189 }
190 Ok(users)
191 } else {
192 Ok(vec![])
193 }
194 }
195 Err(_) => Ok(vec![]), }
197}
198
199async fn list_jwt_roles(client: &VaultClient, mount_path: &str) -> Result<Vec<RoleEntry>> {
201 let list_path = format!("/v1/auth/{}/role", mount_path.trim_end_matches('/'));
202
203 match client.list_json(&list_path).await {
204 Ok(response) => {
205 if let Some(keys) = response
206 .get("data")
207 .and_then(|d| d.get("keys"))
208 .and_then(|k| k.as_array())
209 {
210 let mut roles = Vec::new();
211 for key in keys {
212 if let Some(role_name) = key.as_str() {
213 roles.push(RoleEntry {
214 name: role_name.to_string(),
215 children: vec![],
216 });
217 }
218 }
219 Ok(roles)
220 } else {
221 Ok(vec![])
222 }
223 }
224 Err(_) => Ok(vec![]), }
226}
227
228async fn list_ldap_config(client: &VaultClient, mount_path: &str) -> Result<Vec<RoleEntry>> {
230 let users_path = format!("/v1/auth/{}/users", mount_path.trim_end_matches('/'));
231 let groups_path = format!("/v1/auth/{}/groups", mount_path.trim_end_matches('/'));
232
233 let mut entries = Vec::new();
234
235 if let Ok(response) = client.list_json(&users_path).await {
237 if let Some(keys) = response
238 .get("data")
239 .and_then(|d| d.get("keys"))
240 .and_then(|k| k.as_array())
241 {
242 for key in keys {
243 if let Some(user_name) = key.as_str() {
244 entries.push(RoleEntry {
245 name: format!("user:{}", user_name),
246 children: vec![],
247 });
248 }
249 }
250 }
251 }
252
253 if let Ok(response) = client.list_json(&groups_path).await {
255 if let Some(keys) = response
256 .get("data")
257 .and_then(|d| d.get("keys"))
258 .and_then(|k| k.as_array())
259 {
260 for key in keys {
261 if let Some(group_name) = key.as_str() {
262 entries.push(RoleEntry {
263 name: format!("group:{}", group_name),
264 children: vec![],
265 });
266 }
267 }
268 }
269 }
270
271 Ok(entries)
272}
273
274async fn enumerate_auth_configs(
276 client: &VaultClient,
277 mount_path: &str,
278 auth_type: &str,
279 depth: usize,
280) -> Result<Vec<RoleEntry>> {
281 if depth == 0 {
282 return Ok(vec![]);
283 }
284
285 match auth_type {
286 "kubernetes" => list_k8s_roles(client, mount_path).await,
287 "approle" => list_approle_roles(client, mount_path).await,
288 "userpass" => list_userpass_users(client, mount_path).await,
289 "jwt" | "oidc" => list_jwt_roles(client, mount_path).await,
290 "ldap" => list_ldap_config(client, mount_path).await,
291 _ => Ok(vec![]), }
293}
294
295pub async fn run(
297 vault_addr: Option<&str>,
298 vault_token: Option<&str>,
299 vault_namespace: Option<&str>,
300 insecure: bool,
301 output: Option<&str>,
302 format: &str,
303 depth: usize,
304) -> Result<()> {
305 let client = VaultClient::from_options(vault_addr, vault_token, vault_namespace, insecure)?;
306
307 eprintln!("Querying Vault API for auth mounts...");
308 eprintln!(" Vault Address: {}", client.addr());
309
310 let response: Value = client
312 .get("/v1/sys/auth")
313 .await
314 .context("Failed to query /v1/sys/auth")?;
315
316 let mounts_data = response
318 .get("data")
319 .or(Some(&response)) .context("Failed to get auth mounts data")?;
321
322 let mounts = mounts_data
323 .as_object()
324 .context("Expected object response from /v1/sys/auth")?;
325
326 let mut auth_mounts = Vec::new();
327
328 for (path, mount_data) in mounts {
329 if path == "request_id"
331 || path == "lease_id"
332 || path == "renewable"
333 || path == "lease_duration"
334 || path == "data"
335 || path == "wrap_info"
336 || path == "warnings"
337 || path == "auth"
338 {
339 continue;
340 }
341
342 let mount_info: AuthMountInfo = serde_json::from_value(mount_data.clone())
343 .with_context(|| format!("Failed to parse auth mount info for {}", path))?;
344
345 let default_lease_ttl = mount_info
346 .config
347 .get("default_lease_ttl")
348 .and_then(serde_json::Value::as_i64)
349 .map_or_else(|| "0s".to_string(), |v| format!("{}s", v));
350
351 let max_lease_ttl = mount_info
352 .config
353 .get("max_lease_ttl")
354 .and_then(serde_json::Value::as_i64)
355 .map_or_else(|| "0s".to_string(), |v| format!("{}s", v));
356
357 let roles = enumerate_auth_configs(&client, path, &mount_info.auth_type, depth)
359 .await
360 .unwrap_or_else(|_| vec![]);
361
362 auth_mounts.push(AuthMountOutput {
363 path: path.clone(),
364 auth_type: mount_info.auth_type.clone(),
365 description: mount_info.description.clone(),
366 accessor: mount_info.accessor.clone(),
367 local: mount_info.local,
368 seal_wrap: mount_info.seal_wrap,
369 default_lease_ttl,
370 max_lease_ttl,
371 roles,
372 });
373 }
374
375 eprintln!("Found {} auth mounts", auth_mounts.len());
376
377 match format {
379 "json" => {
380 let json_output = serde_json::to_string_pretty(&auth_mounts)
381 .context("Failed to serialize to JSON")?;
382
383 if let Some(output_path) = output {
384 let mut file = File::create(output_path).context("Failed to create output file")?;
385 file.write_all(json_output.as_bytes())
386 .context("Failed to write JSON to file")?;
387 eprintln!("Output written to: {}", output_path);
388 } else {
389 println!("{}", json_output);
390 }
391 }
392 "csv" => {
393 use std::fmt::Write as _;
394 let mut csv_output = String::new();
395 csv_output.push_str("path,type,description,accessor,role_name,depth\n");
396
397 for mount in &auth_mounts {
398 let _ = writeln!(
400 csv_output,
401 "\"{}\",\"{}\",\"{}\",\"{}\",\"\",0",
402 mount.path.replace('"', "\"\""),
403 mount.auth_type,
404 mount.description.replace('"', "\"\""),
405 mount.accessor,
406 );
407
408 for role in &mount.roles {
410 let _ = writeln!(
411 csv_output,
412 "\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",1",
413 mount.path.replace('"', "\"\""),
414 mount.auth_type,
415 mount.description.replace('"', "\"\""),
416 mount.accessor,
417 role.name.replace('"', "\"\""),
418 );
419 }
420 }
421
422 if let Some(output_path) = output {
423 let mut file = File::create(output_path).context("Failed to create output file")?;
424 file.write_all(csv_output.as_bytes())
425 .context("Failed to write CSV to file")?;
426 eprintln!("Output written to: {}", output_path);
427 } else {
428 print!("{}", csv_output);
429 }
430 }
431 "stdout" => {
432 println!("\nAuth Mounts:");
433 println!("{}", "=".repeat(80));
434 for mount in &auth_mounts {
435 println!("Path: {}", mount.path);
436 println!(" Type: {}", mount.auth_type);
437 println!(" Description: {}", mount.description);
438 println!(" Accessor: {}", mount.accessor);
439 println!(" Local: {}", mount.local);
440 println!(" Seal Wrap: {}", mount.seal_wrap);
441 println!(" Default Lease TTL: {}", mount.default_lease_ttl);
442 println!(" Max Lease TTL: {}", mount.max_lease_ttl);
443
444 if !mount.roles.is_empty() {
445 println!(" Roles/Users ({}):", mount.roles.len());
446 for (i, role) in mount.roles.iter().enumerate() {
447 let prefix = if i == mount.roles.len() - 1 {
448 "└──"
449 } else {
450 "├──"
451 };
452 println!(" {} {}", prefix, role.name);
453 }
454 }
455 println!();
456 }
457 }
458 _ => {
459 return Err(anyhow::anyhow!(
460 "Invalid format: {}. Must be one of: csv, json, stdout",
461 format
462 ));
463 }
464 }
465
466 Ok(())
467}