Skip to main content

chronix_security/audit/
logger.rs

1//! Audit logger — captures, stores, and exports audit events.
2
3use 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
14// ── AuditSink trait ─────────────────────────────────────────────────
15
16/// Pluggable sink for audit event output.
17pub trait AuditSink: Send + Sync {
18    /// Write an audit event.
19    fn emit(&self, event: &AuditEvent) -> Result<()>;
20
21    /// Flush any buffered output.
22    fn flush(&self) -> Result<()> {
23        Ok(())
24    }
25
26    /// Whether this sink is considered durable.
27    ///
28    /// Durable sinks persist events to stable storage (disk, remote
29    /// service, etc.) so they survive process restarts. In-memory
30    /// sinks are NOT durable. Production deployments should have at
31    /// least one durable sink for compliance.
32    fn is_durable(&self) -> bool {
33        false
34    }
35}
36
37// ── MemorySink ──────────────────────────────────────────────────────
38
39/// In-memory audit sink, useful for testing and querying.
40pub struct MemorySink {
41    events: RwLock<VecDeque<AuditEvent>>,
42    max_capacity: usize,
43    /// Number of events dropped due to capacity overflow.
44    dropped_count: AtomicU64,
45}
46
47impl MemorySink {
48    /// Create a new memory sink with the given capacity.
49    #[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    /// Number of events that were dropped due to overflow.
59    #[must_use]
60    pub fn dropped_count(&self) -> u64 {
61        self.dropped_count.load(Ordering::Relaxed)
62    }
63
64    /// All stored events.
65    #[must_use]
66    pub fn events(&self) -> Vec<AuditEvent> {
67        self.events.read().iter().cloned().collect()
68    }
69
70    /// Number of stored events.
71    #[must_use]
72    pub fn len(&self) -> usize {
73        self.events.read().len()
74    }
75
76    /// Whether the sink is empty.
77    #[must_use]
78    pub fn is_empty(&self) -> bool {
79        self.events.read().is_empty()
80    }
81
82    /// Query events by principal.
83    #[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    /// Query events by decision.
94    #[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    /// Query events by resource.
105    #[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    /// Query events within a time range.
116    #[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    /// Drain all events.
127    pub fn drain(&self) -> Vec<AuditEvent> {
128        let mut q = self.events.write();
129        std::mem::take(&mut *q).into()
130    }
131
132    /// Fraction of capacity currently used (0.0 – 1.0).
133    #[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            // Warn at 1, 10, 100, 1000, then every 1000 drops to avoid
156            // large silent gaps (the old `is_power_of_two()` pattern had a
157            // 64K-event gap between the 65 536 and 131 072 warnings).
158            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
173// ── WriterSink ──────────────────────────────────────────────────────
174
175/// Writes audit events as JSON lines to an `io::Write` destination.
176///
177/// Suitable for file or stdout output.
178pub struct WriterSink<W: Write + Send + Sync> {
179    writer: parking_lot::Mutex<W>,
180}
181
182impl<W: Write + Send + Sync> WriterSink<W> {
183    /// Create a new writer sink.
184    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    /// WriterSink is durable — it writes to persistent output (file, etc.).
206    fn is_durable(&self) -> bool {
207        true
208    }
209}
210
211// ── FileSink ────────────────────────────────────────────────────────
212
213/// An append-only audit file, one JSON event per line.
214///
215/// This is the sink that makes an audit log an audit log: `WriterSink` over
216/// a `File` writes through the same handle but never calls `sync_data`, so
217/// a power loss takes the tail of the log with it — and the tail is where
218/// the interesting events are, because an attacker's last act is what
219/// crashed the box.
220///
221/// `sync_each` trades throughput for that guarantee. Leave it on unless
222/// the log is also shipped somewhere else synchronously.
223pub struct FileSink {
224    file: parking_lot::Mutex<std::fs::File>,
225    sync_each: bool,
226}
227
228impl FileSink {
229    /// Open (or create) an audit log at `path` in append mode.
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if the file or its parent directory cannot be
234    /// created or opened.
235    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
277/// The last event of an audit log, for anchoring a new chain onto it.
278///
279/// Returns `None` for a missing or empty file — a first start. A trailing
280/// partial line (a crash mid-write) is skipped: it is not a sealed event,
281/// so the chain continues from the last one that is.
282///
283/// # Errors
284///
285/// Returns an error if the file exists but cannot be read.
286pub 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
298/// Read every sealed event from an audit log, oldest first.
299///
300/// Partial trailing lines are skipped, as in [`last_event`].
301///
302/// # Errors
303///
304/// Returns an error if the file cannot be read.
305pub 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
317// ── TracingSink ─────────────────────────────────────────────────────
318
319/// Emits audit events as structured tracing events.
320pub 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
356// ── AuditLogger ─────────────────────────────────────────────────────
357
358/// The main audit logger, dispatching events to configured sinks.
359///
360/// Thread-safe and designed for high-throughput operation.
361/// Sinks can be added at any time, even after wrapping in `Arc`.
362///
363/// # Retention Policy
364///
365/// `AuditLogger` itself does **not** enforce a retention policy — it
366/// forwards events to sinks and forgets them.  Retention is the
367/// responsibility of each sink:
368///
369/// - [`MemorySink`]: bounded by `max_capacity` (FIFO eviction).
370/// - [`WriterSink`]: appends forever; pair with external log rotation
371///   (e.g. `logrotate`, cloud blob lifecycle rules).
372/// - [`TracingSink`]: inherits the tracing subscriber's retention.
373///
374/// For production deployments, configure a log rotation / archival
375/// pipeline and monitor the `chronix_audit_events_total` counter to
376/// ensure events are not silently dropped.
377pub struct AuditLogger {
378    sinks: RwLock<Vec<Box<dyn AuditSink>>>,
379    sequence: AtomicU64,
380    /// SHA-256 hash of the most recently emitted event.
381    prev_hash: parking_lot::Mutex<Option<String>>,
382    /// Optional HMAC key for tamper-resistant hash chain.
383    /// When set, `seal_with_key` is used instead of plain SHA-256.
384    hmac_key: Option<Vec<u8>>,
385}
386
387impl AuditLogger {
388    /// Create a new audit logger with no sinks.
389    #[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    /// Create an audit logger with an HMAC key.
400    ///
401    /// When set, each event is sealed with HMAC-SHA-256 instead of
402    /// plain SHA-256.  An attacker with database write access cannot
403    /// recompute the hash chain without possessing the key.
404    #[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    /// Continue an existing chain instead of starting a new one.
411    ///
412    /// A restart used to reset the sequence to 1 and the previous hash to
413    /// `None`, so every restart began a fresh chain in the same file. A
414    /// verifier then could not tell a restart from a truncation: both look
415    /// like an event whose `prev_hash` is `None` in the middle of the log.
416    /// Anchoring on the last event already on disk makes the file one
417    /// chain across the whole life of the deployment.
418    #[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    /// Whether the chain is sealed with a key rather than a bare hash.
426    #[must_use]
427    pub fn is_keyed(&self) -> bool {
428        self.hmac_key.is_some()
429    }
430
431    /// Add a sink. Can be called at any time, including after
432    /// wrapping in `Arc`.
433    pub fn add_sink(&self, sink: Box<dyn AuditSink>) {
434        self.sinks.write().push(sink);
435    }
436
437    /// **** Validate that at least one durable audit sink is configured.
438    ///
439    /// Returns `Ok(())` if at least one sink reports `is_durable() == true`.
440    /// Returns an error if no durable sinks are present, which means audit
441    /// events would be lost on process restart — a compliance violation in
442    /// regulated environments.
443    ///
444    /// Call this at startup after configuring all sinks.
445    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    /// Check if at least one durable sink is configured.
460    #[must_use]
461    pub fn has_durable_sink(&self) -> bool {
462        self.sinks.read().iter().any(|s| s.is_durable())
463    }
464
465    /// Log an audit event to all sinks.
466    ///
467    /// Assigns a monotonic sequence number, seals the event with a
468    /// SHA-256 hash chain, and updates metrics.
469    pub fn log(&self, mut event: AuditEvent) {
470        event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
471
472        // Seal the event into the hash chain.
473        //
474        // With the key, when one is configured: sealing with plain SHA-256
475        // here while `log_strict` sealed with HMAC produced a file whose
476        // links were computed two different ways, so it verified under
477        // neither — and a plain hash is recomputable by anyone who can
478        // write the file, which is the whole threat the key exists for.
479        {
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                // Track dropped events per-sink so operators can
502                // alert on audit pipeline failures. The event is lost for
503                // this sink but may succeed on others.
504                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    /// Current sequence number (next ID to be assigned).
516    #[must_use]
517    pub fn next_sequence(&self) -> u64 {
518        self.sequence.load(Ordering::Relaxed)
519    }
520
521    /// Number of sinks.
522    #[must_use]
523    pub fn sink_count(&self) -> usize {
524        self.sinks.read().len()
525    }
526
527    /// Flush all sinks.
528    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    /// Strict audit logging — fail-closed mode for regulated environments.
538    ///
539    /// Unlike [`log`](Self::log), this attempts **all** sinks even when some
540    /// fail, and returns a [`PartialDelivery`](crate::audit::error::AuditError::PartialDelivery)
541    /// error if any sink could not emit. This gives the caller full visibility
542    /// into which sinks succeeded vs failed so it can make an informed decision
543    /// on whether to abort the protected operation.
544    pub fn log_strict(&self, mut event: AuditEvent) -> Result<()> {
545        event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
546
547        // Seal the event into the hash chain.
548        {
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                // Track dropped events per-sink for alerting.
569                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    /// Strict flush — tries **all** sinks, reports partial failures.
598    ///
599    /// Use this together with [`log_strict`](Self::log_strict) for
600    /// fail-closed audit contexts where all buffered events must be
601    /// durably persisted before acknowledging the protected operation.
602    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    // ── MemorySink tests ────────
645
646    #[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    // ── WriterSink tests ────────
772
773    #[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    // ── TracingSink tests ───────
798
799    #[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    // ── AuditLogger tests ───────
810
811    #[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        // Log both allow and deny
851        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    // ── Helper: AuditSink wrapper for Arc<MemorySink> ─
900
901    /// Wrapper to make Arc<MemorySink> implement AuditSink.
902    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    // ── Failing sink for strict-mode tests ─
915
916    /// A sink that always fails on emit.
917    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    // ── log_strict / flush_strict tests ─
930
931    #[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)); // second sink fails
958
959        let result = logger.log_strict(make_event(AuditAction::Delete, AuditDecision::Deny));
960        // All sinks attempted — first sink received the event
961        assert_eq!(mem.len(), 1);
962        // PartialDelivery reports exactly one success and one failure
963        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)); // first sink fails
1000        logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem)))); // second succeeds
1001
1002        // Best-effort log should still emit to the healthy sink
1003        logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
1004        assert_eq!(mem.len(), 1);
1005    }
1006
1007    // ── Durable sink enforcement ────────────────────────
1008
1009    #[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    // ── MemorySink overflow observability ─────────────────
1047
1048    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        // Restart: anchor on what is already on disk.
1122        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    /// Without the key, the chain detects corruption but not tampering:
1142    /// anyone who can rewrite the file can recompute a plain SHA-256 chain.
1143    /// `log` used to seal without the key while `log_strict` sealed with it,
1144    /// so a mixed log verified under neither.
1145    #[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    /// A sink that shares one buffer with the test.
1166    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    /// A crash mid-write leaves a partial line. It is not a sealed event,
1178    /// so the chain continues from the last one that is, rather than
1179    /// refusing to start.
1180    #[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}