1#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[repr(u8)]
29pub enum AuditCategory {
30 Authentication = 0,
32 Authorization = 1,
34 FileAccess = 2,
36 NetworkAccess = 3,
38 ProcessLifecycle = 4,
40 CapabilityOps = 5,
42 SecurityPolicy = 6,
44 SystemCall = 7,
46}
47
48impl AuditCategory {
49 pub fn to_flag(self) -> u16 {
51 1u16 << (self as u8)
52 }
53
54 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 const COUNT: usize = 8;
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
78#[repr(u8)]
79pub enum AuditSeverity {
80 Info = 0,
82 Warning = 1,
84 Error = 2,
86 Critical = 3,
88}
89
90impl AuditSeverity {
91 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#[derive(Debug, Clone)]
108pub struct AuditEntry {
109 pub sequence: u64,
111 pub timestamp: u64,
113 pub pid: u64,
115 pub tid: u64,
117 pub category: AuditCategory,
119 pub severity: AuditSeverity,
121 pub message: String,
123 pub success: bool,
125 pub coalesce_count: u32,
127}
128
129impl AuditEntry {
130 #[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#[derive(Debug, Clone, Copy)]
157pub struct AuditQueryFilter {
158 pub category_mask: u16,
160 pub min_severity: Option<AuditSeverity>,
162 pub pid: u64,
164 pub time_min: u64,
166 pub time_max: u64,
168 pub failures_only: bool,
170}
171
172impl AuditQueryFilter {
173 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 pub fn matches(&self, entry: &AuditEntry) -> bool {
187 if self.category_mask != 0 && (self.category_mask & entry.category.to_flag()) == 0 {
189 return false;
190 }
191
192 if let Some(min) = self.min_severity {
194 if (entry.severity as u8) < (min as u8) {
195 return false;
196 }
197 }
198
199 if self.pid != 0 && entry.pid != self.pid {
201 return false;
202 }
203
204 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 if self.failures_only && entry.success {
214 return false;
215 }
216
217 true
218 }
219}
220
221#[derive(Debug, Clone, Copy)]
227pub struct AuditActiveFilter {
228 pub category_mask: u16,
230 pub min_severity: AuditSeverity,
232}
233
234impl AuditActiveFilter {
235 pub const fn accept_all() -> Self {
237 Self {
238 category_mask: 0xFFFF,
239 min_severity: AuditSeverity::Info,
240 }
241 }
242
243 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#[derive(Debug, Clone, Copy)]
256pub struct EnhancedAuditStats {
257 pub total_logged: u64,
259 pub total_dropped: u64,
261 pub total_filtered: u64,
263 pub total_coalesced: u64,
265 pub buffer_count: u64,
267 pub buffer_capacity: u64,
269 pub per_category: [u64; AuditCategory::COUNT],
271}
272
273const DEFAULT_CAPACITY: usize = 8192;
279
280const COALESCE_WINDOW_SECS: u64 = 1;
282
283#[cfg(feature = "alloc")]
285struct AuditLog {
286 entries: VecDeque<AuditEntry>,
288 capacity: usize,
290 active_filter: AuditActiveFilter,
292 next_sequence: u64,
294}
295
296#[cfg(feature = "alloc")]
297impl AuditLog {
298 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 fn insert(&mut self, mut entry: AuditEntry) -> bool {
313 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 last.timestamp = entry.timestamp;
325 return false;
326 }
327 }
328
329 entry.sequence = self.next_sequence;
331 self.next_sequence = self.next_sequence.wrapping_add(1);
332 entry.coalesce_count = 1;
333
334 if self.entries.len() >= self.capacity {
336 self.entries.pop_front();
337 }
338
339 self.entries.push_back(entry);
340 true
341 }
342
343 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 results.reverse();
356 results
357 }
358
359 fn clear(&mut self) {
361 self.entries.clear();
362 self.next_sequence = 1;
363 }
364
365 fn len(&self) -> usize {
367 self.entries.len()
368 }
369}
370
371#[cfg(feature = "alloc")]
377static AUDIT_LOG: RwLock<Option<AuditLog>> = RwLock::new(None);
378
379static ENABLED: AtomicBool = AtomicBool::new(false);
381
382static 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
398fn 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#[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#[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#[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 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 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, 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 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#[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#[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#[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#[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
554pub 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
596pub fn enable() {
598 ENABLED.store(true, Ordering::Release);
599}
600
601pub fn disable() {
603 ENABLED.store(false, Ordering::Release);
604}
605
606pub fn is_enabled() -> bool {
608 ENABLED.load(Ordering::Acquire)
609}
610
611#[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#[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#[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#[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#[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#[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#[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#[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#[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, ..entry1.clone()
924 };
925
926 assert!(log.insert(entry1));
927 assert!(!log.insert(entry2)); 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)); 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, 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 assert_eq!(log.len(), 4);
979
980 let results = log.query(&AuditQueryFilter::match_all(), 100);
981 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 assert!(stats.per_category.len() == AuditCategory::COUNT);
1039 }
1040}