1use crate::models::{EventMeta, MemoryEvent};
14use crate::observability::{
15 RequestContext, current_request_id, global_event_bus, scope_request_context,
16};
17use crate::{Error, Result};
18use chrono::{DateTime, Utc};
19use hmac::{Hmac, Mac};
20use serde::{Deserialize, Serialize};
21use sha2::Sha256;
22use std::path::PathBuf;
23use std::sync::Mutex;
24use std::sync::OnceLock;
25use tracing::Instrument;
26
27type HmacSha256 = Hmac<Sha256>;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AuditEntry {
33 pub id: String,
35 pub timestamp: DateTime<Utc>,
37 pub event_type: String,
39 pub actor: String,
41 pub resource: Option<String>,
43 pub action: String,
45 pub outcome: AuditOutcome,
47 pub metadata: serde_json::Value,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub hmac_signature: Option<String>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub previous_hmac: Option<String>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum AuditOutcome {
65 Success,
67 Failure,
69 Denied,
71}
72
73impl AuditEntry {
74 #[must_use]
76 pub fn new(event_type: impl Into<String>, action: impl Into<String>) -> Self {
77 Self {
78 id: uuid::Uuid::new_v4().to_string(),
79 timestamp: Utc::now(),
80 event_type: event_type.into(),
81 actor: "system".to_string(),
82 resource: None,
83 action: action.into(),
84 outcome: AuditOutcome::Success,
85 metadata: serde_json::Value::Null,
86 hmac_signature: None,
87 previous_hmac: None,
88 }
89 }
90
91 #[must_use]
95 pub fn canonical_string(&self, previous_hmac: &str) -> String {
96 format!(
97 "{}|{}|{}|{}|{}",
98 self.id,
99 self.timestamp.to_rfc3339(),
100 self.event_type,
101 self.action,
102 previous_hmac
103 )
104 }
105
106 #[must_use]
110 pub fn compute_hmac(&self, key: &[u8], previous_hmac: &str) -> Option<String> {
111 let canonical = self.canonical_string(previous_hmac);
112 let mut mac = HmacSha256::new_from_slice(key).ok()?;
113 mac.update(canonical.as_bytes());
114 let result = mac.finalize();
115 Some(hex::encode(result.into_bytes()))
116 }
117
118 pub fn sign(&mut self, key: &[u8], previous_hmac: &str) -> bool {
122 if let Some(sig) = self.compute_hmac(key, previous_hmac) {
123 self.previous_hmac = Some(previous_hmac.to_string());
124 self.hmac_signature = Some(sig);
125 true
126 } else {
127 false
128 }
129 }
130
131 #[must_use]
135 pub fn verify(&self, key: &[u8]) -> bool {
136 let Some(ref signature) = self.hmac_signature else {
137 return false;
138 };
139 let Some(ref previous) = self.previous_hmac else {
140 return false;
141 };
142
143 self.compute_hmac(key, previous)
144 .is_some_and(|computed| computed == *signature)
145 }
146
147 #[must_use]
149 pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
150 self.actor = actor.into();
151 self
152 }
153
154 #[must_use]
156 pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
157 self.resource = Some(resource.into());
158 self
159 }
160
161 #[must_use]
163 pub const fn with_outcome(mut self, outcome: AuditOutcome) -> Self {
164 self.outcome = outcome;
165 self
166 }
167
168 #[must_use]
170 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
171 self.metadata = metadata;
172 self
173 }
174}
175
176pub const GENESIS_HMAC: &str = "genesis";
178
179#[derive(Debug, Clone)]
181pub struct AuditConfig {
182 pub log_path: Option<PathBuf>,
184 pub log_stderr: bool,
186 pub retention_days: u32,
188 pub include_content: bool,
190 pub hmac_key: Option<Vec<u8>>,
194}
195
196impl Default for AuditConfig {
197 fn default() -> Self {
198 Self {
199 log_path: None,
200 log_stderr: false,
201 retention_days: 90,
202 include_content: false,
203 hmac_key: None,
204 }
205 }
206}
207
208impl AuditConfig {
209 #[must_use]
211 pub fn new() -> Self {
212 Self::default()
213 }
214
215 #[must_use]
217 pub fn with_log_path(mut self, path: impl Into<PathBuf>) -> Self {
218 self.log_path = Some(path.into());
219 self
220 }
221
222 #[must_use]
224 pub const fn with_stderr(mut self) -> Self {
225 self.log_stderr = true;
226 self
227 }
228
229 #[must_use]
231 pub const fn with_retention(mut self, days: u32) -> Self {
232 self.retention_days = days;
233 self
234 }
235
236 #[must_use]
240 pub fn with_hmac_key(mut self, key: Vec<u8>) -> Self {
241 self.hmac_key = Some(key);
242 self
243 }
244}
245
246pub struct AuditLogger {
257 config: AuditConfig,
258 entries: Mutex<Vec<AuditEntry>>,
259 last_hmac: Mutex<String>,
261}
262
263static GLOBAL_AUDIT_LOGGER: OnceLock<AuditLogger> = OnceLock::new();
264
265impl AuditLogger {
266 #[must_use]
268 pub fn new() -> Self {
269 Self {
270 config: AuditConfig::default(),
271 entries: Mutex::new(Vec::new()),
272 last_hmac: Mutex::new(GENESIS_HMAC.to_string()),
273 }
274 }
275
276 #[must_use]
278 pub fn with_config(config: AuditConfig) -> Self {
279 Self {
280 config,
281 entries: Mutex::new(Vec::new()),
282 last_hmac: Mutex::new(GENESIS_HMAC.to_string()),
283 }
284 }
285
286 pub fn log(&self, event: &MemoryEvent) {
288 let entry = self.event_to_entry(event);
289 self.log_entry(entry);
290 }
291
292 pub fn log_entry(&self, entry: AuditEntry) {
297 let signed_entry = self.sign_entry(entry);
298
299 if let Ok(mut entries) = self.entries.lock() {
301 entries.push(signed_entry.clone());
302 }
303
304 if let Some(ref path) = self.config.log_path {
306 let _ = self.append_to_file(path, &signed_entry);
307 }
308 }
309
310 fn sign_entry(&self, mut entry: AuditEntry) -> AuditEntry {
312 let Some(ref key) = self.config.hmac_key else {
313 return entry;
314 };
315 let Ok(mut last) = self.last_hmac.lock() else {
316 return entry;
317 };
318
319 if entry.sign(key, &last)
320 && let Some(ref sig) = entry.hmac_signature
321 {
322 last.clone_from(sig);
323 }
324 entry
325 }
326
327 pub fn log_capture(&self, memory_id: &str, namespace: &str) {
329 let entry = AuditEntry::new("memory.capture", "create")
330 .with_resource(memory_id)
331 .with_metadata(serde_json::json!({
332 "namespace": namespace
333 }));
334 self.log_entry(entry);
335 }
336
337 pub fn log_recall(&self, query: &str, result_count: usize) {
339 let entry = AuditEntry::new("memory.recall", "search").with_metadata(serde_json::json!({
340 "query_length": query.len(),
341 "result_count": result_count
342 }));
343 self.log_entry(entry);
344 }
345
346 pub fn log_sync(&self, pushed: usize, pulled: usize) {
348 let entry = AuditEntry::new("memory.sync", "sync").with_metadata(serde_json::json!({
349 "pushed": pushed,
350 "pulled": pulled
351 }));
352 self.log_entry(entry);
353 }
354
355 pub fn log_redaction(&self, memory_id: &str, redaction_types: &[String]) {
357 let entry = AuditEntry::new("security.redaction", "redact")
358 .with_resource(memory_id)
359 .with_metadata(serde_json::json!({
360 "redaction_types": redaction_types
361 }));
362 self.log_entry(entry);
363 }
364
365 pub fn log_pii_detection(&self, pii_types: &[String], context: Option<&str>) {
371 let entry =
372 AuditEntry::new("security.pii_detection", "detect").with_metadata(serde_json::json!({
373 "pii_types": pii_types,
374 "pii_count": pii_types.len(),
375 "context": context
376 }));
377 self.log_entry(entry);
378 }
379
380 pub fn log_pii_disclosure(
398 &self,
399 destination: &str,
400 pii_types: &[String],
401 data_subject_id: Option<&str>,
402 purpose: &str,
403 legal_basis: &str,
404 ) {
405 let entry = AuditEntry::new("security.pii_disclosure", "disclose").with_metadata(
406 serde_json::json!({
407 "destination": destination,
408 "pii_types": pii_types,
409 "pii_count": pii_types.len(),
410 "data_subject_id": data_subject_id,
411 "purpose": purpose,
412 "legal_basis": legal_basis,
413 "timestamp_utc": Utc::now().to_rfc3339()
414 }),
415 );
416 self.log_entry(entry);
417 }
418
419 pub fn log_bulk_pii_disclosure(
432 &self,
433 destination: &str,
434 record_count: usize,
435 pii_categories: &[String],
436 purpose: &str,
437 legal_basis: &str,
438 ) {
439 let entry = AuditEntry::new("security.pii_bulk_disclosure", "bulk_disclose").with_metadata(
440 serde_json::json!({
441 "destination": destination,
442 "record_count": record_count,
443 "pii_categories": pii_categories,
444 "purpose": purpose,
445 "legal_basis": legal_basis,
446 "timestamp_utc": Utc::now().to_rfc3339()
447 }),
448 );
449 self.log_entry(entry);
450 }
451
452 pub fn log_denied(&self, action: &str, reason: &str) {
454 let entry = AuditEntry::new("security.denied", action)
455 .with_outcome(AuditOutcome::Denied)
456 .with_metadata(serde_json::json!({
457 "reason": reason
458 }));
459 self.log_entry(entry);
460 }
461
462 #[must_use]
464 pub fn recent_entries(&self, limit: usize) -> Vec<AuditEntry> {
465 if let Ok(entries) = self.entries.lock() {
466 entries.iter().rev().take(limit).cloned().collect()
467 } else {
468 Vec::new()
469 }
470 }
471
472 #[must_use]
474 pub fn entries_since(&self, since: DateTime<Utc>) -> Vec<AuditEntry> {
475 if let Ok(entries) = self.entries.lock() {
476 entries
477 .iter()
478 .filter(|e| e.timestamp >= since)
479 .cloned()
480 .collect()
481 } else {
482 Vec::new()
483 }
484 }
485
486 pub fn cleanup(&self) {
488 let cutoff = Utc::now() - chrono::Duration::days(i64::from(self.config.retention_days));
489 if let Ok(mut entries) = self.entries.lock() {
490 entries.retain(|e| e.timestamp >= cutoff);
491 }
492 }
493
494 pub fn verify_chain(&self) -> Result<()> {
506 let key = self
507 .config
508 .hmac_key
509 .as_ref()
510 .ok_or_else(|| Error::OperationFailed {
511 operation: "verify_chain".to_string(),
512 cause: "no HMAC key configured".to_string(),
513 })?;
514
515 let entries: Vec<AuditEntry> = self
517 .entries
518 .lock()
519 .map_err(|_| Error::OperationFailed {
520 operation: "verify_chain".to_string(),
521 cause: "failed to acquire lock".to_string(),
522 })?
523 .clone();
524
525 let mut expected_previous = GENESIS_HMAC.to_string();
526
527 for (i, entry) in entries.iter().enumerate() {
528 let Some(ref signature) = entry.hmac_signature else {
530 return Err(Error::OperationFailed {
531 operation: "verify_chain".to_string(),
532 cause: format!("entry {i} missing hmac_signature"),
533 });
534 };
535 let Some(ref previous) = entry.previous_hmac else {
536 return Err(Error::OperationFailed {
537 operation: "verify_chain".to_string(),
538 cause: format!("entry {i} missing previous_hmac"),
539 });
540 };
541
542 if *previous != expected_previous {
544 return Err(Error::OperationFailed {
545 operation: "verify_chain".to_string(),
546 cause: format!(
547 "entry {i} chain broken: expected previous '{expected_previous}', got '{previous}'"
548 ),
549 });
550 }
551
552 if !entry.verify(key) {
554 return Err(Error::OperationFailed {
555 operation: "verify_chain".to_string(),
556 cause: format!("entry {i} has invalid signature"),
557 });
558 }
559
560 expected_previous.clone_from(signature);
562 }
563
564 Ok(())
565 }
566
567 #[must_use]
569 pub const fn is_signing_enabled(&self) -> bool {
570 self.config.hmac_key.is_some()
571 }
572
573 #[allow(clippy::too_many_lines)]
575 fn event_to_entry(&self, event: &MemoryEvent) -> AuditEntry {
576 fn base_metadata(meta: &EventMeta) -> serde_json::Map<String, serde_json::Value> {
577 let mut metadata = serde_json::Map::new();
578 metadata.insert(
579 "event_id".to_string(),
580 serde_json::Value::String(meta.event_id.clone()),
581 );
582 metadata.insert(
583 "correlation_id".to_string(),
584 meta.correlation_id
585 .clone()
586 .map_or(serde_json::Value::Null, serde_json::Value::String),
587 );
588 metadata.insert(
589 "source".to_string(),
590 serde_json::Value::String(meta.source.to_string()),
591 );
592 metadata.insert(
593 "event_timestamp".to_string(),
594 serde_json::Value::Number(meta.timestamp.into()),
595 );
596 metadata
597 }
598
599 match event {
600 MemoryEvent::Captured {
601 meta,
602 memory_id,
603 namespace,
604 domain,
605 content_length,
606 } => {
607 let mut metadata = base_metadata(meta);
608 metadata.insert(
609 "namespace".to_string(),
610 serde_json::Value::String(namespace.as_str().to_string()),
611 );
612 metadata.insert(
613 "domain".to_string(),
614 serde_json::Value::String(domain.to_string()),
615 );
616 metadata.insert(
617 "content_length".to_string(),
618 serde_json::Value::Number(serde_json::Number::from(*content_length as u64)),
619 );
620
621 AuditEntry::new("memory.captured", "create")
622 .with_resource(memory_id.to_string())
623 .with_metadata(serde_json::Value::Object(metadata))
624 },
625
626 MemoryEvent::Retrieved {
627 meta,
628 memory_id,
629 query,
630 score,
631 } => {
632 let mut metadata = base_metadata(meta);
633 metadata.insert(
634 "query_length".to_string(),
635 serde_json::Value::Number(serde_json::Number::from(query.len() as u64)),
636 );
637 metadata.insert(
638 "score".to_string(),
639 serde_json::Value::Number(
640 serde_json::Number::from_f64(f64::from(*score))
641 .unwrap_or_else(|| serde_json::Number::from(0_u64)),
642 ),
643 );
644
645 AuditEntry::new("memory.retrieved", "read")
646 .with_resource(memory_id.to_string())
647 .with_metadata(serde_json::Value::Object(metadata))
648 },
649
650 MemoryEvent::Updated {
651 meta,
652 memory_id,
653 modified_fields,
654 } => {
655 let mut metadata = base_metadata(meta);
656 metadata.insert(
657 "modified_fields".to_string(),
658 serde_json::Value::Array(
659 modified_fields
660 .iter()
661 .cloned()
662 .map(serde_json::Value::String)
663 .collect(),
664 ),
665 );
666
667 AuditEntry::new("memory.updated", "update")
668 .with_resource(memory_id.to_string())
669 .with_metadata(serde_json::Value::Object(metadata))
670 },
671
672 MemoryEvent::Archived {
673 meta,
674 memory_id,
675 reason,
676 } => {
677 let mut metadata = base_metadata(meta);
678 metadata.insert(
679 "reason".to_string(),
680 serde_json::Value::String(reason.clone()),
681 );
682
683 AuditEntry::new("memory.archived", "archive")
684 .with_resource(memory_id.to_string())
685 .with_metadata(serde_json::Value::Object(metadata))
686 },
687
688 MemoryEvent::Deleted {
689 meta,
690 memory_id,
691 reason,
692 } => {
693 let mut metadata = base_metadata(meta);
694 metadata.insert(
695 "reason".to_string(),
696 serde_json::Value::String(reason.clone()),
697 );
698
699 AuditEntry::new("memory.deleted", "delete")
700 .with_resource(memory_id.to_string())
701 .with_metadata(serde_json::Value::Object(metadata))
702 },
703
704 MemoryEvent::Redacted {
705 meta,
706 memory_id,
707 redaction_type,
708 } => {
709 let mut metadata = base_metadata(meta);
710 metadata.insert(
711 "redaction_type".to_string(),
712 serde_json::Value::String(redaction_type.clone()),
713 );
714
715 AuditEntry::new("security.redacted", "redact")
716 .with_resource(memory_id.to_string())
717 .with_metadata(serde_json::Value::Object(metadata))
718 },
719
720 MemoryEvent::Synced {
721 meta,
722 pushed,
723 pulled,
724 conflicts,
725 } => {
726 let mut metadata = base_metadata(meta);
727 metadata.insert(
728 "pushed".to_string(),
729 serde_json::Value::Number(serde_json::Number::from(*pushed as u64)),
730 );
731 metadata.insert(
732 "pulled".to_string(),
733 serde_json::Value::Number(serde_json::Number::from(*pulled as u64)),
734 );
735 metadata.insert(
736 "conflicts".to_string(),
737 serde_json::Value::Number(serde_json::Number::from(*conflicts as u64)),
738 );
739
740 AuditEntry::new("memory.synced", "sync")
741 .with_metadata(serde_json::Value::Object(metadata))
742 },
743
744 MemoryEvent::Consolidated {
745 meta,
746 processed,
747 archived,
748 merged,
749 } => {
750 let mut metadata = base_metadata(meta);
751 metadata.insert(
752 "processed".to_string(),
753 serde_json::Value::Number(serde_json::Number::from(*processed as u64)),
754 );
755 metadata.insert(
756 "archived".to_string(),
757 serde_json::Value::Number(serde_json::Number::from(*archived as u64)),
758 );
759 metadata.insert(
760 "merged".to_string(),
761 serde_json::Value::Number(serde_json::Number::from(*merged as u64)),
762 );
763
764 AuditEntry::new("memory.consolidated", "consolidate")
765 .with_metadata(serde_json::Value::Object(metadata))
766 },
767
768 MemoryEvent::McpStarted {
769 meta,
770 transport,
771 port,
772 } => {
773 let mut metadata = base_metadata(meta);
774 metadata.insert(
775 "transport".to_string(),
776 serde_json::Value::String(transport.clone()),
777 );
778 metadata.insert(
779 "port".to_string(),
780 port.map_or(serde_json::Value::Null, |p| {
781 serde_json::Value::Number(p.into())
782 }),
783 );
784
785 AuditEntry::new("mcp.started", "start")
786 .with_metadata(serde_json::Value::Object(metadata))
787 },
788
789 MemoryEvent::McpAuthFailed {
790 meta,
791 client_id,
792 reason,
793 } => {
794 let mut metadata = base_metadata(meta);
795 metadata.insert(
796 "client_id".to_string(),
797 client_id
798 .clone()
799 .map_or(serde_json::Value::Null, serde_json::Value::String),
800 );
801 metadata.insert(
802 "reason".to_string(),
803 serde_json::Value::String(reason.clone()),
804 );
805
806 AuditEntry::new("mcp.auth_failed", "authenticate")
807 .with_outcome(AuditOutcome::Denied)
808 .with_metadata(serde_json::Value::Object(metadata))
809 },
810
811 MemoryEvent::McpToolExecuted {
812 meta,
813 tool_name,
814 status,
815 duration_ms,
816 error,
817 } => {
818 let mut metadata = base_metadata(meta);
819 metadata.insert(
820 "tool_name".to_string(),
821 serde_json::Value::String(tool_name.clone()),
822 );
823 metadata.insert(
824 "status".to_string(),
825 serde_json::Value::String(status.clone()),
826 );
827 metadata.insert(
828 "duration_ms".to_string(),
829 serde_json::Value::Number((*duration_ms).into()),
830 );
831 metadata.insert(
832 "error".to_string(),
833 error
834 .clone()
835 .map_or(serde_json::Value::Null, serde_json::Value::String),
836 );
837
838 let outcome = if status == "success" {
839 AuditOutcome::Success
840 } else {
841 AuditOutcome::Failure
842 };
843
844 AuditEntry::new("mcp.tool_executed", "execute")
845 .with_outcome(outcome)
846 .with_metadata(serde_json::Value::Object(metadata))
847 },
848
849 MemoryEvent::McpRequestError {
850 meta,
851 operation,
852 error,
853 } => {
854 let mut metadata = base_metadata(meta);
855 metadata.insert(
856 "operation".to_string(),
857 serde_json::Value::String(operation.clone()),
858 );
859 metadata.insert(
860 "error".to_string(),
861 serde_json::Value::String(error.clone()),
862 );
863
864 AuditEntry::new("mcp.request_error", "request")
865 .with_outcome(AuditOutcome::Failure)
866 .with_metadata(serde_json::Value::Object(metadata))
867 },
868
869 MemoryEvent::HookInvoked { meta, hook } => {
870 let mut metadata = base_metadata(meta);
871 metadata.insert("hook".to_string(), serde_json::Value::String(hook.clone()));
872
873 AuditEntry::new("hook.invoked", "invoke")
874 .with_metadata(serde_json::Value::Object(metadata))
875 },
876
877 MemoryEvent::HookClassified {
878 meta,
879 hook,
880 classification,
881 classifier,
882 confidence,
883 } => {
884 let mut metadata = base_metadata(meta);
885 metadata.insert("hook".to_string(), serde_json::Value::String(hook.clone()));
886 metadata.insert(
887 "classification".to_string(),
888 serde_json::Value::String(classification.clone()),
889 );
890 metadata.insert(
891 "classifier".to_string(),
892 serde_json::Value::String(classifier.clone()),
893 );
894 metadata.insert(
895 "confidence".to_string(),
896 serde_json::Value::Number(
897 serde_json::Number::from_f64(f64::from(*confidence))
898 .unwrap_or_else(|| serde_json::Number::from(0_u64)),
899 ),
900 );
901
902 AuditEntry::new("hook.classified", "classify")
903 .with_metadata(serde_json::Value::Object(metadata))
904 },
905
906 MemoryEvent::HookCaptureDecision {
907 meta,
908 hook,
909 decision,
910 namespace,
911 memory_id,
912 } => {
913 let mut metadata = base_metadata(meta);
914 metadata.insert("hook".to_string(), serde_json::Value::String(hook.clone()));
915 metadata.insert(
916 "decision".to_string(),
917 serde_json::Value::String(decision.clone()),
918 );
919 metadata.insert(
920 "namespace".to_string(),
921 namespace
922 .clone()
923 .map_or(serde_json::Value::Null, serde_json::Value::String),
924 );
925 metadata.insert(
926 "memory_id".to_string(),
927 memory_id.as_ref().map_or(serde_json::Value::Null, |id| {
928 serde_json::Value::String(id.to_string())
929 }),
930 );
931
932 AuditEntry::new("hook.capture_decision", "decision")
933 .with_metadata(serde_json::Value::Object(metadata))
934 },
935
936 MemoryEvent::HookFailed { meta, hook, error } => {
937 let mut metadata = base_metadata(meta);
938 metadata.insert("hook".to_string(), serde_json::Value::String(hook.clone()));
939 metadata.insert(
940 "error".to_string(),
941 serde_json::Value::String(error.clone()),
942 );
943
944 AuditEntry::new("hook.failed", "invoke")
945 .with_outcome(AuditOutcome::Failure)
946 .with_metadata(serde_json::Value::Object(metadata))
947 },
948 }
949 }
950
951 fn append_to_file(&self, path: &std::path::Path, entry: &AuditEntry) -> std::io::Result<()> {
960 use std::fs::OpenOptions;
961 use std::io::Write;
962
963 let canonical_path = Self::canonicalize_path(path)?;
966
967 #[cfg(unix)]
971 let mut file = {
972 use std::os::unix::fs::OpenOptionsExt;
973 OpenOptions::new()
974 .create(true)
975 .append(true)
976 .mode(0o600) .open(&canonical_path)?
978 };
979
980 #[cfg(not(unix))]
981 let mut file = OpenOptions::new()
982 .create(true)
983 .append(true)
984 .open(&canonical_path)?;
985
986 let json = serde_json::to_string(entry)
987 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
988
989 writeln!(file, "{json}")?;
990 Ok(())
991 }
992
993 fn canonicalize_path(path: &std::path::Path) -> std::io::Result<PathBuf> {
995 if path.exists() {
996 return path.canonicalize();
997 }
998
999 let Some(parent) = path.parent() else {
1000 return Ok(path.to_path_buf());
1001 };
1002
1003 if !parent.exists() {
1004 return Ok(path.to_path_buf());
1006 }
1007
1008 let file_name = path.file_name().ok_or_else(|| {
1009 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid file name")
1010 })?;
1011
1012 Ok(parent.canonicalize()?.join(file_name))
1013 }
1014}
1015
1016pub fn init_global(config: AuditConfig) -> Result<()> {
1023 if let Some(ref path) = config.log_path
1024 && let Some(parent) = path.parent()
1025 {
1026 std::fs::create_dir_all(parent).map_err(|e| Error::OperationFailed {
1027 operation: "init_audit_logger".to_string(),
1028 cause: e.to_string(),
1029 })?;
1030 }
1031
1032 GLOBAL_AUDIT_LOGGER
1033 .set(AuditLogger::with_config(config))
1034 .map_err(|_logger| Error::OperationFailed {
1035 operation: "init_audit_logger".to_string(),
1036 cause: "audit logger already initialized".to_string(),
1037 })?;
1038
1039 start_audit_subscription();
1040
1041 Ok(())
1042}
1043
1044#[must_use]
1046pub fn global_logger() -> Option<&'static AuditLogger> {
1047 GLOBAL_AUDIT_LOGGER.get()
1048}
1049
1050fn log_event_if_configured(event: &MemoryEvent) {
1051 if let Some(logger) = global_logger() {
1052 logger.log(event);
1053 }
1054}
1055
1056fn start_audit_subscription() {
1057 if tokio::runtime::Handle::try_current().is_err() {
1058 tracing::warn!("Audit event subscription requires a Tokio runtime");
1059 return;
1060 }
1061
1062 let mut receiver = global_event_bus().subscribe();
1063 let span = tracing::Span::current();
1064 let request_context = current_request_id().map(RequestContext::from_id);
1065 tokio::spawn(
1066 async move {
1067 let run = async move {
1068 while let Ok(event) = receiver.recv().await {
1069 log_event_if_configured(&event);
1070 }
1071 };
1072
1073 if let Some(context) = request_context {
1074 scope_request_context(context, run).await;
1075 } else {
1076 run.await;
1077 }
1078 }
1079 .instrument(span),
1080 );
1081}
1082
1083pub fn record_event(event: MemoryEvent) {
1085 global_event_bus().publish(event);
1086}
1087
1088impl Default for AuditLogger {
1089 fn default() -> Self {
1090 Self::new()
1091 }
1092}
1093
1094#[derive(Debug, Clone, Serialize, Deserialize)]
1105pub struct AccessReviewReport {
1106 pub generated_at: DateTime<Utc>,
1108 pub period_start: DateTime<Utc>,
1110 pub period_end: DateTime<Utc>,
1112 pub total_events: usize,
1114 pub by_actor: std::collections::HashMap<String, ActorAccessSummary>,
1116 pub by_resource_type: std::collections::HashMap<String, usize>,
1118 pub by_action: std::collections::HashMap<String, usize>,
1120 pub by_outcome: OutcomeSummary,
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize)]
1126pub struct ActorAccessSummary {
1127 pub event_count: usize,
1129 pub resources_accessed: std::collections::HashSet<String>,
1131 pub actions: std::collections::HashMap<String, usize>,
1133 pub first_access: DateTime<Utc>,
1135 pub last_access: DateTime<Utc>,
1137}
1138
1139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1141pub struct OutcomeSummary {
1142 pub success: usize,
1144 pub failure: usize,
1146 pub denied: usize,
1148}
1149
1150impl AccessReviewReport {
1151 #[must_use]
1159 pub fn generate(
1160 entries: &[AuditEntry],
1161 period_start: DateTime<Utc>,
1162 period_end: DateTime<Utc>,
1163 ) -> Self {
1164 let mut by_actor: std::collections::HashMap<String, ActorAccessSummary> =
1165 std::collections::HashMap::new();
1166 let mut by_resource_type: std::collections::HashMap<String, usize> =
1167 std::collections::HashMap::new();
1168 let mut by_action: std::collections::HashMap<String, usize> =
1169 std::collections::HashMap::new();
1170 let mut by_outcome = OutcomeSummary::default();
1171
1172 let filtered: Vec<_> = entries
1174 .iter()
1175 .filter(|e| e.timestamp >= period_start && e.timestamp <= period_end)
1176 .collect();
1177
1178 for entry in &filtered {
1179 let actor_summary =
1181 by_actor
1182 .entry(entry.actor.clone())
1183 .or_insert_with(|| ActorAccessSummary {
1184 event_count: 0,
1185 resources_accessed: std::collections::HashSet::new(),
1186 actions: std::collections::HashMap::new(),
1187 first_access: entry.timestamp,
1188 last_access: entry.timestamp,
1189 });
1190
1191 actor_summary.event_count += 1;
1192 if let Some(ref resource) = entry.resource {
1193 actor_summary.resources_accessed.insert(resource.clone());
1194 }
1195 *actor_summary
1196 .actions
1197 .entry(entry.action.clone())
1198 .or_insert(0) += 1;
1199 if entry.timestamp < actor_summary.first_access {
1200 actor_summary.first_access = entry.timestamp;
1201 }
1202 if entry.timestamp > actor_summary.last_access {
1203 actor_summary.last_access = entry.timestamp;
1204 }
1205
1206 *by_resource_type
1208 .entry(entry.event_type.clone())
1209 .or_insert(0) += 1;
1210
1211 *by_action.entry(entry.action.clone()).or_insert(0) += 1;
1213
1214 match entry.outcome {
1216 AuditOutcome::Success => by_outcome.success += 1,
1217 AuditOutcome::Failure => by_outcome.failure += 1,
1218 AuditOutcome::Denied => by_outcome.denied += 1,
1219 }
1220 }
1221
1222 Self {
1223 generated_at: Utc::now(),
1224 period_start,
1225 period_end,
1226 total_events: filtered.len(),
1227 by_actor,
1228 by_resource_type,
1229 by_action,
1230 by_outcome,
1231 }
1232 }
1233}
1234
1235impl AuditLogger {
1236 #[must_use]
1257 pub fn generate_access_review(
1258 &self,
1259 period_start: DateTime<Utc>,
1260 period_end: DateTime<Utc>,
1261 ) -> AccessReviewReport {
1262 let entries = self.entries_since(period_start);
1263 AccessReviewReport::generate(&entries, period_start, period_end)
1264 }
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269 use super::*;
1270 use crate::models::{Domain, EventMeta, MemoryId, Namespace};
1271
1272 #[test]
1273 fn test_audit_entry_creation() {
1274 let entry = AuditEntry::new("test.event", "test_action")
1275 .with_actor("test_user")
1276 .with_resource("resource_id")
1277 .with_outcome(AuditOutcome::Success);
1278
1279 assert_eq!(entry.event_type, "test.event");
1280 assert_eq!(entry.action, "test_action");
1281 assert_eq!(entry.actor, "test_user");
1282 assert_eq!(entry.resource, Some("resource_id".to_string()));
1283 assert_eq!(entry.outcome, AuditOutcome::Success);
1284 }
1285
1286 #[test]
1287 fn test_log_capture() {
1288 let logger = AuditLogger::new();
1289 logger.log_capture("mem_123", "decisions");
1290
1291 let entries = logger.recent_entries(10);
1292 assert_eq!(entries.len(), 1);
1293 assert_eq!(entries[0].event_type, "memory.capture");
1294 }
1295
1296 #[test]
1297 fn test_log_recall() {
1298 let logger = AuditLogger::new();
1299 logger.log_recall("test query", 5);
1300
1301 let entries = logger.recent_entries(10);
1302 assert_eq!(entries.len(), 1);
1303 assert_eq!(entries[0].event_type, "memory.recall");
1304 }
1305
1306 #[test]
1307 fn test_log_denied() {
1308 let logger = AuditLogger::new();
1309 logger.log_denied("capture", "secrets detected");
1310
1311 let entries = logger.recent_entries(10);
1312 assert_eq!(entries.len(), 1);
1313 assert_eq!(entries[0].outcome, AuditOutcome::Denied);
1314 }
1315
1316 #[test]
1317 fn test_log_memory_event() {
1318 let logger = AuditLogger::new();
1319 let event = MemoryEvent::Captured {
1320 meta: EventMeta::with_timestamp("test", None, 1_234_567_890),
1321 memory_id: MemoryId::new("test_id"),
1322 namespace: Namespace::Decisions,
1323 domain: Domain::new(),
1324 content_length: 100,
1325 };
1326
1327 logger.log(&event);
1328
1329 let entries = logger.recent_entries(10);
1330 assert_eq!(entries.len(), 1);
1331 assert_eq!(entries[0].event_type, "memory.captured");
1332 }
1333
1334 #[test]
1335 fn test_entries_since() {
1336 let logger = AuditLogger::new();
1337
1338 logger.log_capture("mem_1", "decisions");
1340 logger.log_capture("mem_2", "learnings");
1341
1342 let since = Utc::now() - chrono::Duration::hours(1);
1343 let entries = logger.entries_since(since);
1344
1345 assert_eq!(entries.len(), 2);
1346 }
1347
1348 #[test]
1349 fn test_cleanup() {
1350 let config = AuditConfig::new().with_retention(0); let logger = AuditLogger::with_config(config);
1352
1353 logger.log_capture("mem_1", "decisions");
1354
1355 std::thread::sleep(std::time::Duration::from_millis(10));
1357 logger.cleanup();
1358
1359 let entries = logger.recent_entries(10);
1360 assert!(entries.is_empty());
1361 }
1362
1363 #[test]
1364 fn test_audit_entry_serialization() {
1365 let entry = AuditEntry::new("test.event", "action")
1366 .with_metadata(serde_json::json!({"key": "value"}));
1367
1368 let json = serde_json::to_string(&entry).unwrap();
1369 assert!(json.contains("test.event"));
1370
1371 let deserialized: AuditEntry = serde_json::from_str(&json).unwrap();
1372 assert_eq!(deserialized.event_type, entry.event_type);
1373 }
1374
1375 #[test]
1378 fn test_hmac_sign_and_verify() {
1379 let key = b"test_key_32_bytes_long_xxxxxxxx";
1380 let mut entry = AuditEntry::new("test.event", "action");
1381
1382 assert!(entry.sign(key, GENESIS_HMAC));
1383 assert!(entry.hmac_signature.is_some());
1384 assert_eq!(entry.previous_hmac, Some(GENESIS_HMAC.to_string()));
1385 assert!(entry.verify(key));
1386 }
1387
1388 #[test]
1389 fn test_hmac_verify_fails_with_wrong_key() {
1390 let key = b"test_key_32_bytes_long_xxxxxxxx";
1391 let wrong_key = b"wrong_key_32_bytes_long_xxxxxxx";
1392 let mut entry = AuditEntry::new("test.event", "action");
1393
1394 assert!(entry.sign(key, GENESIS_HMAC));
1395 assert!(!entry.verify(wrong_key));
1396 }
1397
1398 #[test]
1399 fn test_hmac_verify_fails_with_tampered_content() {
1400 let key = b"test_key_32_bytes_long_xxxxxxxx";
1401 let mut entry = AuditEntry::new("test.event", "action");
1402
1403 assert!(entry.sign(key, GENESIS_HMAC));
1404 entry.action = "tampered_action".to_string();
1405 assert!(!entry.verify(key));
1406 }
1407
1408 #[test]
1409 fn test_hmac_chain_signing() {
1410 let key = vec![0u8; 32]; let config = AuditConfig::new().with_hmac_key(key.clone());
1412 let logger = AuditLogger::with_config(config);
1413
1414 logger.log_capture("mem_1", "decisions");
1416 logger.log_capture("mem_2", "learnings");
1417 logger.log_capture("mem_3", "patterns");
1418
1419 let entries = logger.recent_entries(10);
1420 assert_eq!(entries.len(), 3);
1421
1422 for entry in &entries {
1424 assert!(entry.hmac_signature.is_some());
1425 assert!(entry.previous_hmac.is_some());
1426 assert!(entry.verify(&key));
1427 }
1428 }
1429
1430 #[test]
1431 fn test_hmac_chain_verification() {
1432 let key = vec![0u8; 32];
1433 let config = AuditConfig::new().with_hmac_key(key);
1434 let logger = AuditLogger::with_config(config);
1435
1436 logger.log_capture("mem_1", "decisions");
1437 logger.log_capture("mem_2", "learnings");
1438
1439 assert!(logger.verify_chain().is_ok());
1440 }
1441
1442 #[test]
1443 fn test_hmac_chain_verification_no_key() {
1444 let logger = AuditLogger::new();
1445 logger.log_capture("mem_1", "decisions");
1446
1447 assert!(logger.verify_chain().is_err());
1449 }
1450
1451 #[test]
1452 fn test_is_signing_enabled() {
1453 let logger_without_key = AuditLogger::new();
1454 assert!(!logger_without_key.is_signing_enabled());
1455
1456 let config = AuditConfig::new().with_hmac_key(vec![0u8; 32]);
1457 let logger_with_key = AuditLogger::with_config(config);
1458 assert!(logger_with_key.is_signing_enabled());
1459 }
1460
1461 #[test]
1462 fn test_hmac_entry_serialization_with_signature() {
1463 let key = b"test_key_32_bytes_long_xxxxxxxx";
1464 let mut entry = AuditEntry::new("test.event", "action");
1465 assert!(entry.sign(key, GENESIS_HMAC));
1466
1467 let json = serde_json::to_string(&entry).unwrap();
1468 assert!(json.contains("hmac_signature"));
1469 assert!(json.contains("previous_hmac"));
1470 assert!(json.contains("genesis"));
1471
1472 let deserialized: AuditEntry = serde_json::from_str(&json).unwrap();
1473 assert_eq!(deserialized.hmac_signature, entry.hmac_signature);
1474 assert_eq!(deserialized.previous_hmac, entry.previous_hmac);
1475 assert!(deserialized.verify(key));
1476 }
1477
1478 #[test]
1479 fn test_unsigned_entry_omits_hmac_fields() {
1480 let entry = AuditEntry::new("test.event", "action");
1481
1482 let json = serde_json::to_string(&entry).unwrap();
1483 assert!(!json.contains("hmac_signature"));
1485 assert!(!json.contains("previous_hmac"));
1486 }
1487
1488 #[test]
1489 fn test_log_pii_detection() {
1490 let logger = AuditLogger::new();
1491 let pii_types = vec!["Email Address".to_string(), "SSN".to_string()];
1492 logger.log_pii_detection(&pii_types, Some("content_redaction"));
1493
1494 let entries = logger.recent_entries(10);
1495 assert_eq!(entries.len(), 1);
1496 assert_eq!(entries[0].event_type, "security.pii_detection");
1497 assert_eq!(entries[0].action, "detect");
1498
1499 let metadata = &entries[0].metadata;
1501 assert_eq!(metadata["pii_count"], 2);
1502 assert_eq!(metadata["context"], "content_redaction");
1503 }
1504
1505 #[test]
1506 fn test_log_pii_detection_without_context() {
1507 let logger = AuditLogger::new();
1508 let pii_types = vec!["Phone Number".to_string()];
1509 logger.log_pii_detection(&pii_types, None);
1510
1511 let entries = logger.recent_entries(10);
1512 assert_eq!(entries.len(), 1);
1513 assert_eq!(entries[0].metadata["pii_count"], 1);
1514 assert!(entries[0].metadata["context"].is_null());
1515 }
1516
1517 #[test]
1520 fn test_access_review_report_empty() {
1521 let entries: Vec<AuditEntry> = vec![];
1522 let start = Utc::now() - chrono::Duration::hours(1);
1523 let end = Utc::now();
1524
1525 let report = AccessReviewReport::generate(&entries, start, end);
1526
1527 assert_eq!(report.total_events, 0);
1528 assert!(report.by_actor.is_empty());
1529 assert!(report.by_resource_type.is_empty());
1530 assert!(report.by_action.is_empty());
1531 assert_eq!(report.by_outcome.success, 0);
1532 assert_eq!(report.by_outcome.failure, 0);
1533 assert_eq!(report.by_outcome.denied, 0);
1534 }
1535
1536 #[test]
1537 fn test_access_review_report_single_entry() {
1538 let entry = AuditEntry::new("memory.capture", "capture")
1539 .with_actor("user1")
1540 .with_resource("mem_123")
1541 .with_outcome(AuditOutcome::Success);
1542
1543 let start = Utc::now() - chrono::Duration::hours(1);
1544 let end = Utc::now() + chrono::Duration::hours(1);
1545
1546 let report = AccessReviewReport::generate(&[entry], start, end);
1547
1548 assert_eq!(report.total_events, 1);
1549 assert_eq!(report.by_actor.len(), 1);
1550 assert!(report.by_actor.contains_key("user1"));
1551
1552 let actor_summary = report.by_actor.get("user1").unwrap();
1553 assert_eq!(actor_summary.event_count, 1);
1554 assert!(actor_summary.resources_accessed.contains("mem_123"));
1555 assert_eq!(actor_summary.actions.get("capture"), Some(&1));
1556
1557 assert_eq!(report.by_outcome.success, 1);
1558 assert_eq!(report.by_outcome.failure, 0);
1559 assert_eq!(report.by_outcome.denied, 0);
1560 }
1561
1562 #[test]
1563 fn test_access_review_report_multiple_actors() {
1564 let entry1 = AuditEntry::new("memory.capture", "capture")
1565 .with_actor("user1")
1566 .with_outcome(AuditOutcome::Success);
1567 let entry2 = AuditEntry::new("memory.recall", "recall")
1568 .with_actor("user2")
1569 .with_outcome(AuditOutcome::Success);
1570 let entry3 = AuditEntry::new("memory.capture", "capture")
1571 .with_actor("user1")
1572 .with_outcome(AuditOutcome::Denied);
1573
1574 let start = Utc::now() - chrono::Duration::hours(1);
1575 let end = Utc::now() + chrono::Duration::hours(1);
1576
1577 let report = AccessReviewReport::generate(&[entry1, entry2, entry3], start, end);
1578
1579 assert_eq!(report.total_events, 3);
1580 assert_eq!(report.by_actor.len(), 2);
1581
1582 let user1_summary = report.by_actor.get("user1").unwrap();
1583 assert_eq!(user1_summary.event_count, 2);
1584 assert_eq!(user1_summary.actions.get("capture"), Some(&2));
1585
1586 let user2_summary = report.by_actor.get("user2").unwrap();
1587 assert_eq!(user2_summary.event_count, 1);
1588 assert_eq!(user2_summary.actions.get("recall"), Some(&1));
1589 }
1590
1591 #[test]
1592 fn test_access_review_report_outcome_counting() {
1593 let entry1 = AuditEntry::new("test.event", "action").with_outcome(AuditOutcome::Success);
1594 let entry2 = AuditEntry::new("test.event", "action").with_outcome(AuditOutcome::Success);
1595 let entry3 = AuditEntry::new("test.event", "action").with_outcome(AuditOutcome::Failure);
1596 let entry4 = AuditEntry::new("test.event", "action").with_outcome(AuditOutcome::Denied);
1597 let entry5 = AuditEntry::new("test.event", "action").with_outcome(AuditOutcome::Denied);
1598
1599 let start = Utc::now() - chrono::Duration::hours(1);
1600 let end = Utc::now() + chrono::Duration::hours(1);
1601
1602 let report =
1603 AccessReviewReport::generate(&[entry1, entry2, entry3, entry4, entry5], start, end);
1604
1605 assert_eq!(report.by_outcome.success, 2);
1606 assert_eq!(report.by_outcome.failure, 1);
1607 assert_eq!(report.by_outcome.denied, 2);
1608 }
1609
1610 #[test]
1611 fn test_access_review_report_filters_by_period() {
1612 let now = Utc::now();
1613
1614 let mut entry_old = AuditEntry::new("memory.capture", "capture");
1616 entry_old.timestamp = now - chrono::Duration::days(10);
1617
1618 let mut entry_in_range = AuditEntry::new("memory.recall", "recall");
1619 entry_in_range.timestamp = now - chrono::Duration::hours(12);
1620
1621 let mut entry_future = AuditEntry::new("memory.delete", "delete");
1622 entry_future.timestamp = now + chrono::Duration::days(10);
1623
1624 let start = now - chrono::Duration::days(1);
1625 let end = now + chrono::Duration::days(1);
1626
1627 let report =
1628 AccessReviewReport::generate(&[entry_old, entry_in_range, entry_future], start, end);
1629
1630 assert_eq!(report.total_events, 1);
1632 assert_eq!(report.by_action.get("recall"), Some(&1));
1633 assert!(!report.by_action.contains_key("capture"));
1634 assert!(!report.by_action.contains_key("delete"));
1635 }
1636
1637 #[test]
1638 fn test_access_review_report_resource_aggregation() {
1639 let entry1 = AuditEntry::new("memory.capture", "capture")
1640 .with_actor("user1")
1641 .with_resource("mem_1");
1642 let entry2 = AuditEntry::new("memory.capture", "capture")
1643 .with_actor("user1")
1644 .with_resource("mem_2");
1645 let entry3 = AuditEntry::new("memory.recall", "recall")
1646 .with_actor("user1")
1647 .with_resource("mem_1"); let start = Utc::now() - chrono::Duration::hours(1);
1650 let end = Utc::now() + chrono::Duration::hours(1);
1651
1652 let report = AccessReviewReport::generate(&[entry1, entry2, entry3], start, end);
1653
1654 let user1_summary = report.by_actor.get("user1").unwrap();
1655 assert_eq!(user1_summary.event_count, 3);
1656 assert_eq!(user1_summary.resources_accessed.len(), 2);
1658 assert!(user1_summary.resources_accessed.contains("mem_1"));
1659 assert!(user1_summary.resources_accessed.contains("mem_2"));
1660 }
1661
1662 #[test]
1663 fn test_access_review_report_action_counting() {
1664 let entry1 = AuditEntry::new("memory.capture", "capture");
1665 let entry2 = AuditEntry::new("memory.capture", "capture");
1666 let entry3 = AuditEntry::new("memory.recall", "recall");
1667 let entry4 = AuditEntry::new("memory.delete", "delete");
1668
1669 let start = Utc::now() - chrono::Duration::hours(1);
1670 let end = Utc::now() + chrono::Duration::hours(1);
1671
1672 let report = AccessReviewReport::generate(&[entry1, entry2, entry3, entry4], start, end);
1673
1674 assert_eq!(report.by_action.get("capture"), Some(&2));
1675 assert_eq!(report.by_action.get("recall"), Some(&1));
1676 assert_eq!(report.by_action.get("delete"), Some(&1));
1677 }
1678
1679 #[test]
1680 fn test_access_review_report_resource_type_counting() {
1681 let entry1 = AuditEntry::new("memory.capture", "action");
1682 let entry2 = AuditEntry::new("memory.capture", "action");
1683 let entry3 = AuditEntry::new("memory.recall", "action");
1684 let entry4 = AuditEntry::new("security.pii_detection", "action");
1685
1686 let start = Utc::now() - chrono::Duration::hours(1);
1687 let end = Utc::now() + chrono::Duration::hours(1);
1688
1689 let report = AccessReviewReport::generate(&[entry1, entry2, entry3, entry4], start, end);
1690
1691 assert_eq!(report.by_resource_type.get("memory.capture"), Some(&2));
1692 assert_eq!(report.by_resource_type.get("memory.recall"), Some(&1));
1693 assert_eq!(
1694 report.by_resource_type.get("security.pii_detection"),
1695 Some(&1)
1696 );
1697 }
1698
1699 #[test]
1700 fn test_audit_logger_generate_access_review() {
1701 let logger = AuditLogger::new();
1702
1703 logger.log_capture("mem_1", "decisions");
1705 logger.log_recall("test query", 5);
1706 logger.log_denied("capture", "secrets detected");
1707
1708 let start = Utc::now() - chrono::Duration::hours(1);
1709 let end = Utc::now() + chrono::Duration::hours(1);
1710
1711 let report = logger.generate_access_review(start, end);
1712
1713 assert_eq!(report.total_events, 3);
1714 assert_eq!(report.by_outcome.success, 2);
1715 assert_eq!(report.by_outcome.denied, 1);
1716 }
1717
1718 #[test]
1719 fn test_access_review_report_serialization() {
1720 let entry = AuditEntry::new("memory.capture", "capture")
1721 .with_actor("user1")
1722 .with_outcome(AuditOutcome::Success);
1723
1724 let start = Utc::now() - chrono::Duration::hours(1);
1725 let end = Utc::now() + chrono::Duration::hours(1);
1726
1727 let report = AccessReviewReport::generate(&[entry], start, end);
1728
1729 let json = serde_json::to_string(&report).unwrap();
1730 assert!(json.contains("generated_at"));
1731 assert!(json.contains("period_start"));
1732 assert!(json.contains("period_end"));
1733 assert!(json.contains("total_events"));
1734 assert!(json.contains("by_actor"));
1735 assert!(json.contains("by_outcome"));
1736
1737 let deserialized: AccessReviewReport = serde_json::from_str(&json).unwrap();
1739 assert_eq!(deserialized.total_events, report.total_events);
1740 }
1741
1742 #[test]
1745 fn test_log_pii_disclosure() {
1746 let logger = AuditLogger::new();
1747 let pii_types = vec!["Email Address".to_string(), "Name".to_string()];
1748
1749 logger.log_pii_disclosure(
1750 "anthropic",
1751 &pii_types,
1752 Some("user_hash_123"),
1753 "llm_processing",
1754 "consent",
1755 );
1756
1757 let entries = logger.recent_entries(10);
1758 assert_eq!(entries.len(), 1);
1759 assert_eq!(entries[0].event_type, "security.pii_disclosure");
1760 assert_eq!(entries[0].action, "disclose");
1761
1762 let metadata = &entries[0].metadata;
1763 assert_eq!(metadata["destination"], "anthropic");
1764 assert_eq!(metadata["pii_count"], 2);
1765 assert_eq!(metadata["purpose"], "llm_processing");
1766 assert_eq!(metadata["legal_basis"], "consent");
1767 assert_eq!(metadata["data_subject_id"], "user_hash_123");
1768 }
1769
1770 #[test]
1771 fn test_log_pii_disclosure_without_data_subject() {
1772 let logger = AuditLogger::new();
1773 let pii_types = vec!["IP Address".to_string()];
1774
1775 logger.log_pii_disclosure(
1776 "openai",
1777 &pii_types,
1778 None,
1779 "embedding",
1780 "legitimate_interest",
1781 );
1782
1783 let entries = logger.recent_entries(10);
1784 assert_eq!(entries.len(), 1);
1785 assert_eq!(entries[0].metadata["destination"], "openai");
1786 assert!(entries[0].metadata["data_subject_id"].is_null());
1787 assert_eq!(entries[0].metadata["purpose"], "embedding");
1788 assert_eq!(entries[0].metadata["legal_basis"], "legitimate_interest");
1789 }
1790
1791 #[test]
1792 fn test_log_pii_disclosure_multiple_destinations() {
1793 let logger = AuditLogger::new();
1794 let pii_types = vec!["Name".to_string()];
1795
1796 logger.log_pii_disclosure("anthropic", &pii_types, None, "enrichment", "consent");
1797 logger.log_pii_disclosure("openai", &pii_types, None, "enrichment", "consent");
1798 logger.log_pii_disclosure("ollama", &pii_types, None, "enrichment", "consent");
1799
1800 let entries = logger.recent_entries(10);
1801 assert_eq!(entries.len(), 3);
1802
1803 let destinations: Vec<_> = entries
1805 .iter()
1806 .map(|e| e.metadata["destination"].as_str().unwrap())
1807 .collect();
1808 assert!(destinations.contains(&"anthropic"));
1809 assert!(destinations.contains(&"openai"));
1810 assert!(destinations.contains(&"ollama"));
1811 }
1812
1813 #[test]
1814 fn test_log_bulk_pii_disclosure() {
1815 let logger = AuditLogger::new();
1816 let pii_categories = vec![
1817 "Personal Identifiers".to_string(),
1818 "Contact Information".to_string(),
1819 ];
1820
1821 logger.log_bulk_pii_disclosure(
1822 "remote_sync",
1823 100,
1824 &pii_categories,
1825 "backup",
1826 "legitimate_interest",
1827 );
1828
1829 let entries = logger.recent_entries(10);
1830 assert_eq!(entries.len(), 1);
1831 assert_eq!(entries[0].event_type, "security.pii_bulk_disclosure");
1832 assert_eq!(entries[0].action, "bulk_disclose");
1833
1834 let metadata = &entries[0].metadata;
1835 assert_eq!(metadata["destination"], "remote_sync");
1836 assert_eq!(metadata["record_count"], 100);
1837 assert_eq!(metadata["purpose"], "backup");
1838 assert_eq!(metadata["legal_basis"], "legitimate_interest");
1839 }
1840
1841 #[test]
1842 fn test_log_bulk_pii_disclosure_zero_records() {
1843 let logger = AuditLogger::new();
1844 let pii_categories = vec!["Names".to_string()];
1845
1846 logger.log_bulk_pii_disclosure("api_export", 0, &pii_categories, "export", "consent");
1847
1848 let entries = logger.recent_entries(10);
1849 assert_eq!(entries.len(), 1);
1850 assert_eq!(entries[0].metadata["record_count"], 0);
1851 }
1852
1853 #[test]
1854 fn test_pii_disclosure_timestamp_included() {
1855 let logger = AuditLogger::new();
1856 let pii_types = vec!["Email".to_string()];
1857
1858 let before = Utc::now();
1859 logger.log_pii_disclosure("provider", &pii_types, None, "purpose", "basis");
1860 let after = Utc::now();
1861
1862 let entries = logger.recent_entries(10);
1863 assert_eq!(entries.len(), 1);
1864
1865 let timestamp_str = entries[0].metadata["timestamp_utc"].as_str().unwrap();
1867 let timestamp: DateTime<Utc> = timestamp_str.parse().unwrap();
1868 assert!(timestamp >= before && timestamp <= after);
1869 }
1870
1871 #[test]
1872 fn test_pii_disclosure_in_access_review() {
1873 let logger = AuditLogger::new();
1874 let pii_types = vec!["SSN".to_string()];
1875
1876 logger.log_pii_disclosure("external_api", &pii_types, None, "verification", "consent");
1877 logger.log_bulk_pii_disclosure("backup", 50, &pii_types, "archival", "legitimate_interest");
1878
1879 let start = Utc::now() - chrono::Duration::hours(1);
1880 let end = Utc::now() + chrono::Duration::hours(1);
1881
1882 let report = logger.generate_access_review(start, end);
1883
1884 assert_eq!(report.total_events, 2);
1885 assert_eq!(
1886 report.by_resource_type.get("security.pii_disclosure"),
1887 Some(&1)
1888 );
1889 assert_eq!(
1890 report.by_resource_type.get("security.pii_bulk_disclosure"),
1891 Some(&1)
1892 );
1893 }
1894}