use std::{borrow::Cow, net::IpAddr};
use smallvec::SmallVec;
use crate::{
Timestamp,
anomaly_fields::KeyFields,
event::{AnomalyKind, Severity},
};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct OwnedAnomaly {
pub kind: Cow<'static, str>,
pub severity: Severity,
pub ts: Timestamp,
pub src_ip: Option<IpAddr>,
pub src_port: Option<u16>,
pub dest_ip: Option<IpAddr>,
pub dest_port: Option<u16>,
pub proto: Option<&'static str>,
pub observations: SmallVec<[(&'static str, Cow<'static, str>); 4]>,
pub metrics: SmallVec<[(&'static str, f64); 4]>,
pub flowscope_kind: Option<AnomalyKind>,
}
impl OwnedAnomaly {
pub fn new(kind: impl Into<Cow<'static, str>>, severity: Severity, ts: Timestamp) -> Self {
Self {
kind: kind.into(),
severity,
ts,
src_ip: None,
src_port: None,
dest_ip: None,
dest_port: None,
proto: None,
observations: SmallVec::new(),
metrics: SmallVec::new(),
flowscope_kind: None,
}
}
pub fn with_key<K: KeyFields + ?Sized>(mut self, key: &K) -> Self {
self.src_ip = key.src_ip();
self.src_port = key.src_port();
self.dest_ip = key.dest_ip();
self.dest_port = key.dest_port();
if let Some(p) = key.proto_str() {
self.proto = Some(p);
}
self
}
pub fn with_observation(
mut self,
label: &'static str,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.observations.push((label, value.into()));
self
}
pub fn with_metric(mut self, label: &'static str, value: f64) -> Self {
self.metrics.push((label, value));
self
}
pub fn from_flow_anomaly<K: KeyFields>(key: &K, kind: AnomalyKind, ts: Timestamp) -> Self {
let slug = kind.short_kind();
let severity = kind.severity();
Self {
kind: Cow::Borrowed(slug),
severity,
ts,
src_ip: key.src_ip(),
src_port: key.src_port(),
dest_ip: key.dest_ip(),
dest_port: key.dest_port(),
proto: key.proto_str(),
observations: SmallVec::new(),
metrics: SmallVec::new(),
flowscope_kind: Some(kind),
}
}
}
pub trait DetectorScore {
fn name(&self) -> &'static str;
fn into_anomaly(self, ts: Timestamp) -> OwnedAnomaly;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Severity;
#[test]
fn new_default_fields_are_empty() {
let a = OwnedAnomaly::new("Test", Severity::Info, Timestamp::new(100, 0));
assert_eq!(a.kind, "Test");
assert_eq!(a.severity, Severity::Info);
assert!(a.src_ip.is_none());
assert!(a.observations.is_empty());
assert!(a.metrics.is_empty());
assert!(a.flowscope_kind.is_none());
}
#[test]
fn smallvec_observations_stay_inline_under_4() {
let a = OwnedAnomaly::new("Test", Severity::Info, Timestamp::new(0, 0))
.with_observation("a", "1")
.with_observation("b", "2")
.with_observation("c", "3")
.with_observation("d", "4");
assert_eq!(a.observations.len(), 4);
assert!(!a.observations.spilled(), "4 observations stay inline");
}
#[test]
fn smallvec_observations_spill_above_4() {
let mut a = OwnedAnomaly::new("Test", Severity::Info, Timestamp::new(0, 0));
for i in 0..6 {
a.observations
.push(("test_label", Cow::Owned(format!("{i}"))));
}
assert_eq!(a.observations.len(), 6);
assert!(a.observations.spilled(), "6 observations spill to heap");
}
#[test]
fn with_metric_appends() {
let a = OwnedAnomaly::new("Test", Severity::Info, Timestamp::new(0, 0))
.with_metric("score", 0.87)
.with_metric("n_observed", 42.0);
assert_eq!(a.metrics.len(), 2);
assert_eq!(a.metrics[0], ("score", 0.87));
assert_eq!(a.metrics[1], ("n_observed", 42.0));
}
#[cfg(feature = "extractors")]
#[test]
fn with_key_flattens_5tuple_from_fivetuple_key() {
use std::net::{Ipv4Addr, SocketAddr};
use crate::{extract::FiveTupleKey, extractor::L4Proto};
let key = FiveTupleKey {
proto: L4Proto::Tcp,
a: SocketAddr::from((Ipv4Addr::new(10, 0, 0, 1), 33000)),
b: SocketAddr::from((Ipv4Addr::new(10, 0, 0, 2), 80)),
};
let a = OwnedAnomaly::new("Test", Severity::Info, Timestamp::new(0, 0)).with_key(&key);
assert_eq!(a.src_ip, Some(IpAddr::from(Ipv4Addr::new(10, 0, 0, 1))));
assert_eq!(a.src_port, Some(33000));
assert_eq!(a.dest_ip, Some(IpAddr::from(Ipv4Addr::new(10, 0, 0, 2))));
assert_eq!(a.dest_port, Some(80));
assert_eq!(a.proto, Some("TCP"));
}
#[cfg(feature = "extractors")]
#[test]
fn from_flow_anomaly_retains_typed_kind() {
use std::net::{Ipv4Addr, SocketAddr};
use crate::{
event::{AnomalyKind, FlowSide},
extract::FiveTupleKey,
extractor::L4Proto,
};
let key = FiveTupleKey {
proto: L4Proto::Tcp,
a: SocketAddr::from((Ipv4Addr::new(10, 0, 0, 1), 33000)),
b: SocketAddr::from((Ipv4Addr::new(10, 0, 0, 2), 80)),
};
let kind = AnomalyKind::OutOfOrderSegment {
side: FlowSide::Initiator,
count: 3,
};
let a = OwnedAnomaly::from_flow_anomaly(&key, kind.clone(), Timestamp::new(0, 0));
assert_eq!(a.kind, "ooo_segment");
assert!(matches!(
a.flowscope_kind,
Some(AnomalyKind::OutOfOrderSegment { count: 3, .. })
));
assert_eq!(a.src_ip, Some(IpAddr::from(Ipv4Addr::new(10, 0, 0, 1))));
}
}