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