mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-24 19:00:27 +09:00
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:
parent
dd857a982d
commit
ac8773cdd8
@ -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>,
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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
159
mantis/src/model/alert.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod alert;
|
||||
pub mod config;
|
||||
pub mod direction;
|
||||
pub mod error;
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user