use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::net::IpAddr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SecuritySeverity {
Info = 0,
Low = 1,
Medium = 2,
High = 3,
Critical = 4,
}
impl fmt::Display for SecuritySeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Info => write!(f, "INFO"),
Self::Low => write!(f, "LOW"),
Self::Medium => write!(f, "MEDIUM"),
Self::High => write!(f, "HIGH"),
Self::Critical => write!(f, "CRITICAL"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum EventCategory {
Authentication,
Authorization,
InputValidation,
Configuration,
DataAccess,
Network,
Integrity,
Cryptography,
Policy,
Other,
}
impl fmt::Display for EventCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Authentication => write!(f, "AUTHENTICATION"),
Self::Authorization => write!(f, "AUTHORIZATION"),
Self::InputValidation => write!(f, "INPUT_VALIDATION"),
Self::Configuration => write!(f, "CONFIGURATION"),
Self::DataAccess => write!(f, "DATA_ACCESS"),
Self::Network => write!(f, "NETWORK"),
Self::Integrity => write!(f, "INTEGRITY"),
Self::Cryptography => write!(f, "CRYPTOGRAPHY"),
Self::Policy => write!(f, "POLICY"),
Self::Other => write!(f, "OTHER"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttackPattern {
SqlInjection,
Xss,
CommandInjection,
PathTraversal,
BruteForce,
DenialOfService,
BufferOverflow,
XxeAttack,
Ssrf,
Deserialization,
LdapInjection,
TemplateInjection,
None,
}
impl fmt::Display for AttackPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SqlInjection => write!(f, "SQL_INJECTION"),
Self::Xss => write!(f, "XSS"),
Self::CommandInjection => write!(f, "COMMAND_INJECTION"),
Self::PathTraversal => write!(f, "PATH_TRAVERSAL"),
Self::BruteForce => write!(f, "BRUTE_FORCE"),
Self::DenialOfService => write!(f, "DENIAL_OF_SERVICE"),
Self::BufferOverflow => write!(f, "BUFFER_OVERFLOW"),
Self::XxeAttack => write!(f, "XXE_ATTACK"),
Self::Ssrf => write!(f, "SSRF"),
Self::Deserialization => write!(f, "DESERIALIZATION"),
Self::LdapInjection => write!(f, "LDAP_INJECTION"),
Self::TemplateInjection => write!(f, "TEMPLATE_INJECTION"),
Self::None => write!(f, "NONE"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityEvent {
pub id: String,
pub timestamp: DateTime<Utc>,
pub severity: SecuritySeverity,
pub category: EventCategory,
pub message: String,
pub source_ip: Option<IpAddr>,
pub user_id: Option<String>,
pub resource: Option<String>,
pub action: Option<String>,
pub attack_pattern: AttackPattern,
pub metadata: HashMap<String, String>,
pub success: bool,
}
impl SecurityEvent {
pub fn new(
severity: SecuritySeverity, category: EventCategory, message: impl Into<String>,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
severity,
category,
message: message.into(),
source_ip: None,
user_id: None,
resource: None,
action: None,
attack_pattern: AttackPattern::None,
metadata: HashMap::new(),
success: true,
}
}
pub fn authentication_failed(user: impl Into<String>, source_ip: IpAddr) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
severity: SecuritySeverity::Medium,
category: EventCategory::Authentication,
message: "Authentication failed".to_string(),
source_ip: Some(source_ip),
user_id: Some(user.into()),
resource: None,
action: Some("login".to_string()),
attack_pattern: AttackPattern::None,
metadata: HashMap::new(),
success: false,
}
}
pub fn authorization_failed(
user: impl Into<String>, resource: impl Into<String>, action: impl Into<String>,
) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
severity: SecuritySeverity::High,
category: EventCategory::Authorization,
message: "Authorization denied".to_string(),
source_ip: None,
user_id: Some(user.into()),
resource: Some(resource.into()),
action: Some(action.into()),
attack_pattern: AttackPattern::None,
metadata: HashMap::new(),
success: false,
}
}
pub fn input_validation_failed(input: impl Into<String>, pattern: AttackPattern) -> Self {
let mut metadata = HashMap::new();
metadata.insert("input".to_string(), input.into());
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
severity: SecuritySeverity::High,
category: EventCategory::InputValidation,
message: format!("Input validation failed: {}", pattern),
source_ip: None,
user_id: None,
resource: None,
action: Some("validate_input".to_string()),
attack_pattern: pattern,
metadata,
success: false,
}
}
pub fn configuration_changed(
user: impl Into<String>, setting: impl Into<String>, old_value: impl Into<String>,
new_value: impl Into<String>,
) -> Self {
let mut metadata = HashMap::new();
metadata.insert("setting".to_string(), setting.into());
metadata.insert("old_value".to_string(), old_value.into());
metadata.insert("new_value".to_string(), new_value.into());
Self {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
severity: SecuritySeverity::Medium,
category: EventCategory::Configuration,
message: "Configuration changed".to_string(),
source_ip: None,
user_id: Some(user.into()),
resource: None,
action: Some("update_config".to_string()),
attack_pattern: AttackPattern::None,
metadata,
success: true,
}
}
pub fn with_source_ip(mut self, ip: IpAddr) -> Self {
self.source_ip = Some(ip);
self
}
pub fn with_user(mut self, user: impl Into<String>) -> Self {
self.user_id = Some(user.into());
self
}
pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
self.resource = Some(resource.into());
self
}
pub fn with_action(mut self, action: impl Into<String>) -> Self {
self.action = Some(action.into());
self
}
pub fn with_attack_pattern(mut self, pattern: AttackPattern) -> Self {
self.attack_pattern = pattern;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_success(mut self, success: bool) -> Self {
self.success = success;
self
}
pub fn requires_alert(&self) -> bool {
match self.severity {
SecuritySeverity::Critical | SecuritySeverity::High => true,
SecuritySeverity::Medium => {
matches!(
self.category,
EventCategory::Authentication | EventCategory::Authorization
) && !self.success
}
_ => false,
}
}
pub fn is_attack(&self) -> bool {
!matches!(self.attack_pattern, AttackPattern::None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn test_security_event_creation() {
let event = SecurityEvent::new(
SecuritySeverity::High,
EventCategory::Authentication,
"Test event",
);
assert_eq!(event.severity, SecuritySeverity::High);
assert_eq!(event.category, EventCategory::Authentication);
assert_eq!(event.message, "Test event");
assert!(event.success);
}
#[test]
fn test_authentication_failed_event() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
let event = SecurityEvent::authentication_failed("user123", ip);
assert_eq!(event.severity, SecuritySeverity::Medium);
assert_eq!(event.category, EventCategory::Authentication);
assert_eq!(event.user_id, Some("user123".to_string()));
assert_eq!(event.source_ip, Some(ip));
assert!(!event.success);
}
#[test]
fn test_authorization_failed_event() {
let event = SecurityEvent::authorization_failed("user123", "/admin", "read");
assert_eq!(event.severity, SecuritySeverity::High);
assert_eq!(event.category, EventCategory::Authorization);
assert_eq!(event.user_id, Some("user123".to_string()));
assert_eq!(event.resource, Some("/admin".to_string()));
assert_eq!(event.action, Some("read".to_string()));
assert!(!event.success);
}
#[test]
fn test_input_validation_failed_event() {
let event = SecurityEvent::input_validation_failed(
"SELECT * FROM users",
AttackPattern::SqlInjection,
);
assert_eq!(event.severity, SecuritySeverity::High);
assert_eq!(event.category, EventCategory::InputValidation);
assert_eq!(event.attack_pattern, AttackPattern::SqlInjection);
assert!(!event.success);
assert!(event.metadata.contains_key("input"));
}
#[test]
fn test_configuration_changed_event() {
let event = SecurityEvent::configuration_changed("admin", "max_connections", "100", "200");
assert_eq!(event.severity, SecuritySeverity::Medium);
assert_eq!(event.category, EventCategory::Configuration);
assert_eq!(event.user_id, Some("admin".to_string()));
assert!(event.success);
assert_eq!(
event.metadata.get("setting"),
Some(&"max_connections".to_string())
);
}
#[test]
fn test_event_builder_pattern() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let event = SecurityEvent::new(
SecuritySeverity::High,
EventCategory::DataAccess,
"Data accessed",
)
.with_source_ip(ip)
.with_user("user456")
.with_resource("/api/users")
.with_action("read")
.with_attack_pattern(AttackPattern::None)
.with_metadata("table", "users")
.with_success(true);
assert_eq!(event.source_ip, Some(ip));
assert_eq!(event.user_id, Some("user456".to_string()));
assert_eq!(event.resource, Some("/api/users".to_string()));
assert_eq!(event.action, Some("read".to_string()));
assert!(event.success);
assert_eq!(event.metadata.get("table"), Some(&"users".to_string()));
}
#[test]
fn test_requires_alert_critical() {
let event = SecurityEvent::new(
SecuritySeverity::Critical,
EventCategory::Integrity,
"System compromised",
);
assert!(event.requires_alert());
}
#[test]
fn test_requires_alert_high() {
let event = SecurityEvent::new(
SecuritySeverity::High,
EventCategory::Network,
"Port scan detected",
);
assert!(event.requires_alert());
}
#[test]
fn test_requires_alert_medium_auth_failure() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
let event = SecurityEvent::authentication_failed("user", ip);
assert!(event.requires_alert());
}
#[test]
fn test_requires_alert_low() {
let event = SecurityEvent::new(
SecuritySeverity::Low,
EventCategory::Policy,
"Policy violation",
);
assert!(!event.requires_alert());
}
#[test]
fn test_is_attack() {
let event1 =
SecurityEvent::input_validation_failed("' OR '1'='1", AttackPattern::SqlInjection);
let event2 = SecurityEvent::new(
SecuritySeverity::Info,
EventCategory::Authentication,
"Login successful",
);
assert!(event1.is_attack());
assert!(!event2.is_attack());
}
#[test]
fn test_severity_ordering() {
assert!(SecuritySeverity::Critical > SecuritySeverity::High);
assert!(SecuritySeverity::High > SecuritySeverity::Medium);
assert!(SecuritySeverity::Medium > SecuritySeverity::Low);
assert!(SecuritySeverity::Low > SecuritySeverity::Info);
}
#[test]
fn test_event_serialization() {
let event = SecurityEvent::new(
SecuritySeverity::High,
EventCategory::Authentication,
"Test",
);
let json = serde_json::to_string(&event);
assert!(json.is_ok());
let json_str = json.unwrap();
assert!(json_str.contains("HIGH"));
assert!(json_str.contains("AUTHENTICATION"));
}
}