1use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17use super::types::{PermissionContext, PermissionResult};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
26pub enum AuditLogLevel {
27 Debug,
29 #[default]
31 Info,
32 Warn,
34 Error,
36}
37
38impl AuditLogLevel {
39 pub fn should_log(&self, message_level: AuditLogLevel) -> bool {
42 let self_priority = self.priority();
43 let message_priority = message_level.priority();
44 message_priority >= self_priority
45 }
46
47 fn priority(&self) -> u8 {
49 match self {
50 AuditLogLevel::Debug => 0,
51 AuditLogLevel::Info => 1,
52 AuditLogLevel::Warn => 2,
53 AuditLogLevel::Error => 3,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct AuditLogEntry {
65 pub timestamp: i64,
67 pub level: AuditLogLevel,
69 pub event_type: String,
71 pub tool_name: String,
73 pub parameters: HashMap<String, serde_json::Value>,
75 pub context: PermissionContext,
77 pub result: Option<PermissionResult>,
79 pub duration_ms: Option<u64>,
81 pub metadata: HashMap<String, serde_json::Value>,
83}
84
85impl Default for AuditLogEntry {
86 fn default() -> Self {
87 Self {
88 timestamp: 0,
89 level: AuditLogLevel::Info,
90 event_type: String::new(),
91 tool_name: String::new(),
92 parameters: HashMap::new(),
93 context: PermissionContext::default(),
94 result: None,
95 duration_ms: None,
96 metadata: HashMap::new(),
97 }
98 }
99}
100
101impl AuditLogEntry {
102 pub fn new(event_type: impl Into<String>, tool_name: impl Into<String>) -> Self {
104 Self {
105 timestamp: chrono::Utc::now().timestamp(),
106 event_type: event_type.into(),
107 tool_name: tool_name.into(),
108 ..Default::default()
109 }
110 }
111
112 pub fn with_level(mut self, level: AuditLogLevel) -> Self {
114 self.level = level;
115 self
116 }
117
118 pub fn with_parameters(mut self, parameters: HashMap<String, serde_json::Value>) -> Self {
120 self.parameters = parameters;
121 self
122 }
123
124 pub fn with_context(mut self, context: PermissionContext) -> Self {
126 self.context = context;
127 self
128 }
129
130 pub fn with_result(mut self, result: PermissionResult) -> Self {
132 self.result = Some(result);
133 self
134 }
135
136 pub fn with_duration_ms(mut self, duration_ms: u64) -> Self {
138 self.duration_ms = Some(duration_ms);
139 self
140 }
141
142 pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
144 self.metadata = metadata;
145 self
146 }
147
148 pub fn add_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
150 self.metadata.insert(key.into(), value);
151 self
152 }
153}
154
155#[derive(Debug, Clone)]
162pub struct AuditLogger {
163 level: AuditLogLevel,
165 enabled: bool,
167}
168
169impl Default for AuditLogger {
170 fn default() -> Self {
171 Self {
172 level: AuditLogLevel::Info,
173 enabled: true,
174 }
175 }
176}
177
178impl AuditLogger {
179 pub fn new(level: AuditLogLevel) -> Self {
186 Self {
187 level,
188 enabled: true,
189 }
190 }
191
192 pub fn level(&self) -> AuditLogLevel {
194 self.level
195 }
196
197 pub fn is_enabled(&self) -> bool {
199 self.enabled
200 }
201
202 pub fn set_level(&mut self, level: AuditLogLevel) {
209 self.level = level;
210 }
211
212 pub fn enable(&mut self) {
216 self.enabled = true;
217 }
218
219 pub fn disable(&mut self) {
223 self.enabled = false;
224 }
225
226 pub fn log_permission_check(&self, entry: AuditLogEntry) {
241 let _ = self.try_log_permission_check(entry);
243 }
244
245 fn try_log_permission_check(&self, entry: AuditLogEntry) -> Result<(), ()> {
247 if !self.enabled {
248 return Ok(());
249 }
250
251 if !self.level.should_log(entry.level) {
252 return Ok(());
253 }
254
255 let entry_json = serde_json::to_string(&entry).map_err(|_| ())?;
257
258 match entry.level {
259 AuditLogLevel::Debug => {
260 tracing::debug!(
261 event_type = %entry.event_type,
262 tool_name = %entry.tool_name,
263 allowed = ?entry.result.as_ref().map(|r| r.allowed),
264 session_id = %entry.context.session_id,
265 audit_entry = %entry_json,
266 "Permission check"
267 );
268 }
269 AuditLogLevel::Info => {
270 tracing::info!(
271 event_type = %entry.event_type,
272 tool_name = %entry.tool_name,
273 allowed = ?entry.result.as_ref().map(|r| r.allowed),
274 session_id = %entry.context.session_id,
275 audit_entry = %entry_json,
276 "Permission check"
277 );
278 }
279 AuditLogLevel::Warn => {
280 tracing::warn!(
281 event_type = %entry.event_type,
282 tool_name = %entry.tool_name,
283 allowed = ?entry.result.as_ref().map(|r| r.allowed),
284 session_id = %entry.context.session_id,
285 audit_entry = %entry_json,
286 "Permission check"
287 );
288 }
289 AuditLogLevel::Error => {
290 tracing::error!(
291 event_type = %entry.event_type,
292 tool_name = %entry.tool_name,
293 allowed = ?entry.result.as_ref().map(|r| r.allowed),
294 session_id = %entry.context.session_id,
295 audit_entry = %entry_json,
296 "Permission check"
297 );
298 }
299 }
300
301 Ok(())
302 }
303
304 pub fn log_tool_execution(&self, entry: AuditLogEntry) {
319 let _ = self.try_log_tool_execution(entry);
321 }
322
323 fn try_log_tool_execution(&self, entry: AuditLogEntry) -> Result<(), ()> {
325 if !self.enabled {
326 return Ok(());
327 }
328
329 if !self.level.should_log(entry.level) {
330 return Ok(());
331 }
332
333 let entry_json = serde_json::to_string(&entry).map_err(|_| ())?;
335
336 match entry.level {
337 AuditLogLevel::Debug => {
338 tracing::debug!(
339 event_type = %entry.event_type,
340 tool_name = %entry.tool_name,
341 duration_ms = ?entry.duration_ms,
342 session_id = %entry.context.session_id,
343 audit_entry = %entry_json,
344 "Tool execution"
345 );
346 }
347 AuditLogLevel::Info => {
348 tracing::info!(
349 event_type = %entry.event_type,
350 tool_name = %entry.tool_name,
351 duration_ms = ?entry.duration_ms,
352 session_id = %entry.context.session_id,
353 audit_entry = %entry_json,
354 "Tool execution"
355 );
356 }
357 AuditLogLevel::Warn => {
358 tracing::warn!(
359 event_type = %entry.event_type,
360 tool_name = %entry.tool_name,
361 duration_ms = ?entry.duration_ms,
362 session_id = %entry.context.session_id,
363 audit_entry = %entry_json,
364 "Tool execution"
365 );
366 }
367 AuditLogLevel::Error => {
368 tracing::error!(
369 event_type = %entry.event_type,
370 tool_name = %entry.tool_name,
371 duration_ms = ?entry.duration_ms,
372 session_id = %entry.context.session_id,
373 audit_entry = %entry_json,
374 "Tool execution"
375 );
376 }
377 }
378
379 Ok(())
380 }
381
382 pub fn log(&self, entry: AuditLogEntry) {
391 let _ = self.try_log(entry);
393 }
394
395 fn try_log(&self, entry: AuditLogEntry) -> Result<(), ()> {
397 if !self.enabled {
398 return Ok(());
399 }
400
401 if !self.level.should_log(entry.level) {
402 return Ok(());
403 }
404
405 let entry_json = serde_json::to_string(&entry).map_err(|_| ())?;
407
408 match entry.level {
409 AuditLogLevel::Debug => {
410 tracing::debug!(
411 event_type = %entry.event_type,
412 tool_name = %entry.tool_name,
413 session_id = %entry.context.session_id,
414 audit_entry = %entry_json,
415 "Audit event"
416 );
417 }
418 AuditLogLevel::Info => {
419 tracing::info!(
420 event_type = %entry.event_type,
421 tool_name = %entry.tool_name,
422 session_id = %entry.context.session_id,
423 audit_entry = %entry_json,
424 "Audit event"
425 );
426 }
427 AuditLogLevel::Warn => {
428 tracing::warn!(
429 event_type = %entry.event_type,
430 tool_name = %entry.tool_name,
431 session_id = %entry.context.session_id,
432 audit_entry = %entry_json,
433 "Audit event"
434 );
435 }
436 AuditLogLevel::Error => {
437 tracing::error!(
438 event_type = %entry.event_type,
439 tool_name = %entry.tool_name,
440 session_id = %entry.context.session_id,
441 audit_entry = %entry_json,
442 "Audit event"
443 );
444 }
445 }
446
447 Ok(())
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use std::path::PathBuf;
455
456 fn create_test_context() -> PermissionContext {
457 PermissionContext {
458 working_directory: PathBuf::from("/home/user/project"),
459 session_id: "test-session-123".to_string(),
460 timestamp: 1700000000,
461 user: Some("testuser".to_string()),
462 environment: HashMap::new(),
463 metadata: HashMap::new(),
464 }
465 }
466
467 fn create_test_result(allowed: bool) -> PermissionResult {
468 PermissionResult {
469 allowed,
470 reason: if allowed {
471 None
472 } else {
473 Some("Test denial".to_string())
474 },
475 restricted: false,
476 suggestions: Vec::new(),
477 matched_rule: None,
478 violations: Vec::new(),
479 }
480 }
481
482 #[test]
483 fn test_audit_log_level_default() {
484 assert_eq!(AuditLogLevel::default(), AuditLogLevel::Info);
485 }
486
487 #[test]
488 fn test_audit_log_level_should_log() {
489 let debug_level = AuditLogLevel::Debug;
490 let info_level = AuditLogLevel::Info;
491 let warn_level = AuditLogLevel::Warn;
492 let error_level = AuditLogLevel::Error;
493
494 assert!(debug_level.should_log(AuditLogLevel::Debug));
496 assert!(debug_level.should_log(AuditLogLevel::Info));
497 assert!(debug_level.should_log(AuditLogLevel::Warn));
498 assert!(debug_level.should_log(AuditLogLevel::Error));
499
500 assert!(!info_level.should_log(AuditLogLevel::Debug));
502 assert!(info_level.should_log(AuditLogLevel::Info));
503 assert!(info_level.should_log(AuditLogLevel::Warn));
504 assert!(info_level.should_log(AuditLogLevel::Error));
505
506 assert!(!warn_level.should_log(AuditLogLevel::Debug));
508 assert!(!warn_level.should_log(AuditLogLevel::Info));
509 assert!(warn_level.should_log(AuditLogLevel::Warn));
510 assert!(warn_level.should_log(AuditLogLevel::Error));
511
512 assert!(!error_level.should_log(AuditLogLevel::Debug));
514 assert!(!error_level.should_log(AuditLogLevel::Info));
515 assert!(!error_level.should_log(AuditLogLevel::Warn));
516 assert!(error_level.should_log(AuditLogLevel::Error));
517 }
518
519 #[test]
520 fn test_audit_log_entry_new() {
521 let entry = AuditLogEntry::new("permission_check", "bash");
522
523 assert_eq!(entry.event_type, "permission_check");
524 assert_eq!(entry.tool_name, "bash");
525 assert!(entry.timestamp > 0);
526 assert_eq!(entry.level, AuditLogLevel::Info);
527 }
528
529 #[test]
530 fn test_audit_log_entry_builder() {
531 let context = create_test_context();
532 let result = create_test_result(true);
533 let mut params = HashMap::new();
534 params.insert("command".to_string(), serde_json::json!("ls -la"));
535
536 let entry = AuditLogEntry::new("permission_check", "bash")
537 .with_level(AuditLogLevel::Debug)
538 .with_parameters(params.clone())
539 .with_context(context.clone())
540 .with_result(result.clone())
541 .with_duration_ms(100)
542 .add_metadata("custom_field", serde_json::json!("custom_value"));
543
544 assert_eq!(entry.level, AuditLogLevel::Debug);
545 assert_eq!(entry.parameters, params);
546 assert_eq!(entry.context.session_id, context.session_id);
547 assert!(entry.result.is_some());
548 assert!(entry.result.unwrap().allowed);
549 assert_eq!(entry.duration_ms, Some(100));
550 assert!(entry.metadata.contains_key("custom_field"));
551 }
552
553 #[test]
554 fn test_audit_logger_new() {
555 let logger = AuditLogger::new(AuditLogLevel::Warn);
556
557 assert_eq!(logger.level(), AuditLogLevel::Warn);
558 assert!(logger.is_enabled());
559 }
560
561 #[test]
562 fn test_audit_logger_default() {
563 let logger = AuditLogger::default();
564
565 assert_eq!(logger.level(), AuditLogLevel::Info);
566 assert!(logger.is_enabled());
567 }
568
569 #[test]
570 fn test_audit_logger_set_level() {
571 let mut logger = AuditLogger::new(AuditLogLevel::Info);
572
573 logger.set_level(AuditLogLevel::Error);
574
575 assert_eq!(logger.level(), AuditLogLevel::Error);
576 }
577
578 #[test]
579 fn test_audit_logger_enable_disable() {
580 let mut logger = AuditLogger::new(AuditLogLevel::Info);
581
582 assert!(logger.is_enabled());
583
584 logger.disable();
585 assert!(!logger.is_enabled());
586
587 logger.enable();
588 assert!(logger.is_enabled());
589 }
590
591 #[test]
592 fn test_audit_logger_log_permission_check() {
593 let logger = AuditLogger::new(AuditLogLevel::Debug);
594 let context = create_test_context();
595 let result = create_test_result(true);
596
597 let entry = AuditLogEntry::new("permission_check", "bash")
598 .with_context(context)
599 .with_result(result);
600
601 logger.log_permission_check(entry);
603 }
604
605 #[test]
606 fn test_audit_logger_log_tool_execution() {
607 let logger = AuditLogger::new(AuditLogLevel::Debug);
608 let context = create_test_context();
609
610 let entry = AuditLogEntry::new("tool_execution", "bash")
611 .with_context(context)
612 .with_duration_ms(150);
613
614 logger.log_tool_execution(entry);
616 }
617
618 #[test]
619 fn test_audit_logger_disabled_does_not_log() {
620 let mut logger = AuditLogger::new(AuditLogLevel::Debug);
621 logger.disable();
622
623 let entry = AuditLogEntry::new("permission_check", "bash");
624
625 logger.log_permission_check(entry);
627 }
628
629 #[test]
630 fn test_audit_logger_level_filtering() {
631 let logger = AuditLogger::new(AuditLogLevel::Error);
632
633 let entry = AuditLogEntry::new("permission_check", "bash").with_level(AuditLogLevel::Info);
635
636 logger.log_permission_check(entry);
638 }
639
640 #[test]
641 fn test_audit_log_entry_serialization() {
642 let context = create_test_context();
643 let result = create_test_result(false);
644
645 let entry = AuditLogEntry::new("permission_check", "bash")
646 .with_context(context)
647 .with_result(result);
648
649 let json = serde_json::to_string(&entry).unwrap();
650 let deserialized: AuditLogEntry = serde_json::from_str(&json).unwrap();
651
652 assert_eq!(entry.event_type, deserialized.event_type);
653 assert_eq!(entry.tool_name, deserialized.tool_name);
654 assert_eq!(entry.level, deserialized.level);
655 }
656
657 #[test]
658 fn test_audit_logger_failure_resilience() {
659 let logger = AuditLogger::new(AuditLogLevel::Debug);
660
661 let entry = AuditLogEntry::new("permission_check", "bash");
663
664 logger.log_permission_check(entry.clone());
666 logger.log_tool_execution(entry.clone());
667 logger.log(entry);
668 }
669}