Skip to main content

vtcode_core/audit/
permission_log.rs

1//! Permission audit logging system
2//! Tracks all permission decisions (allow/deny/prompt) with context
3//! Writes to ~/.vtcode/audit/permissions-{date}.log in JSON format
4
5use 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/// Record of a single permission decision
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PermissionEvent {
18    /// When the decision was made
19    pub timestamp: DateTime<Local>,
20
21    /// What was being requested (command, tool, path, etc.)
22    pub subject: String,
23
24    /// Type of permission check
25    pub event_type: PermissionEventType,
26
27    /// The decision reached
28    pub decision: PermissionDecision,
29
30    /// Why the decision was made
31    pub reason: String,
32
33    /// Optional resolved path (if applicable)
34    pub resolved_path: Option<PathBuf>,
35
36    /// Tool or component that made the request
37    pub requested_by: String,
38}
39
40/// Type of file access permission
41#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
42pub enum FileAccessPermission {
43    Read,
44    Write,
45    ReadWrite,
46}
47
48/// Type of permission event
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum PermissionEventType {
51    CommandExecution,
52    ToolUsage,
53    FileAccess(FileAccessPermission),
54    NetworkAccess { domain: String },
55    HookExecution,
56}
57
58/// The decision reached
59#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
60pub enum PermissionDecision {
61    Allowed,
62    Denied,
63    Prompted,
64    Cached,
65}
66
67/// Audit log for permission decisions
68pub struct PermissionAuditLog {
69    /// Path to the audit log file
70    log_path: PathBuf,
71
72    /// Writer for the log file
73    writer: Option<BufWriter<std::fs::File>>,
74
75    /// Count of events logged this session
76    event_count: usize,
77}
78
79impl PermissionAuditLog {
80    /// Create or open the audit log for today
81    pub fn new(audit_dir: PathBuf) -> Result<Self> {
82        // Create audit directory if needed
83        ensure_dir_exists_sync(&audit_dir).context(ERR_CREATE_AUDIT_DIR)?;
84
85        // Use today's date in filename
86        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    /// Record a permission event
99    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    /// Get the number of events logged
121    pub fn event_count(&self) -> usize {
122        self.event_count
123    }
124
125    /// Get path to the log file
126    pub fn log_path(&self) -> &PathBuf {
127        &self.log_path
128    }
129
130    /// Helper to create and log a permission event
131    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
167/// Generate a human-readable summary of permission decisions
168pub 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}