use std::net::IpAddr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityOutcome {
Success,
Failure,
Denied,
Error,
}
impl SecurityOutcome {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Failure => "failure",
Self::Denied => "denied",
Self::Error => "error",
}
}
}
impl std::fmt::Display for SecurityOutcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub struct SecurityEvent<'a> {
event_type: &'a str,
action: &'a str,
outcome: SecurityOutcome,
actor: Option<&'a str>,
source_ip: Option<IpAddr>,
resource: Option<&'a str>,
reason: Option<&'a str>,
detail: Option<&'a str>,
}
impl<'a> SecurityEvent<'a> {
#[must_use]
pub fn new(event_type: &'a str, action: &'a str, outcome: SecurityOutcome) -> Self {
Self {
event_type,
action,
outcome,
actor: None,
source_ip: None,
resource: None,
reason: None,
detail: None,
}
}
#[must_use]
pub fn actor(mut self, actor: &'a str) -> Self {
self.actor = Some(actor);
self
}
#[must_use]
pub fn source_ip(mut self, ip: IpAddr) -> Self {
self.source_ip = Some(ip);
self
}
#[must_use]
pub fn resource(mut self, resource: &'a str) -> Self {
self.resource = Some(resource);
self
}
#[must_use]
pub fn reason(mut self, reason: &'a str) -> Self {
self.reason = Some(reason);
self
}
#[must_use]
pub fn detail(mut self, detail: &'a str) -> Self {
self.detail = Some(detail);
self
}
pub fn emit(&self) {
let source_ip_str = self.source_ip.map(|ip| ip.to_string());
let source_ip_ref = source_ip_str.as_deref().unwrap_or("-");
match self.outcome {
SecurityOutcome::Success => {
tracing::info!(
target: "security",
event_type = self.event_type,
action = self.action,
outcome = self.outcome.as_str(),
actor = self.actor.unwrap_or("-"),
source_ip = source_ip_ref,
resource = self.resource.unwrap_or("-"),
"security event"
);
}
SecurityOutcome::Failure | SecurityOutcome::Denied => {
tracing::warn!(
target: "security",
event_type = self.event_type,
action = self.action,
outcome = self.outcome.as_str(),
actor = self.actor.unwrap_or("-"),
source_ip = source_ip_ref,
resource = self.resource.unwrap_or("-"),
reason = self.reason.unwrap_or("-"),
"security event"
);
}
SecurityOutcome::Error => {
tracing::error!(
target: "security",
event_type = self.event_type,
action = self.action,
outcome = self.outcome.as_str(),
actor = self.actor.unwrap_or("-"),
source_ip = source_ip_ref,
resource = self.resource.unwrap_or("-"),
reason = self.reason.unwrap_or("-"),
detail = self.detail.unwrap_or("-"),
"security event"
);
}
}
}
}
pub fn auth_success(action: &str, actor: &str, source_ip: Option<IpAddr>) {
let mut event =
SecurityEvent::new("auth.success", action, SecurityOutcome::Success).actor(actor);
if let Some(ip) = source_ip {
event = event.source_ip(ip);
}
event.emit();
}
pub fn auth_failure(action: &str, reason: &str, source_ip: Option<IpAddr>) {
let mut event =
SecurityEvent::new("auth.failure", action, SecurityOutcome::Failure).reason(reason);
if let Some(ip) = source_ip {
event = event.source_ip(ip);
}
event.emit();
}
pub fn access_denied(action: &str, actor: &str, resource: &str, source_ip: Option<IpAddr>) {
let mut event = SecurityEvent::new("access.denied", action, SecurityOutcome::Denied)
.actor(actor)
.resource(resource);
if let Some(ip) = source_ip {
event = event.source_ip(ip);
}
event.emit();
}
pub fn config_changed(action: &str, actor: &str, detail: &str) {
SecurityEvent::new("config.changed", action, SecurityOutcome::Success)
.actor(actor)
.detail(detail)
.emit();
}
pub fn tls_event(
action: &str,
outcome: SecurityOutcome,
reason: Option<&str>,
source_ip: Option<IpAddr>,
) {
let mut event = SecurityEvent::new("tls.event", action, outcome);
if let Some(r) = reason {
event = event.reason(r);
}
if let Some(ip) = source_ip {
event = event.source_ip(ip);
}
event.emit();
}
pub fn rate_limit_triggered(actor: &str, resource: &str, source_ip: Option<IpAddr>) {
let mut event = SecurityEvent::new(
"rate_limit.triggered",
"rate_limit",
SecurityOutcome::Denied,
)
.actor(actor)
.resource(resource);
if let Some(ip) = source_ip {
event = event.source_ip(ip);
}
event.emit();
}
pub fn token_rotated(action: &str, detail: &str) {
SecurityEvent::new("token.rotated", action, SecurityOutcome::Success)
.detail(detail)
.emit();
}
pub fn input_validation_failure(action: &str, reason: &str, source_ip: Option<IpAddr>) {
let mut event =
SecurityEvent::new("input.validation_failure", action, SecurityOutcome::Failure)
.reason(reason);
if let Some(ip) = source_ip {
event = event.source_ip(ip);
}
event.emit();
}
pub fn record_dlq(action: &str, reason: &str, detail: Option<&str>) {
let mut event =
SecurityEvent::new("data.dlq_routed", action, SecurityOutcome::Failure).reason(reason);
if let Some(d) = detail {
event = event.detail(d);
}
event.emit();
}
pub fn data_quality_alert(action: &str, detail: &str) {
SecurityEvent::new("data.quality_alert", action, SecurityOutcome::Failure)
.detail(detail)
.emit();
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn test_security_event_builder() {
let event = SecurityEvent::new("auth.failure", "bearer_validate", SecurityOutcome::Failure)
.actor("user@example.com")
.source_ip(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))
.resource("/api/data")
.reason("invalid_token");
event.emit();
}
#[test]
fn test_convenience_functions() {
let ip = Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)));
auth_success("bearer_validate", "admin", ip);
auth_failure("bearer_validate", "expired_token", ip);
access_denied("read", "guest", "/admin", ip);
config_changed("reload", "system", "auth config updated");
tls_event(
"handshake",
SecurityOutcome::Failure,
Some("cert_expired"),
ip,
);
rate_limit_triggered("client_abc", "/api/ingest", ip);
token_rotated("bearer_refresh", "3 tokens loaded from vault");
input_validation_failure("json_parse", "invalid_json", ip);
}
#[test]
fn test_outcome_as_str() {
assert_eq!(SecurityOutcome::Success.as_str(), "success");
assert_eq!(SecurityOutcome::Failure.as_str(), "failure");
assert_eq!(SecurityOutcome::Denied.as_str(), "denied");
assert_eq!(SecurityOutcome::Error.as_str(), "error");
}
#[test]
fn test_outcome_display() {
assert_eq!(format!("{}", SecurityOutcome::Success), "success");
assert_eq!(format!("{}", SecurityOutcome::Error), "error");
}
#[test]
fn test_minimal_event() {
SecurityEvent::new("test.event", "test_action", SecurityOutcome::Success).emit();
}
#[test]
fn test_error_outcome_event() {
SecurityEvent::new("auth.error", "token_validate", SecurityOutcome::Error)
.reason("backend_unavailable")
.detail("vault connection timed out after 5s")
.source_ip(IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1)))
.emit();
}
#[test]
fn test_ipv6_source() {
let ipv6: IpAddr = "::1".parse().unwrap();
SecurityEvent::new("auth.success", "login", SecurityOutcome::Success)
.source_ip(ipv6)
.actor("admin")
.emit();
}
#[test]
fn test_no_source_ip() {
auth_success("api_key", "svc-internal", None);
auth_failure("bearer_validate", "malformed", None);
}
}