use std::{net::IpAddr, time::Duration};
use lru::LruCache;
use crate::{
DetectorKind, OwnedAnomaly, Timestamp,
anomaly_fields::KeyFields,
detect::patterns::{BeaconDetector, DgaScorer, PortScanDetector, RitaBeaconDetector},
event::{FlowEvent, FlowStats},
extractor::L4Proto,
history::HistoryString,
};
pub trait Detector<K>: Send {
fn kind(&self) -> DetectorKind;
fn on_flow_start(&mut self, _key: &K, _ts: Timestamp, _out: &mut Vec<OwnedAnomaly>) {}
fn on_flow_established(&mut self, _key: &K, _ts: Timestamp, _out: &mut Vec<OwnedAnomaly>) {}
fn on_flow_end(
&mut self,
_key: &K,
_stats: &FlowStats,
_history: &HistoryString,
_l4: Option<L4Proto>,
_ts: Timestamp,
_out: &mut Vec<OwnedAnomaly>,
) {
}
fn on_flow_tick(
&mut self,
_key: &K,
_stats: &FlowStats,
_ts: Timestamp,
_out: &mut Vec<OwnedAnomaly>,
) {
}
fn on_dns_query(
&mut self,
_key: &K,
_qname: &str,
_ts: Timestamp,
_out: &mut Vec<OwnedAnomaly>,
) {
}
fn tracked(&self) -> usize {
0
}
fn evict_expired(&mut self, _now: Timestamp) {}
}
#[must_use = "a registry does nothing until driven via observe/observe_dns"]
pub struct DetectorRegistry<K> {
detectors: Vec<Box<dyn Detector<K>>>,
}
impl<K: KeyFields> DetectorRegistry<K> {
pub fn new() -> Self {
Self {
detectors: Vec::new(),
}
}
pub fn register(&mut self, detector: impl Detector<K> + 'static) -> &mut Self {
self.detectors.push(Box::new(detector));
self
}
pub fn observe(&mut self, event: &FlowEvent<K>, out: &mut Vec<OwnedAnomaly>) {
match event {
FlowEvent::Started { key, ts, .. } => {
for d in &mut self.detectors {
d.on_flow_start(key, *ts, out);
}
}
FlowEvent::Established { key, ts, .. } => {
for d in &mut self.detectors {
d.on_flow_established(key, *ts, out);
}
}
FlowEvent::Ended {
key,
stats,
history,
l4,
..
} => {
let ts = stats.last_seen;
for d in &mut self.detectors {
d.on_flow_end(key, stats, history, *l4, ts, out);
}
}
FlowEvent::Tick { key, stats, ts } => {
for d in &mut self.detectors {
d.on_flow_tick(key, stats, *ts, out);
}
}
_ => {}
}
}
#[cfg(all(feature = "extractors", feature = "reassembler", feature = "session"))]
pub fn observe_event(&mut self, event: &crate::driver::Event<K>, out: &mut Vec<OwnedAnomaly>) {
use crate::driver::Event;
match event {
Event::Started { key, ts, .. } => {
for d in &mut self.detectors {
d.on_flow_start(key, *ts, out);
}
}
Event::Established { key, ts, .. } => {
for d in &mut self.detectors {
d.on_flow_established(key, *ts, out);
}
}
Event::Ended {
key,
stats,
history,
l4,
ts,
..
} => {
for d in &mut self.detectors {
d.on_flow_end(key, stats, history, *l4, *ts, out);
}
}
Event::Tick { key, stats, ts } => {
for d in &mut self.detectors {
d.on_flow_tick(key, stats, *ts, out);
}
}
_ => {}
}
}
pub fn observe_dns(
&mut self,
key: &K,
qname: &str,
ts: Timestamp,
out: &mut Vec<OwnedAnomaly>,
) {
for d in &mut self.detectors {
d.on_dns_query(key, qname, ts, out);
}
}
pub fn evict_expired(&mut self, now: Timestamp) {
for d in &mut self.detectors {
d.evict_expired(now);
}
}
pub fn tracked(&self) -> usize {
self.detectors.iter().map(|d| d.tracked()).sum()
}
pub fn kinds(&self) -> impl Iterator<Item = DetectorKind> + '_ {
self.detectors.iter().map(|d| d.kind())
}
pub fn len(&self) -> usize {
self.detectors.len()
}
pub fn is_empty(&self) -> bool {
self.detectors.is_empty()
}
}
impl<K: KeyFields> Default for DetectorRegistry<K> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SrcHost(pub IpAddr);
impl SrcHost {
pub fn from_key<K: KeyFields + ?Sized>(key: &K) -> Option<Self> {
key.src_ip().map(SrcHost)
}
}
impl KeyFields for SrcHost {
fn src_ip(&self) -> Option<IpAddr> {
Some(self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HostPair {
pub src: IpAddr,
pub dst: IpAddr,
pub dst_port: Option<u16>,
}
impl HostPair {
pub fn from_key<K: KeyFields + ?Sized>(key: &K) -> Option<Self> {
Some(Self {
src: key.src_ip()?,
dst: key.dest_ip()?,
dst_port: key.dest_port(),
})
}
}
impl KeyFields for HostPair {
fn src_ip(&self) -> Option<IpAddr> {
Some(self.src)
}
fn dest_ip(&self) -> Option<IpAddr> {
Some(self.dst)
}
fn dest_port(&self) -> Option<u16> {
self.dst_port
}
}
fn flow_succeeded(history: &HistoryString, stats: &FlowStats, l4: Option<L4Proto>) -> bool {
match l4 {
Some(L4Proto::Tcp) => history.contains('s'),
_ => stats.packets_responder > 0,
}
}
impl<K: KeyFields + Send> Detector<K> for BeaconDetector<HostPair> {
fn kind(&self) -> DetectorKind {
DetectorKind::BeaconCv
}
fn on_flow_end(
&mut self,
key: &K,
stats: &FlowStats,
_history: &HistoryString,
_l4: Option<L4Proto>,
ts: Timestamp,
out: &mut Vec<OwnedAnomaly>,
) {
let Some(pair) = HostPair::from_key(key) else {
return;
};
if let Some(score) = self.observe_gated(pair, ts, stats.bytes_initiator) {
out.push(score.into_anomaly(ts));
}
}
fn tracked(&self) -> usize {
BeaconDetector::tracked(self)
}
fn evict_expired(&mut self, now: Timestamp) {
self.evict_stale(now, Duration::from_secs(24 * 60 * 60));
}
}
impl<K: KeyFields + Send> Detector<K> for RitaBeaconDetector<HostPair> {
fn kind(&self) -> DetectorKind {
DetectorKind::BeaconRita
}
fn on_flow_end(
&mut self,
key: &K,
stats: &FlowStats,
_history: &HistoryString,
_l4: Option<L4Proto>,
ts: Timestamp,
out: &mut Vec<OwnedAnomaly>,
) {
let Some(pair) = HostPair::from_key(key) else {
return;
};
if let Some(score) = self.observe_gated(pair, ts, stats.bytes_initiator) {
out.push(score.into_anomaly(ts));
}
}
fn tracked(&self) -> usize {
RitaBeaconDetector::tracked(self)
}
fn evict_expired(&mut self, now: Timestamp) {
self.evict_stale(now, Duration::from_secs(24 * 60 * 60));
}
}
impl<K: KeyFields + Send> Detector<K> for PortScanDetector<SrcHost> {
fn kind(&self) -> DetectorKind {
DetectorKind::PortScanTrw
}
fn on_flow_end(
&mut self,
key: &K,
stats: &FlowStats,
history: &HistoryString,
l4: Option<L4Proto>,
ts: Timestamp,
out: &mut Vec<OwnedAnomaly>,
) {
let Some(src) = SrcHost::from_key(key) else {
return;
};
let success = flow_succeeded(history, stats, l4);
let score = self.observe(src, success);
if matches!(score.verdict, crate::detect::patterns::ScanVerdict::Scanner) {
out.push(score.into_anomaly(ts));
}
}
fn tracked(&self) -> usize {
PortScanDetector::tracked(self)
}
}
pub struct DgaDetector {
scorer: DgaScorer,
threshold: f32,
seen: LruCache<(Option<IpAddr>, String), ()>,
}
impl DgaDetector {
pub fn new() -> Self {
Self {
scorer: DgaScorer::new(),
threshold: -3.5,
seen: LruCache::new(std::num::NonZeroUsize::new(4096).unwrap()),
}
}
pub fn with_threshold(mut self, threshold: f32) -> Self {
self.threshold = threshold;
self
}
pub fn with_suppression_capacity(mut self, capacity: usize) -> Self {
self.seen = LruCache::new(std::num::NonZeroUsize::new(capacity.max(1)).unwrap());
self
}
fn sld(qname: &str) -> Option<String> {
let trimmed = qname.trim().trim_end_matches('.');
if trimmed.is_empty() {
return None;
}
let lower = trimmed.to_ascii_lowercase();
let labels: Vec<&str> = lower.split('.').collect();
let sld = match labels.len() {
0 => return None,
1 => labels[0],
n => labels[n - 2],
};
if sld.is_empty() {
None
} else {
Some(sld.to_string())
}
}
}
impl Default for DgaDetector {
fn default() -> Self {
Self::new()
}
}
impl<K: KeyFields + Send> Detector<K> for DgaDetector {
fn kind(&self) -> DetectorKind {
DetectorKind::Dga
}
fn on_dns_query(&mut self, key: &K, qname: &str, ts: Timestamp, out: &mut Vec<OwnedAnomaly>) {
let Some(sld) = Self::sld(qname) else {
return;
};
if !self.scorer.is_dga_with_threshold(&sld, self.threshold) {
return;
}
let suppress_key = (key.src_ip(), sld.clone());
if self.seen.contains(&suppress_key) {
return;
}
self.seen.put(suppress_key, ());
out.push(
self.scorer
.score(&sld)
.into_anomaly(ts, Some(key as &dyn KeyFields))
.with_observation("qname", qname.to_string())
.with_observation("sld", sld),
);
}
fn tracked(&self) -> usize {
self.seen.len()
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct NoIpKey;
impl KeyFields for NoIpKey {}
fn key_a() -> SrcHost {
SrcHost(IpAddr::from(Ipv4Addr::new(10, 0, 0, 1)))
}
#[test]
fn derived_keys_from_keyfields() {
let pair = HostPair {
src: IpAddr::from(Ipv4Addr::new(10, 0, 0, 1)),
dst: IpAddr::from(Ipv4Addr::new(10, 0, 0, 2)),
dst_port: Some(443),
};
assert_eq!(HostPair::from_key(&pair), Some(pair));
assert_eq!(SrcHost::from_key(&pair), Some(SrcHost(pair.src)));
assert_eq!(HostPair::from_key(&NoIpKey), None);
assert_eq!(SrcHost::from_key(&NoIpKey), None);
}
#[test]
fn dga_detector_flags_and_suppresses() {
let mut d = DgaDetector::new();
let mut out = Vec::new();
let key = key_a();
<DgaDetector as Detector<SrcHost>>::on_dns_query(
&mut d,
&key,
"kq3v9z7xj2wq.com",
Timestamp::new(0, 0),
&mut out,
);
assert_eq!(out.len(), 1, "DGA-shaped SLD emits");
assert_eq!(out[0].kind, DetectorKind::Dga);
<DgaDetector as Detector<SrcHost>>::on_dns_query(
&mut d,
&key,
"www.kq3v9z7xj2wq.com",
Timestamp::new(1, 0),
&mut out,
);
assert_eq!(out.len(), 1, "repeat domain suppressed");
<DgaDetector as Detector<SrcHost>>::on_dns_query(
&mut d,
&key,
"www.google.com",
Timestamp::new(2, 0),
&mut out,
);
assert_eq!(out.len(), 1);
}
#[test]
fn flow_succeeded_rules() {
let mut history = HistoryString::new();
let mut stats = FlowStats::default();
history.push_str("S");
assert!(!flow_succeeded(&history, &stats, Some(L4Proto::Tcp)));
let mut refused = HistoryString::new();
refused.push_str("Sr");
assert!(!flow_succeeded(&refused, &stats, Some(L4Proto::Tcp)));
let mut ok = HistoryString::new();
ok.push_str("Ss");
assert!(flow_succeeded(&ok, &stats, Some(L4Proto::Tcp)));
assert!(!flow_succeeded(&history, &stats, Some(L4Proto::Udp)));
stats.packets_responder = 1;
assert!(flow_succeeded(&history, &stats, Some(L4Proto::Udp)));
}
}