1use serde::{Deserialize, Serialize};
2use std::net::SocketAddr;
3use std::time::SystemTime;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum AuditSeverity {
8 Info,
10 Warning,
12 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#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "event_type", rename_all = "snake_case")]
29pub enum AuditEvent {
30 AuthAttempt {
32 success: bool,
33 reason: Option<String>,
34 error_code: Option<String>,
35 },
36 TokenMinted {
38 key_id: String,
39 key_class: String,
40 ttl_seconds: u64,
41 },
42 SuspiciousPattern {
44 pattern_type: String,
45 details: String,
46 },
47 RateLimitExceeded {
49 limit_type: String,
50 current_count: u32,
51 limit: u32,
52 },
53 OriginValidationFailed {
55 expected: Option<String>,
56 actual: Option<String>,
57 },
58 KeyRotation {
60 old_key_id: Option<String>,
61 new_key_id: String,
62 },
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SecurityAuditEvent {
68 pub event_id: String,
70 pub timestamp_ms: u64,
72 pub severity: AuditSeverity,
74 pub event: AuditEvent,
76 pub client_ip: Option<String>,
78 pub origin: Option<String>,
80 pub user_agent: Option<String>,
82 pub path: Option<String>,
84 pub deployment_id: Option<String>,
86 pub subject: Option<String>,
88 pub metering_key: Option<String>,
90}
91
92impl SecurityAuditEvent {
93 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 pub fn with_client_ip(mut self, ip: SocketAddr) -> Self {
115 self.client_ip = Some(ip.ip().to_string());
116 self
117 }
118
119 pub fn with_origin(mut self, origin: impl Into<String>) -> Self {
121 self.origin = Some(origin.into());
122 self
123 }
124
125 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 pub fn with_path(mut self, path: impl Into<String>) -> Self {
133 self.path = Some(path.into());
134 self
135 }
136
137 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 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
145 self.subject = Some(subject.into());
146 self
147 }
148
149 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#[async_trait::async_trait]
158pub trait SecurityAuditLogger: Send + Sync {
159 async fn log(&self, event: SecurityAuditEvent);
161}
162
163pub struct NoOpAuditLogger;
165
166#[async_trait::async_trait]
167impl SecurityAuditLogger for NoOpAuditLogger {
168 async fn log(&self, _event: SecurityAuditEvent) {
169 }
171}
172
173pub struct ChannelAuditLogger {
175 sender: tokio::sync::mpsc::UnboundedSender<SecurityAuditEvent>,
176}
177
178impl ChannelAuditLogger {
179 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
196pub 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
208pub 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
221pub 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}