1use std::net::IpAddr;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum SecurityOutcome {
37 Success,
39 Failure,
41 Denied,
43 Error,
45}
46
47impl SecurityOutcome {
48 #[must_use]
50 pub fn as_str(self) -> &'static str {
51 match self {
52 Self::Success => "success",
53 Self::Failure => "failure",
54 Self::Denied => "denied",
55 Self::Error => "error",
56 }
57 }
58}
59
60impl std::fmt::Display for SecurityOutcome {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(self.as_str())
63 }
64}
65
66pub struct SecurityEvent<'a> {
74 event_type: &'a str,
76 action: &'a str,
78 outcome: SecurityOutcome,
80 actor: Option<&'a str>,
82 source_ip: Option<IpAddr>,
84 resource: Option<&'a str>,
86 reason: Option<&'a str>,
88 detail: Option<&'a str>,
90}
91
92impl<'a> SecurityEvent<'a> {
93 #[must_use]
95 pub fn new(event_type: &'a str, action: &'a str, outcome: SecurityOutcome) -> Self {
96 Self {
97 event_type,
98 action,
99 outcome,
100 actor: None,
101 source_ip: None,
102 resource: None,
103 reason: None,
104 detail: None,
105 }
106 }
107
108 #[must_use]
110 pub fn actor(mut self, actor: &'a str) -> Self {
111 self.actor = Some(actor);
112 self
113 }
114
115 #[must_use]
117 pub fn source_ip(mut self, ip: IpAddr) -> Self {
118 self.source_ip = Some(ip);
119 self
120 }
121
122 #[must_use]
124 pub fn resource(mut self, resource: &'a str) -> Self {
125 self.resource = Some(resource);
126 self
127 }
128
129 #[must_use]
131 pub fn reason(mut self, reason: &'a str) -> Self {
132 self.reason = Some(reason);
133 self
134 }
135
136 #[must_use]
138 pub fn detail(mut self, detail: &'a str) -> Self {
139 self.detail = Some(detail);
140 self
141 }
142
143 pub fn emit(&self) {
154 let source_ip_str = self.source_ip.map(|ip| ip.to_string());
155 let source_ip_ref = source_ip_str.as_deref().unwrap_or("-");
156
157 match self.outcome {
158 SecurityOutcome::Success => {
159 tracing::info!(
160 target: "security",
161 event_type = self.event_type,
162 action = self.action,
163 outcome = self.outcome.as_str(),
164 actor = self.actor.unwrap_or("-"),
165 source_ip = source_ip_ref,
166 resource = self.resource.unwrap_or("-"),
167 "security event"
168 );
169 }
170 SecurityOutcome::Failure | SecurityOutcome::Denied => {
171 tracing::warn!(
172 target: "security",
173 event_type = self.event_type,
174 action = self.action,
175 outcome = self.outcome.as_str(),
176 actor = self.actor.unwrap_or("-"),
177 source_ip = source_ip_ref,
178 resource = self.resource.unwrap_or("-"),
179 reason = self.reason.unwrap_or("-"),
180 "security event"
181 );
182 }
183 SecurityOutcome::Error => {
184 tracing::error!(
185 target: "security",
186 event_type = self.event_type,
187 action = self.action,
188 outcome = self.outcome.as_str(),
189 actor = self.actor.unwrap_or("-"),
190 source_ip = source_ip_ref,
191 resource = self.resource.unwrap_or("-"),
192 reason = self.reason.unwrap_or("-"),
193 detail = self.detail.unwrap_or("-"),
194 "security event"
195 );
196 }
197 }
198 }
199}
200
201pub fn auth_success(action: &str, actor: &str, source_ip: Option<IpAddr>) {
207 let mut event =
208 SecurityEvent::new("auth.success", action, SecurityOutcome::Success).actor(actor);
209 if let Some(ip) = source_ip {
210 event = event.source_ip(ip);
211 }
212 event.emit();
213}
214
215pub fn auth_failure(action: &str, reason: &str, source_ip: Option<IpAddr>) {
217 let mut event =
218 SecurityEvent::new("auth.failure", action, SecurityOutcome::Failure).reason(reason);
219 if let Some(ip) = source_ip {
220 event = event.source_ip(ip);
221 }
222 event.emit();
223}
224
225pub fn access_denied(action: &str, actor: &str, resource: &str, source_ip: Option<IpAddr>) {
227 let mut event = SecurityEvent::new("access.denied", action, SecurityOutcome::Denied)
228 .actor(actor)
229 .resource(resource);
230 if let Some(ip) = source_ip {
231 event = event.source_ip(ip);
232 }
233 event.emit();
234}
235
236pub fn config_changed(action: &str, actor: &str, detail: &str) {
238 SecurityEvent::new("config.changed", action, SecurityOutcome::Success)
239 .actor(actor)
240 .detail(detail)
241 .emit();
242}
243
244pub fn tls_event(
246 action: &str,
247 outcome: SecurityOutcome,
248 reason: Option<&str>,
249 source_ip: Option<IpAddr>,
250) {
251 let mut event = SecurityEvent::new("tls.event", action, outcome);
252 if let Some(r) = reason {
253 event = event.reason(r);
254 }
255 if let Some(ip) = source_ip {
256 event = event.source_ip(ip);
257 }
258 event.emit();
259}
260
261pub fn rate_limit_triggered(actor: &str, resource: &str, source_ip: Option<IpAddr>) {
263 let mut event = SecurityEvent::new(
264 "rate_limit.triggered",
265 "rate_limit",
266 SecurityOutcome::Denied,
267 )
268 .actor(actor)
269 .resource(resource);
270 if let Some(ip) = source_ip {
271 event = event.source_ip(ip);
272 }
273 event.emit();
274}
275
276pub fn token_rotated(action: &str, detail: &str) {
278 SecurityEvent::new("token.rotated", action, SecurityOutcome::Success)
279 .detail(detail)
280 .emit();
281}
282
283pub fn input_validation_failure(action: &str, reason: &str, source_ip: Option<IpAddr>) {
285 let mut event =
286 SecurityEvent::new("input.validation_failure", action, SecurityOutcome::Failure)
287 .reason(reason);
288 if let Some(ip) = source_ip {
289 event = event.source_ip(ip);
290 }
291 event.emit();
292}
293
294pub fn record_dlq(action: &str, reason: &str, detail: Option<&str>) {
296 let mut event =
297 SecurityEvent::new("data.dlq_routed", action, SecurityOutcome::Failure).reason(reason);
298 if let Some(d) = detail {
299 event = event.detail(d);
300 }
301 event.emit();
302}
303
304pub fn data_quality_alert(action: &str, detail: &str) {
306 SecurityEvent::new("data.quality_alert", action, SecurityOutcome::Failure)
307 .detail(detail)
308 .emit();
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use std::net::{IpAddr, Ipv4Addr};
315
316 #[test]
317 fn test_security_event_builder() {
318 let event = SecurityEvent::new("auth.failure", "bearer_validate", SecurityOutcome::Failure)
319 .actor("user@example.com")
320 .source_ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))
321 .resource("/api/data")
322 .reason("invalid_token");
323 event.emit();
325 }
326
327 #[test]
328 fn test_convenience_functions() {
329 let ip = Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
330 auth_success("bearer_validate", "admin", ip);
331 auth_failure("bearer_validate", "expired_token", ip);
332 access_denied("read", "guest", "/admin", ip);
333 config_changed("reload", "system", "auth config updated");
334 tls_event(
335 "handshake",
336 SecurityOutcome::Failure,
337 Some("cert_expired"),
338 ip,
339 );
340 rate_limit_triggered("client_abc", "/api/ingest", ip);
341 token_rotated("bearer_refresh", "3 tokens loaded from vault");
342 input_validation_failure("json_parse", "invalid_json", ip);
343 }
344
345 #[test]
346 fn test_outcome_as_str() {
347 assert_eq!(SecurityOutcome::Success.as_str(), "success");
348 assert_eq!(SecurityOutcome::Failure.as_str(), "failure");
349 assert_eq!(SecurityOutcome::Denied.as_str(), "denied");
350 assert_eq!(SecurityOutcome::Error.as_str(), "error");
351 }
352
353 #[test]
354 fn test_outcome_display() {
355 assert_eq!(format!("{}", SecurityOutcome::Success), "success");
356 assert_eq!(format!("{}", SecurityOutcome::Error), "error");
357 }
358
359 #[test]
360 fn test_minimal_event() {
361 SecurityEvent::new("test.event", "test_action", SecurityOutcome::Success).emit();
363 }
364
365 #[test]
366 fn test_error_outcome_event() {
367 SecurityEvent::new("auth.error", "token_validate", SecurityOutcome::Error)
368 .reason("backend_unavailable")
369 .detail("vault connection timed out after 5s")
370 .source_ip(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)))
371 .emit();
372 }
373
374 #[test]
375 fn test_ipv6_source() {
376 let ipv6: IpAddr = "::1".parse().unwrap();
377 SecurityEvent::new("auth.success", "login", SecurityOutcome::Success)
378 .source_ip(ipv6)
379 .actor("admin")
380 .emit();
381 }
382
383 #[test]
384 fn test_no_source_ip() {
385 auth_success("api_key", "svc-internal", None);
386 auth_failure("bearer_validate", "malformed", None);
387 }
388}