Extract UnifiedAlert/AlertSource/AlertSeverity to model/alert.rs

These types are shared across ML, rule, fusion, and now TCP anomaly
detection — they do not belong in ml_detection.rs. Move them to a
dedicated src/model/alert.rs and update all import sites accordingly.
ml_detection.rs now contains only ML-pipeline-specific types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138PxtKH73hqxv7h1oaoSdS
This commit is contained in:
Claude 2026-06-21 06:12:14 +00:00
parent dd857a982d
commit ac8773cdd8
No known key found for this signature in database
7 changed files with 166 additions and 159 deletions

View File

@ -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<MapData>,

View File

@ -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<UnifiedAlert>,

View File

@ -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)]

159
mantis/src/model/alert.rs Normal file
View File

@ -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<String>,
pub confidence: f32,
pub ae_score: f32,
pub rule_sid: Option<u32>,
pub rule_msg: Option<String>,
}
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::<u16>().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,
}
}
}

View File

@ -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<String>,
pub confidence: f32,
pub ae_score: f32,
pub rule_sid: Option<u32>,
pub rule_msg: Option<String>,
}
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::<u16>().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,
}
}
}

View File

@ -1,3 +1,4 @@
pub mod alert;
pub mod config;
pub mod direction;
pub mod error;

View File

@ -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<UnifiedAlert>) {
let (mut sender, mut receiver) = socket.split();