vault_audit_tools/commands/
k8s_auth.rs

1//! Kubernetes authentication analysis command.
2//!
3//! Analyzes Kubernetes auth method usage to understand service account
4//! access patterns and identify high-volume K8s clients.
5//! Supports multi-file analysis for tracking over time.
6//!
7//! # Usage
8//!
9//! ```bash
10//! # Single file analysis
11//! vault-audit k8s-auth audit.log
12//!
13//! # Multi-day analysis with CSV export
14//! vault-audit k8s-auth logs/*.log --output k8s-usage.csv
15//! ```
16//!
17//! # Output
18//!
19//! Displays or exports Kubernetes authentication statistics:
20//! - Service account name
21//! - Namespace
22//! - Pod name (if available)
23//! - Authentication count
24//! - Associated entity ID
25//!
26//! Helps identify:
27//! - Most active K8s service accounts
28//! - Service accounts with excessive auth requests
29//! - K8s authentication patterns by namespace
30//! - Pods making frequent Vault requests
31
32use crate::audit::types::AuditEntry;
33use crate::utils::progress::ProgressBar;
34use anyhow::Result;
35use std::collections::HashMap;
36use std::fs::File;
37use std::io::{BufRead, BufReader};
38
39fn format_number(n: usize) -> String {
40    let s = n.to_string();
41    let mut result = String::new();
42    for (i, c) in s.chars().rev().enumerate() {
43        if i > 0 && i % 3 == 0 {
44            result.push(',');
45        }
46        result.push(c);
47    }
48    result.chars().rev().collect()
49}
50
51pub fn run(log_files: &[String], output: Option<&str>) -> Result<()> {
52    let mut k8s_logins = 0;
53    let mut entities_seen: HashMap<String, usize> = HashMap::new();
54    let mut total_lines = 0;
55
56    // Process each log file sequentially
57    for (file_idx, log_file) in log_files.iter().enumerate() {
58        eprintln!(
59            "[{}/{}] Processing: {}",
60            file_idx + 1,
61            log_files.len(),
62            log_file
63        );
64
65        // Get file size for progress tracking
66        let file_size = std::fs::metadata(log_file).ok().map(|m| m.len() as usize);
67        let mut progress = if let Some(size) = file_size {
68            ProgressBar::new(size, "Processing")
69        } else {
70            ProgressBar::new_spinner("Processing")
71        };
72
73        let file = File::open(log_file)?;
74        let reader = BufReader::new(file);
75
76        let mut file_lines = 0;
77        let mut bytes_read = 0;
78
79        for line in reader.lines() {
80            file_lines += 1;
81            total_lines += 1;
82            let line = line?;
83            bytes_read += line.len() + 1; // +1 for newline
84
85            // Update progress every 10k lines for smooth animation
86            if file_lines % 10_000 == 0 {
87                if let Some(size) = file_size {
88                    progress.update(bytes_read.min(size)); // Cap at file size
89                } else {
90                    progress.update(file_lines);
91                }
92            }
93
94            let entry: AuditEntry = match serde_json::from_str(&line) {
95                Ok(e) => e,
96                Err(_) => continue,
97            };
98
99            // Filter for successful Kubernetes auth operations (response type, no error)
100            if entry.entry_type != "response" || entry.error.is_some() {
101                continue;
102            }
103
104            let request = match &entry.request {
105                Some(r) => r,
106                None => continue,
107            };
108
109            let path = match &request.path {
110                Some(p) => p.as_str(),
111                None => continue,
112            };
113
114            if !path.ends_with("/login") {
115                continue;
116            }
117
118            // Check if it's a K8s/OpenShift login by path OR mount_type
119            let is_k8s_by_path = path.contains("kubernetes") || path.contains("openshift");
120            let is_k8s_by_mount = request
121                .mount_type
122                .as_deref()
123                .map(|mt| mt == "kubernetes" || mt == "openshift")
124                .unwrap_or(false);
125
126            if is_k8s_by_path || is_k8s_by_mount {
127                k8s_logins += 1;
128
129                if let Some(entity_id) = entry.auth.as_ref().and_then(|a| a.entity_id.as_deref()) {
130                    *entities_seen.entry(entity_id.to_string()).or_insert(0) += 1;
131                }
132            }
133        }
134
135        // Ensure 100% progress for this file
136        if let Some(size) = file_size {
137            progress.update(size);
138        }
139
140        progress.finish_with_message(&format!(
141            "Processed {} lines from this file",
142            format_number(file_lines)
143        ));
144    }
145
146    eprintln!(
147        "\nTotal: Processed {} lines, found {} K8s logins",
148        format_number(total_lines),
149        format_number(k8s_logins)
150    );
151
152    println!("\n{}", "=".repeat(80));
153    println!("KUBERNETES/OPENSHIFT AUTHENTICATION ANALYSIS");
154    println!("{}", "=".repeat(80));
155
156    println!("\nSummary:");
157    println!("  Total lines processed: {}", format_number(total_lines));
158    println!(
159        "  Total K8s/OpenShift logins: {}",
160        format_number(k8s_logins)
161    );
162    println!("  Unique entities: {}", format_number(entities_seen.len()));
163
164    if k8s_logins > 0 {
165        let ratio = k8s_logins as f64 / entities_seen.len() as f64;
166        println!("  Login-to-Entity ratio: {:.2}", ratio);
167
168        println!("\nTop 20 Entities by Login Count:");
169        println!("{}", "-".repeat(80));
170
171        let mut sorted: Vec<_> = entities_seen.iter().collect();
172        sorted.sort_by(|a, b| b.1.cmp(a.1));
173
174        for (i, (entity, count)) in sorted.iter().take(20).enumerate() {
175            println!("{}. {} - {} logins", i + 1, entity, format_number(**count));
176        }
177    }
178
179    if let Some(output_file) = output {
180        use std::fs::File;
181        use std::io::Write;
182        let mut file = File::create(output_file)?;
183        writeln!(file, "entity_id,login_count")?;
184        for (entity, count) in &entities_seen {
185            writeln!(file, "{},{}", entity, count)?;
186        }
187        println!("\nOutput written to: {}", output_file);
188    }
189
190    println!("\n{}", "=".repeat(80));
191
192    Ok(())
193}