Skip to main content

aster/permission/
audit.rs

1//! Audit Logging Module for Tool Permission System
2//!
3//! This module provides structured audit logging for permission checks and tool executions.
4//! It uses the `tracing` crate for structured logging with configurable log levels.
5//!
6//! Features:
7//! - Configurable log levels (Debug, Info, Warn, Error)
8//! - Structured logging with JSON-compatible fields
9//! - Failure resilience - logging failures don't block main operations
10//! - Enable/disable toggle for audit logging
11//!
12//! Requirements: 10.1, 10.2, 10.3, 10.4, 10.5
13
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17use super::types::{PermissionContext, PermissionResult};
18
19/// Audit log level
20///
21/// Defines the severity level for audit log entries.
22/// Each level includes messages of higher severity levels.
23///
24/// Requirements: 10.3
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
26pub enum AuditLogLevel {
27    /// Debug level - most verbose, includes all messages
28    Debug,
29    /// Info level - standard operational messages
30    #[default]
31    Info,
32    /// Warn level - warning messages and above
33    Warn,
34    /// Error level - only error messages
35    Error,
36}
37
38impl AuditLogLevel {
39    /// Check if a message at the given level should be logged
40    /// based on the current configured level
41    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    /// Get the numeric priority of the log level (higher = more severe)
48    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/// Audit log entry
59///
60/// Contains all information about a permission check or tool execution event.
61///
62/// Requirements: 10.1, 10.2, 10.4
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct AuditLogEntry {
65    /// Unix timestamp of the event
66    pub timestamp: i64,
67    /// Log level for this entry
68    pub level: AuditLogLevel,
69    /// Type of event (e.g., "permission_check", "tool_execution")
70    pub event_type: String,
71    /// Name of the tool being checked/executed
72    pub tool_name: String,
73    /// Parameters passed to the tool
74    pub parameters: HashMap<String, serde_json::Value>,
75    /// Permission context at the time of the event
76    pub context: PermissionContext,
77    /// Result of the permission check (if applicable)
78    pub result: Option<PermissionResult>,
79    /// Duration of the operation in milliseconds (for tool execution)
80    pub duration_ms: Option<u64>,
81    /// Additional metadata
82    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    /// Create a new audit log entry with the current timestamp
103    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    /// Set the log level
113    pub fn with_level(mut self, level: AuditLogLevel) -> Self {
114        self.level = level;
115        self
116    }
117
118    /// Set the parameters
119    pub fn with_parameters(mut self, parameters: HashMap<String, serde_json::Value>) -> Self {
120        self.parameters = parameters;
121        self
122    }
123
124    /// Set the context
125    pub fn with_context(mut self, context: PermissionContext) -> Self {
126        self.context = context;
127        self
128    }
129
130    /// Set the result
131    pub fn with_result(mut self, result: PermissionResult) -> Self {
132        self.result = Some(result);
133        self
134    }
135
136    /// Set the duration
137    pub fn with_duration_ms(mut self, duration_ms: u64) -> Self {
138        self.duration_ms = Some(duration_ms);
139        self
140    }
141
142    /// Set the metadata
143    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
144        self.metadata = metadata;
145        self
146    }
147
148    /// Add a single metadata entry
149    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/// Audit logger
156///
157/// Provides structured audit logging for permission checks and tool executions.
158/// Uses the `tracing` crate for output with configurable log levels.
159///
160/// Requirements: 10.3, 10.5
161#[derive(Debug, Clone)]
162pub struct AuditLogger {
163    /// Current log level threshold
164    level: AuditLogLevel,
165    /// Whether audit logging is enabled
166    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    /// Create a new audit logger with the specified log level
180    ///
181    /// # Arguments
182    /// * `level` - The minimum log level to record
183    ///
184    /// Requirements: 10.3
185    pub fn new(level: AuditLogLevel) -> Self {
186        Self {
187            level,
188            enabled: true,
189        }
190    }
191
192    /// Get the current log level
193    pub fn level(&self) -> AuditLogLevel {
194        self.level
195    }
196
197    /// Check if the logger is enabled
198    pub fn is_enabled(&self) -> bool {
199        self.enabled
200    }
201
202    /// Set the log level
203    ///
204    /// # Arguments
205    /// * `level` - The new minimum log level to record
206    ///
207    /// Requirements: 10.3
208    pub fn set_level(&mut self, level: AuditLogLevel) {
209        self.level = level;
210    }
211
212    /// Enable audit logging
213    ///
214    /// Requirements: 10.3
215    pub fn enable(&mut self) {
216        self.enabled = true;
217    }
218
219    /// Disable audit logging
220    ///
221    /// Requirements: 10.3
222    pub fn disable(&mut self) {
223        self.enabled = false;
224    }
225
226    /// Log a permission check event
227    ///
228    /// Records when a permission check is performed, including the tool name,
229    /// parameters, context, and result.
230    ///
231    /// # Arguments
232    /// * `entry` - The audit log entry to record
233    ///
234    /// # Behavior
235    /// - If logging is disabled, returns immediately
236    /// - If the entry's level is below the configured threshold, returns immediately
237    /// - Logging failures are caught and do not propagate (Requirement 10.5)
238    ///
239    /// Requirements: 10.1, 10.4, 10.5
240    pub fn log_permission_check(&self, entry: AuditLogEntry) {
241        // Requirement 10.5: Ensure logging failures don't block main flow
242        let _ = self.try_log_permission_check(entry);
243    }
244
245    /// Internal method that can fail - wrapped by log_permission_check for resilience
246    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        // Serialize entry to JSON for structured logging
256        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    /// Log a tool execution event
305    ///
306    /// Records when a tool execution completes, including the tool name,
307    /// parameters, result, and duration.
308    ///
309    /// # Arguments
310    /// * `entry` - The audit log entry to record
311    ///
312    /// # Behavior
313    /// - If logging is disabled, returns immediately
314    /// - If the entry's level is below the configured threshold, returns immediately
315    /// - Logging failures are caught and do not propagate (Requirement 10.5)
316    ///
317    /// Requirements: 10.2, 10.4, 10.5
318    pub fn log_tool_execution(&self, entry: AuditLogEntry) {
319        // Requirement 10.5: Ensure logging failures don't block main flow
320        let _ = self.try_log_tool_execution(entry);
321    }
322
323    /// Internal method that can fail - wrapped by log_tool_execution for resilience
324    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        // Serialize entry to JSON for structured logging
334        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    /// Log a generic audit event
383    ///
384    /// A general-purpose logging method for custom audit events.
385    ///
386    /// # Arguments
387    /// * `entry` - The audit log entry to record
388    ///
389    /// Requirements: 10.4, 10.5
390    pub fn log(&self, entry: AuditLogEntry) {
391        // Requirement 10.5: Ensure logging failures don't block main flow
392        let _ = self.try_log(entry);
393    }
394
395    /// Internal method that can fail - wrapped by log for resilience
396    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        // Serialize entry to JSON for structured logging
406        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        // Debug level logs everything
495        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        // Info level logs Info and above
501        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        // Warn level logs Warn and above
507        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        // Error level logs only Error
513        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        // This should not panic even without a tracing subscriber
602        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        // This should not panic even without a tracing subscriber
615        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        // This should return immediately without logging
626        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        // Info level entry should not be logged when logger is at Error level
634        let entry = AuditLogEntry::new("permission_check", "bash").with_level(AuditLogLevel::Info);
635
636        // This should return immediately without logging
637        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        // Even with potentially problematic data, logging should not panic
662        let entry = AuditLogEntry::new("permission_check", "bash");
663
664        // These should all complete without panicking
665        logger.log_permission_check(entry.clone());
666        logger.log_tool_execution(entry.clone());
667        logger.log(entry);
668    }
669}