1use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fmt;
11use std::path::PathBuf;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14use tracing::{debug, info};
15use uuid::Uuid;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct EnterpriseAuditConfig {
20 pub enabled: bool,
22 pub storage: AuditStorageConfig,
24 pub filtering: AuditFilterConfig,
26 pub retention: AuditRetentionConfig,
28 pub compliance: ComplianceConfig,
30 pub encryption: AuditEncryptionConfig,
32 pub streaming: AuditStreamingConfig,
34}
35
36impl Default for EnterpriseAuditConfig {
37 fn default() -> Self {
38 Self {
39 enabled: true,
40 storage: AuditStorageConfig::default(),
41 filtering: AuditFilterConfig::default(),
42 retention: AuditRetentionConfig::default(),
43 compliance: ComplianceConfig::default(),
44 encryption: AuditEncryptionConfig::default(),
45 streaming: AuditStreamingConfig::default(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AuditStorageConfig {
53 pub backend: AuditStorageBackend,
55 pub file_path: Option<PathBuf>,
57 pub database_url: Option<String>,
59 pub s3_config: Option<S3AuditConfig>,
61 pub buffer_size: usize,
63 pub flush_interval_secs: u64,
65}
66
67impl Default for AuditStorageConfig {
68 fn default() -> Self {
69 Self {
70 backend: AuditStorageBackend::File,
71 file_path: Some(PathBuf::from("/var/log/oxirs/audit.jsonl")),
72 database_url: None,
73 s3_config: None,
74 buffer_size: 1000,
75 flush_interval_secs: 60,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
82pub enum AuditStorageBackend {
83 File,
85 Database,
87 S3,
89 Elasticsearch,
91 Splunk,
93 Custom,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct S3AuditConfig {
100 pub bucket: String,
102 pub region: String,
104 pub prefix: String,
106 pub access_key_id: Option<String>,
108 pub secret_access_key: Option<String>,
110 pub server_side_encryption: bool,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct AuditFilterConfig {
117 pub min_severity: AuditSeverity,
119 pub include_event_types: Vec<AuditEventType>,
121 pub exclude_event_types: Vec<AuditEventType>,
123 pub exclude_users: Vec<String>,
125 pub exclude_resources: Vec<String>,
127}
128
129impl Default for AuditFilterConfig {
130 fn default() -> Self {
131 Self {
132 min_severity: AuditSeverity::Info,
133 include_event_types: vec![],
134 exclude_event_types: vec![],
135 exclude_users: vec![],
136 exclude_resources: vec![],
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct AuditRetentionConfig {
144 pub retention_days: u32,
146 pub archive_enabled: bool,
148 pub archive_destination: Option<String>,
150 pub archive_compression: CompressionType,
152 pub auto_cleanup: bool,
154}
155
156impl Default for AuditRetentionConfig {
157 fn default() -> Self {
158 Self {
159 retention_days: 365, archive_enabled: true,
161 archive_destination: None,
162 archive_compression: CompressionType::Gzip,
163 auto_cleanup: true,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
170pub enum CompressionType {
171 None,
172 Gzip,
173 Zstd,
174 Bzip2,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ComplianceConfig {
180 pub standards: Vec<ComplianceStandard>,
182 pub reporting_enabled: bool,
184 pub report_interval_days: u32,
186 pub report_destination: Option<String>,
188}
189
190impl Default for ComplianceConfig {
191 fn default() -> Self {
192 Self {
193 standards: vec![ComplianceStandard::SOC2],
194 reporting_enabled: true,
195 report_interval_days: 30,
196 report_destination: None,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
203pub enum ComplianceStandard {
204 GDPR,
206 HIPAA,
208 SOC2,
210 PCIDSS,
212 ISO27001,
214}
215
216impl fmt::Display for ComplianceStandard {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 match self {
219 ComplianceStandard::GDPR => write!(f, "GDPR"),
220 ComplianceStandard::HIPAA => write!(f, "HIPAA"),
221 ComplianceStandard::SOC2 => write!(f, "SOC2"),
222 ComplianceStandard::PCIDSS => write!(f, "PCI-DSS"),
223 ComplianceStandard::ISO27001 => write!(f, "ISO 27001"),
224 }
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct AuditEncryptionConfig {
231 pub enabled: bool,
233 pub algorithm: EncryptionAlgorithm,
235 pub key_management: KeyManagementConfig,
237}
238
239impl Default for AuditEncryptionConfig {
240 fn default() -> Self {
241 Self {
242 enabled: true,
243 algorithm: EncryptionAlgorithm::AES256GCM,
244 key_management: KeyManagementConfig::default(),
245 }
246 }
247}
248
249#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
251pub enum EncryptionAlgorithm {
252 AES256GCM,
253 ChaCha20Poly1305,
254 AES256CBC,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct KeyManagementConfig {
260 pub kms_type: KmsType,
262 pub kms_url: Option<String>,
264 pub rotation_interval_days: u32,
266}
267
268impl Default for KeyManagementConfig {
269 fn default() -> Self {
270 Self {
271 kms_type: KmsType::Local,
272 kms_url: None,
273 rotation_interval_days: 90,
274 }
275 }
276}
277
278#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
280pub enum KmsType {
281 Local,
283 AwsKms,
285 AzureKeyVault,
287 GcpKms,
289 HashiCorpVault,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, Default)]
295pub struct AuditStreamingConfig {
296 pub enabled: bool,
298 pub destinations: Vec<StreamingDestination>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct StreamingDestination {
305 pub destination_type: DestinationType,
307 pub endpoint: String,
309 pub auth: Option<DestinationAuth>,
311}
312
313#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
315pub enum DestinationType {
316 Kafka,
317 Kinesis,
318 Webhook,
319 SIEM,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct DestinationAuth {
325 pub auth_type: AuthType,
326 pub credentials: HashMap<String, String>,
327}
328
329#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
331pub enum AuthType {
332 ApiKey,
333 OAuth2,
334 Basic,
335 Certificate,
336}
337
338#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
340pub enum AuditEventType {
341 Authentication,
343 Authorization,
345 DataAccess,
347 DataModification,
349 ConfigurationChange,
351 Security,
353 System,
355 Administrative,
357 Compliance,
359}
360
361impl fmt::Display for AuditEventType {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 match self {
364 AuditEventType::Authentication => write!(f, "Authentication"),
365 AuditEventType::Authorization => write!(f, "Authorization"),
366 AuditEventType::DataAccess => write!(f, "DataAccess"),
367 AuditEventType::DataModification => write!(f, "DataModification"),
368 AuditEventType::ConfigurationChange => write!(f, "ConfigurationChange"),
369 AuditEventType::Security => write!(f, "Security"),
370 AuditEventType::System => write!(f, "System"),
371 AuditEventType::Administrative => write!(f, "Administrative"),
372 AuditEventType::Compliance => write!(f, "Compliance"),
373 }
374 }
375}
376
377#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
379pub enum AuditSeverity {
380 Debug,
381 Info,
382 Warning,
383 Error,
384 Critical,
385}
386
387impl fmt::Display for AuditSeverity {
388 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
389 match self {
390 AuditSeverity::Debug => write!(f, "DEBUG"),
391 AuditSeverity::Info => write!(f, "INFO"),
392 AuditSeverity::Warning => write!(f, "WARNING"),
393 AuditSeverity::Error => write!(f, "ERROR"),
394 AuditSeverity::Critical => write!(f, "CRITICAL"),
395 }
396 }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct EnterpriseAuditEvent {
402 pub event_id: String,
404 pub timestamp: DateTime<Utc>,
406 pub event_type: AuditEventType,
408 pub severity: AuditSeverity,
410 pub user_id: Option<String>,
412 pub source_ip: Option<String>,
414 pub resource: String,
416 pub action: String,
418 pub result: ActionResult,
420 pub details: HashMap<String, serde_json::Value>,
422 pub compliance_tags: Vec<ComplianceStandard>,
424 pub session_id: Option<String>,
426 pub request_id: Option<String>,
428 pub correlation_id: Option<String>,
430}
431
432#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
434pub enum ActionResult {
435 Success,
436 Failure,
437 PartialSuccess,
438 Denied,
439}
440
441impl fmt::Display for ActionResult {
442 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
443 match self {
444 ActionResult::Success => write!(f, "SUCCESS"),
445 ActionResult::Failure => write!(f, "FAILURE"),
446 ActionResult::PartialSuccess => write!(f, "PARTIAL_SUCCESS"),
447 ActionResult::Denied => write!(f, "DENIED"),
448 }
449 }
450}
451
452pub struct EnterpriseAuditLogger {
454 config: EnterpriseAuditConfig,
455 buffer: Arc<RwLock<Vec<EnterpriseAuditEvent>>>,
456 metrics: Arc<RwLock<AuditMetrics>>,
457}
458
459#[derive(Debug, Clone, Default, Serialize, Deserialize)]
461pub struct AuditMetrics {
462 pub events_total: u64,
464 pub events_by_type: HashMap<String, u64>,
466 pub events_by_severity: HashMap<String, u64>,
468 pub events_by_result: HashMap<String, u64>,
470 pub events_failed: u64,
472 pub buffer_flushes: u64,
474 pub last_flush: Option<DateTime<Utc>>,
476}
477
478impl EnterpriseAuditLogger {
479 pub fn new(config: EnterpriseAuditConfig) -> Self {
481 Self {
482 config,
483 buffer: Arc::new(RwLock::new(Vec::new())),
484 metrics: Arc::new(RwLock::new(AuditMetrics::default())),
485 }
486 }
487
488 pub async fn initialize(&self) -> Result<()> {
490 if !self.config.enabled {
491 info!("Enterprise audit logging is disabled");
492 return Ok(());
493 }
494
495 info!("Initializing enterprise audit logger");
496
497 self.initialize_storage().await?;
499
500 self.start_background_flusher().await?;
502
503 if self.config.streaming.enabled {
505 self.initialize_streaming().await?;
506 }
507
508 info!("Enterprise audit logger initialized successfully");
509 Ok(())
510 }
511
512 async fn initialize_storage(&self) -> Result<()> {
514 match self.config.storage.backend {
515 AuditStorageBackend::File => {
516 if let Some(path) = &self.config.storage.file_path {
517 if let Some(parent) = path.parent() {
519 tokio::fs::create_dir_all(parent).await?;
520 }
521 debug!("File storage initialized: {:?}", path);
522 }
523 }
524 AuditStorageBackend::Database => {
525 debug!("Database storage initialization (placeholder)");
526 }
527 AuditStorageBackend::S3 => {
528 debug!("S3 storage initialization (placeholder)");
529 }
530 AuditStorageBackend::Elasticsearch => {
531 debug!("Elasticsearch storage initialization (placeholder)");
532 }
533 AuditStorageBackend::Splunk => {
534 debug!("Splunk storage initialization (placeholder)");
535 }
536 AuditStorageBackend::Custom => {
537 debug!("Custom storage initialization (placeholder)");
538 }
539 }
540
541 Ok(())
542 }
543
544 async fn start_background_flusher(&self) -> Result<()> {
546 debug!("Starting background audit log flusher");
547 Ok(())
550 }
551
552 async fn initialize_streaming(&self) -> Result<()> {
554 debug!("Initializing audit event streaming");
555 for destination in &self.config.streaming.destinations {
556 debug!(
557 "Setting up streaming to {:?}: {}",
558 destination.destination_type, destination.endpoint
559 );
560 }
561 Ok(())
562 }
563
564 pub async fn log_event(&self, event: EnterpriseAuditEvent) -> Result<()> {
566 if !self.config.enabled {
567 return Ok(());
568 }
569
570 if !self.should_log_event(&event).await {
572 return Ok(());
573 }
574
575 {
577 let mut buffer = self.buffer.write().await;
578 buffer.push(event.clone());
579
580 if buffer.len() >= self.config.storage.buffer_size {
582 drop(buffer);
583 self.flush_buffer().await?;
584 }
585 }
586
587 {
589 let mut metrics = self.metrics.write().await;
590 metrics.events_total += 1;
591 *metrics
592 .events_by_type
593 .entry(event.event_type.to_string())
594 .or_insert(0) += 1;
595 *metrics
596 .events_by_severity
597 .entry(event.severity.to_string())
598 .or_insert(0) += 1;
599 *metrics
600 .events_by_result
601 .entry(event.result.to_string())
602 .or_insert(0) += 1;
603 }
604
605 if self.config.streaming.enabled {
607 self.stream_event(&event).await?;
608 }
609
610 Ok(())
611 }
612
613 async fn should_log_event(&self, event: &EnterpriseAuditEvent) -> bool {
615 if event.severity < self.config.filtering.min_severity {
617 return false;
618 }
619
620 if self
622 .config
623 .filtering
624 .exclude_event_types
625 .contains(&event.event_type)
626 {
627 return false;
628 }
629
630 if !self.config.filtering.include_event_types.is_empty()
632 && !self
633 .config
634 .filtering
635 .include_event_types
636 .contains(&event.event_type)
637 {
638 return false;
639 }
640
641 if let Some(user_id) = &event.user_id {
643 if self.config.filtering.exclude_users.contains(user_id) {
644 return false;
645 }
646 }
647
648 true
649 }
650
651 pub async fn flush_buffer(&self) -> Result<()> {
653 let events = {
654 let mut buffer = self.buffer.write().await;
655 if buffer.is_empty() {
656 return Ok(());
657 }
658 std::mem::take(&mut *buffer)
659 };
660
661 debug!("Flushing {} audit events to storage", events.len());
662
663 if let Err(e) = self.write_to_storage(&events).await {
667 let mut buffer = self.buffer.write().await;
668 let mut restored = events;
669 restored.append(&mut buffer);
670 *buffer = restored;
671 return Err(e);
672 }
673
674 {
676 let mut metrics = self.metrics.write().await;
677 metrics.buffer_flushes += 1;
678 metrics.last_flush = Some(Utc::now());
679 }
680
681 Ok(())
682 }
683
684 async fn write_to_storage(&self, events: &[EnterpriseAuditEvent]) -> Result<()> {
691 match self.config.storage.backend {
692 AuditStorageBackend::File => {
693 let path = self.config.storage.file_path.as_ref().ok_or_else(|| {
694 anyhow::anyhow!(
695 "Audit File storage backend selected but no file_path configured"
696 )
697 })?;
698
699 let mut content = String::new();
700 for event in events {
701 let json = serde_json::to_string(event)?;
702 content.push_str(&json);
703 content.push('\n');
704 }
705
706 if let Some(parent) = path.parent() {
707 if !parent.as_os_str().is_empty() {
708 tokio::fs::create_dir_all(parent).await?;
709 }
710 }
711
712 use tokio::io::AsyncWriteExt;
713 let mut file = tokio::fs::OpenOptions::new()
714 .create(true)
715 .append(true)
716 .open(path)
717 .await?;
718 file.write_all(content.as_bytes()).await?;
719 file.flush().await?;
720
721 debug!("Wrote {} bytes to audit log {:?}", content.len(), path);
722 Ok(())
723 }
724 other => Err(anyhow::anyhow!(
725 "Audit storage backend {:?} is not implemented; cannot persist {} event(s)",
726 other,
727 events.len()
728 )),
729 }
730 }
731
732 async fn stream_event(&self, event: &EnterpriseAuditEvent) -> Result<()> {
734 for destination in &self.config.streaming.destinations {
735 debug!(
736 "Streaming event {} to {:?}",
737 event.event_id, destination.destination_type
738 );
739 }
741 Ok(())
742 }
743
744 pub async fn get_metrics(&self) -> AuditMetrics {
746 self.metrics.read().await.clone()
747 }
748
749 pub async fn generate_compliance_report(
751 &self,
752 standard: ComplianceStandard,
753 start_date: DateTime<Utc>,
754 end_date: DateTime<Utc>,
755 ) -> Result<ComplianceReport> {
756 info!(
757 "Generating compliance report for {} from {} to {}",
758 standard, start_date, end_date
759 );
760
761 let events = self.query_events(start_date, end_date, standard).await?;
764 let total_events = events.len() as u64;
765
766 let mut findings = Vec::new();
767 for event in &events {
768 let finding = match event.result {
769 ActionResult::Denied => Some((
770 FindingType::NonCompliance,
771 format!("Denied {} on resource '{}'", event.action, event.resource),
772 Some(
773 "Review access control policy and verify the denial was expected"
774 .to_string(),
775 ),
776 )),
777 ActionResult::Failure => Some((
778 FindingType::Warning,
779 format!("Failed {} on resource '{}'", event.action, event.resource),
780 Some(
781 "Investigate the failure and confirm no data integrity impact".to_string(),
782 ),
783 )),
784 _ => None,
785 };
786
787 if let Some((finding_type, description, remediation)) = finding {
788 findings.push(ComplianceFinding {
789 finding_id: Uuid::new_v4().to_string(),
790 finding_type,
791 severity: event.severity,
792 description,
793 affected_events: vec![event.event_id.clone()],
794 remediation,
795 });
796 }
797 }
798
799 let summary = format!(
800 "{standard} compliance report: {total_events} event(s) analyzed, {} finding(s) ({} non-compliance, {} warning)",
801 findings.len(),
802 findings
803 .iter()
804 .filter(|f| f.finding_type == FindingType::NonCompliance)
805 .count(),
806 findings
807 .iter()
808 .filter(|f| f.finding_type == FindingType::Warning)
809 .count(),
810 );
811
812 Ok(ComplianceReport {
813 standard,
814 report_id: Uuid::new_v4().to_string(),
815 generated_at: Utc::now(),
816 period_start: start_date,
817 period_end: end_date,
818 total_events,
819 findings,
820 summary,
821 })
822 }
823
824 async fn query_events(
831 &self,
832 start: DateTime<Utc>,
833 end: DateTime<Utc>,
834 standard: ComplianceStandard,
835 ) -> Result<Vec<EnterpriseAuditEvent>> {
836 self.flush_buffer().await?;
838
839 match self.config.storage.backend {
840 AuditStorageBackend::File => {
841 let path = self.config.storage.file_path.as_ref().ok_or_else(|| {
842 anyhow::anyhow!(
843 "Audit File storage backend selected but no file_path configured"
844 )
845 })?;
846
847 let contents = match tokio::fs::read_to_string(path).await {
848 Ok(contents) => contents,
849 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
851 Err(e) => return Err(e.into()),
852 };
853
854 let mut events = Vec::new();
855 for line in contents.lines() {
856 let line = line.trim();
857 if line.is_empty() {
858 continue;
859 }
860 let event: EnterpriseAuditEvent = serde_json::from_str(line)?;
861 if event.timestamp >= start
862 && event.timestamp <= end
863 && event.compliance_tags.contains(&standard)
864 {
865 events.push(event);
866 }
867 }
868 Ok(events)
869 }
870 other => Err(anyhow::anyhow!(
871 "Compliance querying is not implemented for audit storage backend {:?}",
872 other
873 )),
874 }
875 }
876}
877
878#[derive(Debug, Clone, Serialize, Deserialize)]
880pub struct ComplianceReport {
881 pub standard: ComplianceStandard,
883 pub report_id: String,
885 pub generated_at: DateTime<Utc>,
887 pub period_start: DateTime<Utc>,
889 pub period_end: DateTime<Utc>,
891 pub total_events: u64,
893 pub findings: Vec<ComplianceFinding>,
895 pub summary: String,
897}
898
899#[derive(Debug, Clone, Serialize, Deserialize)]
901pub struct ComplianceFinding {
902 pub finding_id: String,
904 pub finding_type: FindingType,
906 pub severity: AuditSeverity,
908 pub description: String,
910 pub affected_events: Vec<String>,
912 pub remediation: Option<String>,
914}
915
916#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
918pub enum FindingType {
919 NonCompliance,
920 Warning,
921 BestPractice,
922}
923
924#[cfg(test)]
925mod tests {
926 use super::*;
927
928 #[tokio::test]
929 async fn test_audit_config_default() {
930 let config = EnterpriseAuditConfig::default();
931 assert!(config.enabled);
932 assert_eq!(config.retention.retention_days, 365);
933 }
934
935 #[tokio::test]
936 async fn test_audit_logger_creation() {
937 let config = EnterpriseAuditConfig::default();
938 let logger = EnterpriseAuditLogger::new(config);
939 let metrics = logger.get_metrics().await;
940 assert_eq!(metrics.events_total, 0);
941 }
942
943 #[tokio::test]
944 async fn test_compliance_standard_display() {
945 assert_eq!(ComplianceStandard::GDPR.to_string(), "GDPR");
946 assert_eq!(ComplianceStandard::HIPAA.to_string(), "HIPAA");
947 assert_eq!(ComplianceStandard::SOC2.to_string(), "SOC2");
948 }
949
950 fn temp_audit_config() -> EnterpriseAuditConfig {
951 let mut config = EnterpriseAuditConfig::default();
952 let path = std::env::temp_dir().join(format!("oxirs-audit-{}.jsonl", Uuid::new_v4()));
953 config.storage.file_path = Some(path);
954 config
955 }
956
957 fn sample_event(result: ActionResult) -> EnterpriseAuditEvent {
958 EnterpriseAuditEvent {
959 event_id: Uuid::new_v4().to_string(),
960 timestamp: Utc::now(),
961 event_type: AuditEventType::DataAccess,
962 severity: AuditSeverity::Critical,
963 user_id: Some("user-1".to_string()),
964 source_ip: None,
965 resource: "graph:secret".to_string(),
966 action: "read".to_string(),
967 result,
968 details: HashMap::new(),
969 compliance_tags: vec![ComplianceStandard::SOC2],
970 session_id: None,
971 request_id: None,
972 correlation_id: None,
973 }
974 }
975
976 #[tokio::test]
977 async fn regression_audit_events_persisted_to_file() {
978 let config = temp_audit_config();
979 let path = config
980 .storage
981 .file_path
982 .clone()
983 .expect("file path configured");
984 let logger = EnterpriseAuditLogger::new(config);
985
986 logger
987 .log_event(sample_event(ActionResult::Success))
988 .await
989 .unwrap();
990 logger.flush_buffer().await.unwrap();
991
992 let contents = tokio::fs::read_to_string(&path).await.unwrap();
994 assert!(contents.contains("graph:secret"));
995
996 let _ = tokio::fs::remove_file(&path).await;
997 }
998
999 #[tokio::test]
1000 async fn regression_unimplemented_backend_fails_loud() {
1001 let mut config = temp_audit_config();
1002 config.storage.backend = AuditStorageBackend::S3;
1003 let logger = EnterpriseAuditLogger::new(config);
1004
1005 logger
1006 .log_event(sample_event(ActionResult::Success))
1007 .await
1008 .unwrap();
1009 assert!(logger.flush_buffer().await.is_err());
1011 assert!(!logger.buffer.read().await.is_empty());
1013 }
1014
1015 #[tokio::test]
1016 async fn regression_compliance_report_reflects_events() {
1017 let config = temp_audit_config();
1018 let path = config
1019 .storage
1020 .file_path
1021 .clone()
1022 .expect("file path configured");
1023 let logger = EnterpriseAuditLogger::new(config);
1024
1025 logger
1026 .log_event(sample_event(ActionResult::Denied))
1027 .await
1028 .unwrap();
1029 logger
1030 .log_event(sample_event(ActionResult::Success))
1031 .await
1032 .unwrap();
1033
1034 let start = Utc::now() - chrono::Duration::hours(1);
1035 let end = Utc::now() + chrono::Duration::hours(1);
1036 let report = logger
1037 .generate_compliance_report(ComplianceStandard::SOC2, start, end)
1038 .await
1039 .unwrap();
1040
1041 assert_eq!(report.total_events, 2);
1043 assert_eq!(report.findings.len(), 1);
1045 assert_eq!(report.findings[0].finding_type, FindingType::NonCompliance);
1046
1047 let _ = tokio::fs::remove_file(&path).await;
1048 }
1049
1050 #[tokio::test]
1051 async fn test_audit_severity_ordering() {
1052 assert!(AuditSeverity::Critical > AuditSeverity::Error);
1053 assert!(AuditSeverity::Error > AuditSeverity::Warning);
1054 assert!(AuditSeverity::Warning > AuditSeverity::Info);
1055 assert!(AuditSeverity::Info > AuditSeverity::Debug);
1056 }
1057
1058 #[tokio::test]
1059 async fn test_action_result_display() {
1060 assert_eq!(ActionResult::Success.to_string(), "SUCCESS");
1061 assert_eq!(ActionResult::Failure.to_string(), "FAILURE");
1062 assert_eq!(ActionResult::Denied.to_string(), "DENIED");
1063 }
1064}