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 crate::utils::format::format_number;
45use anyhow::{Context, Result};
46use std::fs::File;
47use std::io::BufReader;
48
49pub fn run(csv_file: &str) -> Result<()> {
50    let file = File::open(csv_file).context("Failed to open CSV file")?;
51    let reader = BufReader::new(file);
52    let mut csv_reader = csv::Reader::from_reader(reader);
53
54    let mut rows = Vec::new();
55    for result in csv_reader.records() {
56        let record = result?;
57        rows.push(record);
58    }
59
60    if rows.is_empty() {
61        println!("No data found in {}", csv_file);
62        return Ok(());
63    }
64
65    println!("\n{}", "=".repeat(70));
66    println!("{:^70}", "KV Usage Summary Report");
67    println!("{:^70}", format!("Source: {}", csv_file));
68    println!("{}\n", "=".repeat(70));
69
70    // Calculate totals
71    let total_paths = rows.len();
72    let mut total_clients = 0;
73    let mut total_operations = 0;
74
75    // Get headers
76    let headers = csv_reader.headers()?.clone();
77    let unique_clients_idx = headers.iter().position(|h| h == "unique_clients");
78    let operations_idx = headers.iter().position(|h| h == "operations_count");
79
80    for row in &rows {
81        if let Some(idx) = unique_clients_idx {
82            if let Ok(n) = row.get(idx).unwrap_or("0").parse::<usize>() {
83                total_clients += n;
84            }
85        }
86        if let Some(idx) = operations_idx {
87            if let Ok(n) = row.get(idx).unwrap_or("0").parse::<usize>() {
88                total_operations += n;
89            }
90        }
91    }
92
93    println!("Overview:");
94    println!("   • Total KV Paths: {}", total_paths);
95    println!(
96        "   • Total Unique Clients: {}",
97        format_number(total_clients)
98    );
99    println!("   • Total Operations: {}", format_number(total_operations));
100    println!("\n{}\n", "-".repeat(70));
101
102    // Get column indices
103    let kv_path_idx = headers.iter().position(|h| h == "kv_path");
104    let entity_ids_idx = headers.iter().position(|h| h == "entity_ids");
105    let alias_names_idx = headers.iter().position(|h| h == "alias_names");
106    let sample_paths_idx = headers.iter().position(|h| h == "sample_paths_accessed");
107
108    for (i, row) in rows.iter().enumerate() {
109        println!(
110            "{}. KV Path: {}",
111            i + 1,
112            kv_path_idx.and_then(|idx| row.get(idx)).unwrap_or("N/A")
113        );
114
115        if let Some(idx) = unique_clients_idx {
116            println!("   Unique Clients: {}", row.get(idx).unwrap_or("0"));
117        }
118
119        if let Some(idx) = operations_idx {
120            println!("   Total Operations: {}", row.get(idx).unwrap_or("0"));
121        }
122
123        if let Some(idx) = entity_ids_idx {
124            println!("   Entity IDs: {}", row.get(idx).unwrap_or("N/A"));
125        }
126
127        if let Some(idx) = alias_names_idx {
128            if let Some(names) = row.get(idx) {
129                if !names.is_empty() {
130                    println!("   Alias Names: {}", names);
131                }
132            }
133        }
134
135        if let Some(idx) = sample_paths_idx {
136            if let Some(paths) = row.get(idx) {
137                let display_paths = if paths.len() > 80 {
138                    format!("{}...", &paths[..77])
139                } else {
140                    paths.to_string()
141                };
142                println!("   Sample Paths: {}", display_paths);
143            }
144        }
145
146        println!();
147    }
148
149    println!("{}", "-".repeat(70));
150    println!("Report complete. Analyzed {} KV paths.\n", total_paths);
151
152    Ok(())
153}