use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnomalySignalType {
PayloadSizeHigh,
PayloadSizeLow,
UnexpectedParam,
MissingExpectedParam,
ParamValueAnomaly,
ContentTypeMismatch,
RateBurst,
ParamCountAnomaly,
HeaderMissingRequired,
HeaderUnexpected,
HeaderValueAnomaly,
HeaderEntropyAnomaly,
HeaderLengthAnomaly,
AbnormalErrorRate,
}
impl AnomalySignalType {
pub fn as_str(&self) -> &'static str {
match self {
Self::PayloadSizeHigh => "payload_size_high",
Self::PayloadSizeLow => "payload_size_low",
Self::UnexpectedParam => "unexpected_param",
Self::MissingExpectedParam => "missing_expected_param",
Self::ParamValueAnomaly => "param_value_anomaly",
Self::ContentTypeMismatch => "content_type_mismatch",
Self::RateBurst => "rate_burst",
Self::ParamCountAnomaly => "param_count_anomaly",
Self::HeaderMissingRequired => "header_missing_required",
Self::HeaderUnexpected => "header_unexpected",
Self::HeaderValueAnomaly => "header_value_anomaly",
Self::HeaderEntropyAnomaly => "header_entropy_anomaly",
Self::HeaderLengthAnomaly => "header_length_anomaly",
Self::AbnormalErrorRate => "abnormal_error_rate",
}
}
pub fn default_severity(&self) -> u8 {
match self {
Self::PayloadSizeHigh => 5,
Self::PayloadSizeLow => 2,
Self::UnexpectedParam => 3,
Self::MissingExpectedParam => 2,
Self::ParamValueAnomaly => 4,
Self::ContentTypeMismatch => 5,
Self::RateBurst => 6,
Self::ParamCountAnomaly => 3,
Self::HeaderMissingRequired => 4, Self::HeaderUnexpected => 2, Self::HeaderValueAnomaly => 5, Self::HeaderEntropyAnomaly => 6, Self::HeaderLengthAnomaly => 4, Self::AbnormalErrorRate => 5, }
}
pub fn default_risk(&self) -> u16 {
match self {
Self::PayloadSizeHigh => 15,
Self::PayloadSizeLow => 5,
Self::UnexpectedParam => 8,
Self::MissingExpectedParam => 5,
Self::ParamValueAnomaly => 12,
Self::ContentTypeMismatch => 15,
Self::RateBurst => 20,
Self::ParamCountAnomaly => 8,
Self::HeaderMissingRequired => 10,
Self::HeaderUnexpected => 5,
Self::HeaderValueAnomaly => 15,
Self::HeaderEntropyAnomaly => 20,
Self::HeaderLengthAnomaly => 10,
Self::AbnormalErrorRate => 15, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalySignal {
pub signal_type: AnomalySignalType,
pub severity: u8,
pub detail: String,
}
impl AnomalySignal {
#[inline]
pub fn new(signal_type: AnomalySignalType, severity: u8, detail: String) -> Self {
Self {
signal_type,
severity: severity.min(10),
detail,
}
}
#[inline]
pub fn with_default_severity(signal_type: AnomalySignalType, detail: String) -> Self {
Self::new(signal_type, signal_type.default_severity(), detail)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnomalyResult {
pub total_score: f64,
pub signals: Vec<AnomalySignal>,
}
impl AnomalyResult {
#[inline]
pub fn none() -> Self {
Self {
total_score: 0.0,
signals: Vec::new(),
}
}
#[inline]
pub fn new() -> Self {
Self {
total_score: 0.0,
signals: Vec::with_capacity(4), }
}
#[inline]
pub fn add(&mut self, signal_type: AnomalySignalType, severity: u8, detail: String) {
self.total_score += severity as f64;
self.signals
.push(AnomalySignal::new(signal_type, severity, detail));
}
#[inline]
pub fn add_signal(&mut self, signal: AnomalySignal) {
self.total_score += signal.severity as f64;
self.signals.push(signal);
}
#[inline]
pub fn has_anomalies(&self) -> bool {
!self.signals.is_empty()
}
#[inline]
pub fn signal_count(&self) -> usize {
self.signals.len()
}
pub fn normalize(&mut self) {
self.total_score = self.total_score.clamp(-10.0, 10.0);
}
pub fn max_severity(&self) -> u8 {
self.signals.iter().map(|s| s.severity).max().unwrap_or(0)
}
pub fn signal_types(&self) -> Vec<AnomalySignalType> {
self.signals.iter().map(|s| s.signal_type).collect()
}
pub fn merge(&mut self, other: AnomalyResult) {
self.total_score += other.total_score;
self.signals.extend(other.signals);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_anomaly_signal_type_as_str() {
assert_eq!(
AnomalySignalType::PayloadSizeHigh.as_str(),
"payload_size_high"
);
assert_eq!(AnomalySignalType::RateBurst.as_str(), "rate_burst");
}
#[test]
fn test_anomaly_signal_creation() {
let signal = AnomalySignal::new(
AnomalySignalType::PayloadSizeHigh,
7,
"Large payload detected".to_string(),
);
assert_eq!(signal.signal_type, AnomalySignalType::PayloadSizeHigh);
assert_eq!(signal.severity, 7);
}
#[test]
fn test_anomaly_signal_severity_clamped() {
let signal = AnomalySignal::new(
AnomalySignalType::RateBurst,
15, "Test".to_string(),
);
assert_eq!(signal.severity, 10); }
#[test]
fn test_anomaly_result_empty() {
let result = AnomalyResult::none();
assert!(!result.has_anomalies());
assert_eq!(result.total_score, 0.0);
assert_eq!(result.signal_count(), 0);
}
#[test]
fn test_anomaly_result_add() {
let mut result = AnomalyResult::new();
result.add(
AnomalySignalType::UnexpectedParam,
3,
"New param".to_string(),
);
result.add(AnomalySignalType::RateBurst, 6, "High rate".to_string());
assert!(result.has_anomalies());
assert_eq!(result.signal_count(), 2);
assert_eq!(result.total_score, 9.0);
}
#[test]
fn test_anomaly_result_normalize() {
let mut result = AnomalyResult::new();
for _ in 0..5 {
result.add(AnomalySignalType::RateBurst, 6, "Test".to_string());
}
assert_eq!(result.total_score, 30.0);
result.normalize();
assert_eq!(result.total_score, 10.0);
}
#[test]
fn test_anomaly_result_max_severity() {
let mut result = AnomalyResult::new();
result.add(AnomalySignalType::PayloadSizeLow, 2, "Small".to_string());
result.add(AnomalySignalType::RateBurst, 8, "Burst".to_string());
result.add(AnomalySignalType::UnexpectedParam, 3, "Param".to_string());
assert_eq!(result.max_severity(), 8);
}
#[test]
fn test_anomaly_result_signal_types() {
let mut result = AnomalyResult::new();
result.add(AnomalySignalType::PayloadSizeHigh, 5, "Test".to_string());
result.add(AnomalySignalType::RateBurst, 6, "Test".to_string());
let types = result.signal_types();
assert!(types.contains(&AnomalySignalType::PayloadSizeHigh));
assert!(types.contains(&AnomalySignalType::RateBurst));
}
#[test]
fn test_anomaly_result_merge() {
let mut result1 = AnomalyResult::new();
result1.add(AnomalySignalType::PayloadSizeHigh, 5, "Test1".to_string());
let mut result2 = AnomalyResult::new();
result2.add(AnomalySignalType::RateBurst, 6, "Test2".to_string());
result1.merge(result2);
assert_eq!(result1.signal_count(), 2);
assert_eq!(result1.total_score, 11.0);
}
#[test]
fn test_default_severity() {
assert_eq!(AnomalySignalType::PayloadSizeHigh.default_severity(), 5);
assert_eq!(AnomalySignalType::RateBurst.default_severity(), 6);
assert_eq!(AnomalySignalType::PayloadSizeLow.default_severity(), 2);
}
#[test]
fn test_signal_with_default_severity() {
let signal = AnomalySignal::with_default_severity(
AnomalySignalType::RateBurst,
"Test burst".to_string(),
);
assert_eq!(signal.severity, 6);
}
#[test]
fn test_header_anomaly_signal_types_as_str() {
assert_eq!(
AnomalySignalType::HeaderMissingRequired.as_str(),
"header_missing_required"
);
assert_eq!(
AnomalySignalType::HeaderUnexpected.as_str(),
"header_unexpected"
);
assert_eq!(
AnomalySignalType::HeaderValueAnomaly.as_str(),
"header_value_anomaly"
);
assert_eq!(
AnomalySignalType::HeaderEntropyAnomaly.as_str(),
"header_entropy_anomaly"
);
assert_eq!(
AnomalySignalType::HeaderLengthAnomaly.as_str(),
"header_length_anomaly"
);
}
#[test]
fn test_header_anomaly_default_severities() {
assert_eq!(
AnomalySignalType::HeaderMissingRequired.default_severity(),
4
);
assert_eq!(AnomalySignalType::HeaderUnexpected.default_severity(), 2);
assert_eq!(AnomalySignalType::HeaderValueAnomaly.default_severity(), 5);
assert_eq!(
AnomalySignalType::HeaderEntropyAnomaly.default_severity(),
6
);
assert_eq!(AnomalySignalType::HeaderLengthAnomaly.default_severity(), 4);
}
#[test]
fn test_header_anomaly_default_risks() {
assert_eq!(AnomalySignalType::HeaderMissingRequired.default_risk(), 10);
assert_eq!(AnomalySignalType::HeaderUnexpected.default_risk(), 5);
assert_eq!(AnomalySignalType::HeaderValueAnomaly.default_risk(), 15);
assert_eq!(AnomalySignalType::HeaderEntropyAnomaly.default_risk(), 20);
assert_eq!(AnomalySignalType::HeaderLengthAnomaly.default_risk(), 10);
}
#[test]
fn test_header_anomaly_in_result() {
let mut result = AnomalyResult::new();
result.add(
AnomalySignalType::HeaderMissingRequired,
4,
"Missing Authorization header".to_string(),
);
result.add(
AnomalySignalType::HeaderEntropyAnomaly,
6,
"High entropy in X-Token".to_string(),
);
assert!(result.has_anomalies());
assert_eq!(result.signal_count(), 2);
assert_eq!(result.total_score, 10.0);
let types = result.signal_types();
assert!(types.contains(&AnomalySignalType::HeaderMissingRequired));
assert!(types.contains(&AnomalySignalType::HeaderEntropyAnomaly));
}
}