1use std::collections::VecDeque;
4use std::io::Write;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use metrics::{counter, gauge};
8use parking_lot::RwLock;
9use tracing::{debug, warn};
10
11use crate::audit::error::{AuditError, Result};
12use crate::audit::model::{AuditDecision, AuditEvent};
13
14pub trait AuditSink: Send + Sync {
18 fn emit(&self, event: &AuditEvent) -> Result<()>;
20
21 fn flush(&self) -> Result<()> {
23 Ok(())
24 }
25
26 fn is_durable(&self) -> bool {
33 false
34 }
35}
36
37pub struct MemorySink {
41 events: RwLock<VecDeque<AuditEvent>>,
42 max_capacity: usize,
43 dropped_count: AtomicU64,
45}
46
47impl MemorySink {
48 #[must_use]
50 pub fn new(max_capacity: usize) -> Self {
51 Self {
52 events: RwLock::new(VecDeque::with_capacity(max_capacity)),
53 max_capacity,
54 dropped_count: AtomicU64::new(0),
55 }
56 }
57
58 #[must_use]
60 pub fn dropped_count(&self) -> u64 {
61 self.dropped_count.load(Ordering::Relaxed)
62 }
63
64 #[must_use]
66 pub fn events(&self) -> Vec<AuditEvent> {
67 self.events.read().iter().cloned().collect()
68 }
69
70 #[must_use]
72 pub fn len(&self) -> usize {
73 self.events.read().len()
74 }
75
76 #[must_use]
78 pub fn is_empty(&self) -> bool {
79 self.events.read().is_empty()
80 }
81
82 #[must_use]
84 pub fn query_by_principal(&self, principal: &str) -> Vec<AuditEvent> {
85 self.events
86 .read()
87 .iter()
88 .filter(|e| e.principal == principal)
89 .cloned()
90 .collect()
91 }
92
93 #[must_use]
95 pub fn query_by_decision(&self, decision: AuditDecision) -> Vec<AuditEvent> {
96 self.events
97 .read()
98 .iter()
99 .filter(|e| e.decision == decision)
100 .cloned()
101 .collect()
102 }
103
104 #[must_use]
106 pub fn query_by_resource(&self, resource: &str) -> Vec<AuditEvent> {
107 self.events
108 .read()
109 .iter()
110 .filter(|e| e.resource == resource)
111 .cloned()
112 .collect()
113 }
114
115 #[must_use]
117 pub fn query_by_time_range(&self, min_ts: i64, max_ts: i64) -> Vec<AuditEvent> {
118 self.events
119 .read()
120 .iter()
121 .filter(|e| e.timestamp >= min_ts && e.timestamp <= max_ts)
122 .cloned()
123 .collect()
124 }
125
126 pub fn drain(&self) -> Vec<AuditEvent> {
128 let mut q = self.events.write();
129 std::mem::take(&mut *q).into()
130 }
131
132 #[must_use]
134 pub fn fill_ratio(&self) -> f64 {
135 if self.max_capacity == 0 {
136 return 1.0;
137 }
138 self.events.read().len() as f64 / self.max_capacity as f64
139 }
140}
141
142impl Default for MemorySink {
143 fn default() -> Self {
144 Self::new(100_000)
145 }
146}
147
148impl AuditSink for MemorySink {
149 fn emit(&self, event: &AuditEvent) -> Result<()> {
150 let mut q = self.events.write();
151 if q.len() >= self.max_capacity {
152 q.pop_front();
153 let dropped = self.dropped_count.fetch_add(1, Ordering::Relaxed) + 1;
154 counter!("chronix.audit.memory_sink.dropped").increment(1);
155 if dropped == 1 || dropped == 10 || dropped == 100 || dropped.is_multiple_of(1000) {
159 warn!(
160 dropped,
161 capacity = self.max_capacity,
162 "MemorySink overflow: oldest audit events being dropped"
163 );
164 }
165 }
166 q.push_back(event.clone());
167 let fill = q.len() as f64 / self.max_capacity.max(1) as f64;
168 gauge!("chronix.audit.memory_sink.fill_ratio").set(fill);
169 Ok(())
170 }
171}
172
173pub struct WriterSink<W: Write + Send + Sync> {
179 writer: parking_lot::Mutex<W>,
180}
181
182impl<W: Write + Send + Sync> WriterSink<W> {
183 pub fn new(writer: W) -> Self {
185 Self {
186 writer: parking_lot::Mutex::new(writer),
187 }
188 }
189}
190
191impl<W: Write + Send + Sync> AuditSink for WriterSink<W> {
192 fn emit(&self, event: &AuditEvent) -> Result<()> {
193 let json =
194 serde_json::to_string(event).map_err(|e| AuditError::Serialization(e.to_string()))?;
195 let mut w = self.writer.lock();
196 writeln!(w, "{json}")?;
197 Ok(())
198 }
199
200 fn flush(&self) -> Result<()> {
201 self.writer.lock().flush()?;
202 Ok(())
203 }
204
205 fn is_durable(&self) -> bool {
207 true
208 }
209}
210
211pub struct FileSink {
224 file: parking_lot::Mutex<std::fs::File>,
225 sync_each: bool,
226}
227
228impl FileSink {
229 pub fn open(path: impl AsRef<std::path::Path>, sync_each: bool) -> Result<Self> {
236 let path = path.as_ref();
237 if let Some(parent) = path.parent() {
238 if !parent.as_os_str().is_empty() {
239 std::fs::create_dir_all(parent)?;
240 }
241 }
242 let file = std::fs::OpenOptions::new()
243 .create(true)
244 .append(true)
245 .open(path)?;
246 Ok(Self {
247 file: parking_lot::Mutex::new(file),
248 sync_each,
249 })
250 }
251}
252
253impl AuditSink for FileSink {
254 fn emit(&self, event: &AuditEvent) -> Result<()> {
255 let json =
256 serde_json::to_string(event).map_err(|e| AuditError::Serialization(e.to_string()))?;
257 let mut f = self.file.lock();
258 writeln!(f, "{json}")?;
259 if self.sync_each {
260 f.sync_data()?;
261 }
262 Ok(())
263 }
264
265 fn flush(&self) -> Result<()> {
266 let mut f = self.file.lock();
267 f.flush()?;
268 f.sync_data()?;
269 Ok(())
270 }
271
272 fn is_durable(&self) -> bool {
273 true
274 }
275}
276
277pub fn last_event(path: impl AsRef<std::path::Path>) -> Result<Option<AuditEvent>> {
287 let path = path.as_ref();
288 if !path.exists() {
289 return Ok(None);
290 }
291 let content = std::fs::read_to_string(path)?;
292 Ok(content
293 .lines()
294 .rev()
295 .find_map(|line| serde_json::from_str::<AuditEvent>(line).ok()))
296}
297
298pub fn read_events(path: impl AsRef<std::path::Path>) -> Result<Vec<AuditEvent>> {
306 let path = path.as_ref();
307 if !path.exists() {
308 return Ok(Vec::new());
309 }
310 let content = std::fs::read_to_string(path)?;
311 Ok(content
312 .lines()
313 .filter_map(|line| serde_json::from_str::<AuditEvent>(line).ok())
314 .collect())
315}
316
317pub struct TracingSink;
321
322impl AuditSink for TracingSink {
323 fn emit(&self, event: &AuditEvent) -> Result<()> {
324 match event.decision {
325 AuditDecision::Allow => {
326 tracing::info!(
327 target: "chronix::audit",
328 id = event.id,
329 principal = %event.principal,
330 action = %event.action,
331 resource = %event.resource,
332 decision = "allow",
333 source_ip = event.source_ip.as_deref().unwrap_or("-"),
334 request_id = event.request_id.as_deref().unwrap_or("-"),
335 "Audit event"
336 );
337 }
338 AuditDecision::Deny => {
339 tracing::warn!(
340 target: "chronix::audit",
341 id = event.id,
342 principal = %event.principal,
343 action = %event.action,
344 resource = %event.resource,
345 decision = "deny",
346 source_ip = event.source_ip.as_deref().unwrap_or("-"),
347 request_id = event.request_id.as_deref().unwrap_or("-"),
348 "Audit event (DENIED)"
349 );
350 }
351 }
352 Ok(())
353 }
354}
355
356pub struct AuditLogger {
378 sinks: RwLock<Vec<Box<dyn AuditSink>>>,
379 sequence: AtomicU64,
380 prev_hash: parking_lot::Mutex<Option<String>>,
382 hmac_key: Option<Vec<u8>>,
385}
386
387impl AuditLogger {
388 #[must_use]
390 pub fn new() -> Self {
391 Self {
392 sinks: RwLock::new(Vec::new()),
393 sequence: AtomicU64::new(1),
394 prev_hash: parking_lot::Mutex::new(None),
395 hmac_key: None,
396 }
397 }
398
399 #[must_use]
405 pub fn with_hmac_key(mut self, key: Vec<u8>) -> Self {
406 self.hmac_key = Some(key);
407 self
408 }
409
410 #[must_use]
419 pub fn resume_from(self, last_id: u64, last_hash: Option<String>) -> Self {
420 self.sequence.store(last_id + 1, Ordering::Relaxed);
421 *self.prev_hash.lock() = last_hash;
422 self
423 }
424
425 #[must_use]
427 pub fn is_keyed(&self) -> bool {
428 self.hmac_key.is_some()
429 }
430
431 pub fn add_sink(&self, sink: Box<dyn AuditSink>) {
434 self.sinks.write().push(sink);
435 }
436
437 pub fn validate_has_durable_sink(&self) -> Result<()> {
446 let sinks = self.sinks.read();
447 let has_durable = sinks.iter().any(|s| s.is_durable());
448 if !has_durable {
449 warn!(
450 "No durable audit sink configured! Audit events will be \
451 lost on process restart. Add a WriterSink (file) or equivalent \
452 persistent sink for compliance."
453 );
454 return Err(AuditError::NoDurableSink);
455 }
456 Ok(())
457 }
458
459 #[must_use]
461 pub fn has_durable_sink(&self) -> bool {
462 self.sinks.read().iter().any(|s| s.is_durable())
463 }
464
465 pub fn log(&self, mut event: AuditEvent) {
470 event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
471
472 {
480 let mut prev = self.prev_hash.lock();
481 event.seal_with_key(prev.as_deref(), self.hmac_key.as_deref());
482 *prev = event.event_hash.clone();
483 }
484
485 counter!(
486 "chronix_audit_events_total",
487 "action" => event.action.to_string(),
488 "decision" => event.decision.to_string()
489 )
490 .increment(1);
491
492 let sinks = self.sinks.read();
493 for sink in sinks.iter() {
494 if let Err(e) = sink.emit(&event) {
495 warn!(
496 sink = std::any::type_name_of_val(sink),
497 error = %e,
498 "Failed to emit audit event"
499 );
500 counter!("chronix_audit_emit_errors_total").increment(1);
501 counter!(
505 "chronix_audit_events_dropped_total",
506 "sink" => std::any::type_name_of_val(sink).to_string(),
507 )
508 .increment(1);
509 }
510 }
511
512 debug!(id = event.id, action = %event.action, "Audit event logged");
513 }
514
515 #[must_use]
517 pub fn next_sequence(&self) -> u64 {
518 self.sequence.load(Ordering::Relaxed)
519 }
520
521 #[must_use]
523 pub fn sink_count(&self) -> usize {
524 self.sinks.read().len()
525 }
526
527 pub fn flush(&self) {
529 let sinks = self.sinks.read();
530 for sink in sinks.iter() {
531 if let Err(e) = sink.flush() {
532 warn!(error = %e, "Failed to flush audit sink");
533 }
534 }
535 }
536
537 pub fn log_strict(&self, mut event: AuditEvent) -> Result<()> {
545 event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
546
547 {
549 let mut prev = self.prev_hash.lock();
550 event.seal_with_key(prev.as_deref(), self.hmac_key.as_deref());
551 *prev = event.event_hash.clone();
552 }
553
554 counter!(
555 "chronix_audit_events_total",
556 "action" => event.action.to_string(),
557 "decision" => event.decision.to_string()
558 )
559 .increment(1);
560
561 let sinks = self.sinks.read();
562 let total = sinks.len();
563 let mut failures: Vec<(String, String)> = Vec::new();
564
565 for sink in sinks.iter() {
566 if let Err(e) = sink.emit(&event) {
567 counter!("chronix_audit_emit_errors_total").increment(1);
568 counter!(
570 "chronix_audit_events_dropped_total",
571 "sink" => std::any::type_name_of_val(sink).to_string(),
572 )
573 .increment(1);
574 let sink_name = std::any::type_name_of_val(sink).to_string();
575 warn!(
576 sink = %sink_name,
577 error = %e,
578 "Strict audit emit failed"
579 );
580 failures.push((sink_name, e.to_string()));
581 }
582 }
583
584 if failures.is_empty() {
585 debug!(id = event.id, action = %event.action, "Audit event logged (strict)");
586 Ok(())
587 } else {
588 let succeeded = total - failures.len();
589 Err(AuditError::PartialDelivery {
590 succeeded,
591 total,
592 failures,
593 })
594 }
595 }
596
597 pub fn flush_strict(&self) -> Result<()> {
603 let sinks = self.sinks.read();
604 let total = sinks.len();
605 let mut failures: Vec<(String, String)> = Vec::new();
606
607 for sink in sinks.iter() {
608 if let Err(e) = sink.flush() {
609 let sink_name = std::any::type_name_of_val(sink).to_string();
610 warn!(sink = %sink_name, error = %e, "Strict audit flush failed");
611 failures.push((sink_name, e.to_string()));
612 }
613 }
614
615 if failures.is_empty() {
616 Ok(())
617 } else {
618 let succeeded = total - failures.len();
619 Err(AuditError::PartialDelivery {
620 succeeded,
621 total,
622 failures,
623 })
624 }
625 }
626}
627
628impl Default for AuditLogger {
629 fn default() -> Self {
630 Self::new()
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::audit::model::{AuditAction, AuditDecision, AuditEvent};
638 use std::sync::Arc;
639
640 fn make_event(action: AuditAction, decision: AuditDecision) -> AuditEvent {
641 AuditEvent::new("alice", action, "cpu", decision).with_timestamp(1_700_000_000_000)
642 }
643
644 #[test]
647 fn memory_sink_stores_events() {
648 let sink = MemorySink::new(100);
649 assert!(sink.is_empty());
650
651 let event = make_event(AuditAction::Write, AuditDecision::Allow);
652 sink.emit(&event).unwrap();
653 assert_eq!(sink.len(), 1);
654 }
655
656 #[test]
657 fn memory_sink_evicts_on_capacity() {
658 let sink = MemorySink::new(2);
659 for i in 0..3 {
660 let mut event = make_event(AuditAction::Read, AuditDecision::Allow);
661 event.timestamp = i;
662 sink.emit(&event).unwrap();
663 }
664 assert_eq!(sink.len(), 2);
665 let events = sink.events();
666 assert_eq!(events[0].timestamp, 1);
667 assert_eq!(events[1].timestamp, 2);
668 }
669
670 #[test]
671 fn memory_sink_query_by_principal() {
672 let sink = MemorySink::new(100);
673 sink.emit(&AuditEvent::new(
674 "alice",
675 AuditAction::Write,
676 "cpu",
677 AuditDecision::Allow,
678 ))
679 .unwrap();
680 sink.emit(&AuditEvent::new(
681 "bob",
682 AuditAction::Read,
683 "cpu",
684 AuditDecision::Deny,
685 ))
686 .unwrap();
687 sink.emit(&AuditEvent::new(
688 "alice",
689 AuditAction::Delete,
690 "logs",
691 AuditDecision::Allow,
692 ))
693 .unwrap();
694
695 let alice = sink.query_by_principal("alice");
696 assert_eq!(alice.len(), 2);
697 let bob = sink.query_by_principal("bob");
698 assert_eq!(bob.len(), 1);
699 }
700
701 #[test]
702 fn memory_sink_query_by_decision() {
703 let sink = MemorySink::new(100);
704 sink.emit(&make_event(AuditAction::Write, AuditDecision::Allow))
705 .unwrap();
706 sink.emit(&make_event(AuditAction::Read, AuditDecision::Deny))
707 .unwrap();
708 sink.emit(&make_event(AuditAction::Delete, AuditDecision::Deny))
709 .unwrap();
710
711 let denied = sink.query_by_decision(AuditDecision::Deny);
712 assert_eq!(denied.len(), 2);
713 }
714
715 #[test]
716 fn memory_sink_query_by_resource() {
717 let sink = MemorySink::new(100);
718 sink.emit(&AuditEvent::new(
719 "a",
720 AuditAction::Write,
721 "cpu",
722 AuditDecision::Allow,
723 ))
724 .unwrap();
725 sink.emit(&AuditEvent::new(
726 "b",
727 AuditAction::Write,
728 "memory",
729 AuditDecision::Allow,
730 ))
731 .unwrap();
732 sink.emit(&AuditEvent::new(
733 "c",
734 AuditAction::Write,
735 "cpu",
736 AuditDecision::Allow,
737 ))
738 .unwrap();
739
740 let cpu = sink.query_by_resource("cpu");
741 assert_eq!(cpu.len(), 2);
742 }
743
744 #[test]
745 fn memory_sink_query_by_time_range() {
746 let sink = MemorySink::new(100);
747 for ts in [100, 200, 300] {
748 let mut event = make_event(AuditAction::Read, AuditDecision::Allow);
749 event.timestamp = ts;
750 sink.emit(&event).unwrap();
751 }
752
753 let range = sink.query_by_time_range(150, 250);
754 assert_eq!(range.len(), 1);
755 assert_eq!(range[0].timestamp, 200);
756 }
757
758 #[test]
759 fn memory_sink_drain() {
760 let sink = MemorySink::new(100);
761 sink.emit(&make_event(AuditAction::Write, AuditDecision::Allow))
762 .unwrap();
763 sink.emit(&make_event(AuditAction::Read, AuditDecision::Allow))
764 .unwrap();
765
766 let drained = sink.drain();
767 assert_eq!(drained.len(), 2);
768 assert!(sink.is_empty());
769 }
770
771 #[test]
774 fn writer_sink_json_lines() {
775 let buf: Vec<u8> = Vec::new();
776 let sink = WriterSink::new(buf);
777
778 let event1 = make_event(AuditAction::Write, AuditDecision::Allow);
779 let event2 = make_event(AuditAction::Read, AuditDecision::Deny);
780 sink.emit(&event1).unwrap();
781 sink.emit(&event2).unwrap();
782 sink.flush().unwrap();
783
784 let output = sink.writer.lock();
785 let text = String::from_utf8(output.clone()).unwrap();
786 let lines: Vec<&str> = text.lines().collect();
787 assert_eq!(lines.len(), 2);
788
789 let parsed: AuditEvent = serde_json::from_str(lines[0]).unwrap();
790 assert_eq!(parsed.principal, "alice");
791 assert_eq!(parsed.decision, AuditDecision::Allow);
792
793 let parsed2: AuditEvent = serde_json::from_str(lines[1]).unwrap();
794 assert_eq!(parsed2.decision, AuditDecision::Deny);
795 }
796
797 #[test]
800 fn tracing_sink_emits_ok() {
801 let sink = TracingSink;
802 let event = make_event(AuditAction::Write, AuditDecision::Allow);
803 assert!(sink.emit(&event).is_ok());
804
805 let deny = make_event(AuditAction::Delete, AuditDecision::Deny);
806 assert!(sink.emit(&deny).is_ok());
807 }
808
809 #[test]
812 fn logger_assigns_sequence_numbers() {
813 let mem = Arc::new(MemorySink::new(100));
814 let logger = AuditLogger::new();
815 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
816
817 logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
818 logger.log(make_event(AuditAction::Read, AuditDecision::Allow));
819 logger.log(make_event(AuditAction::Delete, AuditDecision::Deny));
820
821 let events = mem.events();
822 assert_eq!(events.len(), 3);
823 assert_eq!(events[0].id, 1);
824 assert_eq!(events[1].id, 2);
825 assert_eq!(events[2].id, 3);
826 assert_eq!(logger.next_sequence(), 4);
827 }
828
829 #[test]
830 fn logger_dispatches_to_multiple_sinks() {
831 let mem1 = Arc::new(MemorySink::new(100));
832 let mem2 = Arc::new(MemorySink::new(100));
833
834 let logger = AuditLogger::new();
835 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem1))));
836 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem2))));
837 assert_eq!(logger.sink_count(), 2);
838
839 logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
840 assert_eq!(mem1.len(), 1);
841 assert_eq!(mem2.len(), 1);
842 }
843
844 #[test]
845 fn logger_all_actions_logged() {
846 let mem = Arc::new(MemorySink::new(100));
847 let logger = AuditLogger::new();
848 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
849
850 logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
852 logger.log(make_event(AuditAction::Write, AuditDecision::Deny));
853 logger.log(make_event(AuditAction::LoginSuccess, AuditDecision::Allow));
854 logger.log(make_event(AuditAction::LoginFailure, AuditDecision::Deny));
855 logger.log(make_event(AuditAction::KeyRotation, AuditDecision::Allow));
856
857 assert_eq!(mem.len(), 5);
858 let denied = mem.query_by_decision(AuditDecision::Deny);
859 assert_eq!(denied.len(), 2);
860 }
861
862 #[test]
863 fn logger_with_file_sink() {
864 let dir = tempfile::tempdir().unwrap();
865 let path = dir.path().join("audit.log");
866 let file = std::fs::File::create(&path).unwrap();
867
868 let logger = AuditLogger::new();
869 logger.add_sink(Box::new(WriterSink::new(file)));
870
871 logger.log(
872 AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
873 .with_source_ip("10.0.0.1")
874 .with_request_id("req-001")
875 .with_timestamp(1_700_000_000_000),
876 );
877 logger.log(
878 AuditEvent::new("bob", AuditAction::Read, "memory", AuditDecision::Deny)
879 .with_timestamp(1_700_000_000_001),
880 );
881 logger.flush();
882
883 let content = std::fs::read_to_string(&path).unwrap();
884 let lines: Vec<&str> = content.lines().collect();
885 assert_eq!(lines.len(), 2);
886
887 let e1: AuditEvent = serde_json::from_str(lines[0]).unwrap();
888 assert_eq!(e1.principal, "alice");
889 assert_eq!(e1.source_ip.as_deref(), Some("10.0.0.1"));
890 assert_eq!(e1.request_id.as_deref(), Some("req-001"));
891 assert_eq!(e1.id, 1);
892
893 let e2: AuditEvent = serde_json::from_str(lines[1]).unwrap();
894 assert_eq!(e2.principal, "bob");
895 assert_eq!(e2.decision, AuditDecision::Deny);
896 assert_eq!(e2.id, 2);
897 }
898
899 struct SinkWrapper(Arc<MemorySink>);
903
904 impl AuditSink for SinkWrapper {
905 fn emit(&self, event: &AuditEvent) -> Result<()> {
906 self.0.emit(event)
907 }
908
909 fn flush(&self) -> Result<()> {
910 self.0.flush()
911 }
912 }
913
914 struct FailingSink;
918
919 impl AuditSink for FailingSink {
920 fn emit(&self, _event: &AuditEvent) -> Result<()> {
921 Err(AuditError::Internal("sink unavailable".into()))
922 }
923
924 fn flush(&self) -> Result<()> {
925 Err(AuditError::Internal("flush failed".into()))
926 }
927 }
928
929 #[test]
932 fn log_strict_succeeds_with_healthy_sinks() {
933 let mem = Arc::new(MemorySink::new(100));
934 let logger = AuditLogger::new();
935 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
936
937 logger
938 .log_strict(make_event(AuditAction::Write, AuditDecision::Allow))
939 .unwrap();
940 assert_eq!(mem.len(), 1);
941 }
942
943 #[test]
944 fn log_strict_returns_error_on_sink_failure() {
945 let logger = AuditLogger::new();
946 logger.add_sink(Box::new(FailingSink));
947
948 let result = logger.log_strict(make_event(AuditAction::Write, AuditDecision::Allow));
949 assert!(result.is_err());
950 }
951
952 #[test]
953 fn log_strict_fails_if_any_sink_fails() {
954 let mem = Arc::new(MemorySink::new(100));
955 let logger = AuditLogger::new();
956 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
957 logger.add_sink(Box::new(FailingSink)); let result = logger.log_strict(make_event(AuditAction::Delete, AuditDecision::Deny));
960 assert_eq!(mem.len(), 1);
962 match result {
964 Err(AuditError::PartialDelivery {
965 succeeded,
966 total,
967 failures,
968 }) => {
969 assert_eq!(succeeded, 1);
970 assert_eq!(total, 2);
971 assert_eq!(failures.len(), 1);
972 }
973 other => panic!("Expected PartialDelivery, got: {other:?}"),
974 }
975 }
976
977 #[test]
978 fn flush_strict_propagates_failure() {
979 let logger = AuditLogger::new();
980 logger.add_sink(Box::new(FailingSink));
981
982 let result = logger.flush_strict();
983 assert!(result.is_err());
984 }
985
986 #[test]
987 fn flush_strict_succeeds_with_healthy_sinks() {
988 let mem = Arc::new(MemorySink::new(100));
989 let logger = AuditLogger::new();
990 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
991
992 logger.flush_strict().unwrap();
993 }
994
995 #[test]
996 fn log_best_effort_continues_on_sink_failure() {
997 let mem = Arc::new(MemorySink::new(100));
998 let logger = AuditLogger::new();
999 logger.add_sink(Box::new(FailingSink)); logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem)))); logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
1004 assert_eq!(mem.len(), 1);
1005 }
1006
1007 #[test]
1010 fn memory_sink_is_not_durable() {
1011 let sink = MemorySink::new(100);
1012 assert!(!sink.is_durable());
1013 }
1014
1015 #[test]
1016 fn writer_sink_is_durable() {
1017 let sink = WriterSink::new(Vec::<u8>::new());
1018 assert!(sink.is_durable());
1019 }
1020
1021 #[test]
1022 fn validate_no_durable_sink_fails() {
1023 let logger = AuditLogger::new();
1024 logger.add_sink(Box::new(MemorySink::new(100)));
1025 let result = logger.validate_has_durable_sink();
1026 assert!(matches!(result, Err(AuditError::NoDurableSink)));
1027 assert!(!logger.has_durable_sink());
1028 }
1029
1030 #[test]
1031 fn validate_with_durable_sink_succeeds() {
1032 let logger = AuditLogger::new();
1033 logger.add_sink(Box::new(MemorySink::new(100)));
1034 logger.add_sink(Box::new(WriterSink::new(Vec::<u8>::new())));
1035 logger.validate_has_durable_sink().unwrap();
1036 assert!(logger.has_durable_sink());
1037 }
1038
1039 #[test]
1040 fn validate_empty_logger_fails() {
1041 let logger = AuditLogger::new();
1042 let result = logger.validate_has_durable_sink();
1043 assert!(matches!(result, Err(AuditError::NoDurableSink)));
1044 }
1045
1046 fn make_named_event(principal: &str) -> AuditEvent {
1049 AuditEvent::new(principal, AuditAction::Write, "cpu", AuditDecision::Allow)
1050 .with_timestamp(1_700_000_000_000)
1051 }
1052
1053 #[test]
1054 fn overflow_increments_dropped_count() {
1055 let sink = MemorySink::new(3);
1056 for i in 0..5 {
1057 sink.emit(&make_named_event(&format!("user{i}"))).unwrap();
1058 }
1059 assert_eq!(sink.dropped_count(), 2);
1060 assert_eq!(sink.len(), 3);
1061 }
1062
1063 #[test]
1064 fn fill_ratio_reports_correctly() {
1065 let sink = MemorySink::new(10);
1066 assert!((sink.fill_ratio() - 0.0).abs() < f64::EPSILON);
1067 for i in 0..5 {
1068 sink.emit(&make_named_event(&format!("u{i}"))).unwrap();
1069 }
1070 assert!((sink.fill_ratio() - 0.5).abs() < f64::EPSILON);
1071 for i in 5..10 {
1072 sink.emit(&make_named_event(&format!("u{i}"))).unwrap();
1073 }
1074 assert!((sink.fill_ratio() - 1.0).abs() < f64::EPSILON);
1075 }
1076
1077 #[test]
1078 fn fill_ratio_stays_at_one_when_overflowing() {
1079 let sink = MemorySink::new(2);
1080 for i in 0..5 {
1081 sink.emit(&make_named_event(&format!("u{i}"))).unwrap();
1082 }
1083 assert!((sink.fill_ratio() - 1.0).abs() < f64::EPSILON);
1084 assert_eq!(sink.dropped_count(), 3);
1085 }
1086
1087 #[test]
1088 fn overflow_preserves_newest_events() {
1089 let sink = MemorySink::new(3);
1090 for i in 0..6 {
1091 sink.emit(&make_named_event(&format!("user{i}"))).unwrap();
1092 }
1093 let events = sink.events();
1094 let principals: Vec<&str> = events.iter().map(|e| e.principal.as_str()).collect();
1095 assert_eq!(principals, vec!["user3", "user4", "user5"]);
1096 }
1097}
1098
1099#[cfg(test)]
1100mod durability_tests {
1101 use super::*;
1102 use crate::audit::model::verify_chain_from;
1103 use crate::audit::{AuditAction, AuditDecision};
1104
1105 fn event(principal: &str) -> AuditEvent {
1106 AuditEvent::new(principal, AuditAction::Delete, "cpu", AuditDecision::Allow)
1107 }
1108
1109 #[test]
1110 fn a_restart_continues_one_chain() {
1111 let dir = tempfile::tempdir().unwrap();
1112 let path = dir.path().join("audit.jsonl");
1113
1114 {
1115 let logger = AuditLogger::new();
1116 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
1117 logger.log(event("alice"));
1118 logger.log(event("bob"));
1119 }
1120
1121 let last = last_event(&path).unwrap().expect("a previous event");
1123 assert_eq!(last.id, 2);
1124 {
1125 let logger = AuditLogger::new().resume_from(last.id, last.event_hash.clone());
1126 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
1127 logger.log(event("carol"));
1128 }
1129
1130 let events = read_events(&path).unwrap();
1131 assert_eq!(events.len(), 3, "every event survives the restart");
1132 assert_eq!(
1133 events.iter().map(|e| e.id).collect::<Vec<_>>(),
1134 vec![1, 2, 3],
1135 "sequence numbers continue rather than restarting at 1"
1136 );
1137 verify_chain_from(&events, None, None)
1138 .expect("the chain must verify straight through the restart");
1139 }
1140
1141 #[test]
1146 fn a_keyed_chain_verifies_only_with_its_key() {
1147 let key = b"seal".to_vec();
1148 let logger = AuditLogger::new().with_hmac_key(key.clone());
1149 let handle = std::sync::Arc::new(MemorySink::new(16));
1150 logger.add_sink(Box::new(SharedMemorySink(handle.clone())));
1151
1152 logger.log(event("alice"));
1153 logger.log_strict(event("bob")).unwrap();
1154
1155 let events = handle.events();
1156 assert_eq!(events.len(), 2);
1157 verify_chain_from(&events, None, Some(&key))
1158 .expect("both `log` and `log_strict` must seal the same way");
1159 assert!(
1160 verify_chain_from(&events, None, None).is_err(),
1161 "a keyed chain must not verify as an unkeyed one"
1162 );
1163 }
1164
1165 struct SharedMemorySink(std::sync::Arc<MemorySink>);
1167
1168 impl AuditSink for SharedMemorySink {
1169 fn emit(&self, event: &AuditEvent) -> Result<()> {
1170 self.0.emit(event)
1171 }
1172 fn flush(&self) -> Result<()> {
1173 self.0.flush()
1174 }
1175 }
1176
1177 #[test]
1181 fn a_partial_trailing_line_is_skipped() {
1182 let dir = tempfile::tempdir().unwrap();
1183 let path = dir.path().join("audit.jsonl");
1184 {
1185 let logger = AuditLogger::new();
1186 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
1187 logger.log(event("alice"));
1188 }
1189 {
1190 use std::io::Write as _;
1191 let mut f = std::fs::OpenOptions::new()
1192 .append(true)
1193 .open(&path)
1194 .unwrap();
1195 write!(f, "{{\"id\":2,\"principal\":\"tr").unwrap();
1196 }
1197 let last = last_event(&path).unwrap().expect("the complete event");
1198 assert_eq!(last.id, 1);
1199 assert_eq!(read_events(&path).unwrap().len(), 1);
1200 }
1201}