⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/security/
audit_enhanced.rs

1//! Enhanced audit logging with structured entries, ring buffer, and filtering
2//!
3//! Provides a next-generation audit subsystem for VeridianOS with:
4//! - Structured log entries with timestamps, PIDs, TIDs, categories, severity
5//! - Ring buffer storage with configurable capacity (default 8192 entries)
6//! - Multi-dimensional filtering (category, severity, PID, time range)
7//! - Event coalescing for repeated identical events within 1 second
8//! - Thread-safe access via `spin::RwLock`
9//!
10//! This module complements the existing `security::audit` module by adding
11//! richer categorization, severity levels, and query capabilities.
12
13#[cfg(feature = "alloc")]
14extern crate alloc;
15
16#[cfg(feature = "alloc")]
17use alloc::{collections::VecDeque, string::String, vec::Vec};
18use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19
20use spin::RwLock;
21
22// ---------------------------------------------------------------------------
23// Audit Category
24// ---------------------------------------------------------------------------
25
26/// Category of an audit event, enabling fine-grained filtering.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[repr(u8)]
29pub enum AuditCategory {
30    /// Authentication events (login, logout, credential checks)
31    Authentication = 0,
32    /// Authorization / access control decisions
33    Authorization = 1,
34    /// File and directory access
35    FileAccess = 2,
36    /// Network connection, bind, send, receive
37    NetworkAccess = 3,
38    /// Process/thread creation and termination
39    ProcessLifecycle = 4,
40    /// Capability create, delegate, revoke, derive
41    CapabilityOps = 5,
42    /// Security policy changes (MAC, filter updates)
43    SecurityPolicy = 6,
44    /// System call audit trail
45    SystemCall = 7,
46}
47
48impl AuditCategory {
49    /// Convert to a bitmask flag for filtering.
50    pub fn to_flag(self) -> u16 {
51        1u16 << (self as u8)
52    }
53
54    /// Human-readable name.
55    pub fn as_str(self) -> &'static str {
56        match self {
57            Self::Authentication => "AUTH",
58            Self::Authorization => "AUTHZ",
59            Self::FileAccess => "FILE",
60            Self::NetworkAccess => "NET",
61            Self::ProcessLifecycle => "PROC",
62            Self::CapabilityOps => "CAP",
63            Self::SecurityPolicy => "POLICY",
64            Self::SystemCall => "SYSCALL",
65        }
66    }
67
68    /// Total number of categories (for array sizing).
69    const COUNT: usize = 8;
70}
71
72// ---------------------------------------------------------------------------
73// Audit Severity
74// ---------------------------------------------------------------------------
75
76/// Severity level for an audit event.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
78#[repr(u8)]
79pub enum AuditSeverity {
80    /// Informational, normal operation
81    Info = 0,
82    /// Warning, unusual but non-critical
83    Warning = 1,
84    /// Error, operation failed
85    Error = 2,
86    /// Critical, security-relevant failure
87    Critical = 3,
88}
89
90impl AuditSeverity {
91    /// Human-readable name.
92    pub fn as_str(self) -> &'static str {
93        match self {
94            Self::Info => "INFO",
95            Self::Warning => "WARN",
96            Self::Error => "ERROR",
97            Self::Critical => "CRIT",
98        }
99    }
100}
101
102// ---------------------------------------------------------------------------
103// Audit Entry
104// ---------------------------------------------------------------------------
105
106/// A structured audit log entry.
107#[derive(Debug, Clone)]
108pub struct AuditEntry {
109    /// Monotonic sequence number (unique per entry)
110    pub sequence: u64,
111    /// Timestamp in seconds since boot
112    pub timestamp: u64,
113    /// Process ID that generated the event
114    pub pid: u64,
115    /// Thread ID within the process
116    pub tid: u64,
117    /// Event category
118    pub category: AuditCategory,
119    /// Severity level
120    pub severity: AuditSeverity,
121    /// Short description of the event
122    pub message: String,
123    /// Whether the operation succeeded
124    pub success: bool,
125    /// How many times this event was coalesced (1 = no coalescing)
126    pub coalesce_count: u32,
127}
128
129impl AuditEntry {
130    /// Serialize to pipe-delimited text format.
131    ///
132    /// Format: `seq|timestamp|pid|tid|category|severity|success|count|message\
133    /// n`
134    #[cfg(feature = "alloc")]
135    pub fn serialize(&self) -> String {
136        alloc::format!(
137            "{}|{}|{}|{}|{}|{}|{}|{}|{}\n",
138            self.sequence,
139            self.timestamp,
140            self.pid,
141            self.tid,
142            self.category.as_str(),
143            self.severity.as_str(),
144            if self.success { "OK" } else { "FAIL" },
145            self.coalesce_count,
146            self.message,
147        )
148    }
149}
150
151// ---------------------------------------------------------------------------
152// Audit Filter
153// ---------------------------------------------------------------------------
154
155/// Multi-dimensional filter for querying audit events.
156#[derive(Debug, Clone, Copy)]
157pub struct AuditQueryFilter {
158    /// Bitmask of enabled categories (0 = match all)
159    pub category_mask: u16,
160    /// Minimum severity to include (None = all)
161    pub min_severity: Option<AuditSeverity>,
162    /// Filter by PID (0 = match all)
163    pub pid: u64,
164    /// Start of time range (0 = no lower bound)
165    pub time_min: u64,
166    /// End of time range (0 = no upper bound)
167    pub time_max: u64,
168    /// Only include failures (false = include all)
169    pub failures_only: bool,
170}
171
172impl AuditQueryFilter {
173    /// Create a filter that matches everything.
174    pub const fn match_all() -> Self {
175        Self {
176            category_mask: 0,
177            min_severity: None,
178            pid: 0,
179            time_min: 0,
180            time_max: 0,
181            failures_only: false,
182        }
183    }
184
185    /// Check if a given entry passes this filter.
186    pub fn matches(&self, entry: &AuditEntry) -> bool {
187        // Category filter
188        if self.category_mask != 0 && (self.category_mask & entry.category.to_flag()) == 0 {
189            return false;
190        }
191
192        // Severity filter
193        if let Some(min) = self.min_severity {
194            if (entry.severity as u8) < (min as u8) {
195                return false;
196            }
197        }
198
199        // PID filter
200        if self.pid != 0 && entry.pid != self.pid {
201            return false;
202        }
203
204        // Time range filter
205        if self.time_min != 0 && entry.timestamp < self.time_min {
206            return false;
207        }
208        if self.time_max != 0 && entry.timestamp > self.time_max {
209            return false;
210        }
211
212        // Failures-only filter
213        if self.failures_only && entry.success {
214            return false;
215        }
216
217        true
218    }
219}
220
221// ---------------------------------------------------------------------------
222// Active Filter (controls which events get logged)
223// ---------------------------------------------------------------------------
224
225/// Active filter controlling which events are accepted into the log.
226#[derive(Debug, Clone, Copy)]
227pub struct AuditActiveFilter {
228    /// Bitmask of enabled categories (all bits set = log everything)
229    pub category_mask: u16,
230    /// Minimum severity to log
231    pub min_severity: AuditSeverity,
232}
233
234impl AuditActiveFilter {
235    /// Create a filter that accepts all events.
236    pub const fn accept_all() -> Self {
237        Self {
238            category_mask: 0xFFFF,
239            min_severity: AuditSeverity::Info,
240        }
241    }
242
243    /// Check if an event with the given category and severity should be logged.
244    pub fn should_log(&self, category: AuditCategory, severity: AuditSeverity) -> bool {
245        (self.category_mask & category.to_flag()) != 0
246            && (severity as u8) >= (self.min_severity as u8)
247    }
248}
249
250// ---------------------------------------------------------------------------
251// Audit Statistics
252// ---------------------------------------------------------------------------
253
254/// Statistics for the enhanced audit log.
255#[derive(Debug, Clone, Copy)]
256pub struct EnhancedAuditStats {
257    /// Total events logged (including coalesced)
258    pub total_logged: u64,
259    /// Events dropped because the buffer was full and coalescing didn't apply
260    pub total_dropped: u64,
261    /// Events filtered out by the active filter
262    pub total_filtered: u64,
263    /// Events that were coalesced into a previous entry
264    pub total_coalesced: u64,
265    /// Current number of entries in the ring buffer
266    pub buffer_count: u64,
267    /// Maximum capacity of the ring buffer
268    pub buffer_capacity: u64,
269    /// Per-category event counts
270    pub per_category: [u64; AuditCategory::COUNT],
271}
272
273// ---------------------------------------------------------------------------
274// Audit Log (Ring Buffer)
275// ---------------------------------------------------------------------------
276
277/// Default ring buffer capacity.
278const DEFAULT_CAPACITY: usize = 8192;
279
280/// Window (in seconds) within which identical events are coalesced.
281const COALESCE_WINDOW_SECS: u64 = 1;
282
283/// The enhanced audit log ring buffer.
284#[cfg(feature = "alloc")]
285struct AuditLog {
286    /// Ring buffer of entries
287    entries: VecDeque<AuditEntry>,
288    /// Maximum capacity
289    capacity: usize,
290    /// Active filter
291    active_filter: AuditActiveFilter,
292    /// Next sequence number
293    next_sequence: u64,
294}
295
296#[cfg(feature = "alloc")]
297impl AuditLog {
298    /// Create a new audit log with the given capacity.
299    fn new(capacity: usize) -> Self {
300        Self {
301            entries: VecDeque::with_capacity(capacity),
302            capacity,
303            active_filter: AuditActiveFilter::accept_all(),
304            next_sequence: 1,
305        }
306    }
307
308    /// Insert an entry, possibly coalescing with the most recent matching
309    /// entry.
310    ///
311    /// Returns `true` if a new entry was inserted, `false` if coalesced.
312    fn insert(&mut self, mut entry: AuditEntry) -> bool {
313        // Try coalescing: check last entry for identical category+pid+message within
314        // window
315        if let Some(last) = self.entries.back_mut() {
316            if last.category == entry.category
317                && last.pid == entry.pid
318                && last.success == entry.success
319                && last.message == entry.message
320                && entry.timestamp.saturating_sub(last.timestamp) <= COALESCE_WINDOW_SECS
321            {
322                last.coalesce_count = last.coalesce_count.saturating_add(1);
323                // Update timestamp to most recent occurrence
324                last.timestamp = entry.timestamp;
325                return false;
326            }
327        }
328
329        // Assign sequence number
330        entry.sequence = self.next_sequence;
331        self.next_sequence = self.next_sequence.wrapping_add(1);
332        entry.coalesce_count = 1;
333
334        // Evict oldest if at capacity
335        if self.entries.len() >= self.capacity {
336            self.entries.pop_front();
337        }
338
339        self.entries.push_back(entry);
340        true
341    }
342
343    /// Query entries matching a filter.
344    fn query(&self, filter: &AuditQueryFilter, max_results: usize) -> Vec<AuditEntry> {
345        let mut results = Vec::new();
346        for entry in self.entries.iter().rev() {
347            if results.len() >= max_results {
348                break;
349            }
350            if filter.matches(entry) {
351                results.push(entry.clone());
352            }
353        }
354        // Reverse so oldest is first
355        results.reverse();
356        results
357    }
358
359    /// Clear all entries.
360    fn clear(&mut self) {
361        self.entries.clear();
362        self.next_sequence = 1;
363    }
364
365    /// Number of entries currently stored.
366    fn len(&self) -> usize {
367        self.entries.len()
368    }
369}
370
371// ---------------------------------------------------------------------------
372// Global State
373// ---------------------------------------------------------------------------
374
375/// Global enhanced audit log, protected by RwLock for concurrent access.
376#[cfg(feature = "alloc")]
377static AUDIT_LOG: RwLock<Option<AuditLog>> = RwLock::new(None);
378
379/// Whether the enhanced audit subsystem is enabled.
380static ENABLED: AtomicBool = AtomicBool::new(false);
381
382/// Monotonic counters for statistics (lock-free).
383static STAT_LOGGED: AtomicU64 = AtomicU64::new(0);
384static STAT_DROPPED: AtomicU64 = AtomicU64::new(0);
385static STAT_FILTERED: AtomicU64 = AtomicU64::new(0);
386static STAT_COALESCED: AtomicU64 = AtomicU64::new(0);
387static PER_CATEGORY_COUNTS: [AtomicU64; AuditCategory::COUNT] = [
388    AtomicU64::new(0),
389    AtomicU64::new(0),
390    AtomicU64::new(0),
391    AtomicU64::new(0),
392    AtomicU64::new(0),
393    AtomicU64::new(0),
394    AtomicU64::new(0),
395    AtomicU64::new(0),
396];
397
398// ---------------------------------------------------------------------------
399// Timestamp Helper
400// ---------------------------------------------------------------------------
401
402/// Get a timestamp for audit events (seconds since boot).
403fn get_timestamp() -> u64 {
404    #[cfg(any(
405        target_arch = "x86_64",
406        target_arch = "aarch64",
407        target_arch = "riscv64"
408    ))]
409    {
410        crate::arch::timer::get_timestamp_secs()
411    }
412    #[cfg(not(any(
413        target_arch = "x86_64",
414        target_arch = "aarch64",
415        target_arch = "riscv64"
416    )))]
417    {
418        0
419    }
420}
421
422// ---------------------------------------------------------------------------
423// Public API
424// ---------------------------------------------------------------------------
425
426/// Initialize the enhanced audit subsystem.
427#[cfg(feature = "alloc")]
428pub fn init() {
429    let mut log = AUDIT_LOG.write();
430    *log = Some(AuditLog::new(DEFAULT_CAPACITY));
431    ENABLED.store(true, Ordering::Release);
432}
433
434/// Initialize with a custom capacity.
435#[cfg(feature = "alloc")]
436pub fn init_with_capacity(capacity: usize) {
437    let cap = if capacity == 0 {
438        DEFAULT_CAPACITY
439    } else {
440        capacity
441    };
442    let mut log = AUDIT_LOG.write();
443    *log = Some(AuditLog::new(cap));
444    ENABLED.store(true, Ordering::Release);
445}
446
447/// Log a structured audit event.
448///
449/// This is the primary entry point. The event is checked against the active
450/// filter. If accepted, it is inserted into the ring buffer (with coalescing).
451/// Uses `try_write()` to avoid deadlocks in interrupt context.
452#[cfg(feature = "alloc")]
453pub fn log_event(
454    pid: u64,
455    tid: u64,
456    category: AuditCategory,
457    severity: AuditSeverity,
458    message: String,
459    success: bool,
460) {
461    if !ENABLED.load(Ordering::Acquire) {
462        return;
463    }
464
465    // Acquire the log with try_write to avoid deadlock
466    let mut log_guard = match AUDIT_LOG.try_write() {
467        Some(g) => g,
468        None => {
469            STAT_DROPPED.fetch_add(1, Ordering::Relaxed);
470            return;
471        }
472    };
473
474    let log = match log_guard.as_mut() {
475        Some(l) => l,
476        None => {
477            STAT_DROPPED.fetch_add(1, Ordering::Relaxed);
478            return;
479        }
480    };
481
482    // Check active filter
483    if !log.active_filter.should_log(category, severity) {
484        STAT_FILTERED.fetch_add(1, Ordering::Relaxed);
485        return;
486    }
487
488    let entry = AuditEntry {
489        sequence: 0, // Will be assigned by insert()
490        timestamp: get_timestamp(),
491        pid,
492        tid,
493        category,
494        severity,
495        message,
496        success,
497        coalesce_count: 1,
498    };
499
500    let inserted = log.insert(entry);
501
502    // Update statistics
503    STAT_LOGGED.fetch_add(1, Ordering::Relaxed);
504    let cat_idx = category as usize;
505    if cat_idx < PER_CATEGORY_COUNTS.len() {
506        PER_CATEGORY_COUNTS[cat_idx].fetch_add(1, Ordering::Relaxed);
507    }
508
509    if !inserted {
510        STAT_COALESCED.fetch_add(1, Ordering::Relaxed);
511    }
512}
513
514/// Query audit events matching a filter.
515///
516/// Returns up to `max_results` entries in chronological order (oldest first).
517#[cfg(feature = "alloc")]
518pub fn query_events(filter: &AuditQueryFilter, max_results: usize) -> Vec<AuditEntry> {
519    let log_guard = AUDIT_LOG.read();
520    match log_guard.as_ref() {
521        Some(log) => log.query(filter, max_results),
522        None => Vec::new(),
523    }
524}
525
526/// Clear all audit log entries.
527#[cfg(feature = "alloc")]
528pub fn clear_log() {
529    let mut log_guard = AUDIT_LOG.write();
530    if let Some(log) = log_guard.as_mut() {
531        log.clear();
532    }
533}
534
535/// Set the active filter that controls which events are logged.
536#[cfg(feature = "alloc")]
537pub fn set_filter(filter: AuditActiveFilter) {
538    let mut log_guard = AUDIT_LOG.write();
539    if let Some(log) = log_guard.as_mut() {
540        log.active_filter = filter;
541    }
542}
543
544/// Get the current active filter.
545#[cfg(feature = "alloc")]
546pub fn get_filter() -> AuditActiveFilter {
547    let log_guard = AUDIT_LOG.read();
548    match log_guard.as_ref() {
549        Some(log) => log.active_filter,
550        None => AuditActiveFilter::accept_all(),
551    }
552}
553
554/// Get audit statistics.
555pub fn get_stats() -> EnhancedAuditStats {
556    let buffer_count;
557    let buffer_capacity;
558
559    #[cfg(feature = "alloc")]
560    {
561        let log_guard = AUDIT_LOG.read();
562        match log_guard.as_ref() {
563            Some(log) => {
564                buffer_count = log.len() as u64;
565                buffer_capacity = log.capacity as u64;
566            }
567            None => {
568                buffer_count = 0;
569                buffer_capacity = 0;
570            }
571        }
572    }
573
574    #[cfg(not(feature = "alloc"))]
575    {
576        buffer_count = 0;
577        buffer_capacity = 0;
578    }
579
580    let mut per_category = [0u64; AuditCategory::COUNT];
581    for (i, counter) in PER_CATEGORY_COUNTS.iter().enumerate() {
582        per_category[i] = counter.load(Ordering::Relaxed);
583    }
584
585    EnhancedAuditStats {
586        total_logged: STAT_LOGGED.load(Ordering::Relaxed),
587        total_dropped: STAT_DROPPED.load(Ordering::Relaxed),
588        total_filtered: STAT_FILTERED.load(Ordering::Relaxed),
589        total_coalesced: STAT_COALESCED.load(Ordering::Relaxed),
590        buffer_count,
591        buffer_capacity,
592        per_category,
593    }
594}
595
596/// Enable the enhanced audit subsystem.
597pub fn enable() {
598    ENABLED.store(true, Ordering::Release);
599}
600
601/// Disable the enhanced audit subsystem.
602pub fn disable() {
603    ENABLED.store(false, Ordering::Release);
604}
605
606/// Check if the enhanced audit subsystem is enabled.
607pub fn is_enabled() -> bool {
608    ENABLED.load(Ordering::Acquire)
609}
610
611// ---------------------------------------------------------------------------
612// Convenience logging functions
613// ---------------------------------------------------------------------------
614
615/// Log an authentication event.
616#[cfg(feature = "alloc")]
617pub fn log_auth(pid: u64, tid: u64, message: String, success: bool) {
618    let severity = if success {
619        AuditSeverity::Info
620    } else {
621        AuditSeverity::Warning
622    };
623    log_event(
624        pid,
625        tid,
626        AuditCategory::Authentication,
627        severity,
628        message,
629        success,
630    );
631}
632
633/// Log an authorization / access control event.
634#[cfg(feature = "alloc")]
635pub fn log_authz(pid: u64, tid: u64, message: String, success: bool) {
636    let severity = if success {
637        AuditSeverity::Info
638    } else {
639        AuditSeverity::Error
640    };
641    log_event(
642        pid,
643        tid,
644        AuditCategory::Authorization,
645        severity,
646        message,
647        success,
648    );
649}
650
651/// Log a file access event.
652#[cfg(feature = "alloc")]
653pub fn log_file(pid: u64, tid: u64, message: String, success: bool) {
654    log_event(
655        pid,
656        tid,
657        AuditCategory::FileAccess,
658        AuditSeverity::Info,
659        message,
660        success,
661    );
662}
663
664/// Log a network access event.
665#[cfg(feature = "alloc")]
666pub fn log_network(pid: u64, tid: u64, message: String, success: bool) {
667    log_event(
668        pid,
669        tid,
670        AuditCategory::NetworkAccess,
671        AuditSeverity::Info,
672        message,
673        success,
674    );
675}
676
677/// Log a process lifecycle event.
678#[cfg(feature = "alloc")]
679pub fn log_process(pid: u64, tid: u64, message: String, success: bool) {
680    log_event(
681        pid,
682        tid,
683        AuditCategory::ProcessLifecycle,
684        AuditSeverity::Info,
685        message,
686        success,
687    );
688}
689
690/// Log a capability operation event.
691#[cfg(feature = "alloc")]
692pub fn log_capability(pid: u64, tid: u64, message: String, success: bool) {
693    let severity = if success {
694        AuditSeverity::Info
695    } else {
696        AuditSeverity::Warning
697    };
698    log_event(
699        pid,
700        tid,
701        AuditCategory::CapabilityOps,
702        severity,
703        message,
704        success,
705    );
706}
707
708/// Log a security policy change.
709#[cfg(feature = "alloc")]
710pub fn log_policy(pid: u64, tid: u64, message: String, success: bool) {
711    log_event(
712        pid,
713        tid,
714        AuditCategory::SecurityPolicy,
715        AuditSeverity::Critical,
716        message,
717        success,
718    );
719}
720
721/// Log a system call audit event.
722#[cfg(feature = "alloc")]
723pub fn log_syscall(pid: u64, tid: u64, message: String, success: bool) {
724    log_event(
725        pid,
726        tid,
727        AuditCategory::SystemCall,
728        AuditSeverity::Info,
729        message,
730        success,
731    );
732}
733
734// ---------------------------------------------------------------------------
735// Tests
736// ---------------------------------------------------------------------------
737
738#[cfg(test)]
739mod tests {
740    #[cfg(feature = "alloc")]
741    #[allow(unused_imports)]
742    use alloc::string::ToString;
743
744    use super::*;
745
746    #[test]
747    fn test_category_flags() {
748        assert_eq!(AuditCategory::Authentication.to_flag(), 1);
749        assert_eq!(AuditCategory::Authorization.to_flag(), 2);
750        assert_eq!(AuditCategory::FileAccess.to_flag(), 4);
751        assert_eq!(AuditCategory::SystemCall.to_flag(), 128);
752    }
753
754    #[test]
755    fn test_severity_ordering() {
756        assert!(AuditSeverity::Info < AuditSeverity::Warning);
757        assert!(AuditSeverity::Warning < AuditSeverity::Error);
758        assert!(AuditSeverity::Error < AuditSeverity::Critical);
759    }
760
761    #[test]
762    fn test_active_filter_accept_all() {
763        let filter = AuditActiveFilter::accept_all();
764        assert!(filter.should_log(AuditCategory::Authentication, AuditSeverity::Info));
765        assert!(filter.should_log(AuditCategory::SystemCall, AuditSeverity::Critical));
766    }
767
768    #[test]
769    fn test_active_filter_severity() {
770        let filter = AuditActiveFilter {
771            category_mask: 0xFFFF,
772            min_severity: AuditSeverity::Warning,
773        };
774        assert!(!filter.should_log(AuditCategory::FileAccess, AuditSeverity::Info));
775        assert!(filter.should_log(AuditCategory::FileAccess, AuditSeverity::Warning));
776        assert!(filter.should_log(AuditCategory::FileAccess, AuditSeverity::Critical));
777    }
778
779    #[test]
780    fn test_active_filter_category() {
781        let filter = AuditActiveFilter {
782            category_mask: AuditCategory::Authentication.to_flag()
783                | AuditCategory::CapabilityOps.to_flag(),
784            min_severity: AuditSeverity::Info,
785        };
786        assert!(filter.should_log(AuditCategory::Authentication, AuditSeverity::Info));
787        assert!(filter.should_log(AuditCategory::CapabilityOps, AuditSeverity::Info));
788        assert!(!filter.should_log(AuditCategory::FileAccess, AuditSeverity::Info));
789    }
790
791    #[test]
792    fn test_query_filter_match_all() {
793        let filter = AuditQueryFilter::match_all();
794        let entry = AuditEntry {
795            sequence: 1,
796            timestamp: 100,
797            pid: 42,
798            tid: 1,
799            category: AuditCategory::FileAccess,
800            severity: AuditSeverity::Info,
801            message: String::from("test"),
802            success: true,
803            coalesce_count: 1,
804        };
805        assert!(filter.matches(&entry));
806    }
807
808    #[test]
809    fn test_query_filter_pid() {
810        let filter = AuditQueryFilter {
811            pid: 42,
812            ..AuditQueryFilter::match_all()
813        };
814        let entry_match = AuditEntry {
815            sequence: 1,
816            timestamp: 100,
817            pid: 42,
818            tid: 1,
819            category: AuditCategory::FileAccess,
820            severity: AuditSeverity::Info,
821            message: String::from("test"),
822            success: true,
823            coalesce_count: 1,
824        };
825        let entry_no_match = AuditEntry {
826            pid: 99,
827            ..entry_match.clone()
828        };
829        assert!(filter.matches(&entry_match));
830        assert!(!filter.matches(&entry_no_match));
831    }
832
833    #[test]
834    fn test_query_filter_time_range() {
835        let filter = AuditQueryFilter {
836            time_min: 50,
837            time_max: 150,
838            ..AuditQueryFilter::match_all()
839        };
840        let make_entry = |ts: u64| AuditEntry {
841            sequence: 1,
842            timestamp: ts,
843            pid: 1,
844            tid: 1,
845            category: AuditCategory::FileAccess,
846            severity: AuditSeverity::Info,
847            message: String::from("test"),
848            success: true,
849            coalesce_count: 1,
850        };
851        assert!(!filter.matches(&make_entry(10)));
852        assert!(filter.matches(&make_entry(50)));
853        assert!(filter.matches(&make_entry(100)));
854        assert!(filter.matches(&make_entry(150)));
855        assert!(!filter.matches(&make_entry(200)));
856    }
857
858    #[test]
859    fn test_query_filter_failures_only() {
860        let filter = AuditQueryFilter {
861            failures_only: true,
862            ..AuditQueryFilter::match_all()
863        };
864        let success = AuditEntry {
865            sequence: 1,
866            timestamp: 100,
867            pid: 1,
868            tid: 1,
869            category: AuditCategory::Authentication,
870            severity: AuditSeverity::Info,
871            message: String::from("login"),
872            success: true,
873            coalesce_count: 1,
874        };
875        let failure = AuditEntry {
876            success: false,
877            ..success.clone()
878        };
879        assert!(!filter.matches(&success));
880        assert!(filter.matches(&failure));
881    }
882
883    #[cfg(feature = "alloc")]
884    #[test]
885    fn test_audit_log_insert_and_query() {
886        let mut log = AuditLog::new(16);
887        let entry = AuditEntry {
888            sequence: 0,
889            timestamp: 100,
890            pid: 1,
891            tid: 1,
892            category: AuditCategory::FileAccess,
893            severity: AuditSeverity::Info,
894            message: String::from("open /etc/passwd"),
895            success: true,
896            coalesce_count: 1,
897        };
898        assert!(log.insert(entry));
899        assert_eq!(log.len(), 1);
900
901        let results = log.query(&AuditQueryFilter::match_all(), 100);
902        assert_eq!(results.len(), 1);
903        assert_eq!(results[0].sequence, 1);
904    }
905
906    #[cfg(feature = "alloc")]
907    #[test]
908    fn test_audit_log_coalescing() {
909        let mut log = AuditLog::new(16);
910        let entry1 = AuditEntry {
911            sequence: 0,
912            timestamp: 100,
913            pid: 1,
914            tid: 1,
915            category: AuditCategory::FileAccess,
916            severity: AuditSeverity::Info,
917            message: String::from("read /tmp/data"),
918            success: true,
919            coalesce_count: 1,
920        };
921        let entry2 = AuditEntry {
922            timestamp: 100, // Same second
923            ..entry1.clone()
924        };
925
926        assert!(log.insert(entry1));
927        assert!(!log.insert(entry2)); // Should coalesce
928        assert_eq!(log.len(), 1);
929
930        let results = log.query(&AuditQueryFilter::match_all(), 100);
931        assert_eq!(results[0].coalesce_count, 2);
932    }
933
934    #[cfg(feature = "alloc")]
935    #[test]
936    fn test_audit_log_no_coalesce_different_message() {
937        let mut log = AuditLog::new(16);
938        let entry1 = AuditEntry {
939            sequence: 0,
940            timestamp: 100,
941            pid: 1,
942            tid: 1,
943            category: AuditCategory::FileAccess,
944            severity: AuditSeverity::Info,
945            message: String::from("read /tmp/a"),
946            success: true,
947            coalesce_count: 1,
948        };
949        let entry2 = AuditEntry {
950            message: String::from("read /tmp/b"),
951            ..entry1.clone()
952        };
953
954        assert!(log.insert(entry1));
955        assert!(log.insert(entry2)); // Different message, no coalesce
956        assert_eq!(log.len(), 2);
957    }
958
959    #[cfg(feature = "alloc")]
960    #[test]
961    fn test_audit_log_ring_buffer_eviction() {
962        let mut log = AuditLog::new(4);
963        for i in 0..6 {
964            let entry = AuditEntry {
965                sequence: 0,
966                timestamp: i * 10, // Different timestamps to prevent coalescing
967                pid: i,
968                tid: 1,
969                category: AuditCategory::ProcessLifecycle,
970                severity: AuditSeverity::Info,
971                message: alloc::format!("event {}", i),
972                success: true,
973                coalesce_count: 1,
974            };
975            log.insert(entry);
976        }
977        // Should have evicted first 2 entries
978        assert_eq!(log.len(), 4);
979
980        let results = log.query(&AuditQueryFilter::match_all(), 100);
981        // Oldest remaining should be entry with pid=2
982        assert_eq!(results[0].pid, 2);
983        assert_eq!(results[3].pid, 5);
984    }
985
986    #[cfg(feature = "alloc")]
987    #[test]
988    fn test_audit_log_clear() {
989        let mut log = AuditLog::new(16);
990        let entry = AuditEntry {
991            sequence: 0,
992            timestamp: 100,
993            pid: 1,
994            tid: 1,
995            category: AuditCategory::Authentication,
996            severity: AuditSeverity::Info,
997            message: String::from("login root"),
998            success: true,
999            coalesce_count: 1,
1000        };
1001        log.insert(entry);
1002        assert_eq!(log.len(), 1);
1003
1004        log.clear();
1005        assert_eq!(log.len(), 0);
1006    }
1007
1008    #[cfg(feature = "alloc")]
1009    #[test]
1010    fn test_entry_serialize() {
1011        let entry = AuditEntry {
1012            sequence: 42,
1013            timestamp: 1000,
1014            pid: 123,
1015            tid: 5,
1016            category: AuditCategory::Authentication,
1017            severity: AuditSeverity::Warning,
1018            message: String::from("failed login"),
1019            success: false,
1020            coalesce_count: 3,
1021        };
1022        let s = entry.serialize();
1023        assert!(s.contains("42"));
1024        assert!(s.contains("1000"));
1025        assert!(s.contains("123"));
1026        assert!(s.contains("AUTH"));
1027        assert!(s.contains("WARN"));
1028        assert!(s.contains("FAIL"));
1029        assert!(s.contains("3"));
1030        assert!(s.contains("failed login"));
1031    }
1032
1033    #[test]
1034    fn test_stats_initial() {
1035        let stats = get_stats();
1036        // Stats may have been modified by other tests running in parallel,
1037        // but capacity should be consistent
1038        assert!(stats.per_category.len() == AuditCategory::COUNT);
1039    }
1040}