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, error, 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    /// Bytes known to hold whole lines — the rewind point.
227    ///
228    /// A `writeln!` that fails part-way leaves a line with no terminator, and
229    /// the *next* event is then appended to it: one line holding a fragment
230    /// and a whole event, which parses as neither. So a failed write loses
231    /// the event it failed on and takes the following one with it, which is
232    /// the opposite of what a trail is for.
233    good_len: std::sync::atomic::AtomicU64,
234}
235
236impl FileSink {
237    /// Open (or create) an audit log at `path` in append mode.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if the file or its parent directory cannot be
242    /// created or opened.
243    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            && !parent.as_os_str().is_empty()
247        {
248            std::fs::create_dir_all(parent)?;
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        // One `write_all` of the whole line, and a rewind if it does not
273        // land: a half-written line swallows the *next* event, because the
274        // fragment carries no terminator and the following write continues
275        // it. Losing the event that failed is unavoidable; losing the one
276        // after it is not.
277        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
306/// The last event of an audit log, for anchoring a new chain onto it.
307///
308/// Returns `None` for a missing or empty file — a first start. A trailing
309/// partial line (a crash mid-write) is skipped: it is not a sealed event,
310/// so the chain continues from the last one that is.
311///
312/// # Errors
313///
314/// Returns an error if the file exists but cannot be read.
315pub 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
327/// Read every sealed event from an audit log, oldest first.
328///
329/// Partial trailing lines are skipped, as in [`last_event`].
330///
331/// # Errors
332///
333/// Returns an error if the file cannot be read.
334pub 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
346// ── TracingSink ─────────────────────────────────────────────────────
347
348/// Emits audit events as structured tracing events.
349pub 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
385// ── AuditLogger ─────────────────────────────────────────────────────
386
387/// The main audit logger, dispatching events to configured sinks.
388///
389/// Thread-safe and designed for high-throughput operation.
390/// Sinks can be added at any time, even after wrapping in `Arc`.
391///
392/// # Retention Policy
393///
394/// `AuditLogger` itself does **not** enforce a retention policy — it
395/// forwards events to sinks and forgets them.  Retention is the
396/// responsibility of each sink:
397///
398/// - [`MemorySink`]: bounded by `max_capacity` (FIFO eviction).
399/// - [`WriterSink`]: appends forever; pair with external log rotation
400///   (e.g. `logrotate`, cloud blob lifecycle rules).
401/// - [`TracingSink`]: inherits the tracing subscriber's retention.
402///
403/// For production deployments, configure a log rotation / archival
404/// pipeline and monitor the `chronix_audit_events_total` counter to
405/// ensure events are not silently dropped.
406pub struct AuditLogger {
407    sinks: RwLock<Vec<Box<dyn AuditSink>>>,
408    sequence: AtomicU64,
409    /// SHA-256 hash of the most recently emitted event.
410    prev_hash: parking_lot::Mutex<Option<String>>,
411    /// Optional HMAC key for tamper-resistant hash chain.
412    /// When set, `seal_with_key` is used instead of plain SHA-256.
413    hmac_key: Option<Vec<u8>>,
414}
415
416impl AuditLogger {
417    /// Create a new audit logger with no sinks.
418    #[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    /// Create an audit logger with an HMAC key.
429    ///
430    /// When set, each event is sealed with HMAC-SHA-256 instead of
431    /// plain SHA-256.  An attacker with database write access cannot
432    /// recompute the hash chain without possessing the key.
433    #[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    /// Continue an existing chain instead of starting a new one.
440    ///
441    /// A restart used to reset the sequence to 1 and the previous hash to
442    /// `None`, so every restart began a fresh chain in the same file. A
443    /// verifier then could not tell a restart from a truncation: both look
444    /// like an event whose `prev_hash` is `None` in the middle of the log.
445    /// Anchoring on the last event already on disk makes the file one
446    /// chain across the whole life of the deployment.
447    #[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    /// Whether the chain is sealed with a key rather than a bare hash.
455    #[must_use]
456    pub fn is_keyed(&self) -> bool {
457        self.hmac_key.is_some()
458    }
459
460    /// Add a sink. Can be called at any time, including after
461    /// wrapping in `Arc`.
462    pub fn add_sink(&self, sink: Box<dyn AuditSink>) {
463        self.sinks.write().push(sink);
464    }
465
466    /// **** Validate that at least one durable audit sink is configured.
467    ///
468    /// Returns `Ok(())` if at least one sink reports `is_durable() == true`.
469    /// Returns an error if no durable sinks are present, which means audit
470    /// events would be lost on process restart — a compliance violation in
471    /// regulated environments.
472    ///
473    /// Call this at startup after configuring all sinks.
474    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    /// Check if at least one durable sink is configured.
489    #[must_use]
490    pub fn has_durable_sink(&self) -> bool {
491        self.sinks.read().iter().any(|s| s.is_durable())
492    }
493
494    /// Log an audit event to all sinks.
495    ///
496    /// Assigns a monotonic sequence number, seals the event into the hash
497    /// chain, and writes it.
498    ///
499    /// # Sealing and writing are one step
500    ///
501    /// They were two, with the lock released in between — so two concurrent
502    /// callers could seal in one order and write in the other, leaving a file
503    /// in which `B` precedes `A` while `B.prev_hash` names `A`. That is
504    /// exactly what `verify_hash_chain` reports as **tampering**, and it
505    /// happened with no write failing and nothing logged: an ordinary pair of
506    /// simultaneous requests made a tamper-evident trail permanently
507    /// unverifiable. A trail whose whole purpose is to be checkable cannot
508    /// have a benign reason to fail its own check, because then no failure of
509    /// it means anything.
510    ///
511    /// So the chain lock is held across the emission. Audit events are rare
512    /// relative to requests, and the file sink serialises on its own mutex
513    /// anyway, so the lock is not a new bottleneck — it is the existing one,
514    /// now covering the invariant it always should have.
515    pub fn log(&self, mut event: AuditEvent) {
516        event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
517
518        // Seal the event into the hash chain.
519        //
520        // With the key, when one is configured: sealing with plain SHA-256
521        // here while `log_strict` sealed with HMAC produced a file whose
522        // links were computed two different ways, so it verified under
523        // neither — and a plain hash is recomputable by anyone who can
524        // write the file, which is the whole threat the key exists for.
525        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                // Track dropped events per-sink so operators can
547                // alert on audit pipeline failures. The event is lost for
548                // this sink but may succeed on others.
549                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        // **The chain advances only if the event reached a durable sink.**
563        // It advanced unconditionally, so an event the file never received
564        // still became the `prev_hash` of the next one — and every later
565        // verification of that file failed, reporting a full disk as
566        // tampering, for ever. A dropped event is gone either way; what this
567        // decides is whether the events *after* it remain checkable.
568        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    /// Current sequence number (next ID to be assigned).
585    #[must_use]
586    pub fn next_sequence(&self) -> u64 {
587        self.sequence.load(Ordering::Relaxed)
588    }
589
590    /// Number of sinks.
591    #[must_use]
592    pub fn sink_count(&self) -> usize {
593        self.sinks.read().len()
594    }
595
596    /// Flush all sinks.
597    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    /// Strict audit logging — fail-closed mode for regulated environments.
607    ///
608    /// Unlike [`log`](Self::log), this attempts **all** sinks even when some
609    /// fail, and returns a [`PartialDelivery`](crate::audit::error::AuditError::PartialDelivery)
610    /// error if any sink could not emit. This gives the caller full visibility
611    /// into which sinks succeeded vs failed so it can make an informed decision
612    /// on whether to abort the protected operation.
613    pub fn log_strict(&self, mut event: AuditEvent) -> Result<()> {
614        event.id = self.sequence.fetch_add(1, Ordering::Relaxed);
615
616        // Seal the event into the hash chain.
617        {
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                // Track dropped events per-sink for alerting.
638                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    /// Strict flush — tries **all** sinks, reports partial failures.
667    ///
668    /// Use this together with [`log_strict`](Self::log_strict) for
669    /// fail-closed audit contexts where all buffered events must be
670    /// durably persisted before acknowledging the protected operation.
671    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    /// **Reproduction.** Two threads logging at once must leave a file that
714    /// verifies.
715    ///
716    /// `log()` seals the event under the `prev_hash` lock and then *releases
717    /// it* before the sinks write. So two concurrent callers can seal in one
718    /// order and write in the other, and the file then holds `B` before `A`
719    /// while `B.prev_hash` names `A` — which is precisely what
720    /// `verify_hash_chain` reports as tampering. No write fails, nothing is
721    /// logged, and the trail whose entire purpose is to be checkable is
722    /// permanently uncheckable.
723    #[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    /// Lets a test keep a handle on a sink the logger owns.
766    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    /// A sink that accepts `fail_after` events and then refuses every one.
778    #[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    /// **An event that reaches no durable sink must not advance the chain.**
802    ///
803    /// It did, so an event the file never received still became the
804    /// `prev_hash` of the next one — and every later verification of that
805    /// file failed, reporting a full disk as *tampering*, for ever. The
806    /// dropped event is gone either way; what this decides is whether the
807    /// events after it stay checkable.
808    #[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        // Fails from the third event on, and it is the only *other* durable
816        // sink — so events 3.. reach the file but not this one, which is the
817        // partial-failure case the chain must survive.
818        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    /// The same question when **no** durable sink accepts the event.
844    #[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        // The memory sink saw all five, but the chain only links the two the
867        // durable sink accepted plus the ones after — so verifying the
868        // *durable* subset is what must hold. Here: events 0 and 1 landed,
869        // 2..5 did not, and each of those left the chain where it was.
870        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    // ── MemorySink tests ────────
884
885    #[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    // ── WriterSink tests ────────
1011
1012    #[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    // ── TracingSink tests ───────
1037
1038    #[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    // ── AuditLogger tests ───────
1049
1050    #[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        // Log both allow and deny
1090        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    // ── Helper: AuditSink wrapper for Arc<MemorySink> ─
1139
1140    /// Wrapper to make Arc<MemorySink> implement AuditSink.
1141    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    // ── Failing sink for strict-mode tests ─
1154
1155    /// A sink that always fails on emit.
1156    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    // ── log_strict / flush_strict tests ─
1169
1170    #[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)); // second sink fails
1197
1198        let result = logger.log_strict(make_event(AuditAction::Delete, AuditDecision::Deny));
1199        // All sinks attempted — first sink received the event
1200        assert_eq!(mem.len(), 1);
1201        // PartialDelivery reports exactly one success and one failure
1202        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)); // first sink fails
1239        logger.add_sink(Box::new(SinkWrapper(Arc::clone(&mem)))); // second succeeds
1240
1241        // Best-effort log should still emit to the healthy sink
1242        logger.log(make_event(AuditAction::Write, AuditDecision::Allow));
1243        assert_eq!(mem.len(), 1);
1244    }
1245
1246    // ── Durable sink enforcement ────────────────────────
1247
1248    #[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    // ── MemorySink overflow observability ─────────────────
1286
1287    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        // Restart: anchor on what is already on disk.
1361        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    /// Without the key, the chain detects corruption but not tampering:
1381    /// anyone who can rewrite the file can recompute a plain SHA-256 chain.
1382    /// `log` used to seal without the key while `log_strict` sealed with it,
1383    /// so a mixed log verified under neither.
1384    #[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    /// A sink that shares one buffer with the test.
1405    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    /// A crash mid-write leaves a partial line. It is not a sealed event,
1417    /// so the chain continues from the last one that is, rather than
1418    /// refusing to start.
1419    #[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}