vtcode_core/audit/
permission_log.rs1use crate::utils::error_messages::ERR_CREATE_AUDIT_DIR;
6use crate::utils::file_utils::ensure_dir_exists_sync;
7use anyhow::{Context, Result};
8use chrono::{DateTime, Local};
9use serde::{Deserialize, Serialize};
10use std::fs::OpenOptions;
11use std::io::BufWriter;
12use std::path::PathBuf;
13use tracing::info;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PermissionEvent {
18 pub timestamp: DateTime<Local>,
20
21 pub subject: String,
23
24 pub event_type: PermissionEventType,
26
27 pub decision: PermissionDecision,
29
30 pub reason: String,
32
33 pub resolved_path: Option<PathBuf>,
35
36 pub requested_by: String,
38}
39
40#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
42pub enum FileAccessPermission {
43 Read,
44 Write,
45 ReadWrite,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum PermissionEventType {
51 CommandExecution,
52 ToolUsage,
53 FileAccess(FileAccessPermission),
54 NetworkAccess { domain: String },
55 HookExecution,
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
60pub enum PermissionDecision {
61 Allowed,
62 Denied,
63 Prompted,
64 Cached,
65}
66
67pub struct PermissionAuditLog {
69 log_path: PathBuf,
71
72 writer: Option<BufWriter<std::fs::File>>,
74
75 event_count: usize,
77}
78
79impl PermissionAuditLog {
80 pub fn new(audit_dir: PathBuf) -> Result<Self> {
82 ensure_dir_exists_sync(&audit_dir).context(ERR_CREATE_AUDIT_DIR)?;
84
85 let date = Local::now().format("%Y-%m-%d");
87 let log_path = audit_dir.join(format!("permissions-{}.log", date));
88
89 info!(?log_path, "Audit log initialized");
90
91 Ok(Self {
92 log_path,
93 writer: None,
94 event_count: 0,
95 })
96 }
97
98 pub fn record(&mut self, event: PermissionEvent) -> Result<()> {
100 use std::io::Write;
101
102 let json = serde_json::to_string(&event).context("Failed to serialize permission event")?;
103
104 let writer = self.writer_mut()?;
105 writeln!(writer, "{}", json).context("Failed to write to audit log")?;
106
107 writer.flush().context("Failed to flush audit log")?;
108
109 self.event_count += 1;
110
111 info!(
112 subject = &event.subject,
113 decision = ?event.decision,
114 "Permission event logged"
115 );
116
117 Ok(())
118 }
119
120 pub fn event_count(&self) -> usize {
122 self.event_count
123 }
124
125 pub fn log_path(&self) -> &PathBuf {
127 &self.log_path
128 }
129
130 pub fn log_command_decision(
132 &mut self,
133 command: &str,
134 decision: PermissionDecision,
135 reason: &str,
136 resolved_path: Option<PathBuf>,
137 ) -> Result<()> {
138 let event = PermissionEvent {
139 timestamp: Local::now(),
140 subject: command.to_owned(),
141 event_type: PermissionEventType::CommandExecution,
142 decision,
143 reason: reason.to_owned(),
144 resolved_path,
145 requested_by: "CommandPolicyEvaluator".into(),
146 };
147
148 self.record(event)
149 }
150
151 fn writer_mut(&mut self) -> Result<&mut BufWriter<std::fs::File>> {
152 if self.writer.is_none() {
153 let file = OpenOptions::new()
154 .create(true)
155 .append(true)
156 .open(&self.log_path)
157 .with_context(|| format!("Failed to open audit log at {:?}", self.log_path))?;
158 self.writer = Some(BufWriter::new(file));
159 }
160
161 self.writer
162 .as_mut()
163 .context("audit log writer was not initialized")
164 }
165}
166
167pub struct PermissionSummary {
169 pub total_events: usize,
170 pub allowed: usize,
171 pub denied: usize,
172 pub prompted: usize,
173 pub cached: usize,
174}
175
176impl PermissionSummary {
177 pub fn format(&self) -> String {
178 format!(
179 "Permission Summary: {} total | {} allowed | {} denied | {} prompted | {} cached",
180 self.total_events, self.allowed, self.denied, self.prompted, self.cached
181 )
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use tempfile::TempDir;
189
190 #[test]
191 fn test_audit_log_creation() -> Result<()> {
192 let dir = TempDir::new()?;
193 let log = PermissionAuditLog::new(dir.path().to_path_buf())?;
194 assert!(dir.path().exists());
195 assert!(!log.log_path().exists());
196 Ok(())
197 }
198
199 #[test]
200 fn test_log_permission_event() -> Result<()> {
201 let dir = TempDir::new()?;
202 let mut log = PermissionAuditLog::new(dir.path().to_path_buf())?;
203
204 log.log_command_decision(
205 "cargo fmt",
206 PermissionDecision::Allowed,
207 "Allow list match",
208 Some(PathBuf::from("/usr/local/cargo")),
209 )?;
210
211 assert_eq!(log.event_count(), 1);
212 assert!(log.log_path().exists());
213 Ok(())
214 }
215}