a3s_code_core/security/
audit.rs1use super::config::SensitivityLevel;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::VecDeque;
10use std::sync::RwLock;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum AuditEventType {
16 TaintRegistered,
18 OutputRedacted,
20 ToolBlocked,
22 InjectionDetected,
24 SessionWiped,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum AuditAction {
32 Allowed,
34 Blocked,
36 Redacted,
38 Logged,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AuditEntry {
45 pub timestamp: DateTime<Utc>,
47 pub session_id: String,
49 pub event_type: AuditEventType,
51 pub severity: SensitivityLevel,
53 pub details: String,
55 pub tool_name: Option<String>,
57 pub action_taken: AuditAction,
59}
60
61pub struct AuditLog {
63 entries: RwLock<VecDeque<AuditEntry>>,
64 max_entries: usize,
65}
66
67impl AuditLog {
68 pub fn new(max_entries: usize) -> Self {
70 Self {
71 entries: RwLock::new(VecDeque::new()),
72 max_entries,
73 }
74 }
75
76 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 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 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 pub fn clear(&self) {
111 if let Ok(mut entries) = self.entries.write() {
112 entries.clear();
113 }
114 }
115
116 pub fn len(&self) -> usize {
118 self.entries.read().map(|e| e.len()).unwrap_or(0)
119 }
120
121 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 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}