mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-24 18:50:28 +09:00
feat: Suricata integration stabilization and config unification
Fix a series of bugs in the Suricata daemon integration and unify configuration so users interact only with config.toml.
This commit is contained in:
parent
f90a80efa0
commit
45fcd7a1bb
16
config.toml
16
config.toml
@ -40,5 +40,17 @@ ml_cpu = 7
|
||||
|
||||
ae_threshold_method = "94"
|
||||
|
||||
# Suricata daemon mode. Remove or comment out to disable the rule engine.
|
||||
suricata_name = "suricata.yaml"
|
||||
# Suricata rule engine. Remove this entire section to disable.
|
||||
[Config.suricata]
|
||||
home_net = "140.130.34.0/24"
|
||||
worker_cpu_set = [4, 6]
|
||||
management_cpu = 0
|
||||
af_packet_threads = "auto"
|
||||
af_packet_ring_size = 2048
|
||||
af_packet_block_size = 131072
|
||||
|
||||
# Suppress known false positives — paste Suricata suppress lines directly.
|
||||
# suppress = [
|
||||
# "suppress gen_id 1, sig_id 2001234",
|
||||
# "suppress gen_id 1, sig_id 2001234, track by_src, ip 192.168.1.0/24",
|
||||
# ]
|
||||
@ -51,12 +51,12 @@ impl EbpfServices {
|
||||
pub async fn run(
|
||||
self: Arc<Self>,
|
||||
ml_engine: Arc<Engine>,
|
||||
suricata_engine: Arc<SuricataEngine>,
|
||||
suricata_engine: Option<Arc<SuricataEngine>>,
|
||||
) -> Result<(), Error> {
|
||||
let xsk_manager = self.xsk_manager.clone();
|
||||
let statistics = self.statistics.clone();
|
||||
|
||||
xsk_manager.run(Some(ml_engine), Some(suricata_engine), &self.shutdowns)?;
|
||||
xsk_manager.run(Some(ml_engine), suricata_engine, &self.shutdowns)?;
|
||||
|
||||
let statistics_shutdown = statistics.run().await;
|
||||
self.shutdowns.push(statistics_shutdown);
|
||||
|
||||
@ -33,7 +33,7 @@ pub struct AppServices {
|
||||
pub fusion_engine: Arc<FusionEngine>,
|
||||
pub ml_models: Arc<MLModels>,
|
||||
pub ml_engine: Arc<Engine>,
|
||||
pub suricata_engine: Arc<SuricataEngine>,
|
||||
pub suricata_engine: Option<Arc<SuricataEngine>>,
|
||||
shutdowns: SegQueue<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
@ -77,14 +77,13 @@ impl AppServices {
|
||||
app_config.ml_cpu,
|
||||
));
|
||||
|
||||
let rule_path = PathBuf::from(env!("RULE_PATH")).join(&app_config.suricata_name);
|
||||
let eve_socket = PathBuf::from(env!("RULE_EVE_PATH"));
|
||||
|
||||
let suricata_engine = SuricataEngine::start(
|
||||
rule_path,
|
||||
eve_socket,
|
||||
fusion_engine.clone(),
|
||||
)?;
|
||||
let suricata_engine = if let Some(ref sc) = app_config.suricata {
|
||||
let rule_path = PathBuf::from(env!("RULE_PATH"));
|
||||
let eve_socket = PathBuf::from(env!("RULE_EVE_PATH"));
|
||||
Some(SuricataEngine::start(sc, &rule_path, &eve_socket, fusion_engine.clone())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
health: Arc::new(health),
|
||||
|
||||
@ -1,20 +1,24 @@
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::mem;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossbeam::channel::{bounded, Sender};
|
||||
use macros::log;
|
||||
|
||||
use crate::detection::fusion::FusionEngine;
|
||||
use crate::model::config::SuricataConfig;
|
||||
use crate::model::error::suricata::SuricataError;
|
||||
use crate::model::log::suricata::SuricataLog;
|
||||
|
||||
use super::output;
|
||||
|
||||
const MIRROR_IFACE: &str = "mantis-mirror";
|
||||
const MIRROR_PEER: &str = "mantis-mirror-peer";
|
||||
const MIRROR_PEER: &str = "mantis-peer";
|
||||
const SURICATA_LOG: &str = "/tmp/suricata.log";
|
||||
|
||||
const CHANNEL_CAP: usize = 4096;
|
||||
|
||||
@ -25,8 +29,9 @@ pub struct SuricataEngine {
|
||||
|
||||
impl SuricataEngine {
|
||||
pub fn start(
|
||||
yaml_path: PathBuf,
|
||||
eve_socket: PathBuf,
|
||||
config: &SuricataConfig,
|
||||
rule_path: &Path,
|
||||
eve_socket: &Path,
|
||||
fusion: Arc<FusionEngine>,
|
||||
) -> Result<Arc<Self>, SuricataError> {
|
||||
Self::setup_veth()?;
|
||||
@ -37,14 +42,90 @@ impl SuricataEngine {
|
||||
output::start_eve_reader(path, fusion);
|
||||
}
|
||||
|
||||
let yaml = yaml_path.to_str()
|
||||
.ok_or_else(|| SuricataError::InvalidPath { path: yaml_path.display().to_string() })?;
|
||||
let rule_path_str = rule_path.to_str()
|
||||
.ok_or_else(|| SuricataError::InvalidPath { path: rule_path.display().to_string() })?;
|
||||
let eve_socket_str = eve_socket.to_str()
|
||||
.ok_or_else(|| SuricataError::InvalidPath { path: eve_socket.display().to_string() })?;
|
||||
|
||||
let suppress = Self::generate_suppress(&config.suppress);
|
||||
let suppress_fd = Self::yaml_to_memfd(&suppress)?;
|
||||
let suppress_path = format!("/proc/self/fd/{}", suppress_fd);
|
||||
|
||||
let yaml = Self::generate_yaml(config, rule_path_str, eve_socket_str, &suppress_path);
|
||||
let config_fd = Self::yaml_to_memfd(&yaml)?;
|
||||
let config_path = format!("/proc/self/fd/{}", config_fd);
|
||||
|
||||
let _ = std::fs::remove_file(SURICATA_LOG);
|
||||
|
||||
let child = Command::new("suricata")
|
||||
.args(["-c", yaml, "-i", MIRROR_PEER, "--runmode=workers"])
|
||||
.args(["-c", &config_path, "-i", MIRROR_PEER, "--runmode=workers"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|e| SuricataError::ProcessSpawnFailed { reason: e.to_string() })?;
|
||||
|
||||
// Child has inherited both fds; close our copies.
|
||||
unsafe { libc::close(suppress_fd) };
|
||||
unsafe { libc::close(config_fd) };
|
||||
|
||||
thread::Builder::new()
|
||||
.name("suricata-log".into())
|
||||
.spawn(|| {
|
||||
let log_path = Path::new(SURICATA_LOG);
|
||||
for _ in 0..100 {
|
||||
if log_path.exists() { break; }
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
let file = match std::fs::File::open(log_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let inotify_fd = unsafe { libc::inotify_init1(libc::IN_CLOEXEC) };
|
||||
if inotify_fd >= 0 {
|
||||
let path_cstr = std::ffi::CString::new(SURICATA_LOG).unwrap();
|
||||
unsafe { libc::inotify_add_watch(inotify_fd, path_cstr.as_ptr(), libc::IN_MODIFY) };
|
||||
}
|
||||
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut line = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
match reader.read_line(&mut line) {
|
||||
Ok(0) => {
|
||||
if inotify_fd >= 0 {
|
||||
// Block until Suricata writes more data.
|
||||
let mut buf = [0u8; 64];
|
||||
let n = unsafe {
|
||||
libc::read(inotify_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
|
||||
};
|
||||
if n <= 0 { break; }
|
||||
} else {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
let trimmed = line.trim_end().to_string();
|
||||
if trimmed.is_empty() { continue; }
|
||||
let lower = trimmed.to_ascii_lowercase();
|
||||
if lower.starts_with("error") || lower.starts_with("critical") {
|
||||
log!(SuricataLog::ProcessError { line: trimmed });
|
||||
} else if lower.starts_with("warn") || lower.starts_with("notice") {
|
||||
log!(SuricataLog::ProcessWarn { line: trimmed });
|
||||
} else {
|
||||
log!(SuricataLog::ProcessInfo { line: trimmed });
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if inotify_fd >= 0 {
|
||||
unsafe { libc::close(inotify_fd) };
|
||||
}
|
||||
})
|
||||
.map_err(|e| SuricataError::ProcessSpawnFailed { reason: e.to_string() })?;
|
||||
|
||||
let (tx, rx) = bounded::<Vec<u8>>(CHANNEL_CAP);
|
||||
|
||||
thread::Builder::new()
|
||||
@ -99,6 +180,162 @@ impl SuricataEngine {
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_suppress(entries: &[String]) -> String {
|
||||
entries.join("\n") + "\n"
|
||||
}
|
||||
|
||||
fn generate_yaml(config: &SuricataConfig, rule_path: &str, eve_socket: &str, suppress_path: &str) -> String {
|
||||
let threading = match (config.worker_cpu_set, config.management_cpu) {
|
||||
(None, None) => r#"threading:
|
||||
set-cpu-affinity: no"#.to_string(),
|
||||
(worker, mgmt) => {
|
||||
let mgmt_cpu = mgmt.unwrap_or(0);
|
||||
let worker_block = match worker {
|
||||
Some([start, end]) => format!(r#" - worker-cpu-set:
|
||||
cpu: [ "{start}-{end}" ]
|
||||
mode: "balanced""#),
|
||||
None => String::new(),
|
||||
};
|
||||
format!(r#"threading:
|
||||
set-cpu-affinity: yes
|
||||
cpu-affinity:
|
||||
- management-cpu-set:
|
||||
cpu: [ {mgmt_cpu} ]
|
||||
{worker_block}"#)
|
||||
}
|
||||
};
|
||||
|
||||
format!(r#"%YAML 1.1
|
||||
---
|
||||
vars:
|
||||
address-groups:
|
||||
HOME_NET: "[{home_net}]"
|
||||
EXTERNAL_NET: "!$HOME_NET"
|
||||
HTTP_SERVERS: "$HOME_NET"
|
||||
SMTP_SERVERS: "$HOME_NET"
|
||||
SQL_SERVERS: "$HOME_NET"
|
||||
DNS_SERVERS: "$HOME_NET"
|
||||
TELNET_SERVERS: "$HOME_NET"
|
||||
AIM_SERVERS: "$EXTERNAL_NET"
|
||||
DC_SERVERS: "$HOME_NET"
|
||||
DNP3_SERVER: "$HOME_NET"
|
||||
DNP3_CLIENT: "$HOME_NET"
|
||||
MODBUS_CLIENT: "$HOME_NET"
|
||||
MODBUS_SERVER: "$HOME_NET"
|
||||
ENIP_CLIENT: "$HOME_NET"
|
||||
ENIP_SERVER: "$HOME_NET"
|
||||
port-groups:
|
||||
HTTP_PORTS: "80"
|
||||
SHELLCODE_PORTS: "!80"
|
||||
ORACLE_PORTS: 1521
|
||||
SSH_PORTS: 22
|
||||
DNP3_PORTS: 20000
|
||||
MODBUS_PORTS: 502
|
||||
FILE_DATA_PORTS: "[$HTTP_PORTS,110,143]"
|
||||
FTP_PORTS: 21
|
||||
VXLAN_PORTS: 4789
|
||||
TEREDO_PORTS: 3544
|
||||
|
||||
default-rule-path: {rule_path}
|
||||
|
||||
rule-files:
|
||||
- "*.rules"
|
||||
|
||||
threshold-file: {suppress_path}
|
||||
|
||||
logging:
|
||||
default-log-level: notice
|
||||
outputs:
|
||||
- console:
|
||||
enabled: no
|
||||
- file:
|
||||
enabled: yes
|
||||
level: info
|
||||
filename: {log_path}
|
||||
|
||||
outputs:
|
||||
- eve-log:
|
||||
enabled: yes
|
||||
filetype: unix_stream
|
||||
filename: {eve_socket}
|
||||
types:
|
||||
- alert:
|
||||
payload: no
|
||||
packet: no
|
||||
metadata: no
|
||||
http-body: no
|
||||
tagged-packets: no
|
||||
- fast:
|
||||
enabled: no
|
||||
- stats:
|
||||
enabled: no
|
||||
|
||||
app-layer:
|
||||
protocols:
|
||||
tls:
|
||||
enabled: yes
|
||||
http:
|
||||
enabled: yes
|
||||
dns:
|
||||
enabled: yes
|
||||
smtp:
|
||||
enabled: yes
|
||||
ssh:
|
||||
enabled: yes
|
||||
|
||||
af-packet:
|
||||
- interface: {iface}
|
||||
threads: {threads}
|
||||
use-mmap: yes
|
||||
tpacket-v3: yes
|
||||
ring-size: {ring_size}
|
||||
block-size: {block_size}
|
||||
|
||||
{threading}
|
||||
|
||||
legacy:
|
||||
uricontent: enabled
|
||||
|
||||
host-mode: sniffer-only
|
||||
"#,
|
||||
home_net = config.home_net,
|
||||
rule_path = rule_path,
|
||||
eve_socket = eve_socket,
|
||||
suppress_path = suppress_path,
|
||||
log_path = SURICATA_LOG,
|
||||
iface = MIRROR_PEER,
|
||||
threads = config.af_packet_threads,
|
||||
ring_size = config.af_packet_ring_size,
|
||||
block_size = config.af_packet_block_size,
|
||||
threading = threading,
|
||||
)
|
||||
}
|
||||
|
||||
fn yaml_to_memfd(yaml: &str) -> Result<i32, SuricataError> {
|
||||
let fd = unsafe {
|
||||
libc::memfd_create(b"suricata-config\0".as_ptr() as *const libc::c_char, 0)
|
||||
};
|
||||
if fd < 0 {
|
||||
let errno = unsafe { *libc::__errno_location() };
|
||||
return Err(SuricataError::MirrorSetupFailed {
|
||||
reason: format!("memfd_create: errno {errno}"),
|
||||
});
|
||||
}
|
||||
let bytes = yaml.as_bytes();
|
||||
let written = unsafe {
|
||||
libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len())
|
||||
};
|
||||
if written < 0 {
|
||||
unsafe { libc::close(fd) };
|
||||
let errno = unsafe { *libc::__errno_location() };
|
||||
return Err(SuricataError::MirrorSetupFailed {
|
||||
reason: format!("memfd write: errno {errno}"),
|
||||
});
|
||||
}
|
||||
unsafe { libc::lseek(fd, 0, libc::SEEK_SET) };
|
||||
Ok(fd)
|
||||
}
|
||||
|
||||
fn setup_veth() -> Result<(), SuricataError> {
|
||||
let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output();
|
||||
|
||||
@ -153,4 +390,4 @@ impl Drop for SuricataEngine {
|
||||
}
|
||||
let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,25 @@ pub struct ConfigTable {
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SuricataConfig {
|
||||
pub home_net: String,
|
||||
pub worker_cpu_set: Option<[u32; 2]>,
|
||||
pub management_cpu: Option<u32>,
|
||||
#[serde(default = "default_af_threads")]
|
||||
pub af_packet_threads: String,
|
||||
#[serde(default = "default_af_ring_size")]
|
||||
pub af_packet_ring_size: u32,
|
||||
#[serde(default = "default_af_block_size")]
|
||||
pub af_packet_block_size: u32,
|
||||
#[serde(default)]
|
||||
pub suppress: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_af_threads() -> String { "auto".to_string() }
|
||||
fn default_af_ring_size() -> u32 { 2048 }
|
||||
fn default_af_block_size() -> u32 { 131072 }
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub ingress_ifname: String,
|
||||
@ -30,35 +49,18 @@ pub struct Config {
|
||||
pub flow_timeout_us: u64,
|
||||
pub traffic_logging_mode: bool,
|
||||
pub traffic_log_csv_path: String,
|
||||
/// Optional path to an NSS key log file (SSLKEYLOGFILE) for TLS decryption.
|
||||
/// Only useful in [external]->Mantis->[internal] deployments where the
|
||||
/// internal server can be configured to write TLS session keys.
|
||||
pub tls_keylog_path: Option<String>,
|
||||
/// CPU core range [start, end] (inclusive) for XSK packet threads.
|
||||
/// Threads are distributed round-robin: core = start + (queue_id % (end - start + 1)).
|
||||
/// Example: [0, 3] with 8 queues spreads 16 threads across cores 0-3 (4 threads each).
|
||||
/// If absent, no affinity is set.
|
||||
pub xsk_cpu_set: Option<[u32; 2]>,
|
||||
/// CPU core pinned to the ML inference spawn_blocking thread.
|
||||
/// If absent, defaults to the last available core.
|
||||
pub ml_cpu: Option<u32>,
|
||||
/// Alert fusion mode: "or" (alert when either source fires) or "and" (require both).
|
||||
/// Defaults to "or" when absent.
|
||||
#[serde(default = "default_fusion_mode")]
|
||||
pub fusion_mode: String,
|
||||
/// Seconds within which both ML and Rule must fire to be correlated as Fusion.
|
||||
/// Only used in "or" (corroboration window) and "and" modes. Defaults to 10.
|
||||
#[serde(default = "default_fusion_window_secs")]
|
||||
pub fusion_window_secs: u64,
|
||||
/// Key into ae_thresholds in inference_config.json that selects the active
|
||||
/// anomaly detection threshold. Valid values: "90".."99", "mean+2std",
|
||||
/// "mean+1std", "Q3+1.5IQR", "Q3+3.0IQR". Defaults to "95" when absent.
|
||||
#[serde(default = "default_ae_threshold_method")]
|
||||
pub ae_threshold_method: String,
|
||||
|
||||
/// Path to suricata.yaml. If absent, Suricata rule engine is disabled.
|
||||
pub suricata_name: String,
|
||||
|
||||
/// Suricata rule engine config. If absent, the rule engine is disabled.
|
||||
pub suricata: Option<SuricataConfig>,
|
||||
}
|
||||
|
||||
fn default_fusion_mode() -> String {
|
||||
@ -71,4 +73,4 @@ fn default_fusion_window_secs() -> u64 {
|
||||
|
||||
fn default_ae_threshold_method() -> String {
|
||||
"95".to_string()
|
||||
}
|
||||
}
|
||||
@ -23,5 +23,14 @@ loggable! {
|
||||
|
||||
#[error("Suricata mirror channel full — packet dropped")]
|
||||
ChannelFull => tracing::Level::WARN,
|
||||
|
||||
#[error("[suricata] {line}")]
|
||||
ProcessInfo { line: String } => tracing::Level::INFO,
|
||||
|
||||
#[error("[suricata] {line}")]
|
||||
ProcessWarn { line: String } => tracing::Level::WARN,
|
||||
|
||||
#[error("[suricata] {line}")]
|
||||
ProcessError { line: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,6 +37,16 @@ rule-files:
|
||||
|
||||
threshold-file: ./mantis/static/rules/suppress.conf
|
||||
|
||||
logging:
|
||||
default-log-level: notice
|
||||
outputs:
|
||||
- console:
|
||||
enabled: no
|
||||
- file:
|
||||
enabled: yes
|
||||
level: info
|
||||
filename: /tmp/suricata.log
|
||||
|
||||
outputs:
|
||||
- eve-log:
|
||||
enabled: yes
|
||||
@ -68,14 +78,20 @@ app-layer:
|
||||
enabled: yes
|
||||
|
||||
af-packet:
|
||||
- interface: mantis-mirror-peer
|
||||
- interface: mantis-peer
|
||||
use-mmap: yes
|
||||
tpacket-v3: yes
|
||||
ring-size: 2048
|
||||
block-size: 131072
|
||||
|
||||
threading:
|
||||
set-cpu-affinity: no
|
||||
set-cpu-affinity: yes
|
||||
cpu-affinity:
|
||||
- management-cpu-set:
|
||||
cpu: [ 0 ]
|
||||
- worker-cpu-set:
|
||||
cpu: [ "4-6" ]
|
||||
mode: "balanced"
|
||||
|
||||
legacy:
|
||||
uricontent: enabled
|
||||
Loading…
x
Reference in New Issue
Block a user