Skip to main content

a3s_code_core/security/
audit.rs

1//! Security Audit Logging
2//!
3//! Provides structured audit logging for all security events:
4//! taint registration, output redaction, tool blocking, injection detection.
5
6use super::config::SensitivityLevel;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::VecDeque;
10use std::sync::RwLock;
11
12/// Types of auditable security events
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum AuditEventType {
16    /// Sensitive data was registered in the taint registry
17    TaintRegistered,
18    /// Output was redacted before delivery
19    OutputRedacted,
20    /// A tool invocation was blocked
21    ToolBlocked,
22    /// A prompt injection attempt was detected
23    InjectionDetected,
24    /// Session taint data was securely wiped
25    SessionWiped,
26}
27
28/// Action taken in response to a security event
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum AuditAction {
32    /// Operation was allowed to proceed
33    Allowed,
34    /// Operation was blocked
35    Blocked,
36    /// Content was redacted
37    Redacted,
38    /// Event was logged only (no action taken)
39    Logged,
40}
41
42/// A single audit log entry
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AuditEntry {
45    /// When the event occurred
46    pub timestamp: DateTime<Utc>,
47    /// Session that triggered the event
48    pub session_id: String,
49    /// Type of security event
50    pub event_type: AuditEventType,
51    /// Severity level
52    pub severity: SensitivityLevel,
53    /// Human-readable description
54    pub details: String,
55    /// Tool name involved (if applicable)
56    pub tool_name: Option<String>,
57    /// Action taken
58    pub action_taken: AuditAction,
59}
60
61/// Thread-safe audit log with bounded capacity
62pub struct AuditLog {
63    entries: RwLock<VecDeque<AuditEntry>>,
64    max_entries: usize,
65}
66
67impl AuditLog {
68    /// Create a new audit log with the given capacity
69    pub fn new(max_entries: usize) -> Self {
70        Self {
71            entries: RwLock::new(VecDeque::new()),
72            max_entries,
73        }
74    }
75
76    /// Log a new audit entry
77    pub fn log(&self, entry: AuditEntry) {
78        let Ok(mut entries) = self.entries.write() else {
79            tracing::error!("Audit log lock poisoned — dropping audit entry");
80            return;
81        };
82        if entries.len() >= self.max_entries {
83            entries.pop_front();
84        }
85        entries.push_back(entry);
86    }
87
88    /// Get all audit entries
89    pub fn entries(&self) -> Vec<AuditEntry> {
90        self.entries
91            .read()
92            .map(|e| e.iter().cloned().collect())
93            .unwrap_or_default()
94    }
95
96    /// Get entries for a specific session
97    pub fn entries_for_session(&self, session_id: &str) -> Vec<AuditEntry> {
98        self.entries
99            .read()
100            .map(|e| {
101                e.iter()
102                    .filter(|e| e.session_id == session_id)
103                    .cloned()
104                    .collect()
105            })
106            .unwrap_or_default()
107    }
108
109    /// Clear all entries
110    pub fn clear(&self) {
111        if let Ok(mut entries) = self.entries.write() {
112            entries.clear();
113        }
114    }
115
116    /// Get the number of entries
117    pub fn len(&self) -> usize {
118        self.entries.read().map(|e| e.len()).unwrap_or(0)
119    }
120
121    /// Check if the log is empty
122    pub fn is_empty(&self) -> bool {
123        self.entries.read().map(|e| e.is_empty()).unwrap_or(true)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn make_entry(session_id: &str, event_type: AuditEventType) -> AuditEntry {
132        AuditEntry {
133            timestamp: Utc::now(),
134            session_id: session_id.to_string(),
135            event_type,
136            severity: SensitivityLevel::Sensitive,
137            details: "test event".to_string(),
138            tool_name: None,
139            action_taken: AuditAction::Logged,
140        }
141    }
142
143    #[test]
144    fn test_log_and_retrieve_entries() {
145        let log = AuditLog::new(100);
146        assert!(log.is_empty());
147
148        log.log(make_entry("s1", AuditEventType::TaintRegistered));
149        log.log(make_entry("s1", AuditEventType::OutputRedacted));
150
151        assert_eq!(log.len(), 2);
152        let entries = log.entries();
153        assert_eq!(entries[0].event_type, AuditEventType::TaintRegistered);
154        assert_eq!(entries[1].event_type, AuditEventType::OutputRedacted);
155    }
156
157    #[test]
158    fn test_filter_by_session() {
159        let log = AuditLog::new(100);
160        log.log(make_entry("s1", AuditEventType::TaintRegistered));
161        log.log(make_entry("s2", AuditEventType::ToolBlocked));
162        log.log(make_entry("s1", AuditEventType::OutputRedacted));
163
164        let s1_entries = log.entries_for_session("s1");
165        assert_eq!(s1_entries.len(), 2);
166
167        let s2_entries = log.entries_for_session("s2");
168        assert_eq!(s2_entries.len(), 1);
169        assert_eq!(s2_entries[0].event_type, AuditEventType::ToolBlocked);
170    }
171
172    #[test]
173    fn test_max_entries_cap() {
174        let log = AuditLog::new(3);
175        log.log(make_entry("s1", AuditEventType::TaintRegistered));
176        log.log(make_entry("s1", AuditEventType::OutputRedacted));
177        log.log(make_entry("s1", AuditEventType::ToolBlocked));
178        log.log(make_entry("s1", AuditEventType::InjectionDetected));
179
180        assert_eq!(log.len(), 3);
181        // First entry should have been evicted
182        let entries = log.entries();
183        assert_eq!(entries[0].event_type, AuditEventType::OutputRedacted);
184        assert_eq!(entries[2].event_type, AuditEventType::InjectionDetected);
185    }
186
187    #[test]
188    fn test_clear() {
189        let log = AuditLog::new(100);
190        log.log(make_entry("s1", AuditEventType::TaintRegistered));
191        log.log(make_entry("s1", AuditEventType::OutputRedacted));
192        assert_eq!(log.len(), 2);
193
194        log.clear();
195        assert!(log.is_empty());
196        assert_eq!(log.len(), 0);
197    }
198
199    #[test]
200    fn test_audit_entry_serialization() {
201        let entry = make_entry("s1", AuditEventType::ToolBlocked);
202        let json = serde_json::to_string(&entry).unwrap();
203        assert!(json.contains("tool_blocked"));
204        assert!(json.contains("s1"));
205
206        let parsed: AuditEntry = serde_json::from_str(&json).unwrap();
207        assert_eq!(parsed.event_type, AuditEventType::ToolBlocked);
208        assert_eq!(parsed.session_id, "s1");
209    }
210}