vault_audit_tools/commands/
kv_summary.rs

1//! KV usage summary from CSV exports.
2//!
3//! Summarizes KV secrets engine usage from CSV files generated by
4//! the `kv-analyzer` command.
5//!
6//! # Usage
7//!
8//! ```bash
9//! # First generate CSV with kv-analyzer
10//! vault-audit kv-analyzer audit.log --output kv-usage.csv
11//!
12//! # Then summarize it
13//! vault-audit kv-summary kv-usage.csv
14//! ```
15//!
16//! # Output
17//!
18//! Displays aggregated statistics:
19//! - Total unique secrets accessed
20//! - Total operations count
21//! - Number of unique entities
22//! - Breakdown by mount point
23//! - Top accessed secrets
24//!
25//! Useful for:
26//! - Understanding overall KV usage patterns
27//! - Identifying most popular secrets
28//! - Planning KV migrations or reorganization
29
30use anyhow::{Context, Result};
31use std::fs::File;
32use std::io::BufReader;
33
34fn format_number(n: usize) -> String {
35    let s = n.to_string();
36    let mut result = String::new();
37    for (i, c) in s.chars().rev().enumerate() {
38        if i > 0 && i % 3 == 0 {
39            result.push(',');
40        }
41        result.push(c);
42    }
43    result.chars().rev().collect()
44}
45
46pub fn run(csv_file: &str) -> Result<()> {
47    let file = File::open(csv_file).context("Failed to open CSV file")?;
48    let reader = BufReader::new(file);
49    let mut csv_reader = csv::Reader::from_reader(reader);
50
51    let mut rows = Vec::new();
52    for result in csv_reader.records() {
53        let record = result?;
54        rows.push(record);
55    }
56
57    if rows.is_empty() {
58        println!("No data found in {}", csv_file);
59        return Ok(());
60    }
61
62    println!("\n{}", "=".repeat(70));
63    println!("{:^70}", "KV Usage Summary Report");
64    println!("{:^70}", format!("Source: {}", csv_file));
65    println!("{}\n", "=".repeat(70));
66
67    // Calculate totals
68    let total_paths = rows.len();
69    let mut total_clients = 0;
70    let mut total_operations = 0;
71
72    // Get headers
73    let headers = csv_reader.headers()?.clone();
74    let unique_clients_idx = headers.iter().position(|h| h == "unique_clients");
75    let operations_idx = headers.iter().position(|h| h == "operations_count");
76
77    for row in &rows {
78        if let Some(idx) = unique_clients_idx {
79            if let Ok(n) = row.get(idx).unwrap_or("0").parse::<usize>() {
80                total_clients += n;
81            }
82        }
83        if let Some(idx) = operations_idx {
84            if let Ok(n) = row.get(idx).unwrap_or("0").parse::<usize>() {
85                total_operations += n;
86            }
87        }
88    }
89
90    println!("Overview:");
91    println!("   • Total KV Paths: {}", total_paths);
92    println!(
93        "   • Total Unique Clients: {}",
94        format_number(total_clients)
95    );
96    println!("   • Total Operations: {}", format_number(total_operations));
97    println!("\n{}\n", "-".repeat(70));
98
99    // Get column indices
100    let kv_path_idx = headers.iter().position(|h| h == "kv_path");
101    let entity_ids_idx = headers.iter().position(|h| h == "entity_ids");
102    let alias_names_idx = headers.iter().position(|h| h == "alias_names");
103    let sample_paths_idx = headers.iter().position(|h| h == "sample_paths_accessed");
104
105    for (i, row) in rows.iter().enumerate() {
106        println!(
107            "{}. KV Path: {}",
108            i + 1,
109            kv_path_idx.and_then(|idx| row.get(idx)).unwrap_or("N/A")
110        );
111
112        if let Some(idx) = unique_clients_idx {
113            println!("   Unique Clients: {}", row.get(idx).unwrap_or("0"));
114        }
115
116        if let Some(idx) = operations_idx {
117            println!("   Total Operations: {}", row.get(idx).unwrap_or("0"));
118        }
119
120        if let Some(idx) = entity_ids_idx {
121            println!("   Entity IDs: {}", row.get(idx).unwrap_or("N/A"));
122        }
123
124        if let Some(idx) = alias_names_idx {
125            if let Some(names) = row.get(idx) {
126                if !names.is_empty() {
127                    println!("   Alias Names: {}", names);
128                }
129            }
130        }
131
132        if let Some(idx) = sample_paths_idx {
133            if let Some(paths) = row.get(idx) {
134                let display_paths = if paths.len() > 80 {
135                    format!("{}...", &paths[..77])
136                } else {
137                    paths.to_string()
138                };
139                println!("   Sample Paths: {}", display_paths);
140            }
141        }
142
143        println!();
144    }
145
146    println!("{}", "-".repeat(70));
147    println!("Report complete. Analyzed {} KV paths.\n", total_paths);
148
149    Ok(())
150}