Skip to main content

config_lib/
audit.rs

1//! Comprehensive Audit Logging System
2//!
3//! Enterprise-grade audit logging with:
4//! - Structured logging for all configuration operations
5//! - Access tracking with user context and timestamps
6//! - Modification logging with before/after values
7//! - Validation failure tracking
8//! - Configurable log levels and outputs
9//! - Performance-optimized with minimal overhead
10
11use crate::value::Value;
12use std::collections::HashMap;
13use std::fmt;
14use std::sync::{Arc, Mutex};
15use std::time::{SystemTime, UNIX_EPOCH};
16
17/// Audit event types for configuration operations
18#[derive(Debug, Clone, PartialEq)]
19pub enum AuditEventType {
20    /// Configuration key was accessed/read
21    Access,
22    /// Configuration key was modified
23    Modification,
24    /// Configuration validation failed
25    ValidationFailure,
26    /// Configuration was reloaded from file
27    Reload,
28    /// Configuration file was loaded initially
29    Load,
30    /// Configuration was serialized/saved
31    Save,
32}
33
34/// Severity levels for audit events
35#[derive(Debug, Clone, PartialEq, PartialOrd)]
36pub enum AuditSeverity {
37    /// Informational events (normal operations)
38    Info = 1,
39    /// Warning events (potential issues)
40    Warning = 2,
41    /// Error events (failures)
42    Error = 3,
43    /// Critical events (security concerns)
44    Critical = 4,
45}
46
47/// Comprehensive audit event record
48#[derive(Debug, Clone)]
49pub struct AuditEvent {
50    /// Unique event ID
51    pub id: String,
52    /// Timestamp when the event occurred
53    pub timestamp: SystemTime,
54    /// Type of operation that triggered this event
55    pub event_type: AuditEventType,
56    /// Severity level of the event
57    pub severity: AuditSeverity,
58    /// Configuration key that was accessed/modified
59    pub key: Option<String>,
60    /// Previous value (for modifications)
61    pub old_value: Option<Value>,
62    /// New value (for modifications)
63    pub new_value: Option<Value>,
64    /// User or system context that triggered the event
65    pub user_context: Option<String>,
66    /// Additional contextual information
67    pub metadata: HashMap<String, String>,
68    /// Error message (for failures)
69    pub error_message: Option<String>,
70    /// Source location (file path, line number, etc.)
71    pub source: Option<String>,
72}
73
74impl AuditEvent {
75    /// Create a new audit event with minimal required fields
76    pub fn new(event_type: AuditEventType, severity: AuditSeverity) -> Self {
77        Self {
78            id: generate_event_id(),
79            timestamp: SystemTime::now(),
80            event_type,
81            severity,
82            key: None,
83            old_value: None,
84            new_value: None,
85            user_context: None,
86            metadata: HashMap::new(),
87            error_message: None,
88            source: None,
89        }
90    }
91
92    /// Set the configuration key for this event
93    pub fn with_key(mut self, key: impl Into<String>) -> Self {
94        self.key = Some(key.into());
95        self
96    }
97
98    /// Set the old value (for modifications)
99    pub fn with_old_value(mut self, value: Value) -> Self {
100        self.old_value = Some(value);
101        self
102    }
103
104    /// Set the new value (for modifications)
105    pub fn with_new_value(mut self, value: Value) -> Self {
106        self.new_value = Some(value);
107        self
108    }
109
110    /// Set the user context
111    pub fn with_user_context(mut self, context: impl Into<String>) -> Self {
112        self.user_context = Some(context.into());
113        self
114    }
115
116    /// Add metadata key-value pair
117    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
118        self.metadata.insert(key.into(), value.into());
119        self
120    }
121
122    /// Set error message
123    pub fn with_error(mut self, message: impl Into<String>) -> Self {
124        self.error_message = Some(message.into());
125        self
126    }
127
128    /// Set source location
129    pub fn with_source(mut self, source: impl Into<String>) -> Self {
130        self.source = Some(source.into());
131        self
132    }
133}
134
135impl fmt::Display for AuditEvent {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        let timestamp_millis = self
138            .timestamp
139            .duration_since(UNIX_EPOCH)
140            .unwrap_or_default()
141            .as_millis();
142
143        write!(
144            f,
145            "[{}] {:?}:{:?} id={} key={} user={}",
146            timestamp_millis,
147            self.event_type,
148            self.severity,
149            self.id,
150            self.key.as_deref().unwrap_or("none"),
151            self.user_context.as_deref().unwrap_or("system")
152        )?;
153
154        if let Some(error) = &self.error_message {
155            write!(f, " error=\"{error}\"")?;
156        }
157
158        if let (Some(old), Some(new)) = (&self.old_value, &self.new_value) {
159            write!(f, " change=\"{old:?}\" -> \"{new:?}\"")?;
160        }
161
162        for (key, value) in &self.metadata {
163            write!(f, " {key}=\"{value}\"")?;
164        }
165
166        Ok(())
167    }
168}
169
170/// Trait for audit log outputs/sinks
171pub trait AuditSink: Send + Sync {
172    /// Write an audit event to this sink
173    fn write_event(&self, event: &AuditEvent) -> Result<(), String>;
174
175    /// Flush any buffered events
176    fn flush(&self) -> Result<(), String>;
177}
178
179/// Console/stdout audit sink for development
180pub struct ConsoleSink {
181    level_filter: AuditSeverity,
182}
183
184impl ConsoleSink {
185    /// Create a new console sink with minimum severity level
186    pub fn new(min_level: AuditSeverity) -> Self {
187        Self {
188            level_filter: min_level,
189        }
190    }
191}
192
193impl AuditSink for ConsoleSink {
194    fn write_event(&self, event: &AuditEvent) -> Result<(), String> {
195        if event.severity >= self.level_filter {
196            // REPS-AUDIT: `ConsoleSink` writes audit events to stdout by
197            // contract — that is the sink's stated purpose. Allowed at the
198            // call site only; the crate-level `clippy::print_stdout` deny
199            // remains in force everywhere else.
200            #[allow(clippy::print_stdout)]
201            {
202                println!("AUDIT: {event}");
203            }
204        }
205        Ok(())
206    }
207
208    fn flush(&self) -> Result<(), String> {
209        Ok(()) // stdout auto-flushes
210    }
211}
212
213/// File-based audit sink for production
214pub struct FileSink {
215    file_path: String,
216    level_filter: AuditSeverity,
217}
218
219impl FileSink {
220    /// Create a new file sink
221    pub fn new(file_path: impl Into<String>, min_level: AuditSeverity) -> Self {
222        Self {
223            file_path: file_path.into(),
224            level_filter: min_level,
225        }
226    }
227}
228
229impl AuditSink for FileSink {
230    fn write_event(&self, event: &AuditEvent) -> Result<(), String> {
231        if event.severity >= self.level_filter {
232            use std::fs::OpenOptions;
233            use std::io::Write;
234
235            let mut file = OpenOptions::new()
236                .create(true)
237                .append(true)
238                .open(&self.file_path)
239                .map_err(|e| format!("Failed to open audit log file: {e}"))?;
240
241            writeln!(file, "{event}").map_err(|e| format!("Failed to write to audit log: {e}"))?;
242        }
243        Ok(())
244    }
245
246    fn flush(&self) -> Result<(), String> {
247        // For append-only files, OS handles flushing
248        Ok(())
249    }
250}
251
252/// Main audit logger with multiple sinks
253pub struct AuditLogger {
254    sinks: Vec<Box<dyn AuditSink>>,
255    enabled: bool,
256}
257
258impl AuditLogger {
259    /// Create a new audit logger
260    pub fn new() -> Self {
261        Self {
262            sinks: Vec::new(),
263            enabled: true,
264        }
265    }
266
267    /// Add a sink to the logger
268    pub fn add_sink(mut self, sink: Box<dyn AuditSink>) -> Self {
269        self.sinks.push(sink);
270        self
271    }
272
273    /// Enable or disable audit logging
274    pub fn set_enabled(mut self, enabled: bool) -> Self {
275        self.enabled = enabled;
276        self
277    }
278
279    /// Log an audit event to all configured sinks
280    pub fn log_event(&self, event: AuditEvent) {
281        if !self.enabled {
282            return;
283        }
284
285        for sink in &self.sinks {
286            if let Err(e) = sink.write_event(&event) {
287                // REPS-AUDIT: last-resort report when an audit sink itself
288                // fails. `log_event` is fire-and-forget (returns `()`); we
289                // cannot bubble the error up. Stderr is the conventional
290                // out-of-band channel for daemon diagnostics. Allowed at
291                // the call site only.
292                #[allow(clippy::print_stderr)]
293                {
294                    eprintln!("Audit sink error: {e}");
295                }
296            }
297        }
298    }
299
300    /// Log a configuration access event
301    pub fn log_access(&self, key: &str, user_context: Option<&str>) {
302        let event = AuditEvent::new(AuditEventType::Access, AuditSeverity::Info)
303            .with_key(key)
304            .with_metadata("operation", "get");
305
306        let event = if let Some(user) = user_context {
307            event.with_user_context(user)
308        } else {
309            event
310        };
311
312        self.log_event(event);
313    }
314
315    /// Log a configuration modification event
316    pub fn log_modification(
317        &self,
318        key: &str,
319        old_value: Option<&Value>,
320        new_value: &Value,
321        user_context: Option<&str>,
322    ) {
323        let mut event = AuditEvent::new(AuditEventType::Modification, AuditSeverity::Warning)
324            .with_key(key)
325            .with_new_value(new_value.clone())
326            .with_metadata("operation", "set");
327
328        if let Some(old) = old_value {
329            event = event.with_old_value(old.clone());
330        }
331
332        if let Some(user) = user_context {
333            event = event.with_user_context(user);
334        }
335
336        self.log_event(event);
337    }
338
339    /// Log a validation failure event
340    pub fn log_validation_failure(
341        &self,
342        key: &str,
343        error: &str,
344        value: &Value,
345        user_context: Option<&str>,
346    ) {
347        let event = AuditEvent::new(AuditEventType::ValidationFailure, AuditSeverity::Error)
348            .with_key(key)
349            .with_new_value(value.clone())
350            .with_error(error)
351            .with_metadata("operation", "validate");
352
353        let event = if let Some(user) = user_context {
354            event.with_user_context(user)
355        } else {
356            event
357        };
358
359        self.log_event(event);
360    }
361
362    /// Log a configuration reload event
363    pub fn log_reload(&self, source: &str, success: bool, error: Option<&str>) {
364        let severity = if success {
365            AuditSeverity::Info
366        } else {
367            AuditSeverity::Error
368        };
369        let mut event = AuditEvent::new(AuditEventType::Reload, severity)
370            .with_source(source)
371            .with_metadata("operation", "reload");
372
373        if let Some(err) = error {
374            event = event.with_error(err);
375        }
376
377        self.log_event(event);
378    }
379
380    /// Flush all sinks
381    pub fn flush(&self) {
382        for sink in &self.sinks {
383            if let Err(e) = sink.flush() {
384                // REPS-AUDIT: same rationale as `log_event` above —
385                // `flush` cannot return an error, so the only way to
386                // surface a sink-side failure is the daemon stderr
387                // channel. Allowed at the call site only.
388                #[allow(clippy::print_stderr)]
389                {
390                    eprintln!("Audit sink flush error: {e}");
391                }
392            }
393        }
394    }
395}
396
397impl Default for AuditLogger {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403/// Thread-safe global audit logger
404static GLOBAL_AUDIT_LOGGER: Mutex<Option<Arc<AuditLogger>>> = Mutex::new(None);
405
406/// Initialize the global audit logger
407pub fn init_audit_logger(logger: AuditLogger) {
408    if let Ok(mut global) = GLOBAL_AUDIT_LOGGER.lock() {
409        *global = Some(Arc::new(logger));
410    }
411    // If mutex is poisoned, we can't initialize the logger but we don't panic
412}
413
414/// Get the global audit logger
415pub fn get_audit_logger() -> Option<Arc<AuditLogger>> {
416    GLOBAL_AUDIT_LOGGER
417        .lock()
418        .ok()
419        .and_then(|guard| guard.clone())
420}
421
422/// Log an event using the global audit logger
423pub fn audit_log(event: AuditEvent) {
424    if let Some(logger) = get_audit_logger() {
425        logger.log_event(event);
426    }
427}
428
429/// Generate a unique event ID
430fn generate_event_id() -> String {
431    use std::sync::atomic::{AtomicU64, Ordering};
432    static COUNTER: AtomicU64 = AtomicU64::new(1);
433
434    let timestamp = SystemTime::now()
435        .duration_since(UNIX_EPOCH)
436        .unwrap_or_default()
437        .as_secs();
438    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
439
440    format!("{timestamp:x}-{counter:x}")
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use std::sync::{Arc, Mutex};
447
448    struct TestSink {
449        events: Arc<Mutex<Vec<AuditEvent>>>,
450    }
451
452    impl TestSink {
453        fn new() -> (Self, Arc<Mutex<Vec<AuditEvent>>>) {
454            let events = Arc::new(Mutex::new(Vec::new()));
455            (
456                Self {
457                    events: Arc::clone(&events),
458                },
459                events,
460            )
461        }
462    }
463
464    impl AuditSink for TestSink {
465        fn write_event(&self, event: &AuditEvent) -> Result<(), String> {
466            self.events.lock().unwrap().push(event.clone());
467            Ok(())
468        }
469
470        fn flush(&self) -> Result<(), String> {
471            Ok(())
472        }
473    }
474
475    #[test]
476    fn test_audit_event_creation() {
477        let event = AuditEvent::new(AuditEventType::Access, AuditSeverity::Info)
478            .with_key("test.key")
479            .with_user_context("test_user")
480            .with_metadata("operation", "get");
481
482        assert_eq!(event.event_type, AuditEventType::Access);
483        assert_eq!(event.severity, AuditSeverity::Info);
484        assert_eq!(event.key, Some("test.key".to_string()));
485        assert_eq!(event.user_context, Some("test_user".to_string()));
486        assert_eq!(event.metadata.get("operation"), Some(&"get".to_string()));
487    }
488
489    #[test]
490    fn test_audit_logger_basic() {
491        let (sink, events) = TestSink::new();
492        let logger = AuditLogger::new().add_sink(Box::new(sink));
493
494        logger.log_access("test.key", Some("test_user"));
495        logger.log_modification(
496            "test.key",
497            None,
498            &Value::String("new_value".to_string()),
499            Some("test_user"),
500        );
501
502        let events = events.lock().unwrap();
503        assert_eq!(events.len(), 2);
504
505        assert_eq!(events[0].event_type, AuditEventType::Access);
506        assert_eq!(events[0].key, Some("test.key".to_string()));
507
508        assert_eq!(events[1].event_type, AuditEventType::Modification);
509        assert_eq!(events[1].key, Some("test.key".to_string()));
510    }
511
512    #[test]
513    fn test_console_sink() {
514        let sink = ConsoleSink::new(AuditSeverity::Info);
515        let event =
516            AuditEvent::new(AuditEventType::Access, AuditSeverity::Info).with_key("test.key");
517
518        // This should not panic
519        assert!(sink.write_event(&event).is_ok());
520    }
521
522    #[test]
523    fn test_event_display() {
524        let event = AuditEvent::new(AuditEventType::Modification, AuditSeverity::Warning)
525            .with_key("test.key")
526            .with_user_context("test_user")
527            .with_old_value(Value::String("old".to_string()))
528            .with_new_value(Value::String("new".to_string()))
529            .with_metadata("operation", "set");
530
531        let display = format!("{event}");
532        assert!(display.contains("Modification"));
533        assert!(display.contains("Warning"));
534        assert!(display.contains("test.key"));
535        assert!(display.contains("test_user"));
536    }
537
538    #[test]
539    fn test_severity_filtering() {
540        let (sink, events) = TestSink::new();
541        let logger = AuditLogger::new().add_sink(Box::new(sink));
542
543        // Log events of different severities
544        logger.log_event(AuditEvent::new(AuditEventType::Access, AuditSeverity::Info));
545        logger.log_event(AuditEvent::new(
546            AuditEventType::ValidationFailure,
547            AuditSeverity::Error,
548        ));
549
550        let events = events.lock().unwrap();
551        assert_eq!(events.len(), 2);
552        assert_eq!(events[0].severity, AuditSeverity::Info);
553        assert_eq!(events[1].severity, AuditSeverity::Error);
554    }
555}