diff --git a/mantis/src/core/ebpf/tcp_anomaly_tracker.rs b/mantis/src/core/ebpf/tcp_anomaly_tracker.rs index 5877270..8e8ae34 100644 --- a/mantis/src/core/ebpf/tcp_anomaly_tracker.rs +++ b/mantis/src/core/ebpf/tcp_anomaly_tracker.rs @@ -6,7 +6,7 @@ use tokio::sync::oneshot; use crate::core::infrastructure::detection_alert::SharedDetectionAlert; use crate::model::error::Error; use crate::model::error::ebpf::EbpfError; -use crate::model::ml_detection::UnifiedAlert; +use crate::model::alert::UnifiedAlert; pub struct TcpAnomalyTracker { ingress_ring_buf: RingBuf, diff --git a/mantis/src/core/infrastructure/detection_alert.rs b/mantis/src/core/infrastructure/detection_alert.rs index cc590c3..d4ddfa0 100644 --- a/mantis/src/core/infrastructure/detection_alert.rs +++ b/mantis/src/core/infrastructure/detection_alert.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use tokio::sync::broadcast; -use crate::model::ml_detection::UnifiedAlert; +use crate::model::alert::UnifiedAlert; pub struct DetectionAlert { broadcast_tx: broadcast::Sender, diff --git a/mantis/src/detection/fusion.rs b/mantis/src/detection/fusion.rs index 13e84a1..1f014b2 100644 --- a/mantis/src/detection/fusion.rs +++ b/mantis/src/detection/fusion.rs @@ -3,7 +3,8 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use crate::core::infrastructure::detection_alert::DetectionAlert; -use crate::model::ml_detection::{DetectionResult, UnifiedAlert}; +use crate::model::alert::UnifiedAlert; +use crate::model::ml_detection::DetectionResult; use crate::model::rule_detection::RuleMatch; #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/mantis/src/model/alert.rs b/mantis/src/model/alert.rs new file mode 100644 index 0000000..d61cefe --- /dev/null +++ b/mantis/src/model/alert.rs @@ -0,0 +1,159 @@ +use common::model::tcp_anomaly::{ + ANOMALY_NULL_SCAN, ANOMALY_RST_SYN, ANOMALY_SYN_FIN, ANOMALY_XMAS_SCAN, DIRECTION_INGRESS, + TcpAnomalyEvent, +}; +use serde::Serialize; + +use crate::model::ml_detection::DetectionResult; +use crate::model::rule_detection::RuleMatch; +use crate::utils::packet_parser::format_ipv4; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AlertSource { + Ml, + Rule, + Fusion, + TcpAnomaly, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AlertSeverity { + /// Single-source detection: either ML or Rule fired alone. + High, + /// Both ML and Rule agreed on the same flow within the fusion window. + Critical, +} + +#[derive(Debug, Clone, Serialize)] +pub struct UnifiedAlert { + pub timestamp: u64, + pub flow_key: String, + pub src_ip: String, + pub dst_ip: String, + pub src_port: u16, + pub dst_port: u16, + pub protocol: u8, + pub source: AlertSource, + pub severity: AlertSeverity, + pub is_attack: bool, + pub attack_type: Option, + pub confidence: f32, + pub ae_score: f32, + pub rule_sid: Option, + pub rule_msg: Option, +} + +fn now_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time is after UNIX_EPOCH") + .as_secs() +} + +fn parse_ip_port(s: &str) -> (String, u16) { + match s.rfind(':') { + Some(pos) => { + let port = s[pos + 1..].parse::().unwrap_or(0); + (s[..pos].to_string(), port) + } + None => (s.to_string(), 0), + } +} + +impl UnifiedAlert { + pub fn from_ml(result: &DetectionResult) -> Self { + Self { + timestamp: now_secs(), + flow_key: result.flow_key.clone(), + src_ip: result.flow_key_raw.src_ip.to_string(), + dst_ip: result.flow_key_raw.dst_ip.to_string(), + src_port: result.flow_key_raw.src_port, + dst_port: result.flow_key_raw.dst_port, + protocol: result.flow_key_raw.protocol, + source: AlertSource::Ml, + severity: AlertSeverity::High, + is_attack: result.is_attack, + attack_type: result.attack_type.clone(), + confidence: result.confidence, + ae_score: result.ae_score, + rule_sid: None, + rule_msg: None, + } + } + + pub fn from_rule(m: &RuleMatch) -> Self { + let (src_ip, src_port) = parse_ip_port(&m.src); + let (dst_ip, dst_port) = parse_ip_port(&m.dst); + let flow_key = format!("{}->{}", m.src, m.dst); + Self { + timestamp: now_secs(), + flow_key, + src_ip, + dst_ip, + src_port, + dst_port, + protocol: 6, + source: AlertSource::Rule, + severity: AlertSeverity::High, + is_attack: true, + attack_type: None, + confidence: 1.0, + ae_score: 0.0, + rule_sid: Some(m.sid), + rule_msg: Some(m.msg.clone()), + } + } + + pub fn from_fusion(result: &DetectionResult, m: &RuleMatch) -> Self { + Self { + timestamp: now_secs(), + flow_key: result.flow_key.clone(), + src_ip: result.flow_key_raw.src_ip.to_string(), + dst_ip: result.flow_key_raw.dst_ip.to_string(), + src_port: result.flow_key_raw.src_port, + dst_port: result.flow_key_raw.dst_port, + protocol: result.flow_key_raw.protocol, + source: AlertSource::Fusion, + severity: AlertSeverity::Critical, + is_attack: result.is_attack, + attack_type: result.attack_type.clone(), + confidence: result.confidence, + ae_score: result.ae_score, + rule_sid: Some(m.sid), + rule_msg: Some(m.msg.clone()), + } + } + + pub fn from_tcp_anomaly(event: &TcpAnomalyEvent) -> Self { + let src_ip = format_ipv4(event.src_ip).to_string(); + let dst_ip = format_ipv4(event.dst_ip).to_string(); + let flow_key = format!("{}:{}-{}:{}", src_ip, event.src_port, dst_ip, event.dst_port); + let direction = if event.direction == DIRECTION_INGRESS { "ingress" } else { "egress" }; + let attack_type = match event.anomaly_type { + ANOMALY_SYN_FIN => format!("TCP SYN+FIN anomaly ({})", direction), + ANOMALY_NULL_SCAN => format!("TCP NULL scan ({})", direction), + ANOMALY_XMAS_SCAN => format!("TCP XMAS scan ({})", direction), + ANOMALY_RST_SYN => format!("TCP RST+SYN anomaly ({})", direction), + _ => format!("TCP flag anomaly ({})", direction), + }; + Self { + timestamp: now_secs(), + flow_key, + src_ip, + dst_ip, + src_port: event.src_port, + dst_port: event.dst_port, + protocol: 6, + source: AlertSource::TcpAnomaly, + severity: AlertSeverity::High, + is_attack: true, + attack_type: Some(attack_type), + confidence: 1.0, + ae_score: 0.0, + rule_sid: None, + rule_msg: None, + } + } +} diff --git a/mantis/src/model/ml_detection.rs b/mantis/src/model/ml_detection.rs index 9091acf..fd697e4 100644 --- a/mantis/src/model/ml_detection.rs +++ b/mantis/src/model/ml_detection.rs @@ -1,13 +1,8 @@ use common::model::event::{Event, TcpFlags}; -use common::model::tcp_anomaly::{ - ANOMALY_NULL_SCAN, ANOMALY_RST_SYN, ANOMALY_SYN_FIN, ANOMALY_XMAS_SCAN, DIRECTION_INGRESS, - TcpAnomalyEvent, -}; use compact_str::CompactString; use serde::{Deserialize, Serialize}; use crate::model::direction::Direction; -use crate::model::rule_detection::RuleMatch; use crate::utils::packet_parser::{format_ipv4, format_ipv6}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -15,6 +10,7 @@ pub struct ClipParams { pub lower: f64, pub upper: f64, } + #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct FlowKey { pub src_ip: CompactString, @@ -124,153 +120,3 @@ impl InferenceStats { pub struct EngineStats { pub active_flows: usize, } - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum AlertSource { - Ml, - Rule, - Fusion, - TcpAnomaly, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum AlertSeverity { - /// Single-source detection: either ML or Rule fired alone. - High, - /// Both ML and Rule agreed on the same flow within the fusion window. - Critical, -} - -#[derive(Debug, Clone, Serialize)] -pub struct UnifiedAlert { - pub timestamp: u64, - pub flow_key: String, - pub src_ip: String, - pub dst_ip: String, - pub src_port: u16, - pub dst_port: u16, - pub protocol: u8, - pub source: AlertSource, - pub severity: AlertSeverity, - pub is_attack: bool, - pub attack_type: Option, - pub confidence: f32, - pub ae_score: f32, - pub rule_sid: Option, - pub rule_msg: Option, -} - -fn now_secs() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("system time is after UNIX_EPOCH") - .as_secs() -} - -fn parse_ip_port(s: &str) -> (String, u16) { - match s.rfind(':') { - Some(pos) => { - let port = s[pos + 1..].parse::().unwrap_or(0); - (s[..pos].to_string(), port) - } - None => (s.to_string(), 0), - } -} - -impl UnifiedAlert { - pub fn from_ml(result: &DetectionResult) -> Self { - Self { - timestamp: now_secs(), - flow_key: result.flow_key.clone(), - src_ip: result.flow_key_raw.src_ip.to_string(), - dst_ip: result.flow_key_raw.dst_ip.to_string(), - src_port: result.flow_key_raw.src_port, - dst_port: result.flow_key_raw.dst_port, - protocol: result.flow_key_raw.protocol, - source: AlertSource::Ml, - severity: AlertSeverity::High, - is_attack: result.is_attack, - attack_type: result.attack_type.clone(), - confidence: result.confidence, - ae_score: result.ae_score, - rule_sid: None, - rule_msg: None, - } - } - - pub fn from_rule(m: &RuleMatch) -> Self { - let (src_ip, src_port) = parse_ip_port(&m.src); - let (dst_ip, dst_port) = parse_ip_port(&m.dst); - let flow_key = format!("{}->{}", m.src, m.dst); - Self { - timestamp: now_secs(), - flow_key, - src_ip, - dst_ip, - src_port, - dst_port, - protocol: 6, - source: AlertSource::Rule, - severity: AlertSeverity::High, - is_attack: true, - attack_type: None, - confidence: 1.0, - ae_score: 0.0, - rule_sid: Some(m.sid), - rule_msg: Some(m.msg.clone()), - } - } - - pub fn from_fusion(result: &DetectionResult, m: &RuleMatch) -> Self { - Self { - timestamp: now_secs(), - flow_key: result.flow_key.clone(), - src_ip: result.flow_key_raw.src_ip.to_string(), - dst_ip: result.flow_key_raw.dst_ip.to_string(), - src_port: result.flow_key_raw.src_port, - dst_port: result.flow_key_raw.dst_port, - protocol: result.flow_key_raw.protocol, - source: AlertSource::Fusion, - severity: AlertSeverity::Critical, - is_attack: result.is_attack, - attack_type: result.attack_type.clone(), - confidence: result.confidence, - ae_score: result.ae_score, - rule_sid: Some(m.sid), - rule_msg: Some(m.msg.clone()), - } - } - - pub fn from_tcp_anomaly(event: &TcpAnomalyEvent) -> Self { - let src_ip = format_ipv4(event.src_ip).to_string(); - let dst_ip = format_ipv4(event.dst_ip).to_string(); - let flow_key = format!("{}:{}-{}:{}", src_ip, event.src_port, dst_ip, event.dst_port); - let direction = if event.direction == DIRECTION_INGRESS { "ingress" } else { "egress" }; - let attack_type = match event.anomaly_type { - ANOMALY_SYN_FIN => format!("TCP SYN+FIN anomaly ({})", direction), - ANOMALY_NULL_SCAN => format!("TCP NULL scan ({})", direction), - ANOMALY_XMAS_SCAN => format!("TCP XMAS scan ({})", direction), - ANOMALY_RST_SYN => format!("TCP RST+SYN anomaly ({})", direction), - _ => format!("TCP flag anomaly ({})", direction), - }; - Self { - timestamp: now_secs(), - flow_key, - src_ip, - dst_ip, - src_port: event.src_port, - dst_port: event.dst_port, - protocol: 6, - source: AlertSource::TcpAnomaly, - severity: AlertSeverity::High, - is_attack: true, - attack_type: Some(attack_type), - confidence: 1.0, - ae_score: 0.0, - rule_sid: None, - rule_msg: None, - } - } -} diff --git a/mantis/src/model/mod.rs b/mantis/src/model/mod.rs index 566f6f0..bd5d3ad 100644 --- a/mantis/src/model/mod.rs +++ b/mantis/src/model/mod.rs @@ -1,3 +1,4 @@ +pub mod alert; pub mod config; pub mod direction; pub mod error; diff --git a/mantis/src/web/websocket/alert_websocket.rs b/mantis/src/web/websocket/alert_websocket.rs index 074f0e4..1a2b33f 100644 --- a/mantis/src/web/websocket/alert_websocket.rs +++ b/mantis/src/web/websocket/alert_websocket.rs @@ -6,7 +6,7 @@ use tokio::sync::broadcast; use crate::model::error::http::HttpError; use crate::model::error::misc::MiscError; use crate::model::log::http::HttpLog; -use crate::model::ml_detection::UnifiedAlert; +use crate::model::alert::UnifiedAlert; pub async fn handle_alert(socket: WebSocket, mut broadcast_rx: broadcast::Receiver) { let (mut sender, mut receiver) = socket.split();