1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum AuditEventType {
15 BlockPut,
16 BlockGet,
17 BlockDelete,
18 BlockPin,
19 BlockUnpin,
20 GarbageCollect,
21 Compaction,
22 Migration,
23 Replication,
24 AccessDenied,
25}
26
27impl AuditEventType {
28 fn as_str(&self) -> &'static str {
29 match self {
30 Self::BlockPut => "BlockPut",
31 Self::BlockGet => "BlockGet",
32 Self::BlockDelete => "BlockDelete",
33 Self::BlockPin => "BlockPin",
34 Self::BlockUnpin => "BlockUnpin",
35 Self::GarbageCollect => "GarbageCollect",
36 Self::Compaction => "Compaction",
37 Self::Migration => "Migration",
38 Self::Replication => "Replication",
39 Self::AccessDenied => "AccessDenied",
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct AuditEntry {
47 pub sequence: u64,
48 pub timestamp: u64,
49 pub event_type: AuditEventType,
50 pub cid: String,
51 pub actor: String,
52 pub details: String,
53 pub bytes_affected: u64,
54 pub success: bool,
55 pub checksum: u64,
56}
57
58#[derive(Debug, Clone)]
60pub struct AuditTrailConfig {
61 pub max_entries: usize,
62 pub retention_seconds: u64,
63 pub enable_checksums: bool,
64 pub log_reads: bool,
65}
66
67impl Default for AuditTrailConfig {
68 fn default() -> Self {
69 Self {
70 max_entries: 10_000,
71 retention_seconds: 86_400, enable_checksums: true,
73 log_reads: false,
74 }
75 }
76}
77
78#[derive(Debug, Clone, Default)]
80pub struct AuditFilter {
81 pub event_type: Option<AuditEventType>,
82 pub cid: Option<String>,
83 pub actor: Option<String>,
84 pub since: Option<u64>,
85 pub until: Option<u64>,
86 pub success_only: bool,
87}
88
89#[derive(Debug, Clone, Default)]
91pub struct AuditTrailStats {
92 pub total_entries: u64,
93 pub entries_pruned: u64,
94 pub integrity_failures: u64,
95 pub events_by_type: HashMap<String, u64>,
96}
97
98pub struct AuditTrail {
100 config: AuditTrailConfig,
101 entries: Vec<AuditEntry>,
102 next_sequence: u64,
103 stats: AuditTrailStats,
104}
105
106pub fn compute_checksum(entry: &AuditEntry) -> u64 {
116 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
117 const FNV_PRIME: u64 = 0x0100_0000_01b3;
118
119 let mut hash = FNV_OFFSET;
120
121 let mut feed = |bytes: &[u8]| {
123 for &b in bytes {
124 hash ^= u64::from(b);
125 hash = hash.wrapping_mul(FNV_PRIME);
126 }
127 };
128
129 feed(&entry.sequence.to_le_bytes());
130 feed(&entry.timestamp.to_le_bytes());
131 feed(entry.event_type.as_str().as_bytes());
132 feed(entry.cid.as_bytes());
133 feed(entry.actor.as_bytes());
134 feed(entry.details.as_bytes());
135 feed(&entry.bytes_affected.to_le_bytes());
136 feed(&[u8::from(entry.success)]);
137
138 hash
139}
140
141fn now_secs() -> u64 {
146 std::time::SystemTime::now()
147 .duration_since(std::time::UNIX_EPOCH)
148 .unwrap_or_default()
149 .as_secs()
150}
151
152impl AuditTrail {
157 pub fn new(config: AuditTrailConfig) -> Self {
159 Self {
160 config,
161 entries: Vec::new(),
162 next_sequence: 1,
163 stats: AuditTrailStats::default(),
164 }
165 }
166
167 pub fn log_event(
172 &mut self,
173 event_type: AuditEventType,
174 cid: &str,
175 actor: &str,
176 details: &str,
177 bytes: u64,
178 success: bool,
179 ) -> u64 {
180 if event_type == AuditEventType::BlockGet && !self.config.log_reads {
182 return 0;
183 }
184
185 let seq = self.next_sequence;
186 self.next_sequence = self.next_sequence.wrapping_add(1);
187
188 let mut entry = AuditEntry {
189 sequence: seq,
190 timestamp: now_secs(),
191 event_type: event_type.clone(),
192 cid: cid.to_string(),
193 actor: actor.to_string(),
194 details: details.to_string(),
195 bytes_affected: bytes,
196 success,
197 checksum: 0,
198 };
199
200 if self.config.enable_checksums {
201 entry.checksum = compute_checksum(&entry);
202 }
203
204 self.stats.total_entries += 1;
206 *self
207 .stats
208 .events_by_type
209 .entry(event_type.as_str().to_string())
210 .or_insert(0) += 1;
211
212 self.entries.push(entry);
213
214 seq
215 }
216
217 pub fn query(&self, filter: &AuditFilter) -> Vec<&AuditEntry> {
219 self.entries
220 .iter()
221 .filter(|e| {
222 if let Some(ref et) = filter.event_type {
223 if e.event_type != *et {
224 return false;
225 }
226 }
227 if let Some(ref c) = filter.cid {
228 if e.cid != *c {
229 return false;
230 }
231 }
232 if let Some(ref a) = filter.actor {
233 if e.actor != *a {
234 return false;
235 }
236 }
237 if let Some(since) = filter.since {
238 if e.timestamp < since {
239 return false;
240 }
241 }
242 if let Some(until) = filter.until {
243 if e.timestamp > until {
244 return false;
245 }
246 }
247 if filter.success_only && !e.success {
248 return false;
249 }
250 true
251 })
252 .collect()
253 }
254
255 pub fn get_entry(&self, sequence: u64) -> Option<&AuditEntry> {
257 self.entries.iter().find(|e| e.sequence == sequence)
258 }
259
260 pub fn verify_integrity(&self) -> Result<(), Vec<u64>> {
265 let bad: Vec<u64> = self
266 .entries
267 .iter()
268 .filter(|e| compute_checksum(e) != e.checksum)
269 .map(|e| e.sequence)
270 .collect();
271
272 if bad.is_empty() {
273 Ok(())
274 } else {
275 Err(bad)
276 }
277 }
278
279 pub fn prune_before(&mut self, timestamp: u64) -> usize {
283 let before = self.entries.len();
284 self.entries.retain(|e| e.timestamp >= timestamp);
285 let pruned = before - self.entries.len();
286 self.stats.entries_pruned += pruned as u64;
287 pruned
288 }
289
290 pub fn prune_to_capacity(&mut self) -> usize {
293 let max = self.config.max_entries;
294 if self.entries.len() <= max {
295 return 0;
296 }
297 let excess = self.entries.len() - max;
298 self.entries.drain(..excess);
299 self.stats.entries_pruned += excess as u64;
300 excess
301 }
302
303 pub fn entry_count(&self) -> usize {
305 self.entries.len()
306 }
307
308 pub fn last_entry(&self) -> Option<&AuditEntry> {
310 self.entries.last()
311 }
312
313 pub fn stats(&self) -> &AuditTrailStats {
315 &self.stats
316 }
317
318 pub fn events_for_cid(&self, cid: &str) -> Vec<&AuditEntry> {
320 self.entries.iter().filter(|e| e.cid == cid).collect()
321 }
322
323 pub fn recent_failures(&self, count: usize) -> Vec<&AuditEntry> {
325 self.entries
326 .iter()
327 .rev()
328 .filter(|e| !e.success)
329 .take(count)
330 .collect()
331 }
332}
333
334#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn default_trail() -> AuditTrail {
343 AuditTrail::new(AuditTrailConfig {
344 max_entries: 100,
345 retention_seconds: 3600,
346 enable_checksums: true,
347 log_reads: true,
348 })
349 }
350
351 #[test]
354 fn test_log_block_put() {
355 let mut trail = default_trail();
356 let seq = trail.log_event(
357 AuditEventType::BlockPut,
358 "cid1",
359 "local",
360 "put block",
361 1024,
362 true,
363 );
364 assert_eq!(seq, 1);
365 assert_eq!(trail.entry_count(), 1);
366 }
367
368 #[test]
369 fn test_log_block_get() {
370 let mut trail = default_trail();
371 let seq = trail.log_event(
372 AuditEventType::BlockGet,
373 "cid2",
374 "peer1",
375 "get block",
376 512,
377 true,
378 );
379 assert!(seq > 0);
380 assert_eq!(trail.entry_count(), 1);
381 }
382
383 #[test]
384 fn test_log_block_delete() {
385 let mut trail = default_trail();
386 let seq = trail.log_event(
387 AuditEventType::BlockDelete,
388 "cid3",
389 "local",
390 "delete",
391 256,
392 true,
393 );
394 assert_eq!(seq, 1);
395 let entry = trail.get_entry(seq).expect("entry should exist");
396 assert_eq!(entry.event_type, AuditEventType::BlockDelete);
397 }
398
399 #[test]
400 fn test_log_block_pin() {
401 let mut trail = default_trail();
402 let seq = trail.log_event(AuditEventType::BlockPin, "cid4", "local", "pin", 0, true);
403 assert_eq!(
404 trail.get_entry(seq).expect("entry").event_type,
405 AuditEventType::BlockPin
406 );
407 }
408
409 #[test]
410 fn test_log_block_unpin() {
411 let mut trail = default_trail();
412 let seq = trail.log_event(
413 AuditEventType::BlockUnpin,
414 "cid5",
415 "local",
416 "unpin",
417 0,
418 true,
419 );
420 assert_eq!(
421 trail.get_entry(seq).expect("entry").event_type,
422 AuditEventType::BlockUnpin
423 );
424 }
425
426 #[test]
427 fn test_log_garbage_collect() {
428 let mut trail = default_trail();
429 let seq = trail.log_event(
430 AuditEventType::GarbageCollect,
431 "",
432 "local",
433 "gc round",
434 4096,
435 true,
436 );
437 assert_eq!(
438 trail.get_entry(seq).expect("entry").event_type,
439 AuditEventType::GarbageCollect
440 );
441 }
442
443 #[test]
444 fn test_log_compaction() {
445 let mut trail = default_trail();
446 let seq = trail.log_event(
447 AuditEventType::Compaction,
448 "",
449 "local",
450 "compact",
451 8192,
452 true,
453 );
454 assert_eq!(
455 trail.get_entry(seq).expect("entry").event_type,
456 AuditEventType::Compaction
457 );
458 }
459
460 #[test]
461 fn test_log_migration() {
462 let mut trail = default_trail();
463 let seq = trail.log_event(
464 AuditEventType::Migration,
465 "cid6",
466 "local",
467 "migrate",
468 2048,
469 true,
470 );
471 assert_eq!(
472 trail.get_entry(seq).expect("entry").event_type,
473 AuditEventType::Migration
474 );
475 }
476
477 #[test]
478 fn test_log_replication() {
479 let mut trail = default_trail();
480 let seq = trail.log_event(
481 AuditEventType::Replication,
482 "cid7",
483 "peer2",
484 "replicate",
485 1024,
486 true,
487 );
488 assert_eq!(
489 trail.get_entry(seq).expect("entry").event_type,
490 AuditEventType::Replication
491 );
492 }
493
494 #[test]
495 fn test_log_access_denied() {
496 let mut trail = default_trail();
497 let seq = trail.log_event(
498 AuditEventType::AccessDenied,
499 "cid8",
500 "peer3",
501 "denied",
502 0,
503 false,
504 );
505 let entry = trail.get_entry(seq).expect("entry");
506 assert_eq!(entry.event_type, AuditEventType::AccessDenied);
507 assert!(!entry.success);
508 }
509
510 #[test]
513 fn test_log_reads_disabled() {
514 let mut trail = AuditTrail::new(AuditTrailConfig {
515 log_reads: false,
516 ..AuditTrailConfig::default()
517 });
518 let seq = trail.log_event(AuditEventType::BlockGet, "cid", "local", "", 0, true);
519 assert_eq!(seq, 0);
520 assert_eq!(trail.entry_count(), 0);
521 }
522
523 #[test]
526 fn test_sequence_numbering() {
527 let mut trail = default_trail();
528 let s1 = trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
529 let s2 = trail.log_event(AuditEventType::BlockPut, "b", "local", "", 0, true);
530 let s3 = trail.log_event(AuditEventType::BlockDelete, "c", "local", "", 0, true);
531 assert_eq!(s1, 1);
532 assert_eq!(s2, 2);
533 assert_eq!(s3, 3);
534 }
535
536 #[test]
539 fn test_query_by_event_type() {
540 let mut trail = default_trail();
541 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
542 trail.log_event(AuditEventType::BlockGet, "b", "local", "", 0, true);
543 trail.log_event(AuditEventType::BlockPut, "c", "local", "", 0, true);
544
545 let filter = AuditFilter {
546 event_type: Some(AuditEventType::BlockPut),
547 ..Default::default()
548 };
549 let results = trail.query(&filter);
550 assert_eq!(results.len(), 2);
551 }
552
553 #[test]
554 fn test_query_by_cid() {
555 let mut trail = default_trail();
556 trail.log_event(AuditEventType::BlockPut, "cid_x", "local", "", 0, true);
557 trail.log_event(AuditEventType::BlockPut, "cid_y", "local", "", 0, true);
558
559 let filter = AuditFilter {
560 cid: Some("cid_x".to_string()),
561 ..Default::default()
562 };
563 assert_eq!(trail.query(&filter).len(), 1);
564 }
565
566 #[test]
567 fn test_query_by_actor() {
568 let mut trail = default_trail();
569 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
570 trail.log_event(AuditEventType::BlockPut, "b", "peer1", "", 0, true);
571
572 let filter = AuditFilter {
573 actor: Some("peer1".to_string()),
574 ..Default::default()
575 };
576 assert_eq!(trail.query(&filter).len(), 1);
577 }
578
579 #[test]
580 fn test_query_by_since() {
581 let mut trail = default_trail();
582 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
583
584 let filter = AuditFilter {
585 since: Some(0),
586 ..Default::default()
587 };
588 assert_eq!(trail.query(&filter).len(), 1);
589
590 let filter2 = AuditFilter {
592 since: Some(u64::MAX),
593 ..Default::default()
594 };
595 assert_eq!(trail.query(&filter2).len(), 0);
596 }
597
598 #[test]
599 fn test_query_by_until() {
600 let mut trail = default_trail();
601 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
602
603 let filter = AuditFilter {
604 until: Some(u64::MAX),
605 ..Default::default()
606 };
607 assert_eq!(trail.query(&filter).len(), 1);
608
609 let filter2 = AuditFilter {
610 until: Some(0),
611 ..Default::default()
612 };
613 assert_eq!(trail.query(&filter2).len(), 0);
614 }
615
616 #[test]
617 fn test_query_success_only() {
618 let mut trail = default_trail();
619 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
620 trail.log_event(AuditEventType::BlockPut, "b", "local", "", 0, false);
621
622 let filter = AuditFilter {
623 success_only: true,
624 ..Default::default()
625 };
626 assert_eq!(trail.query(&filter).len(), 1);
627 }
628
629 #[test]
630 fn test_query_combined_filters() {
631 let mut trail = default_trail();
632 trail.log_event(AuditEventType::BlockPut, "cid_a", "local", "", 10, true);
633 trail.log_event(AuditEventType::BlockPut, "cid_a", "peer1", "", 20, true);
634 trail.log_event(AuditEventType::BlockGet, "cid_a", "local", "", 10, true);
635 trail.log_event(AuditEventType::BlockPut, "cid_b", "local", "", 10, false);
636
637 let filter = AuditFilter {
638 event_type: Some(AuditEventType::BlockPut),
639 cid: Some("cid_a".to_string()),
640 actor: Some("local".to_string()),
641 success_only: true,
642 ..Default::default()
643 };
644 let results = trail.query(&filter);
645 assert_eq!(results.len(), 1);
646 assert_eq!(results[0].cid, "cid_a");
647 assert_eq!(results[0].actor, "local");
648 }
649
650 #[test]
653 fn test_integrity_valid() {
654 let mut trail = default_trail();
655 trail.log_event(AuditEventType::BlockPut, "a", "local", "ok", 100, true);
656 trail.log_event(AuditEventType::BlockDelete, "b", "peer1", "del", 200, true);
657 assert!(trail.verify_integrity().is_ok());
658 }
659
660 #[test]
661 fn test_integrity_tampered() {
662 let mut trail = default_trail();
663 trail.log_event(AuditEventType::BlockPut, "a", "local", "ok", 100, true);
664 trail.log_event(AuditEventType::BlockPut, "b", "local", "ok", 200, true);
665
666 trail.entries[0].bytes_affected = 999_999;
668
669 let result = trail.verify_integrity();
670 assert!(result.is_err());
671 let bad = result.expect_err("should have bad entries");
672 assert_eq!(bad.len(), 1);
673 assert_eq!(bad[0], 1);
674 }
675
676 #[test]
677 fn test_integrity_multiple_tampered() {
678 let mut trail = default_trail();
679 for i in 0..5 {
680 trail.log_event(
681 AuditEventType::BlockPut,
682 &format!("c{i}"),
683 "local",
684 "",
685 0,
686 true,
687 );
688 }
689 trail.entries[1].cid = "TAMPERED".to_string();
690 trail.entries[3].actor = "EVIL".to_string();
691
692 let bad = trail.verify_integrity().expect_err("should fail");
693 assert_eq!(bad.len(), 2);
694 assert!(bad.contains(&2));
695 assert!(bad.contains(&4));
696 }
697
698 #[test]
701 fn test_checksum_deterministic() {
702 let entry = AuditEntry {
703 sequence: 42,
704 timestamp: 1_700_000_000,
705 event_type: AuditEventType::BlockPut,
706 cid: "bafy123".to_string(),
707 actor: "local".to_string(),
708 details: "test".to_string(),
709 bytes_affected: 1024,
710 success: true,
711 checksum: 0,
712 };
713 let c1 = compute_checksum(&entry);
714 let c2 = compute_checksum(&entry);
715 assert_eq!(c1, c2);
716 assert_ne!(c1, 0);
717 }
718
719 #[test]
720 fn test_checksum_changes_with_fields() {
721 let mut entry = AuditEntry {
722 sequence: 1,
723 timestamp: 100,
724 event_type: AuditEventType::BlockPut,
725 cid: "abc".to_string(),
726 actor: "local".to_string(),
727 details: "".to_string(),
728 bytes_affected: 0,
729 success: true,
730 checksum: 0,
731 };
732 let c1 = compute_checksum(&entry);
733 entry.cid = "xyz".to_string();
734 let c2 = compute_checksum(&entry);
735 assert_ne!(c1, c2);
736 }
737
738 #[test]
741 fn test_prune_before() {
742 let mut trail = default_trail();
743 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
745 trail.log_event(AuditEventType::BlockPut, "b", "local", "", 0, true);
746 trail.log_event(AuditEventType::BlockPut, "c", "local", "", 0, true);
747 trail.entries[0].timestamp = 100;
748 trail.entries[1].timestamp = 200;
749 trail.entries[2].timestamp = 300;
750 for e in &mut trail.entries {
752 e.checksum = compute_checksum(e);
753 }
754
755 let removed = trail.prune_before(200);
756 assert_eq!(removed, 1);
757 assert_eq!(trail.entry_count(), 2);
758 assert!(trail.verify_integrity().is_ok());
759 }
760
761 #[test]
762 fn test_prune_before_all() {
763 let mut trail = default_trail();
764 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
765 trail.log_event(AuditEventType::BlockPut, "b", "local", "", 0, true);
766
767 let removed = trail.prune_before(u64::MAX);
768 assert_eq!(removed, 2);
769 assert_eq!(trail.entry_count(), 0);
770 }
771
772 #[test]
773 fn test_prune_to_capacity() {
774 let mut trail = AuditTrail::new(AuditTrailConfig {
775 max_entries: 3,
776 enable_checksums: true,
777 log_reads: true,
778 ..AuditTrailConfig::default()
779 });
780
781 for i in 0..5 {
782 trail.log_event(
783 AuditEventType::BlockPut,
784 &format!("c{i}"),
785 "local",
786 "",
787 0,
788 true,
789 );
790 }
791 assert_eq!(trail.entry_count(), 5);
792
793 let pruned = trail.prune_to_capacity();
794 assert_eq!(pruned, 2);
795 assert_eq!(trail.entry_count(), 3);
796 assert!(trail.get_entry(1).is_none());
798 assert!(trail.get_entry(2).is_none());
799 assert!(trail.get_entry(3).is_some());
800 }
801
802 #[test]
803 fn test_prune_to_capacity_no_op() {
804 let mut trail = default_trail();
805 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
806 assert_eq!(trail.prune_to_capacity(), 0);
807 }
808
809 #[test]
812 fn test_stats_by_event_type() {
813 let mut trail = default_trail();
814 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
815 trail.log_event(AuditEventType::BlockPut, "b", "local", "", 0, true);
816 trail.log_event(AuditEventType::BlockDelete, "c", "local", "", 0, true);
817
818 let s = trail.stats();
819 assert_eq!(s.total_entries, 3);
820 assert_eq!(*s.events_by_type.get("BlockPut").unwrap_or(&0), 2);
821 assert_eq!(*s.events_by_type.get("BlockDelete").unwrap_or(&0), 1);
822 }
823
824 #[test]
825 fn test_stats_pruned_count() {
826 let mut trail = AuditTrail::new(AuditTrailConfig {
827 max_entries: 2,
828 enable_checksums: true,
829 log_reads: true,
830 ..AuditTrailConfig::default()
831 });
832 for i in 0..5 {
833 trail.log_event(
834 AuditEventType::BlockPut,
835 &format!("c{i}"),
836 "local",
837 "",
838 0,
839 true,
840 );
841 }
842 trail.prune_to_capacity();
843 assert_eq!(trail.stats().entries_pruned, 3);
844 }
845
846 #[test]
849 fn test_events_for_cid() {
850 let mut trail = default_trail();
851 trail.log_event(AuditEventType::BlockPut, "target", "local", "", 100, true);
852 trail.log_event(AuditEventType::BlockGet, "target", "peer1", "", 100, true);
853 trail.log_event(AuditEventType::BlockPut, "other", "local", "", 50, true);
854
855 let events = trail.events_for_cid("target");
856 assert_eq!(events.len(), 2);
857 for e in &events {
858 assert_eq!(e.cid, "target");
859 }
860 }
861
862 #[test]
863 fn test_events_for_cid_empty() {
864 let trail = default_trail();
865 assert!(trail.events_for_cid("nonexistent").is_empty());
866 }
867
868 #[test]
871 fn test_recent_failures() {
872 let mut trail = default_trail();
873 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, false);
874 trail.log_event(AuditEventType::BlockPut, "b", "local", "", 0, true);
875 trail.log_event(AuditEventType::BlockPut, "c", "local", "", 0, false);
876 trail.log_event(AuditEventType::BlockDelete, "d", "local", "", 0, false);
877
878 let failures = trail.recent_failures(2);
879 assert_eq!(failures.len(), 2);
880 assert_eq!(failures[0].cid, "d");
882 assert_eq!(failures[1].cid, "c");
883 }
884
885 #[test]
886 fn test_recent_failures_none() {
887 let mut trail = default_trail();
888 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
889 assert!(trail.recent_failures(10).is_empty());
890 }
891
892 #[test]
895 fn test_empty_trail_query() {
896 let trail = default_trail();
897 let filter = AuditFilter::default();
898 assert!(trail.query(&filter).is_empty());
899 }
900
901 #[test]
902 fn test_empty_trail_last_entry() {
903 let trail = default_trail();
904 assert!(trail.last_entry().is_none());
905 }
906
907 #[test]
908 fn test_empty_trail_verify_integrity() {
909 let trail = default_trail();
910 assert!(trail.verify_integrity().is_ok());
911 }
912
913 #[test]
914 fn test_empty_trail_prune() {
915 let mut trail = default_trail();
916 assert_eq!(trail.prune_before(100), 0);
917 assert_eq!(trail.prune_to_capacity(), 0);
918 }
919
920 #[test]
921 fn test_get_entry_missing() {
922 let trail = default_trail();
923 assert!(trail.get_entry(999).is_none());
924 }
925
926 #[test]
927 fn test_last_entry() {
928 let mut trail = default_trail();
929 trail.log_event(AuditEventType::BlockPut, "first", "local", "", 0, true);
930 trail.log_event(AuditEventType::BlockDelete, "last", "local", "", 0, true);
931 let last = trail.last_entry().expect("should have last");
932 assert_eq!(last.cid, "last");
933 }
934
935 #[test]
938 fn test_large_audit_trail() {
939 let mut trail = AuditTrail::new(AuditTrailConfig {
940 max_entries: 5000,
941 enable_checksums: true,
942 log_reads: true,
943 ..AuditTrailConfig::default()
944 });
945
946 for i in 0..1200u64 {
947 let event = if i % 3 == 0 {
948 AuditEventType::BlockPut
949 } else if i % 3 == 1 {
950 AuditEventType::BlockGet
951 } else {
952 AuditEventType::BlockDelete
953 };
954 trail.log_event(event, &format!("cid_{i}"), "local", "", i * 10, true);
955 }
956
957 assert_eq!(trail.entry_count(), 1200);
958 assert!(trail.verify_integrity().is_ok());
959 assert_eq!(trail.stats().total_entries, 1200);
960
961 let filter = AuditFilter {
963 event_type: Some(AuditEventType::BlockDelete),
964 ..Default::default()
965 };
966 assert_eq!(trail.query(&filter).len(), 400);
967 }
968
969 #[test]
972 fn test_retention_period_enforcement() {
973 let mut trail = default_trail();
974 trail.log_event(AuditEventType::BlockPut, "old", "local", "", 0, true);
975 trail.log_event(AuditEventType::BlockPut, "new", "local", "", 0, true);
976
977 trail.entries[0].timestamp = 1_000;
979 trail.entries[0].checksum = compute_checksum(&trail.entries[0]);
980 trail.entries[1].timestamp = 2_000;
981 trail.entries[1].checksum = compute_checksum(&trail.entries[1]);
982
983 let cutoff = trail.entries[1]
984 .timestamp
985 .saturating_sub(trail.config.retention_seconds);
986 trail.config.retention_seconds = 500;
989 let cutoff2 = trail.entries[1]
990 .timestamp
991 .saturating_sub(trail.config.retention_seconds);
992 let pruned = trail.prune_before(cutoff2);
993 assert_eq!(pruned, 1);
994 assert_eq!(trail.entry_count(), 1);
995 assert_eq!(trail.entries[0].cid, "new");
996
997 let _ = cutoff; }
1000
1001 #[test]
1004 fn test_checksums_disabled() {
1005 let mut trail = AuditTrail::new(AuditTrailConfig {
1006 enable_checksums: false,
1007 log_reads: true,
1008 ..AuditTrailConfig::default()
1009 });
1010 trail.log_event(AuditEventType::BlockPut, "a", "local", "", 0, true);
1011 let entry = trail.last_entry().expect("entry");
1012 assert_eq!(entry.checksum, 0);
1013 }
1014
1015 #[test]
1018 fn test_entry_fields_populated() {
1019 let mut trail = default_trail();
1020 trail.log_event(
1021 AuditEventType::Replication,
1022 "bafy_cid",
1023 "peer42",
1024 "replicated to zone-b",
1025 4096,
1026 true,
1027 );
1028 let e = trail.last_entry().expect("entry");
1029 assert_eq!(e.event_type, AuditEventType::Replication);
1030 assert_eq!(e.cid, "bafy_cid");
1031 assert_eq!(e.actor, "peer42");
1032 assert_eq!(e.details, "replicated to zone-b");
1033 assert_eq!(e.bytes_affected, 4096);
1034 assert!(e.success);
1035 assert_ne!(e.checksum, 0);
1036 }
1037}