use std::future::Future;
use crate::anomaly::OwnedAnomaly;
use crate::ctx::Ctx;
use crate::error::Result;
use crate::monitor::async_handler::BoxFuture;
use crate::protocol::event_typed::Event;
#[derive(Debug, Default)]
#[must_use = "Effects do nothing unless returned from an EffectHandler"]
pub struct Effects {
pub(crate) anomalies: Vec<OwnedAnomaly>,
}
impl Effects {
pub fn none() -> Self {
Self::default()
}
pub fn emit(anomaly: OwnedAnomaly) -> Self {
Self {
anomalies: vec![anomaly],
}
}
pub fn and_emit(mut self, anomaly: OwnedAnomaly) -> Self {
self.anomalies.push(anomaly);
self
}
pub fn is_empty(&self) -> bool {
self.anomalies.is_empty()
}
pub fn extend(&mut self, other: Effects) {
self.anomalies.extend(other.anomalies);
}
pub(crate) fn apply(self, sink: &mut dyn crate::anomaly::sink::AnomalySink) {
for anomaly in self.anomalies {
apply_owned_anomaly(sink, anomaly);
}
}
}
fn apply_owned_anomaly(sink: &mut dyn crate::anomaly::sink::AnomalySink, a: OwnedAnomaly) {
use std::borrow::Cow;
use std::net::SocketAddr;
let kind: &'static str = match a.kind {
Cow::Borrowed(s) => s,
Cow::Owned(s) => Box::leak(s.into_boxed_str()),
};
let key: Option<flowscope::extract::FiveTupleKey> = match (
a.src_ip,
a.src_port,
a.dest_ip,
a.dest_port,
a.proto.and_then(l4proto_from_str),
) {
(Some(sip), Some(sp), Some(dip), Some(dp), Some(proto)) => {
Some(flowscope::extract::FiveTupleKey {
proto,
a: SocketAddr::new(sip, sp),
b: SocketAddr::new(dip, dp),
})
}
_ => None,
};
let observations: Vec<(&'static str, Cow<'_, str>)> = a
.observations
.iter()
.map(|(l, v)| (*l, Cow::Borrowed(v.as_ref())))
.collect();
let key_dyn: Option<&dyn crate::anomaly::key::Key> =
key.as_ref().map(|k| k as &dyn crate::anomaly::key::Key);
sink.write(
kind,
a.severity.into(),
a.ts,
key_dyn,
&observations,
&a.metrics,
);
}
fn l4proto_from_str(p: &str) -> Option<flowscope::L4Proto> {
use flowscope::L4Proto;
match p {
"tcp" | "TCP" => Some(L4Proto::Tcp),
"udp" | "UDP" => Some(L4Proto::Udp),
"icmp" | "ICMP" => Some(L4Proto::Icmp),
"icmpv6" | "icmp6" | "ipv6-icmp" => Some(L4Proto::IcmpV6),
_ => None,
}
}
pub trait EffectHandler<E: Event>: Send + Sync + 'static {
fn call(&self, payload: &E::Payload, ctx: &Ctx<'_>) -> BoxFuture<Result<Effects>>;
}
impl<E, F, Fut> EffectHandler<E> for F
where
E: Event,
F: Fn(&E::Payload, &Ctx<'_>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Effects>> + Send + 'static,
{
#[inline]
fn call(&self, payload: &E::Payload, ctx: &Ctx<'_>) -> BoxFuture<Result<Effects>> {
Box::pin(self(payload, ctx))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::anomaly::Severity;
use crate::anomaly::sink::NoopSink;
use crate::ctx::{CounterRegistry, SourceIdx, StateMap};
use crate::protocol::builtin::Tcp;
use crate::protocol::event_typed::FlowStarted;
use flowscope::Timestamp;
fn dummy_evt() -> FlowStarted<Tcp> {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let key = flowscope::extract::FiveTupleKey {
proto: flowscope::L4Proto::Tcp,
a: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 1),
b: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 80),
};
FlowStarted::<Tcp>::new(key, Some(flowscope::L4Proto::Tcp), Timestamp::new(0, 0))
}
fn fresh_ctx<'a>(
state: &'a mut StateMap,
sink: &'a mut NoopSink,
counters: &'a mut CounterRegistry,
flow_states: &'a mut crate::ctx::FlowStateRegistry,
) -> Ctx<'a> {
Ctx {
flow: Some(flowscope::extract::FiveTupleKey {
proto: flowscope::L4Proto::Tcp,
a: "10.0.0.1:1".parse().unwrap(),
b: "10.0.0.2:80".parse().unwrap(),
}),
ts: Timestamp::new(0, 0),
source: SourceIdx(0),
monitor_name: None,
state_map: state,
sink,
counters,
flow_states,
label_table: crate::ctx::default_label_table(),
tracker: None,
arp_table: None,
}
}
#[tokio::test(flavor = "current_thread")]
async fn effect_handler_reads_ctx_sync_then_returns_static_future() {
let handler = |_evt: &FlowStarted<Tcp>, ctx: &Ctx<'_>| {
let key = ctx.flow;
let ts = ctx.ts;
async move {
tokio::task::yield_now().await; let mut a = OwnedAnomaly::new("probe", Severity::Info.into(), ts);
if let Some(k) = key {
a = a.with_key(&k);
}
Ok(Effects::emit(a))
}
};
let mut s = StateMap::default();
let mut sink = NoopSink;
let mut c = CounterRegistry::default();
let mut fs = crate::ctx::FlowStateRegistry::default();
let ctx = fresh_ctx(&mut s, &mut sink, &mut c, &mut fs);
let evt = dummy_evt();
let effects = EffectHandler::<FlowStarted<Tcp>>::call(&handler, &evt, &ctx)
.await
.unwrap();
assert_eq!(effects.anomalies.len(), 1);
assert!(!effects.is_empty());
}
#[test]
fn apply_writes_anomalies_to_the_sink_preserving_kind_and_reconstructed_key() {
use crate::anomaly::sink::AnomalySink;
use std::borrow::Cow;
#[derive(Default)]
struct Capturing {
writes: Vec<(&'static str, bool, usize)>, }
impl AnomalySink for Capturing {
fn write(
&mut self,
kind: &'static str,
_severity: Severity,
_ts: Timestamp,
key: Option<&dyn crate::anomaly::key::Key>,
observations: &[(&'static str, Cow<'_, str>)],
_metrics: &[(&'static str, f64)],
) {
self.writes.push((kind, key.is_some(), observations.len()));
}
}
let key = flowscope::extract::FiveTupleKey {
proto: flowscope::L4Proto::Tcp,
a: "10.0.0.1:1234".parse().unwrap(),
b: "10.0.0.2:443".parse().unwrap(),
};
let anomaly =
OwnedAnomaly::new("ioc_match", Severity::Critical.into(), Timestamp::new(0, 0))
.with_key(&key)
.with_observation("ja4", "t13d1516h2");
let mut sink = Capturing::default();
Effects::emit(anomaly).apply(&mut sink);
assert_eq!(sink.writes.len(), 1);
let (kind, has_key, obs) = sink.writes[0];
assert_eq!(kind, "ioc_match");
assert!(
has_key,
"complete 5-tuple should be reconstructed into a key"
);
assert_eq!(obs, 1);
}
#[test]
fn effects_builder_merges_and_reports_empty() {
assert!(Effects::none().is_empty());
let ts = Timestamp::new(0, 0);
let mut e = Effects::emit(OwnedAnomaly::new("a", Severity::Info.into(), ts))
.and_emit(OwnedAnomaly::new("b", Severity::Warning.into(), ts));
assert_eq!(e.anomalies.len(), 2);
e.extend(Effects::emit(OwnedAnomaly::new(
"c",
Severity::Error.into(),
ts,
)));
assert_eq!(e.anomalies.len(), 3);
}
}