vtcode_safety/command_safety/
audit.rs1use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use tokio::sync::Mutex;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AuditEntry {
13 command: Vec<String>,
15 pub(crate) allowed: bool,
17 reason: String,
19 pub(crate) decision_type: String,
21 timestamp: String,
23}
24
25impl AuditEntry {
26 pub(crate) fn new(command: Vec<String>, allowed: bool, reason: String, decision_type: String) -> Self {
28 let timestamp = std::time::SystemTime::now()
30 .duration_since(std::time::UNIX_EPOCH)
31 .map(|d| d.as_secs().to_string())
32 .unwrap_or_else(|_| "unknown".to_string());
33
34 Self { command, allowed, reason, decision_type, timestamp }
35 }
36}
37
38pub struct SafetyAuditLogger {
40 entries: Arc<Mutex<Vec<AuditEntry>>>,
41 enabled: bool,
42}
43
44impl SafetyAuditLogger {
45 pub(crate) fn new(enabled: bool) -> Self {
47 Self { entries: Arc::new(Mutex::new(Vec::new())), enabled }
48 }
49
50 pub(crate) async fn log(&self, entry: AuditEntry) {
52 if self.enabled {
53 let mut entries = self.entries.lock().await;
54 entries.push(entry);
55 }
56 }
57
58 pub(crate) async fn entries(&self) -> Vec<AuditEntry> {
60 let entries = self.entries.lock().await;
61 entries.clone()
62 }
63
64 pub async fn entries_for_command(&self, cmd: &str) -> Vec<AuditEntry> {
66 let entries = self.entries.lock().await;
67 entries.iter().filter(|e| e.command.join(" ").contains(cmd)).cloned().collect()
68 }
69
70 async fn denied_entries(&self) -> Vec<AuditEntry> {
72 let entries = self.entries.lock().await;
73 entries.iter().filter(|e| !e.allowed).cloned().collect()
74 }
75
76 pub async fn clear(&self) {
78 let mut entries = self.entries.lock().await;
79 entries.clear();
80 }
81
82 pub(crate) async fn count(&self) -> usize {
84 let entries = self.entries.lock().await;
85 entries.len()
86 }
87}
88
89impl Clone for SafetyAuditLogger {
90 fn clone(&self) -> Self {
91 Self {
92 entries: Arc::clone(&self.entries),
93 enabled: self.enabled,
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[tokio::test]
103 async fn creates_audit_entry() {
104 let cmd = vec!["git".to_string(), "status".to_string()];
105 let entry = AuditEntry::new(cmd, true, "git status allowed".to_string(), "Allow".to_string());
106 assert!(entry.allowed);
107 assert!(!entry.timestamp.is_empty());
108 }
109
110 #[tokio::test]
111 async fn logs_entries() {
112 let logger = SafetyAuditLogger::new(true);
113 let cmd = vec!["git".to_string(), "status".to_string()];
114 let entry = AuditEntry::new(cmd, true, "git status allowed".to_string(), "Allow".to_string());
115
116 logger.log(entry).await;
117 assert_eq!(logger.count().await, 1);
118 }
119
120 #[tokio::test]
121 async fn filters_denied_entries() {
122 let logger = SafetyAuditLogger::new(true);
123
124 let cmd1 = vec!["git".to_string(), "status".to_string()];
125 logger
126 .log(AuditEntry::new(cmd1, true, "allowed".to_string(), "Allow".to_string()))
127 .await;
128
129 let cmd2 = vec!["git".to_string(), "reset".to_string()];
130 logger
131 .log(AuditEntry::new(cmd2, false, "denied".to_string(), "Deny".to_string()))
132 .await;
133
134 let denied = logger.denied_entries().await;
135 assert_eq!(denied.len(), 1);
136 assert!(!denied[0].allowed);
137 }
138
139 #[tokio::test]
140 async fn disabled_logger_ignores_entries() {
141 let logger = SafetyAuditLogger::new(false);
142 let cmd = vec!["git".to_string(), "status".to_string()];
143 let entry = AuditEntry::new(cmd, true, "allowed".to_string(), "Allow".to_string());
144
145 logger.log(entry).await;
146 assert_eq!(logger.count().await, 0);
147 }
148
149 #[tokio::test]
150 async fn clones_share_same_entries() {
151 let logger1 = SafetyAuditLogger::new(true);
152 let logger2 = logger1.clone();
153
154 let cmd = vec!["git".to_string(), "status".to_string()];
155 let entry = AuditEntry::new(cmd, true, "allowed".to_string(), "Allow".to_string());
156
157 logger1.log(entry).await;
158
159 assert_eq!(logger1.count().await, 1);
161 assert_eq!(logger2.count().await, 1);
162 }
163}