Skip to main content

hyperstack_auth/
audit.rs

1use serde::{Deserialize, Serialize};
2use std::net::SocketAddr;
3use std::time::SystemTime;
4
5/// Security audit event severity levels
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum AuditSeverity {
8    /// Informational - normal operations
9    Info,
10    /// Warning - suspicious but not necessarily malicious
11    Warning,
12    /// Critical - potential security incident
13    Critical,
14}
15
16impl std::fmt::Display for AuditSeverity {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            AuditSeverity::Info => write!(f, "info"),
20            AuditSeverity::Warning => write!(f, "warning"),
21            AuditSeverity::Critical => write!(f, "critical"),
22        }
23    }
24}
25
26/// Security audit event types
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "event_type", rename_all = "snake_case")]
29pub enum AuditEvent {
30    /// Authentication attempt (success or failure)
31    AuthAttempt {
32        success: bool,
33        reason: Option<String>,
34        error_code: Option<String>,
35    },
36    /// Token minted
37    TokenMinted {
38        key_id: String,
39        key_class: String,
40        ttl_seconds: u64,
41    },
42    /// Suspicious pattern detected
43    SuspiciousPattern {
44        pattern_type: String,
45        details: String,
46    },
47    /// Rate limit exceeded
48    RateLimitExceeded {
49        limit_type: String,
50        current_count: u32,
51        limit: u32,
52    },
53    /// Origin validation failure
54    OriginValidationFailed {
55        expected: Option<String>,
56        actual: Option<String>,
57    },
58    /// Key rotation event
59    KeyRotation {
60        old_key_id: Option<String>,
61        new_key_id: String,
62    },
63}
64
65/// Security audit event
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SecurityAuditEvent {
68    /// Unique event ID
69    pub event_id: String,
70    /// Timestamp when event occurred
71    pub timestamp_ms: u64,
72    /// Event severity
73    pub severity: AuditSeverity,
74    /// Event type with details
75    pub event: AuditEvent,
76    /// Client IP address
77    pub client_ip: Option<String>,
78    /// Client origin
79    pub origin: Option<String>,
80    /// User agent string
81    pub user_agent: Option<String>,
82    /// Request path
83    pub path: Option<String>,
84    /// Deployment ID if applicable
85    pub deployment_id: Option<String>,
86    /// Subject identifier if authenticated
87    pub subject: Option<String>,
88    /// Metering key if available
89    pub metering_key: Option<String>,
90}
91
92impl SecurityAuditEvent {
93    /// Create a new security audit event
94    pub fn new(severity: AuditSeverity, event: AuditEvent) -> Self {
95        Self {
96            event_id: uuid::Uuid::new_v4().to_string(),
97            timestamp_ms: SystemTime::now()
98                .duration_since(SystemTime::UNIX_EPOCH)
99                .unwrap_or_default()
100                .as_millis() as u64,
101            severity,
102            event,
103            client_ip: None,
104            origin: None,
105            user_agent: None,
106            path: None,
107            deployment_id: None,
108            subject: None,
109            metering_key: None,
110        }
111    }
112
113    /// Add client IP address
114    pub fn with_client_ip(mut self, ip: SocketAddr) -> Self {
115        self.client_ip = Some(ip.ip().to_string());
116        self
117    }
118
119    /// Add origin
120    pub fn with_origin(mut self, origin: impl Into<String>) -> Self {
121        self.origin = Some(origin.into());
122        self
123    }
124
125    /// Add user agent
126    pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
127        self.user_agent = Some(user_agent.into());
128        self
129    }
130
131    /// Add request path
132    pub fn with_path(mut self, path: impl Into<String>) -> Self {
133        self.path = Some(path.into());
134        self
135    }
136
137    /// Add deployment ID
138    pub fn with_deployment_id(mut self, deployment_id: impl Into<String>) -> Self {
139        self.deployment_id = Some(deployment_id.into());
140        self
141    }
142
143    /// Add subject
144    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
145        self.subject = Some(subject.into());
146        self
147    }
148
149    /// Add metering key
150    pub fn with_metering_key(mut self, metering_key: impl Into<String>) -> Self {
151        self.metering_key = Some(metering_key.into());
152        self
153    }
154}
155
156/// Trait for security audit loggers
157#[async_trait::async_trait]
158pub trait SecurityAuditLogger: Send + Sync {
159    /// Log a security audit event
160    async fn log(&self, event: SecurityAuditEvent);
161}
162
163/// No-op audit logger for development/testing
164pub struct NoOpAuditLogger;
165
166#[async_trait::async_trait]
167impl SecurityAuditLogger for NoOpAuditLogger {
168    async fn log(&self, _event: SecurityAuditEvent) {
169        // No-op
170    }
171}
172
173/// Channel-based audit logger for async event streaming
174pub struct ChannelAuditLogger {
175    sender: tokio::sync::mpsc::UnboundedSender<SecurityAuditEvent>,
176}
177
178impl ChannelAuditLogger {
179    /// Create a new channel audit logger
180    pub fn new() -> (
181        Self,
182        tokio::sync::mpsc::UnboundedReceiver<SecurityAuditEvent>,
183    ) {
184        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
185        (Self { sender }, receiver)
186    }
187}
188
189#[async_trait::async_trait]
190impl SecurityAuditLogger for ChannelAuditLogger {
191    async fn log(&self, event: SecurityAuditEvent) {
192        let _ = self.sender.send(event);
193    }
194}
195
196/// Helper function to create an auth failure audit event
197pub fn auth_failure_event(error_code: &crate::AuthErrorCode, reason: &str) -> SecurityAuditEvent {
198    SecurityAuditEvent::new(
199        AuditSeverity::Warning,
200        AuditEvent::AuthAttempt {
201            success: false,
202            reason: Some(reason.to_string()),
203            error_code: Some(error_code.to_string()),
204        },
205    )
206}
207
208/// Helper function to create an auth success audit event
209pub fn auth_success_event(subject: &str) -> SecurityAuditEvent {
210    SecurityAuditEvent::new(
211        AuditSeverity::Info,
212        AuditEvent::AuthAttempt {
213            success: true,
214            reason: None,
215            error_code: None,
216        },
217    )
218    .with_subject(subject)
219}
220
221/// Helper function to create a rate limit exceeded audit event
222pub fn rate_limit_event(limit_type: &str, current: u32, limit: u32) -> SecurityAuditEvent {
223    SecurityAuditEvent::new(
224        AuditSeverity::Warning,
225        AuditEvent::RateLimitExceeded {
226            limit_type: limit_type.to_string(),
227            current_count: current,
228            limit,
229        },
230    )
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_audit_event_builder() {
239        let event = SecurityAuditEvent::new(
240            AuditSeverity::Warning,
241            AuditEvent::AuthAttempt {
242                success: false,
243                reason: Some("Token expired".to_string()),
244                error_code: Some("token-expired".to_string()),
245            },
246        )
247        .with_client_ip("192.168.1.1:12345".parse().unwrap())
248        .with_origin("https://example.com")
249        .with_subject("user-123");
250
251        assert_eq!(event.severity, AuditSeverity::Warning);
252        assert_eq!(event.client_ip, Some("192.168.1.1".to_string()));
253        assert_eq!(event.origin, Some("https://example.com".to_string()));
254        assert_eq!(event.subject, Some("user-123".to_string()));
255    }
256
257    #[tokio::test]
258    async fn test_channel_audit_logger() {
259        let (logger, mut receiver) = ChannelAuditLogger::new();
260
261        let event = auth_failure_event(&crate::AuthErrorCode::TokenExpired, "Token has expired");
262
263        logger.log(event.clone()).await;
264
265        let received = receiver.recv().await.expect("Should receive event");
266        match received.event {
267            AuditEvent::AuthAttempt { success, .. } => {
268                assert!(!success);
269            }
270            _ => panic!("Expected AuthAttempt event"),
271        }
272    }
273}