vault_audit_tools/commands/
kv_summary.rs

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