use std::sync::{Arc, Mutex};
use std::time::Duration;
use flowscope::analysis::{AnalyzedFlow, FlowAnalyzer};
use flowscope::detect::RiskSeverity;
use crate::anomaly::Severity;
use crate::ctx::Ctx;
use crate::protocol::FlowKey;
pub const DEFAULT_FLOW_ANALYSIS_TTL: Duration = Duration::from_secs(300);
pub const DEFAULT_FLOW_ANALYSIS_MAX_FLOWS: usize = 65_536;
pub type AnalyzedFlowHandler =
Arc<dyn Fn(&AnalyzedFlow<FlowKey>, &mut Ctx<'_>) + Send + Sync + 'static>;
pub(crate) type AnalyzedFlowHandlers = Arc<Mutex<Vec<AnalyzedFlowHandler>>>;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct FlowAnalysisConfig {
pub ttl: Duration,
pub max_flows: usize,
pub emit_risk_anomalies: bool,
pub min_risk_severity: RiskSeverity,
}
impl Default for FlowAnalysisConfig {
fn default() -> Self {
Self {
ttl: DEFAULT_FLOW_ANALYSIS_TTL,
max_flows: DEFAULT_FLOW_ANALYSIS_MAX_FLOWS,
emit_risk_anomalies: true,
min_risk_severity: RiskSeverity::Low,
}
}
}
pub(crate) struct AnalyzerCell {
pub(crate) analyzer: FlowAnalyzer<FlowKey>,
}
impl AnalyzerCell {
pub(crate) fn new(cfg: &FlowAnalysisConfig) -> Self {
Self {
analyzer: FlowAnalyzer::with_capacity(cfg.ttl, cfg.max_flows),
}
}
}
impl Default for AnalyzerCell {
fn default() -> Self {
Self::new(&FlowAnalysisConfig::default())
}
}
pub(crate) fn risk_severity_to_netring(sev: RiskSeverity) -> Severity {
match sev {
RiskSeverity::Low => Severity::Info,
RiskSeverity::Medium => Severity::Warning,
RiskSeverity::High => Severity::Error,
RiskSeverity::Severe => Severity::Critical,
_ => Severity::Error,
}
}
pub(crate) fn emit_analyzed_flow(
af: AnalyzedFlow<FlowKey>,
emit_risk: bool,
min: RiskSeverity,
ctx: &mut Ctx<'_>,
handlers: &[AnalyzedFlowHandler],
) {
if emit_risk
&& let Some(sev) = af.severity()
&& sev >= min
{
let slugs = af.risk.as_slugs().collect::<Vec<_>>().join(",");
ctx.emit("flow_risk", risk_severity_to_netring(sev))
.with_key(&af.key)
.with("risk", slugs)
.with_metric("risk_score", af.score() as f64)
.emit();
}
for h in handlers {
h(&af, ctx);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_ladder_maps_to_netring() {
assert_eq!(risk_severity_to_netring(RiskSeverity::Low), Severity::Info);
assert_eq!(
risk_severity_to_netring(RiskSeverity::Medium),
Severity::Warning
);
assert_eq!(
risk_severity_to_netring(RiskSeverity::High),
Severity::Error
);
assert_eq!(
risk_severity_to_netring(RiskSeverity::Severe),
Severity::Critical
);
}
#[test]
fn default_config_is_sane() {
let c = FlowAnalysisConfig::default();
assert_eq!(c.ttl, DEFAULT_FLOW_ANALYSIS_TTL);
assert_eq!(c.max_flows, DEFAULT_FLOW_ANALYSIS_MAX_FLOWS);
assert!(c.emit_risk_anomalies);
assert_eq!(c.min_risk_severity, RiskSeverity::Low);
}
#[cfg(feature = "tls")]
#[test]
fn obsolete_tls_handshake_finalizes_to_a_risk_flag() {
use flowscope::Timestamp;
use flowscope::extract::FiveTupleKey;
use flowscope::tls::{TlsHandshake, TlsVersion};
use flowscope::{FlowStats, L4Proto};
let mut cell = AnalyzerCell::new(&FlowAnalysisConfig::default());
let key = FiveTupleKey::new(
L4Proto::Tcp,
"10.0.0.1:44321".parse().unwrap(),
"93.184.216.34:443".parse().unwrap(),
);
let mut hs = TlsHandshake::default();
hs.version = Some(TlsVersion::Tls1_0);
let ts = Timestamp::new(1, 0);
cell.analyzer.observe_tls(&key, &hs, ts);
let af = cell.analyzer.finalize(&key, FlowStats::default());
assert!(
af.risk.as_slugs().any(|s| s == "tls_obsolete_version"),
"expected tls_obsolete_version, got {:?}",
af.risk.as_slugs().collect::<Vec<_>>()
);
assert_eq!(
af.severity().map(risk_severity_to_netring),
Some(Severity::Warning)
);
}
}