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, error, 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_total").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 good_len: std::sync::atomic::AtomicU64,
234}
235
236impl FileSink {
237 pub fn open(path: impl AsRef<std::path::Path>, sync_each: bool) -> Result<Self> {
244 let path = path.as_ref();
245 if let Some(parent) = path.parent() {
246 if !parent.as_os_str().is_empty() {
247 std::fs::create_dir_all(parent)?;
248 }
249 }
250 let file = std::fs::OpenOptions::new()
251 .create(true)
252 .append(true)
253 .open(path)?;
254 let good_len = file.metadata()?.len();
255 Ok(Self {
256 file: parking_lot::Mutex::new(file),
257 sync_each,
258 good_len: std::sync::atomic::AtomicU64::new(good_len),
259 })
260 }
261}
262
263impl AuditSink for FileSink {
264 fn emit(&self, event: &AuditEvent) -> Result<()> {
265 use std::sync::atomic::Ordering;
266
267 let mut line =
268 serde_json::to_vec(event).map_err(|e| AuditError::Serialization(e.to_string()))?;
269 line.push(b'\n');
270
271 let mut f = self.file.lock();
272 match f.write_all(&line) {
278 Ok(()) => {}
279 Err(e) => {
280 let good = self.good_len.load(Ordering::SeqCst);
281 if let Err(t) = f.set_len(good) {
282 tracing::error!(error = %t, offset = good, "audit log rewind failed");
283 }
284 return Err(e.into());
285 }
286 }
287 self.good_len.fetch_add(line.len() as u64, Ordering::SeqCst);
288 if self.sync_each {
289 f.sync_data()?;
290 }
291 Ok(())
292 }
293
294 fn flush(&self) -> Result<()> {
295 let mut f = self.file.lock();
296 f.flush()?;
297 f.sync_data()?;
298 Ok(())
299 }
300
301 fn is_durable(&self) -> bool {
302 true
303 }
304}
305
306pub fn last_event(path: impl AsRef<std::path::Path>) -> Result<Option<AuditEvent>> {
316 let path = path.as_ref();
317 if !path.exists() {
318 return Ok(None);
319 }
320 let content = std::fs::read_to_string(path)?;
321 Ok(content
322 .lines()
323 .rev()
324 .find_map(|line| serde_json::from_str::<AuditEvent>(line).ok()))
325}
326
327pub fn read_events(path: impl AsRef<std::path::Path>) -> Result<Vec<AuditEvent>> {
335 let path = path.as_ref();
336 if !path.exists() {
337 return Ok(Vec::new());
338 }
339 let content = std::fs::read_to_string(path)?;
340 Ok(content
341 .lines()
342 .filter_map(|line| serde_json::from_str::<AuditEvent>(line).ok())
343 .collect())
344}
345
346pub struct TracingSink;
350
351impl AuditSink for TracingSink {
352 fn emit(&self, event: &AuditEvent) -> Result<()> {
353 match event.decision {
354 AuditDecision::Allow => {
355 tracing::info!(
356 target: "chronix::audit",
357 id = event.id,
358 principal = %event.principal,
359 action = %event.action,
360 resource = %event.resource,
361 decision = "allow",
362 source_ip = event.source_ip.as_deref().unwrap_or("-"),
363 request_id = event.request_id.as_deref().unwrap_or("-"),
364 "Audit event"
365 );
366 }
367 AuditDecision::Deny => {
368 tracing::warn!(
369 target: "chronix::audit",
370 id = event.id,
371 principal = %event.principal,
372 action = %event.action,
373 resource = %event.resource,
374 decision = "deny",
375 source_ip = event.source_ip.as_deref().unwrap_or("-"),
376 request_id = event.request_id.as_deref().unwrap_or("-"),
377 "Audit event (DENIED)"
378 );
379 }
380 }
381 Ok(())
382 }
383}
384
385pub struct AuditLogger {
407 sinks: RwLock<Vec<Box<dyn AuditSink>>>,
408 sequence: AtomicU64,
409 prev_hash: parking_lot::Mutex<Option<String>>,
411 hmac_key: Option<Vec<u8>>,
414}
415
416impl AuditLogger {
417 #[must_use]
419 pub fn new() -> Self {
420 Self {
421 sinks: RwLock::new(Vec::new()),
422 sequence: AtomicU64::new(1),
423 prev_hash: parking_lot::Mutex::new(None),
424 hmac_key: None,
425 }
426 }
427
428 #[must_use]
434 pub fn with_hmac_key(mut self, key: Vec<u8>) -> Self {
435 self.hmac_key = Some(key);
436 self
437 }
438
439 #[must_use]
448 pub fn resume_from(self, last_id: u64, last_hash: Option<String>) -> Self {
449 self.sequence.store(last_id + 1, Ordering::Relaxed);
450 *self.prev_hash.lock() = last_hash;
451 self
452 }
453
454 #[must_use]
456 pub fn is_keyed(&self) -> bool {
457 self.hmac_key.is_some()
458 }
459
460 pub fn add_sink(&self, sink: Box<dyn AuditSink>) {
463 self.sinks.write().push(sink);
464 }
465
466 pub fn validate_has_durable_sink(&self) -> Result<()> {
475 let sinks = self.sinks.read();
476 let has_durable = sinks.iter().any(|s| s.is_durable());
477 if !has_durable {
478 warn!(
479 "No durable audit sink configured! Audit events will be \
480 lost on process restart. Add a WriterSink (file) or equivalent \
481 persistent sink for compliance."
482 );
483 return Err(AuditError::NoDurableSink);
484 }
485 Ok(())
486 }
487
488 #[must_use]
490 pub fn has_durable_sink(&self) -> bool {
491 self.sinks.read().iter().any(|s| s.is_durable())
492 }
493
494 pub fn log(&self, mut event: AuditEvent) {
516 event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
517
518 let mut prev = self.prev_hash.lock();
526 event.seal_with_key(prev.as_deref(), self.hmac_key.as_deref());
527
528 counter!(
529 "chronix_audit_events_total",
530 "action" => event.action.to_string(),
531 "decision" => event.decision.to_string()
532 )
533 .increment(1);
534
535 let sinks = self.sinks.read();
536 let mut durable_failed = false;
537 let mut durable_wrote = false;
538 for sink in sinks.iter() {
539 if let Err(e) = sink.emit(&event) {
540 warn!(
541 sink = std::any::type_name_of_val(sink),
542 error = %e,
543 "Failed to emit audit event"
544 );
545 counter!("chronix_audit_emit_errors_total").increment(1);
546 counter!(
550 "chronix_audit_events_dropped_total",
551 "sink" => std::any::type_name_of_val(sink).to_string(),
552 )
553 .increment(1);
554 if sink.is_durable() {
555 durable_failed = true;
556 }
557 } else if sink.is_durable() {
558 durable_wrote = true;
559 }
560 }
561
562 if durable_failed && !durable_wrote {
569 error!(
570 id = event.id,
571 action = %event.action,
572 "audit event reached no durable sink; it is dropped and the \
573 chain continues from the last one that landed"
574 );
575 counter!("chronix_audit_chain_gaps_total").increment(1);
576 } else {
577 *prev = event.event_hash.clone();
578 }
579 drop(prev);
580
581 debug!(id = event.id, action = %event.action, "Audit event logged");
582 }
583
584 #[must_use]
586 pub fn next_sequence(&self) -> u64 {
587 self.sequence.load(Ordering::Relaxed)
588 }
589
590 #[must_use]
592 pub fn sink_count(&self) -> usize {
593 self.sinks.read().len()
594 }
595
596 pub fn flush(&self) {
598 let sinks = self.sinks.read();
599 for sink in sinks.iter() {
600 if let Err(e) = sink.flush() {
601 warn!(error = %e, "Failed to flush audit sink");
602 }
603 }
604 }
605
606 pub fn log_strict(&self, mut event: AuditEvent) -> Result<()> {
614 event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
615
616 {
618 let mut prev = self.prev_hash.lock();
619 event.seal_with_key(prev.as_deref(), self.hmac_key.as_deref());
620 *prev = event.event_hash.clone();
621 }
622
623 counter!(
624 "chronix_audit_events_total",
625 "action" => event.action.to_string(),
626 "decision" => event.decision.to_string()
627 )
628 .increment(1);
629
630 let sinks = self.sinks.read();
631 let total = sinks.len();
632 let mut failures: Vec<(String, String)> = Vec::new();
633
634 for sink in sinks.iter() {
635 if let Err(e) = sink.emit(&event) {
636 counter!("chronix_audit_emit_errors_total").increment(1);
637 counter!(
639 "chronix_audit_events_dropped_total",
640 "sink" => std::any::type_name_of_val(sink).to_string(),
641 )
642 .increment(1);
643 let sink_name = std::any::type_name_of_val(sink).to_string();
644 warn!(
645 sink = %sink_name,
646 error = %e,
647 "Strict audit emit failed"
648 );
649 failures.push((sink_name, e.to_string()));
650 }
651 }
652
653 if failures.is_empty() {
654 debug!(id = event.id, action = %event.action, "Audit event logged (strict)");
655 Ok(())
656 } else {
657 let succeeded = total - failures.len();
658 Err(AuditError::PartialDelivery {
659 succeeded,
660 total,
661 failures,
662 })
663 }
664 }
665
666 pub fn flush_strict(&self) -> Result<()> {
672 let sinks = self.sinks.read();
673 let total = sinks.len();
674 let mut failures: Vec<(String, String)> = Vec::new();
675
676 for sink in sinks.iter() {
677 if let Err(e) = sink.flush() {
678 let sink_name = std::any::type_name_of_val(sink).to_string();
679 warn!(sink = %sink_name, error = %e, "Strict audit flush failed");
680 failures.push((sink_name, e.to_string()));
681 }
682 }
683
684 if failures.is_empty() {
685 Ok(())
686 } else {
687 let succeeded = total - failures.len();
688 Err(AuditError::PartialDelivery {
689 succeeded,
690 total,
691 failures,
692 })
693 }
694 }
695}
696
697impl Default for AuditLogger {
698 fn default() -> Self {
699 Self::new()
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use crate::audit::model::{AuditAction, AuditDecision, AuditEvent};
707 use std::sync::Arc;
708
709 fn make_event(action: AuditAction, decision: AuditDecision) -> AuditEvent {
710 AuditEvent::new("alice", action, "cpu", decision).with_timestamp(1_700_000_000_000)
711 }
712
713 #[test]
724 fn concurrent_events_leave_a_verifiable_chain() {
725 let dir = tempfile::tempdir().unwrap();
726 let path = dir.path().join("audit.jsonl");
727
728 let logger = Arc::new(AuditLogger::new());
729 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
730
731 const THREADS: usize = 8;
732 const PER_THREAD: usize = 25;
733 std::thread::scope(|scope| {
734 for t in 0..THREADS {
735 let logger = Arc::clone(&logger);
736 scope.spawn(move || {
737 for i in 0..PER_THREAD {
738 logger.log(
739 AuditEvent::new(
740 format!("worker-{t}"),
741 AuditAction::Read,
742 format!("m{i}"),
743 AuditDecision::Allow,
744 )
745 .with_timestamp(1_700_000_000_000),
746 );
747 }
748 });
749 }
750 });
751 logger.flush();
752
753 let events = read_events(&path).unwrap();
754 assert_eq!(
755 events.len(),
756 THREADS * PER_THREAD,
757 "every event must reach the file"
758 );
759 crate::audit::model::verify_hash_chain(&events).expect(
760 "a log written by concurrent callers must verify; a chain that does \
761 not is indistinguishable from a tampered one",
762 );
763 }
764
765 struct MemoryHandle(Arc<MemorySink>);
767
768 impl AuditSink for MemoryHandle {
769 fn emit(&self, event: &AuditEvent) -> Result<()> {
770 self.0.emit(event)
771 }
772 fn flush(&self) -> Result<()> {
773 self.0.flush()
774 }
775 }
776
777 #[derive(Debug)]
779 struct BudgetedSink {
780 fail_after: std::sync::atomic::AtomicUsize,
781 durable: bool,
782 }
783
784 impl AuditSink for BudgetedSink {
785 fn emit(&self, _event: &AuditEvent) -> Result<()> {
786 use std::sync::atomic::Ordering;
787 if self.fail_after.load(Ordering::SeqCst) == 0 {
788 return Err(AuditError::Serialization("disk full".into()));
789 }
790 self.fail_after.fetch_sub(1, Ordering::SeqCst);
791 Ok(())
792 }
793 fn flush(&self) -> Result<()> {
794 Ok(())
795 }
796 fn is_durable(&self) -> bool {
797 self.durable
798 }
799 }
800
801 #[test]
809 fn a_dropped_event_does_not_break_the_chain_for_the_rest() {
810 let dir = tempfile::tempdir().unwrap();
811 let path = dir.path().join("audit.jsonl");
812
813 let logger = AuditLogger::new();
814 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
815 let failing = Box::new(BudgetedSink {
819 fail_after: std::sync::atomic::AtomicUsize::new(2),
820 durable: false,
821 });
822 logger.add_sink(failing);
823
824 for i in 0..5 {
825 logger.log(
826 AuditEvent::new(
827 "alice",
828 AuditAction::Read,
829 format!("m{i}"),
830 AuditDecision::Allow,
831 )
832 .with_timestamp(1_700_000_000_000),
833 );
834 }
835 logger.flush();
836
837 let events = read_events(&path).unwrap();
838 assert_eq!(events.len(), 5, "the file sink accepted every event");
839 crate::audit::model::verify_hash_chain(&events)
840 .expect("a non-durable sink failing must not touch the chain");
841 }
842
843 #[test]
845 fn the_chain_skips_an_event_no_durable_sink_took() {
846 let logger = AuditLogger::new();
847 let memory = Arc::new(MemorySink::new(100));
848 logger.add_sink(Box::new(MemoryHandle(Arc::clone(&memory))));
849 logger.add_sink(Box::new(BudgetedSink {
850 fail_after: std::sync::atomic::AtomicUsize::new(2),
851 durable: true,
852 }));
853
854 for i in 0..5 {
855 logger.log(
856 AuditEvent::new(
857 "alice",
858 AuditAction::Read,
859 format!("m{i}"),
860 AuditDecision::Allow,
861 )
862 .with_timestamp(1_700_000_000_000),
863 );
864 }
865
866 let seen = memory.events();
871 assert_eq!(seen.len(), 5);
872 assert_eq!(
873 seen[2].prev_hash, seen[3].prev_hash,
874 "two events that reached no durable sink must hang from the same link"
875 );
876 assert_eq!(
877 seen[1].event_hash.as_deref(),
878 seen[2].prev_hash.as_deref(),
879 "the first dropped event still links to the last one that landed"
880 );
881 }
882
883 #[test]
886 fn memory_sink_stores_events() {
887 let sink = MemorySink::new(100);
888 assert!(sink.is_empty());
889
890 let event = make_event(AuditAction::Write, AuditDecision::Allow);
891 sink.emit(&event).unwrap();
892 assert_eq!(sink.len(), 1);
893 }
894
895 #[test]
896 fn memory_sink_evicts_on_capacity() {
897 let sink = MemorySink::new(2);
898 for i in 0..3 {
899 let mut event = make_event(AuditAction::Read, AuditDecision::Allow);
900 event.timestamp = i;
901 sink.emit(&event).unwrap();
902 }
903 assert_eq!(sink.len(), 2);
904 let events = sink.events();
905 assert_eq!(events[0].timestamp, 1);
906 assert_eq!(events[1].timestamp, 2);
907 }
908
909 #[test]
910 fn memory_sink_query_by_principal() {
911 let sink = MemorySink::new(100);
912 sink.emit(&AuditEvent::new(
913 "alice",
914 AuditAction::Write,
915 "cpu",
916 AuditDecision::Allow,
917 ))
918 .unwrap();
919 sink.emit(&AuditEvent::new(
920 "bob",
921 AuditAction::Read,
922 "cpu",
923 AuditDecision::Deny,
924 ))
925 .unwrap();
926 sink.emit(&AuditEvent::new(
927 "alice",
928 AuditAction::Delete,
929 "logs",
930 AuditDecision::Allow,
931 ))
932 .unwrap();
933
934 let alice = sink.query_by_principal("alice");
935 assert_eq!(alice.len(), 2);
936 let bob = sink.query_by_principal("bob");
937 assert_eq!(bob.len(), 1);
938 }
939
940 #[test]
941 fn memory_sink_query_by_decision() {
942 let sink = MemorySink::new(100);
943 sink.emit(&make_event(AuditAction::Write, AuditDecision::Allow))
944 .unwrap();
945 sink.emit(&make_event(AuditAction::Read, AuditDecision::Deny))
946 .unwrap();
947 sink.emit(&make_event(AuditAction::Delete, AuditDecision::Deny))
948 .unwrap();
949
950 let denied = sink.query_by_decision(AuditDecision::Deny);
951 assert_eq!(denied.len(), 2);
952 }
953
954 #[test]
955 fn memory_sink_query_by_resource() {
956 let sink = MemorySink::new(100);
957 sink.emit(&AuditEvent::new(
958 "a",
959 AuditAction::Write,
960 "cpu",
961 AuditDecision::Allow,
962 ))
963 .unwrap();
964 sink.emit(&AuditEvent::new(
965 "b",
966 AuditAction::Write,
967 "memory",
968 AuditDecision::Allow,
969 ))
970 .unwrap();
971 sink.emit(&AuditEvent::new(
972 "c",
973 AuditAction::Write,
974 "cpu",
975 AuditDecision::Allow,
976 ))
977 .unwrap();
978
979 let cpu = sink.query_by_resource("cpu");
980 assert_eq!(cpu.len(), 2);
981 }
982
983 #[test]
984 fn memory_sink_query_by_time_range() {
985 let sink = MemorySink::new(100);
986 for ts in [100, 200, 300] {
987 let mut event = make_event(AuditAction::Read, AuditDecision::Allow);
988 event.timestamp = ts;
989 sink.emit(&event).unwrap();
990 }
991
992 let range = sink.query_by_time_range(150, 250);
993 assert_eq!(range.len(), 1);
994 assert_eq!(range[0].timestamp, 200);
995 }
996
997 #[test]
998 fn memory_sink_drain() {
999 let sink = MemorySink::new(100);
1000 sink.emit(&make_event(AuditAction::Write, AuditDecision::Allow))
1001 .unwrap();
1002 sink.emit(&make_event(AuditAction::Read, AuditDecision::Allow))
1003 .unwrap();
1004
1005 let drained = sink.drain();
1006 assert_eq!(drained.len(), 2);
1007 assert!(sink.is_empty());
1008 }
1009
1010 #[test]
1013 fn writer_sink_json_lines() {
1014 let buf: Vec<u8> = Vec::new();
1015 let sink = WriterSink::new(buf);
1016
1017 let event1 = make_event(AuditAction::Write, AuditDecision::Allow);
1018 let event2 = make_event(AuditAction::Read, AuditDecision::Deny);
1019 sink.emit(&event1).unwrap();
1020 sink.emit(&event2).unwrap();
1021 sink.flush().unwrap();
1022
1023 let output = sink.writer.lock();
1024 let text = String::from_utf8(output.clone()).unwrap();
1025 let lines: Vec<&str> = text.lines().collect();
1026 assert_eq!(lines.len(), 2);
1027
1028 let parsed: AuditEvent = serde_json::from_str(lines[0]).unwrap();
1029 assert_eq!(parsed.principal, "alice");
1030 assert_eq!(parsed.decision, AuditDecision::Allow);
1031
1032 let parsed2: AuditEvent = serde_json::from_str(lines[1]).unwrap();
1033 assert_eq!(parsed2.decision, AuditDecision::Deny);
1034 }
1035
1036 #[test]
1039 fn tracing_sink_emits_ok() {
1040 let sink = TracingSink;
1041 let event = make_event(AuditAction::Write, AuditDecision::Allow);
1042 assert!(sink.emit(&event).is_ok());
1043
1044 let deny = make_event(AuditAction::Delete, AuditDecision::Deny);
1045 assert!(sink.emit(&deny).is_ok());
1046 }
1047
1048 #[test]
1051 fn logger_assigns_sequence_numbers() {
1052 let mem = Arc::new(MemorySink::new(100));
1053 let logger = AuditLogger::new();
1054 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
1055
1056 logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
1057 logger.log(make_event(AuditAction::Read, AuditDecision::Allow));
1058 logger.log(make_event(AuditAction::Delete, AuditDecision::Deny));
1059
1060 let events = mem.events();
1061 assert_eq!(events.len(), 3);
1062 assert_eq!(events[0].id, 1);
1063 assert_eq!(events[1].id, 2);
1064 assert_eq!(events[2].id, 3);
1065 assert_eq!(logger.next_sequence(), 4);
1066 }
1067
1068 #[test]
1069 fn logger_dispatches_to_multiple_sinks() {
1070 let mem1 = Arc::new(MemorySink::new(100));
1071 let mem2 = Arc::new(MemorySink::new(100));
1072
1073 let logger = AuditLogger::new();
1074 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem1))));
1075 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem2))));
1076 assert_eq!(logger.sink_count(), 2);
1077
1078 logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
1079 assert_eq!(mem1.len(), 1);
1080 assert_eq!(mem2.len(), 1);
1081 }
1082
1083 #[test]
1084 fn logger_all_actions_logged() {
1085 let mem = Arc::new(MemorySink::new(100));
1086 let logger = AuditLogger::new();
1087 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
1088
1089 logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
1091 logger.log(make_event(AuditAction::Write, AuditDecision::Deny));
1092 logger.log(make_event(AuditAction::LoginFailure, AuditDecision::Allow));
1093 logger.log(make_event(AuditAction::LoginFailure, AuditDecision::Deny));
1094 logger.log(make_event(AuditAction::ApiKeyCreate, AuditDecision::Allow));
1095
1096 assert_eq!(mem.len(), 5);
1097 let denied = mem.query_by_decision(AuditDecision::Deny);
1098 assert_eq!(denied.len(), 2);
1099 }
1100
1101 #[test]
1102 fn logger_with_file_sink() {
1103 let dir = tempfile::tempdir().unwrap();
1104 let path = dir.path().join("audit.log");
1105 let file = std::fs::File::create(&path).unwrap();
1106
1107 let logger = AuditLogger::new();
1108 logger.add_sink(Box::new(WriterSink::new(file)));
1109
1110 logger.log(
1111 AuditEvent::new("alice", AuditAction::Write, "cpu", AuditDecision::Allow)
1112 .with_source_ip("10.0.0.1")
1113 .with_request_id("req-001")
1114 .with_timestamp(1_700_000_000_000),
1115 );
1116 logger.log(
1117 AuditEvent::new("bob", AuditAction::Read, "memory", AuditDecision::Deny)
1118 .with_timestamp(1_700_000_000_001),
1119 );
1120 logger.flush();
1121
1122 let content = std::fs::read_to_string(&path).unwrap();
1123 let lines: Vec<&str> = content.lines().collect();
1124 assert_eq!(lines.len(), 2);
1125
1126 let e1: AuditEvent = serde_json::from_str(lines[0]).unwrap();
1127 assert_eq!(e1.principal, "alice");
1128 assert_eq!(e1.source_ip.as_deref(), Some("10.0.0.1"));
1129 assert_eq!(e1.request_id.as_deref(), Some("req-001"));
1130 assert_eq!(e1.id, 1);
1131
1132 let e2: AuditEvent = serde_json::from_str(lines[1]).unwrap();
1133 assert_eq!(e2.principal, "bob");
1134 assert_eq!(e2.decision, AuditDecision::Deny);
1135 assert_eq!(e2.id, 2);
1136 }
1137
1138 struct SinkWrapper(Arc<MemorySink>);
1142
1143 impl AuditSink for SinkWrapper {
1144 fn emit(&self, event: &AuditEvent) -> Result<()> {
1145 self.0.emit(event)
1146 }
1147
1148 fn flush(&self) -> Result<()> {
1149 self.0.flush()
1150 }
1151 }
1152
1153 struct FailingSink;
1157
1158 impl AuditSink for FailingSink {
1159 fn emit(&self, _event: &AuditEvent) -> Result<()> {
1160 Err(AuditError::Internal("sink unavailable".into()))
1161 }
1162
1163 fn flush(&self) -> Result<()> {
1164 Err(AuditError::Internal("flush failed".into()))
1165 }
1166 }
1167
1168 #[test]
1171 fn log_strict_succeeds_with_healthy_sinks() {
1172 let mem = Arc::new(MemorySink::new(100));
1173 let logger = AuditLogger::new();
1174 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
1175
1176 logger
1177 .log_strict(make_event(AuditAction::Write, AuditDecision::Allow))
1178 .unwrap();
1179 assert_eq!(mem.len(), 1);
1180 }
1181
1182 #[test]
1183 fn log_strict_returns_error_on_sink_failure() {
1184 let logger = AuditLogger::new();
1185 logger.add_sink(Box::new(FailingSink));
1186
1187 let result = logger.log_strict(make_event(AuditAction::Write, AuditDecision::Allow));
1188 assert!(result.is_err());
1189 }
1190
1191 #[test]
1192 fn log_strict_fails_if_any_sink_fails() {
1193 let mem = Arc::new(MemorySink::new(100));
1194 let logger = AuditLogger::new();
1195 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
1196 logger.add_sink(Box::new(FailingSink)); let result = logger.log_strict(make_event(AuditAction::Delete, AuditDecision::Deny));
1199 assert_eq!(mem.len(), 1);
1201 match result {
1203 Err(AuditError::PartialDelivery {
1204 succeeded,
1205 total,
1206 failures,
1207 }) => {
1208 assert_eq!(succeeded, 1);
1209 assert_eq!(total, 2);
1210 assert_eq!(failures.len(), 1);
1211 }
1212 other => panic!("Expected PartialDelivery, got: {other:?}"),
1213 }
1214 }
1215
1216 #[test]
1217 fn flush_strict_propagates_failure() {
1218 let logger = AuditLogger::new();
1219 logger.add_sink(Box::new(FailingSink));
1220
1221 let result = logger.flush_strict();
1222 assert!(result.is_err());
1223 }
1224
1225 #[test]
1226 fn flush_strict_succeeds_with_healthy_sinks() {
1227 let mem = Arc::new(MemorySink::new(100));
1228 let logger = AuditLogger::new();
1229 logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem))));
1230
1231 logger.flush_strict().unwrap();
1232 }
1233
1234 #[test]
1235 fn log_best_effort_continues_on_sink_failure() {
1236 let mem = Arc::new(MemorySink::new(100));
1237 let logger = AuditLogger::new();
1238 logger.add_sink(Box::new(FailingSink)); logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem)))); logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
1243 assert_eq!(mem.len(), 1);
1244 }
1245
1246 #[test]
1249 fn memory_sink_is_not_durable() {
1250 let sink = MemorySink::new(100);
1251 assert!(!sink.is_durable());
1252 }
1253
1254 #[test]
1255 fn writer_sink_is_durable() {
1256 let sink = WriterSink::new(Vec::<u8>::new());
1257 assert!(sink.is_durable());
1258 }
1259
1260 #[test]
1261 fn validate_no_durable_sink_fails() {
1262 let logger = AuditLogger::new();
1263 logger.add_sink(Box::new(MemorySink::new(100)));
1264 let result = logger.validate_has_durable_sink();
1265 assert!(matches!(result, Err(AuditError::NoDurableSink)));
1266 assert!(!logger.has_durable_sink());
1267 }
1268
1269 #[test]
1270 fn validate_with_durable_sink_succeeds() {
1271 let logger = AuditLogger::new();
1272 logger.add_sink(Box::new(MemorySink::new(100)));
1273 logger.add_sink(Box::new(WriterSink::new(Vec::<u8>::new())));
1274 logger.validate_has_durable_sink().unwrap();
1275 assert!(logger.has_durable_sink());
1276 }
1277
1278 #[test]
1279 fn validate_empty_logger_fails() {
1280 let logger = AuditLogger::new();
1281 let result = logger.validate_has_durable_sink();
1282 assert!(matches!(result, Err(AuditError::NoDurableSink)));
1283 }
1284
1285 fn make_named_event(principal: &str) -> AuditEvent {
1288 AuditEvent::new(principal, AuditAction::Write, "cpu", AuditDecision::Allow)
1289 .with_timestamp(1_700_000_000_000)
1290 }
1291
1292 #[test]
1293 fn overflow_increments_dropped_count() {
1294 let sink = MemorySink::new(3);
1295 for i in 0..5 {
1296 sink.emit(&make_named_event(&format!("user{i}"))).unwrap();
1297 }
1298 assert_eq!(sink.dropped_count(), 2);
1299 assert_eq!(sink.len(), 3);
1300 }
1301
1302 #[test]
1303 fn fill_ratio_reports_correctly() {
1304 let sink = MemorySink::new(10);
1305 assert!((sink.fill_ratio() - 0.0).abs() < f64::EPSILON);
1306 for i in 0..5 {
1307 sink.emit(&make_named_event(&format!("u{i}"))).unwrap();
1308 }
1309 assert!((sink.fill_ratio() - 0.5).abs() < f64::EPSILON);
1310 for i in 5..10 {
1311 sink.emit(&make_named_event(&format!("u{i}"))).unwrap();
1312 }
1313 assert!((sink.fill_ratio() - 1.0).abs() < f64::EPSILON);
1314 }
1315
1316 #[test]
1317 fn fill_ratio_stays_at_one_when_overflowing() {
1318 let sink = MemorySink::new(2);
1319 for i in 0..5 {
1320 sink.emit(&make_named_event(&format!("u{i}"))).unwrap();
1321 }
1322 assert!((sink.fill_ratio() - 1.0).abs() < f64::EPSILON);
1323 assert_eq!(sink.dropped_count(), 3);
1324 }
1325
1326 #[test]
1327 fn overflow_preserves_newest_events() {
1328 let sink = MemorySink::new(3);
1329 for i in 0..6 {
1330 sink.emit(&make_named_event(&format!("user{i}"))).unwrap();
1331 }
1332 let events = sink.events();
1333 let principals: Vec<&str> = events.iter().map(|e| e.principal.as_str()).collect();
1334 assert_eq!(principals, vec!["user3", "user4", "user5"]);
1335 }
1336}
1337
1338#[cfg(test)]
1339mod durability_tests {
1340 use super::*;
1341 use crate::audit::model::verify_chain_from;
1342 use crate::audit::{AuditAction, AuditDecision};
1343
1344 fn event(principal: &str) -> AuditEvent {
1345 AuditEvent::new(principal, AuditAction::Delete, "cpu", AuditDecision::Allow)
1346 }
1347
1348 #[test]
1349 fn a_restart_continues_one_chain() {
1350 let dir = tempfile::tempdir().unwrap();
1351 let path = dir.path().join("audit.jsonl");
1352
1353 {
1354 let logger = AuditLogger::new();
1355 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
1356 logger.log(event("alice"));
1357 logger.log(event("bob"));
1358 }
1359
1360 let last = last_event(&path).unwrap().expect("a previous event");
1362 assert_eq!(last.id, 2);
1363 {
1364 let logger = AuditLogger::new().resume_from(last.id, last.event_hash.clone());
1365 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
1366 logger.log(event("carol"));
1367 }
1368
1369 let events = read_events(&path).unwrap();
1370 assert_eq!(events.len(), 3, "every event survives the restart");
1371 assert_eq!(
1372 events.iter().map(|e| e.id).collect::<Vec<_>>(),
1373 vec![1, 2, 3],
1374 "sequence numbers continue rather than restarting at 1"
1375 );
1376 verify_chain_from(&events, None, None)
1377 .expect("the chain must verify straight through the restart");
1378 }
1379
1380 #[test]
1385 fn a_keyed_chain_verifies_only_with_its_key() {
1386 let key = b"seal".to_vec();
1387 let logger = AuditLogger::new().with_hmac_key(key.clone());
1388 let handle = std::sync::Arc::new(MemorySink::new(16));
1389 logger.add_sink(Box::new(SharedMemorySink(handle.clone())));
1390
1391 logger.log(event("alice"));
1392 logger.log_strict(event("bob")).unwrap();
1393
1394 let events = handle.events();
1395 assert_eq!(events.len(), 2);
1396 verify_chain_from(&events, None, Some(&key))
1397 .expect("both `log` and `log_strict` must seal the same way");
1398 assert!(
1399 verify_chain_from(&events, None, None).is_err(),
1400 "a keyed chain must not verify as an unkeyed one"
1401 );
1402 }
1403
1404 struct SharedMemorySink(std::sync::Arc<MemorySink>);
1406
1407 impl AuditSink for SharedMemorySink {
1408 fn emit(&self, event: &AuditEvent) -> Result<()> {
1409 self.0.emit(event)
1410 }
1411 fn flush(&self) -> Result<()> {
1412 self.0.flush()
1413 }
1414 }
1415
1416 #[test]
1420 fn a_partial_trailing_line_is_skipped() {
1421 let dir = tempfile::tempdir().unwrap();
1422 let path = dir.path().join("audit.jsonl");
1423 {
1424 let logger = AuditLogger::new();
1425 logger.add_sink(Box::new(FileSink::open(&path, true).unwrap()));
1426 logger.log(event("alice"));
1427 }
1428 {
1429 use std::io::Write as _;
1430 let mut f = std::fs::OpenOptions::new()
1431 .append(true)
1432 .open(&path)
1433 .unwrap();
1434 write!(f, "{{\"id\":2,\"principal\":\"tr").unwrap();
1435 }
1436 let last = last_event(&path).unwrap().expect("the complete event");
1437 assert_eq!(last.id, 1);
1438 assert_eq!(read_events(&path).unwrap().len(), 1);
1439 }
1440}