1use serde::{Deserialize, Serialize};
7use std::fs::OpenOptions;
8use std::io::Write;
9use std::path::PathBuf;
10use std::sync::Mutex;
11use tracing::info;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub enum AuditEventType {
16 ApiKeyAccessed,
18 ApiKeyRotated,
20 AuthenticationAttempt,
22 AuthorizationDecision,
24 ConfigurationLoaded,
26 FileAccessed,
28 FileModified,
30 PermissionDenied,
32 RateLimitExceeded,
34 SecurityError,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct AuditLogEntry {
41 pub timestamp: String,
43 pub event_type: AuditEventType,
45 pub component: String,
47 pub actor: String,
49 pub resource: String,
51 pub result: String,
53 pub details: String,
55}
56
57impl AuditLogEntry {
58 pub fn new(
60 event_type: AuditEventType,
61 component: &str,
62 actor: &str,
63 resource: &str,
64 result: &str,
65 details: &str,
66 ) -> Self {
67 let timestamp = chrono::Local::now().to_rfc3339();
68 Self {
69 timestamp,
70 event_type,
71 component: component.to_string(),
72 actor: actor.to_string(),
73 resource: resource.to_string(),
74 result: result.to_string(),
75 details: details.to_string(),
76 }
77 }
78
79 pub fn to_json(&self) -> Result<String, serde_json::Error> {
81 serde_json::to_string(self)
82 }
83}
84
85pub struct AuditLogger {
87 log_path: PathBuf,
89 lock: Mutex<()>,
91}
92
93impl AuditLogger {
94 pub fn new(log_path: PathBuf) -> Self {
96 Self {
97 log_path,
98 lock: Mutex::new(()),
99 }
100 }
101
102 pub fn log(&self, entry: &AuditLogEntry) -> Result<(), Box<dyn std::error::Error>> {
104 let _guard = self.lock.lock().unwrap();
105
106 let mut file = OpenOptions::new()
108 .create(true)
109 .append(true)
110 .open(&self.log_path)?;
111
112 let json = entry.to_json()?;
114 writeln!(file, "{}", json)?;
115
116 info!(
118 event_type = ?entry.event_type,
119 component = %entry.component,
120 actor = %entry.actor,
121 resource = %entry.resource,
122 result = %entry.result,
123 "Audit event logged"
124 );
125
126 Ok(())
127 }
128
129 pub fn log_api_key_access(
131 &self,
132 provider: &str,
133 actor: &str,
134 result: &str,
135 ) -> Result<(), Box<dyn std::error::Error>> {
136 let entry = AuditLogEntry::new(
137 AuditEventType::ApiKeyAccessed,
138 "providers",
139 actor,
140 provider,
141 result,
142 "API key accessed",
143 );
144 self.log(&entry)
145 }
146
147 pub fn log_api_key_rotation(
149 &self,
150 provider: &str,
151 actor: &str,
152 result: &str,
153 ) -> Result<(), Box<dyn std::error::Error>> {
154 let entry = AuditLogEntry::new(
155 AuditEventType::ApiKeyRotated,
156 "providers",
157 actor,
158 provider,
159 result,
160 "API key rotated",
161 );
162 self.log(&entry)
163 }
164
165 pub fn log_authentication_attempt(
167 &self,
168 provider: &str,
169 actor: &str,
170 result: &str,
171 details: &str,
172 ) -> Result<(), Box<dyn std::error::Error>> {
173 let entry = AuditLogEntry::new(
174 AuditEventType::AuthenticationAttempt,
175 "providers",
176 actor,
177 provider,
178 result,
179 details,
180 );
181 self.log(&entry)
182 }
183
184 pub fn log_authorization_decision(
186 &self,
187 resource: &str,
188 actor: &str,
189 allowed: bool,
190 details: &str,
191 ) -> Result<(), Box<dyn std::error::Error>> {
192 let result = if allowed { "allowed" } else { "denied" };
193 let entry = AuditLogEntry::new(
194 AuditEventType::AuthorizationDecision,
195 "permissions",
196 actor,
197 resource,
198 result,
199 details,
200 );
201 self.log(&entry)
202 }
203
204 pub fn log_rate_limit_exceeded(
206 &self,
207 provider: &str,
208 actor: &str,
209 details: &str,
210 ) -> Result<(), Box<dyn std::error::Error>> {
211 let entry = AuditLogEntry::new(
212 AuditEventType::RateLimitExceeded,
213 "providers",
214 actor,
215 provider,
216 "rate_limit_exceeded",
217 details,
218 );
219 self.log(&entry)
220 }
221
222 pub fn log_security_error(
224 &self,
225 component: &str,
226 actor: &str,
227 resource: &str,
228 error: &str,
229 ) -> Result<(), Box<dyn std::error::Error>> {
230 let entry = AuditLogEntry::new(
231 AuditEventType::SecurityError,
232 component,
233 actor,
234 resource,
235 "error",
236 error,
237 );
238 self.log(&entry)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use tempfile::TempDir;
246
247 #[test]
248 fn test_audit_log_entry_creation() {
249 let entry = AuditLogEntry::new(
250 AuditEventType::ApiKeyAccessed,
251 "providers",
252 "system",
253 "openai",
254 "success",
255 "API key accessed",
256 );
257
258 assert_eq!(entry.event_type, AuditEventType::ApiKeyAccessed);
259 assert_eq!(entry.component, "providers");
260 assert_eq!(entry.actor, "system");
261 assert_eq!(entry.resource, "openai");
262 assert_eq!(entry.result, "success");
263 }
264
265 #[test]
266 fn test_audit_log_entry_to_json() {
267 let entry = AuditLogEntry::new(
268 AuditEventType::ApiKeyAccessed,
269 "providers",
270 "system",
271 "openai",
272 "success",
273 "API key accessed",
274 );
275
276 let json = entry.to_json().unwrap();
277 assert!(json.contains("ApiKeyAccessed"));
278 assert!(json.contains("providers"));
279 assert!(json.contains("openai"));
280 }
281
282 #[test]
283 fn test_audit_logger_log() {
284 let temp_dir = TempDir::new().unwrap();
285 let log_path = temp_dir.path().join("audit.log");
286
287 let logger = AuditLogger::new(log_path.clone());
288 let entry = AuditLogEntry::new(
289 AuditEventType::ApiKeyAccessed,
290 "providers",
291 "system",
292 "openai",
293 "success",
294 "API key accessed",
295 );
296
297 let result = logger.log(&entry);
298 assert!(result.is_ok());
299
300 let content = std::fs::read_to_string(&log_path).unwrap();
302 assert!(content.contains("ApiKeyAccessed"));
303 }
304
305 #[test]
306 fn test_audit_logger_log_api_key_access() {
307 let temp_dir = TempDir::new().unwrap();
308 let log_path = temp_dir.path().join("audit.log");
309
310 let logger = AuditLogger::new(log_path.clone());
311 let result = logger.log_api_key_access("openai", "system", "success");
312 assert!(result.is_ok());
313
314 let content = std::fs::read_to_string(&log_path).unwrap();
315 assert!(content.contains("ApiKeyAccessed"));
316 assert!(content.contains("openai"));
317 }
318
319 #[test]
320 fn test_audit_logger_log_authentication_attempt() {
321 let temp_dir = TempDir::new().unwrap();
322 let log_path = temp_dir.path().join("audit.log");
323
324 let logger = AuditLogger::new(log_path.clone());
325 let result = logger.log_authentication_attempt("openai", "system", "success", "Valid API key");
326 assert!(result.is_ok());
327
328 let content = std::fs::read_to_string(&log_path).unwrap();
329 assert!(content.contains("AuthenticationAttempt"));
330 }
331
332 #[test]
333 fn test_audit_logger_log_authorization_decision() {
334 let temp_dir = TempDir::new().unwrap();
335 let log_path = temp_dir.path().join("audit.log");
336
337 let logger = AuditLogger::new(log_path.clone());
338 let result = logger.log_authorization_decision("tool:read_file", "system", true, "Permission granted");
339 assert!(result.is_ok());
340
341 let content = std::fs::read_to_string(&log_path).unwrap();
342 assert!(content.contains("AuthorizationDecision"));
343 assert!(content.contains("allowed"));
344 }
345
346 #[test]
347 fn test_audit_logger_log_rate_limit_exceeded() {
348 let temp_dir = TempDir::new().unwrap();
349 let log_path = temp_dir.path().join("audit.log");
350
351 let logger = AuditLogger::new(log_path.clone());
352 let result = logger.log_rate_limit_exceeded("openai", "system", "Rate limit: 10 req/sec");
353 assert!(result.is_ok());
354
355 let content = std::fs::read_to_string(&log_path).unwrap();
356 assert!(content.contains("RateLimitExceeded"));
357 }
358
359 #[test]
360 fn test_audit_logger_multiple_entries() {
361 let temp_dir = TempDir::new().unwrap();
362 let log_path = temp_dir.path().join("audit.log");
363
364 let logger = AuditLogger::new(log_path.clone());
365
366 logger.log_api_key_access("openai", "system", "success").unwrap();
367 logger.log_api_key_access("anthropic", "system", "success").unwrap();
368 logger.log_authentication_attempt("openai", "system", "success", "Valid key").unwrap();
369
370 let content = std::fs::read_to_string(&log_path).unwrap();
371 let lines: Vec<&str> = content.lines().collect();
372 assert_eq!(lines.len(), 3);
373 }
374}