refactor: move domain model types from core/infra/interface to model/ directory

Move SOAR playbook data types, report data types, ML drift/flow feature
types, event types, GeoLocation, RateLimitSettings, and ReadinessState
into their proper model/ subdirectories. Original locations re-export
from model to preserve backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-03-31 23:51:28 +08:00
parent bdd40bcf3a
commit 84d3f52cd9
34 changed files with 529 additions and 506 deletions

View File

@ -1,7 +1,8 @@
use actix_web::{HttpResponse, Responder, Scope, web};
use common::define::setting::*;
use crate::core::rate_limit_service::{RateLimitService, RateLimitSettings};
use crate::core::rate_limit_service::RateLimitService;
use crate::model::system::rate_limit_settings::RateLimitSettings;
pub fn initialize() -> Scope {
web::scope("/rate-limit")

View File

@ -2,7 +2,8 @@ use actix_web::{HttpResponse, Scope, web};
use serde::Deserialize;
use crate::core::auth::extractor::AuthClaims;
use crate::core::playbook_service::{CreatePlaybookInput, PlaybookService};
use crate::core::playbook_service::PlaybookService;
use crate::model::soar::playbook_data::CreatePlaybookInput;
#[derive(Deserialize)]
struct CreatePlaybookRequest {

View File

@ -3,7 +3,7 @@ use std::path::PathBuf;
use crate::model::error::ml::MLError;
pub use crate::model::config::MLInferenceConfig;
use crate::model::config::MLInferenceConfig;
/// Backward-compatible alias so existing `use config_loader::InferenceConfig` paths still compile.
pub type InferenceConfig = MLInferenceConfig;

View File

@ -1,40 +1,10 @@
use std::collections::VecDeque;
use std::time::{Duration, Instant};
use crate::model::system::config::MLInferenceConfig;
use crate::model::detection::drift::{DriftReport, FeatureBaselines};
const WINDOW_DURATION: Duration = Duration::from_secs(3600); // 1 hour
/// Baselines loaded from the inference config (scaler mean / std).
/// If inference_config has no scaler data, drift detection is disabled.
pub struct FeatureBaselines {
pub names: Vec<String>,
pub means: Vec<f64>,
pub stds: Vec<f64>,
}
impl FeatureBaselines {
/// Build baselines from the ML inference config.
/// Returns `None` if the config has no features (drift detection disabled).
pub fn from_inference_config(config: &MLInferenceConfig) -> Option<Self> {
if config.ae_feature_names.is_empty() {
return None;
}
Some(Self {
names: config.ae_feature_names.clone(),
means: config.ae_scaler_mean.clone(),
stds: config.ae_scaler_std.clone(),
})
}
}
/// Report emitted when feature drift is detected.
#[derive(Debug, Clone)]
pub struct DriftReport {
pub drifted_features: Vec<String>,
pub max_deviation: f64,
}
/// Tracks rolling mean/stddev of normalized input features over a 1-hour window.
/// Compares against training-time baselines to detect data drift.
pub struct DriftDetector {

View File

@ -9,11 +9,11 @@ use tokio::time::interval;
use super::aggregator::AttackAggregator;
use super::config_loader::InferenceConfig;
use super::drift_detector::DriftDetector;
use super::feature_extractor::FlowFeatures;
use super::flow_tracker::{FlowData, FlowTracker};
use super::inference::Inference;
use super::model_loader::MLModels;
use super::traffic_logger::TrafficLogger;
use crate::model::detection::flow_features::FlowFeatures;
use super::alert::MLAlert;
use crate::model::log::ml::MLLog;

View File

@ -1,15 +1,9 @@
use std::collections::HashMap;
use common::define::tcp_flags::*;
use super::flow_tracker::FlowData;
use crate::model::ml_detection::{ClipParams, PacketData};
use crate::model::ml_detection::PacketData;
#[derive(Debug, Clone)]
pub struct FlowFeatures {
pub features: Vec<f64>,
pub feature_num: usize,
}
use crate::model::detection::flow_features::FlowFeatures;
impl FlowFeatures {
pub fn extract(flow: &FlowData, feature_names: &[String]) -> Self {
@ -24,133 +18,6 @@ impl FlowFeatures {
Self { features, feature_num }
}
pub fn normalize(&mut self, means: &[f64], stds: &[f64]) {
for i in 0..self.feature_num {
if stds[i] > 0.0 {
self.features[i] = (self.features[i] - means[i]) / stds[i];
} else {
self.features[i] = 0.0;
}
}
}
pub fn clip(&mut self, clip_min: f64, clip_max: f64) {
for i in 0..self.feature_num {
self.features[i] = self.features[i].max(clip_min).min(clip_max);
}
}
pub fn winsorize(&mut self, clip_params: &HashMap<String, ClipParams>, feature_names: &[String]) {
for (i, feature_name) in feature_names.iter().enumerate() {
if i < self.feature_num
&& let Some(params) = clip_params.get(feature_name)
{
self.features[i] = self.features[i].clamp(params.lower, params.upper);
}
}
}
pub fn all_feature_names() -> Vec<&'static str> {
vec![
"Destination Port",
"Protocol",
"Flow Duration",
"Total Fwd Packets",
"Total Backward Packets",
"Total Length of Fwd Packets",
"Total Length of Bwd Packets",
"Fwd Packet Length Max",
"Fwd Packet Length Min",
"Fwd Packet Length Mean",
"Fwd Packet Length Std",
"Bwd Packet Length Max",
"Bwd Packet Length Min",
"Bwd Packet Length Mean",
"Bwd Packet Length Std",
"Flow Bytes/s",
"Flow Packets/s",
"Flow IAT Mean",
"Flow IAT Std",
"Flow IAT Max",
"Flow IAT Min",
"Fwd IAT Total",
"Fwd IAT Mean",
"Fwd IAT Std",
"Fwd IAT Max",
"Fwd IAT Min",
"Bwd IAT Total",
"Bwd IAT Mean",
"Bwd IAT Std",
"Bwd IAT Max",
"Bwd IAT Min",
"Fwd PSH Flags",
"Bwd PSH Flags",
"Fwd URG Flags",
"Bwd URG Flags",
"Fwd Header Length",
"Bwd Header Length",
"Fwd Packets/s",
"Bwd Packets/s",
"Min Packet Length",
"Max Packet Length",
"Packet Length Mean",
"Packet Length Std",
"Packet Length Variance",
"FIN Flag Count",
"SYN Flag Count",
"RST Flag Count",
"PSH Flag Count",
"ACK Flag Count",
"URG Flag Count",
"CWE Flag Count",
"ECE Flag Count",
"Down/Up Ratio",
"Average Packet Size",
"Avg Fwd Segment Size",
"Avg Bwd Segment Size",
"Fwd Header Length.1",
"Fwd Avg Bytes/Bulk",
"Fwd Avg Packets/Bulk",
"Fwd Avg Bulk Rate",
"Bwd Avg Bytes/Bulk",
"Bwd Avg Packets/Bulk",
"Bwd Avg Bulk Rate",
"Subflow Fwd Packets",
"Subflow Fwd Bytes",
"Subflow Bwd Packets",
"Subflow Bwd Bytes",
"Init_Win_bytes_forward",
"Init_Win_bytes_backward",
"act_data_pkt_fwd",
"min_seg_size_forward",
"Active Mean",
"Active Std",
"Active Max",
"Active Min",
"Idle Mean",
"Idle Std",
"Idle Max",
"Idle Min",
// Phase 2: new features
"fwd_iat_std",
"bwd_iat_std",
"flow_iat_std",
"fwd_bwd_bytes_ratio",
"pkt_len_variance",
"fwd_iat_skewness",
]
}
pub fn all_feature_names_owned() -> Vec<String> {
Self::all_feature_names().iter().map(|s| s.to_string()).collect()
}
pub fn to_csv_record(&self) -> Vec<String> {
let mut record: Vec<String> = self.features.iter().map(|f| f.to_string()).collect();
record.push("BENIGN".to_string());
record
}
}
/// All statistics pre-computed once from a FlowData, then looked up by feature name.

View File

@ -4,9 +4,9 @@ use macros::log;
use tract_onnx::prelude::*;
use super::config_loader::InferenceConfig;
use super::feature_extractor::FlowFeatures;
use super::flow_tracker::FlowData;
use super::model_loader::MLModels;
use crate::model::detection::flow_features::FlowFeatures;
use crate::model::log::ml::MLLog;
use crate::model::ml_detection::DetectionResult;

View File

@ -7,6 +7,7 @@ use macros::log;
use crate::model::error::Error;
use crate::model::error::soar::SoarError;
use crate::model::soar::playbook_data::*;
/// Domain service for SOAR playbook CRUD operations.
/// Coordinates DB reads/writes, SOAR engine cache refresh, and eBPF unblock.
@ -16,55 +17,6 @@ pub struct PlaybookService {
access_control: Arc<dyn AccessControlPort>,
}
/// Input for creating a new playbook.
pub struct CreatePlaybookInput {
pub name: String,
pub trigger_event: String,
pub condition_threshold: Option<f64>,
pub condition_count: Option<i64>,
pub condition_window_secs: Option<i64>,
pub cooldown_secs: i64,
pub actions: Vec<(String, String)>, // (action_type, params_json)
}
/// Flattened playbook representation for API responses.
pub struct PlaybookData {
pub id: i64,
pub name: String,
pub enabled: bool,
pub trigger_event: String,
pub condition_threshold: Option<f64>,
pub condition_count: Option<i64>,
pub condition_window_secs: Option<i64>,
pub cooldown_secs: i64,
pub actions: Vec<ActionData>,
}
pub struct ActionData {
pub id: i64,
pub action_order: i64,
pub action_type: String,
pub params: serde_json::Value,
}
/// Execution record from soar_executions table.
pub struct ExecutionData {
pub id: i64,
pub playbook_id: i64,
pub source_ip: Option<String>,
pub trigger_event: String,
pub actions_executed: serde_json::Value,
pub created_at: String,
}
/// Active block record from soar_block_rules table.
pub struct ActiveBlockData {
pub id: i64,
pub source_ip: String,
pub playbook_id: i64,
pub expires_at: String,
}
impl PlaybookService {
pub fn new(db: Arc<Database>, soar_engine: Arc<SoarEngine>, access_control: Arc<dyn AccessControlPort>) -> Self {
Self {

View File

@ -44,13 +44,4 @@ impl RateLimitService {
}
}
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct RateLimitSettings {
pub packet_rate: Option<u64>,
pub syn_rate: Option<u64>,
pub udp_rate: Option<u64>,
pub dns_rate: Option<u64>,
pub window_ns: Option<u64>,
}
use crate::model::system::rate_limit_settings::RateLimitSettings;

View File

@ -1,182 +1 @@
use serde::{Deserialize, Serialize};
use crate::interface::port::repository::RepositoryPort;
use crate::model::error::Error;
/// Shared report data structure used by both HTML email and PDF report.
#[derive(Debug, Clone, Serialize)]
pub struct ReportData {
pub period: String,
pub generated_at: String,
pub executive_summary: ExecutiveSummary,
pub threat_breakdown: Vec<ThreatBreakdownItem>,
pub top_blocked_ips: Vec<BlockedIpItem>,
pub geo_distribution: Vec<GeoItem>,
pub soar_activity: SoarActivity,
pub system_health: SystemHealthSummary,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExecutiveSummary {
pub total_threats: u64,
pub total_blocked: u64,
pub uptime_percent: f64,
pub active_rules: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ThreatBreakdownItem {
pub threat_type: String,
pub count: u64,
pub trend: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockedIpItem {
pub ip: String,
pub count: u64,
pub country: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoItem {
pub country: String,
pub threat_count: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct SoarActivity {
pub auto_blocks_executed: u64,
pub playbooks_triggered: u64,
pub auto_unblocks: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemHealthSummary {
pub avg_cpu_percent: f64,
pub avg_memory_percent: f64,
pub disk_usage_percent: f64,
pub ebpf_status: String,
}
impl ReportData {
/// Build report data from database settings (aggregated by the ML pipeline).
pub fn from_database(db: &dyn RepositoryPort) -> Result<Self, Error> {
let now = chrono::Local::now();
let period = format!(
"{} — {}",
(now - chrono::Duration::days(7)).format("%Y-%m-%d"),
now.format("%Y-%m-%d")
);
let threats_count: u64 = db
.get_setting("weekly_threats_count")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let top_ips: Vec<BlockedIpItem> = db
.get_setting("weekly_top_ips")?
.and_then(|v| serde_json::from_str(&v).ok())
.unwrap_or_else(|| {
vec![BlockedIpItem {
ip: "".into(),
count: 0,
country: "N/A".into(),
}]
});
let breakdown: Vec<ThreatBreakdownItem> = db
.get_setting("weekly_threat_breakdown")?
.and_then(|v| {
let obj: serde_json::Value = serde_json::from_str(&v).ok()?;
let items = obj
.as_object()?
.iter()
.map(|(k, v)| ThreatBreakdownItem {
threat_type: k.clone(),
count: v.as_u64().unwrap_or(0),
trend: "".into(),
})
.collect();
Some(items)
})
.unwrap_or_default();
let health: SystemHealthSummary = db
.get_setting("weekly_system_health")?
.and_then(|v| serde_json::from_str(&v).ok())
.unwrap_or(SystemHealthSummary {
avg_cpu_percent: 0.0,
avg_memory_percent: 0.0,
disk_usage_percent: 0.0,
ebpf_status: "running".into(),
});
// Generate recommendations based on data
let mut recommendations = Vec::new();
if threats_count > 10 {
recommendations.push("Consider enabling geo-blocking for high-risk regions".into());
}
if breakdown.iter().any(|b| b.threat_type == "port_scan" && b.count > 50) {
recommendations.push("Review exposed ports and consider tightening protocol filter rules".into());
}
if recommendations.is_empty() {
recommendations.push("No action needed — your network security posture is healthy".into());
}
let uptime_percent: f64 = db
.get_setting("system_uptime_percent")?
.and_then(|v| v.parse().ok())
.unwrap_or(0.0);
let active_rules: u64 = db
.get_setting("active_rules_count")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let geo_distribution: Vec<GeoItem> = db
.get_setting("weekly_geo_distribution")?
.and_then(|v| serde_json::from_str(&v).ok())
.unwrap_or_default();
let auto_blocks: u64 = db
.get_setting("weekly_soar_blocks")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let playbooks_triggered: u64 = db
.get_setting("weekly_soar_triggers")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let auto_unblocks: u64 = db
.get_setting("weekly_soar_unblocks")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let blocked_count: u64 = db
.get_setting("weekly_blocked_count")?
.and_then(|v| v.parse().ok())
.unwrap_or(auto_blocks);
Ok(ReportData {
period,
generated_at: now.format("%Y-%m-%d %H:%M:%S").to_string(),
executive_summary: ExecutiveSummary {
total_threats: threats_count,
total_blocked: blocked_count,
uptime_percent,
active_rules,
},
threat_breakdown: breakdown,
top_blocked_ips: top_ips,
geo_distribution,
soar_activity: SoarActivity {
auto_blocks_executed: auto_blocks,
playbooks_triggered,
auto_unblocks,
},
system_health: health,
recommendations,
})
}
}
// Types are available via crate::model::report::data

View File

@ -1,10 +1,10 @@
use std::path::PathBuf;
use tracing::info;
use crate::core::report::data::ReportData;
use crate::interface::port::repository::RepositoryPort;
use crate::model::error::Error;
use crate::model::error::notification::NotificationError;
use crate::model::report::data::ReportData;
/// Generate a self-contained HTML security report and write to disk.
/// Returns the path to the generated HTML file.

View File

@ -11,11 +11,11 @@ use crate::adapter::persistence::Database;
use crate::core::ebpf::rate_limit::RateLimitConfig;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::infrastructure::geoip::GeoIpService;
use crate::interface::communication::event_types::ThreatDetectedEvent;
use crate::interface::port::access_control::AccessControlPort;
use crate::interface::port::notification::{AlertNotifier, AlertPayload};
use crate::model::error::Error;
use crate::model::error::soar::SoarError;
use crate::model::event::ThreatDetectedEvent;
use crate::model::log::soar::SoarLog;
use crate::model::soar::playbook::{Playbook, PlaybookAction};

View File

@ -23,14 +23,15 @@ use crate::infrastructure::app_services::AppServices;
use crate::infrastructure::audit_logger::AuditLogger;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::infrastructure::geoip::GeoIpService;
use crate::infrastructure::http_server::{HttpServerParams, ReadinessState};
use crate::infrastructure::http_server::HttpServerParams;
use crate::infrastructure::service_factory::ServiceFactory;
use crate::interface::communication::event_types::{DriftDetectedEvent, ThreatDetectedEvent};
use crate::model::error::Error;
use crate::model::error::system::SystemError;
use crate::model::event::{DriftDetectedEvent, ThreatDetectedEvent};
use crate::model::log::ml::MLLog;
use crate::model::log::system::SystemLog;
use crate::model::ml_detection::AlertMessage;
use crate::model::system::readiness::ReadinessState;
/// Orchestrates system lifecycle: startup ordering and shutdown.
/// Construction is delegated to `ServiceFactory::build()`.

View File

@ -9,12 +9,12 @@ use crate::core::ml::alert::MLAlert;
use crate::core::ml::config_loader::InferenceConfig;
use crate::core::ml::drift_detector::DriftDetector;
use crate::core::ml::engine::Engine;
use crate::core::ml::feature_extractor::FlowFeatures;
use crate::core::ml::model_loader::MLModels;
use crate::core::ml::traffic_logger::TrafficLogger;
use crate::infrastructure::app_config::AppConfig;
use crate::infrastructure::health::SystemHealth;
use crate::infrastructure::statistics::FlowStatistics;
use crate::model::detection::flow_features::FlowFeatures;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
use crate::model::error::system::SystemError;

View File

@ -4,7 +4,7 @@ use macros::log;
use crate::adapter::persistence::Database;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::interface::communication::event_types::{AuditEvent, DriftDetectedEvent};
use crate::model::event::{AuditEvent, DriftDetectedEvent};
use crate::model::log::system::SystemLog;
/// Subscribes to `AuditEvent` and persists each entry to the `audit_log` table.

View File

@ -7,11 +7,11 @@ use macros::log;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::interface::communication::command::CommandHandler;
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
use crate::interface::communication::event_types::AuditEvent;
use crate::interface::communication::query::QueryHandler;
use crate::interface::communication::query_types::GetEnforceModeQuery;
use crate::interface::port::repository::RepositoryPort;
use crate::model::error::Error;
use crate::model::event::AuditEvent;
use crate::model::log::system::SystemLog;
/// Map enforce-mode string to u8: monitor=0, ml_only=1, enforce=2.
@ -85,7 +85,7 @@ mod tests {
let db = Arc::new(Database::new(":memory:").unwrap()) as Arc<dyn RepositoryPort>;
let cache = Arc::new(AtomicU8::new(0));
let comm = Arc::new(CommunicationManager::new());
comm.register_event_type::<crate::interface::communication::event_types::AuditEvent>();
comm.register_event_type::<crate::model::event::AuditEvent>();
let handler = Arc::new(EnforceModeHandler::new(db, comm.clone(), cache));
let _ = comm
.clone()

View File

@ -8,18 +8,9 @@ use std::num::NonZeroUsize;
use tokio::sync::RwLock;
use tokio::task;
use crate::model::monitoring::geolocation::GeoLocation;
use crate::utils::ip_address;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GeoLocation {
pub country: Option<String>,
pub country_code: Option<String>,
pub city: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub timezone: Option<String>,
}
pub struct GeoIpService {
reader: Arc<Reader<Vec<u8>>>,
cache: Arc<RwLock<LruCache<IpAddr, Option<GeoLocation>>>>,

View File

@ -31,26 +31,7 @@ use macros::log;
/// Shared flag: true when all services (eBPF, ML, SOAR) are fully initialized.
pub type ReadyFlag = Arc<std::sync::atomic::AtomicBool>;
/// Per-subsystem readiness state exposed by `/health/ready`.
pub struct ReadinessState {
pub db_connected: std::sync::atomic::AtomicBool,
pub ml_model_loaded: std::sync::atomic::AtomicBool,
pub soar_engine_running: std::sync::atomic::AtomicBool,
pub ebpf_attached: std::sync::atomic::AtomicBool,
pub started_at: std::time::Instant,
}
impl ReadinessState {
pub fn new() -> Self {
Self {
db_connected: std::sync::atomic::AtomicBool::new(false),
ml_model_loaded: std::sync::atomic::AtomicBool::new(false),
soar_engine_running: std::sync::atomic::AtomicBool::new(false),
ebpf_attached: std::sync::atomic::AtomicBool::new(false),
started_at: std::time::Instant::now(),
}
}
}
use crate::model::system::readiness::ReadinessState;
/// Parameters for starting the HTTP server, avoiding `#[cfg]` on function params.
pub struct HttpServerParams {

View File

@ -20,7 +20,7 @@ use crate::core::dns_filter_service::DnsFilterService;
use crate::core::ebpf::EbpfServices;
use crate::core::email::scheduler::ReportScheduler;
use crate::core::ml::config_loader::InferenceConfig;
use crate::core::ml::drift_detector::{DriftDetector, FeatureBaselines};
use crate::core::ml::drift_detector::DriftDetector;
use crate::core::notification_service::NotificationService;
use crate::core::playbook_service::PlaybookService;
use crate::core::rate_limit_service::RateLimitService;
@ -36,6 +36,7 @@ use crate::interface::communication::query_types::GetEnforceModeQuery;
use crate::interface::port::access_control::AccessControlPort;
use crate::interface::port::notification::AlertNotifier;
use crate::interface::port::repository::RepositoryPort;
use crate::model::detection::drift::FeatureBaselines;
use crate::model::direction::FlowDirection;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
@ -148,9 +149,9 @@ impl ServiceFactory {
.build();
// Register event type channels
comm.register_event_type::<crate::interface::communication::event_types::ThreatDetectedEvent>();
comm.register_event_type::<crate::interface::communication::event_types::DriftDetectedEvent>();
comm.register_event_type::<crate::interface::communication::event_types::AuditEvent>();
comm.register_event_type::<crate::model::event::ThreatDetectedEvent>();
comm.register_event_type::<crate::model::event::DriftDetectedEvent>();
comm.register_event_type::<crate::model::event::AuditEvent>();
// Seed default SOAR playbooks if empty
db.seed_default_playbooks()?;

View File

@ -1,52 +1 @@
use crate::interface::communication::event::Event;
// ── ML Events ────────────────────────────────────────────────────────
/// Fired when the ML engine detects a potential threat.
/// Consumed by the SOAR engine to trigger automated responses.
#[derive(Debug, Clone)]
pub struct ThreatDetectedEvent {
pub attack_type: String,
pub confidence: f32,
/// Source IP address (e.g. "192.168.1.100")
pub source_ip: String,
/// Destination IP address (e.g. "10.0.0.1")
pub dest_ip: String,
/// Number of alert flows from this src_ip in recent window
pub flow_count: u32,
/// Packets per second of the triggering flow
pub packet_rate: f64,
/// IP protocol number (6=TCP, 17=UDP)
pub protocol: u8,
/// Source country ISO 3166-1 alpha-2 code, None if GeoIP unavailable
pub geoip_country: Option<String>,
/// Whether this src_ip had a block action in the past 24h
pub is_repeat_offender: bool,
}
impl Event for ThreatDetectedEvent {}
/// Fired when the ML drift detector finds feature drift beyond 3σ.
#[derive(Debug, Clone)]
pub struct DriftDetectedEvent {
pub drifted_features: Vec<String>,
pub max_deviation: f64,
}
impl Event for DriftDetectedEvent {}
// ── Audit Events ────────────────────────────────────────────────────
/// Fired for auditable actions (enforce mode changes, playbook CRUD, etc.).
/// Consumed by AuditLogger to persist to DB and structured logs.
#[derive(Debug, Clone)]
pub struct AuditEvent {
/// Who performed the action: "admin", "system", "soar"
pub actor: String,
/// What action was performed: "enforce_mode_changed", "playbook_created", etc.
pub action: String,
/// JSON string with action-specific details
pub detail: String,
}
impl Event for AuditEvent {}
// Types are available via crate::model::event

View File

@ -0,0 +1,31 @@
use crate::model::system::config::MLInferenceConfig;
/// Baselines loaded from the inference config (scaler mean / std).
/// If inference_config has no scaler data, drift detection is disabled.
pub struct FeatureBaselines {
pub names: Vec<String>,
pub means: Vec<f64>,
pub stds: Vec<f64>,
}
impl FeatureBaselines {
/// Build baselines from the ML inference config.
/// Returns `None` if the config has no features (drift detection disabled).
pub fn from_inference_config(config: &MLInferenceConfig) -> Option<Self> {
if config.ae_feature_names.is_empty() {
return None;
}
Some(Self {
names: config.ae_feature_names.clone(),
means: config.ae_scaler_mean.clone(),
stds: config.ae_scaler_std.clone(),
})
}
}
/// Report emitted when feature drift is detected.
#[derive(Debug, Clone)]
pub struct DriftReport {
pub drifted_features: Vec<String>,
pub max_deviation: f64,
}

View File

@ -0,0 +1,138 @@
use std::collections::HashMap;
use crate::model::ml_detection::ClipParams;
#[derive(Debug, Clone)]
pub struct FlowFeatures {
pub features: Vec<f64>,
pub feature_num: usize,
}
impl FlowFeatures {
pub fn normalize(&mut self, means: &[f64], stds: &[f64]) {
for i in 0..self.feature_num {
if stds[i] > 0.0 {
self.features[i] = (self.features[i] - means[i]) / stds[i];
} else {
self.features[i] = 0.0;
}
}
}
pub fn clip(&mut self, clip_min: f64, clip_max: f64) {
for i in 0..self.feature_num {
self.features[i] = self.features[i].max(clip_min).min(clip_max);
}
}
pub fn winsorize(&mut self, clip_params: &HashMap<String, ClipParams>, feature_names: &[String]) {
for (i, feature_name) in feature_names.iter().enumerate() {
if i < self.feature_num
&& let Some(params) = clip_params.get(feature_name)
{
self.features[i] = self.features[i].clamp(params.lower, params.upper);
}
}
}
pub fn all_feature_names() -> Vec<&'static str> {
vec![
"Destination Port",
"Protocol",
"Flow Duration",
"Total Fwd Packets",
"Total Backward Packets",
"Total Length of Fwd Packets",
"Total Length of Bwd Packets",
"Fwd Packet Length Max",
"Fwd Packet Length Min",
"Fwd Packet Length Mean",
"Fwd Packet Length Std",
"Bwd Packet Length Max",
"Bwd Packet Length Min",
"Bwd Packet Length Mean",
"Bwd Packet Length Std",
"Flow Bytes/s",
"Flow Packets/s",
"Flow IAT Mean",
"Flow IAT Std",
"Flow IAT Max",
"Flow IAT Min",
"Fwd IAT Total",
"Fwd IAT Mean",
"Fwd IAT Std",
"Fwd IAT Max",
"Fwd IAT Min",
"Bwd IAT Total",
"Bwd IAT Mean",
"Bwd IAT Std",
"Bwd IAT Max",
"Bwd IAT Min",
"Fwd PSH Flags",
"Bwd PSH Flags",
"Fwd URG Flags",
"Bwd URG Flags",
"Fwd Header Length",
"Bwd Header Length",
"Fwd Packets/s",
"Bwd Packets/s",
"Min Packet Length",
"Max Packet Length",
"Packet Length Mean",
"Packet Length Std",
"Packet Length Variance",
"FIN Flag Count",
"SYN Flag Count",
"RST Flag Count",
"PSH Flag Count",
"ACK Flag Count",
"URG Flag Count",
"CWE Flag Count",
"ECE Flag Count",
"Down/Up Ratio",
"Average Packet Size",
"Avg Fwd Segment Size",
"Avg Bwd Segment Size",
"Fwd Header Length.1",
"Fwd Avg Bytes/Bulk",
"Fwd Avg Packets/Bulk",
"Fwd Avg Bulk Rate",
"Bwd Avg Bytes/Bulk",
"Bwd Avg Packets/Bulk",
"Bwd Avg Bulk Rate",
"Subflow Fwd Packets",
"Subflow Fwd Bytes",
"Subflow Bwd Packets",
"Subflow Bwd Bytes",
"Init_Win_bytes_forward",
"Init_Win_bytes_backward",
"act_data_pkt_fwd",
"min_seg_size_forward",
"Active Mean",
"Active Std",
"Active Max",
"Active Min",
"Idle Mean",
"Idle Std",
"Idle Max",
"Idle Min",
// Phase 2: new features
"fwd_iat_std",
"bwd_iat_std",
"flow_iat_std",
"fwd_bwd_bytes_ratio",
"pkt_len_variance",
"fwd_iat_skewness",
]
}
pub fn all_feature_names_owned() -> Vec<String> {
Self::all_feature_names().iter().map(|s| s.to_string()).collect()
}
pub fn to_csv_record(&self) -> Vec<String> {
let mut record: Vec<String> = self.features.iter().map(|f| f.to_string()).collect();
record.push("BENIGN".to_string());
record
}
}

View File

@ -1 +1,3 @@
pub mod drift;
pub mod flow_features;
pub mod ml_detection;

View File

@ -0,0 +1,52 @@
use crate::interface::communication::event::Event;
// -- ML Events ----------------------------------------------------------------
/// Fired when the ML engine detects a potential threat.
/// Consumed by the SOAR engine to trigger automated responses.
#[derive(Debug, Clone)]
pub struct ThreatDetectedEvent {
pub attack_type: String,
pub confidence: f32,
/// Source IP address (e.g. "192.168.1.100")
pub source_ip: String,
/// Destination IP address (e.g. "10.0.0.1")
pub dest_ip: String,
/// Number of alert flows from this src_ip in recent window
pub flow_count: u32,
/// Packets per second of the triggering flow
pub packet_rate: f64,
/// IP protocol number (6=TCP, 17=UDP)
pub protocol: u8,
/// Source country ISO 3166-1 alpha-2 code, None if GeoIP unavailable
pub geoip_country: Option<String>,
/// Whether this src_ip had a block action in the past 24h
pub is_repeat_offender: bool,
}
impl Event for ThreatDetectedEvent {}
/// Fired when the ML drift detector finds feature drift beyond 3 sigma.
#[derive(Debug, Clone)]
pub struct DriftDetectedEvent {
pub drifted_features: Vec<String>,
pub max_deviation: f64,
}
impl Event for DriftDetectedEvent {}
// -- Audit Events -------------------------------------------------------------
/// Fired for auditable actions (enforce mode changes, playbook CRUD, etc.).
/// Consumed by AuditLogger to persist to DB and structured logs.
#[derive(Debug, Clone)]
pub struct AuditEvent {
/// Who performed the action: "admin", "system", "soar"
pub actor: String,
/// What action was performed: "enforce_mode_changed", "playbook_created", etc.
pub action: String,
/// JSON string with action-specific details
pub detail: String,
}
impl Event for AuditEvent {}

View File

@ -2,9 +2,11 @@
pub mod access_control;
pub mod detection;
pub mod error;
pub mod event;
pub mod identity;
pub mod log;
pub mod monitoring;
pub mod report;
pub mod soar;
pub mod system;

View File

@ -0,0 +1,9 @@
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GeoLocation {
pub country: Option<String>,
pub country_code: Option<String>,
pub city: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub timezone: Option<String>,
}

View File

@ -1,4 +1,5 @@
pub mod direction;
pub mod drop_event;
pub mod flow_stats;
pub mod geolocation;
pub mod user_packet;

View File

@ -0,0 +1,182 @@
use serde::{Deserialize, Serialize};
use crate::interface::port::repository::RepositoryPort;
use crate::model::error::Error;
/// Shared report data structure used by both HTML email and PDF report.
#[derive(Debug, Clone, Serialize)]
pub struct ReportData {
pub period: String,
pub generated_at: String,
pub executive_summary: ExecutiveSummary,
pub threat_breakdown: Vec<ThreatBreakdownItem>,
pub top_blocked_ips: Vec<BlockedIpItem>,
pub geo_distribution: Vec<GeoItem>,
pub soar_activity: SoarActivity,
pub system_health: SystemHealthSummary,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExecutiveSummary {
pub total_threats: u64,
pub total_blocked: u64,
pub uptime_percent: f64,
pub active_rules: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ThreatBreakdownItem {
pub threat_type: String,
pub count: u64,
pub trend: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockedIpItem {
pub ip: String,
pub count: u64,
pub country: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoItem {
pub country: String,
pub threat_count: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct SoarActivity {
pub auto_blocks_executed: u64,
pub playbooks_triggered: u64,
pub auto_unblocks: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemHealthSummary {
pub avg_cpu_percent: f64,
pub avg_memory_percent: f64,
pub disk_usage_percent: f64,
pub ebpf_status: String,
}
impl ReportData {
/// Build report data from database settings (aggregated by the ML pipeline).
pub fn from_database(db: &dyn RepositoryPort) -> Result<Self, Error> {
let now = chrono::Local::now();
let period = format!(
"{} — {}",
(now - chrono::Duration::days(7)).format("%Y-%m-%d"),
now.format("%Y-%m-%d")
);
let threats_count: u64 = db
.get_setting("weekly_threats_count")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let top_ips: Vec<BlockedIpItem> = db
.get_setting("weekly_top_ips")?
.and_then(|v| serde_json::from_str(&v).ok())
.unwrap_or_else(|| {
vec![BlockedIpItem {
ip: "".into(),
count: 0,
country: "N/A".into(),
}]
});
let breakdown: Vec<ThreatBreakdownItem> = db
.get_setting("weekly_threat_breakdown")?
.and_then(|v| {
let obj: serde_json::Value = serde_json::from_str(&v).ok()?;
let items = obj
.as_object()?
.iter()
.map(|(k, v)| ThreatBreakdownItem {
threat_type: k.clone(),
count: v.as_u64().unwrap_or(0),
trend: "".into(),
})
.collect();
Some(items)
})
.unwrap_or_default();
let health: SystemHealthSummary = db
.get_setting("weekly_system_health")?
.and_then(|v| serde_json::from_str(&v).ok())
.unwrap_or(SystemHealthSummary {
avg_cpu_percent: 0.0,
avg_memory_percent: 0.0,
disk_usage_percent: 0.0,
ebpf_status: "running".into(),
});
// Generate recommendations based on data
let mut recommendations = Vec::new();
if threats_count > 10 {
recommendations.push("Consider enabling geo-blocking for high-risk regions".into());
}
if breakdown.iter().any(|b| b.threat_type == "port_scan" && b.count > 50) {
recommendations.push("Review exposed ports and consider tightening protocol filter rules".into());
}
if recommendations.is_empty() {
recommendations.push("No action needed — your network security posture is healthy".into());
}
let uptime_percent: f64 = db
.get_setting("system_uptime_percent")?
.and_then(|v| v.parse().ok())
.unwrap_or(0.0);
let active_rules: u64 = db
.get_setting("active_rules_count")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let geo_distribution: Vec<GeoItem> = db
.get_setting("weekly_geo_distribution")?
.and_then(|v| serde_json::from_str(&v).ok())
.unwrap_or_default();
let auto_blocks: u64 = db
.get_setting("weekly_soar_blocks")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let playbooks_triggered: u64 = db
.get_setting("weekly_soar_triggers")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let auto_unblocks: u64 = db
.get_setting("weekly_soar_unblocks")?
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let blocked_count: u64 = db
.get_setting("weekly_blocked_count")?
.and_then(|v| v.parse().ok())
.unwrap_or(auto_blocks);
Ok(ReportData {
period,
generated_at: now.format("%Y-%m-%d %H:%M:%S").to_string(),
executive_summary: ExecutiveSummary {
total_threats: threats_count,
total_blocked: blocked_count,
uptime_percent,
active_rules,
},
threat_breakdown: breakdown,
top_blocked_ips: top_ips,
geo_distribution,
soar_activity: SoarActivity {
auto_blocks_executed: auto_blocks,
playbooks_triggered,
auto_unblocks,
},
system_health: health,
recommendations,
})
}
}

View File

@ -0,0 +1 @@
pub mod data;

View File

@ -1 +1,2 @@
pub mod playbook;
pub mod playbook_data;

View File

@ -0,0 +1,48 @@
/// Input for creating a new playbook.
pub struct CreatePlaybookInput {
pub name: String,
pub trigger_event: String,
pub condition_threshold: Option<f64>,
pub condition_count: Option<i64>,
pub condition_window_secs: Option<i64>,
pub cooldown_secs: i64,
pub actions: Vec<(String, String)>, // (action_type, params_json)
}
/// Flattened playbook representation for API responses.
pub struct PlaybookData {
pub id: i64,
pub name: String,
pub enabled: bool,
pub trigger_event: String,
pub condition_threshold: Option<f64>,
pub condition_count: Option<i64>,
pub condition_window_secs: Option<i64>,
pub cooldown_secs: i64,
pub actions: Vec<ActionData>,
}
pub struct ActionData {
pub id: i64,
pub action_order: i64,
pub action_type: String,
pub params: serde_json::Value,
}
/// Execution record from soar_executions table.
pub struct ExecutionData {
pub id: i64,
pub playbook_id: i64,
pub source_ip: Option<String>,
pub trigger_event: String,
pub actions_executed: serde_json::Value,
pub created_at: String,
}
/// Active block record from soar_block_rules table.
pub struct ActiveBlockData {
pub id: i64,
pub source_ip: String,
pub playbook_id: i64,
pub expires_at: String,
}

View File

@ -1,2 +1,4 @@
pub mod config;
pub mod health;
pub mod rate_limit_settings;
pub mod readiness;

View File

@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct RateLimitSettings {
pub packet_rate: Option<u64>,
pub syn_rate: Option<u64>,
pub udp_rate: Option<u64>,
pub dns_rate: Option<u64>,
pub window_ns: Option<u64>,
}

View File

@ -0,0 +1,20 @@
/// Per-subsystem readiness state exposed by `/health/ready`.
pub struct ReadinessState {
pub db_connected: std::sync::atomic::AtomicBool,
pub ml_model_loaded: std::sync::atomic::AtomicBool,
pub soar_engine_running: std::sync::atomic::AtomicBool,
pub ebpf_attached: std::sync::atomic::AtomicBool,
pub started_at: std::time::Instant,
}
impl ReadinessState {
pub fn new() -> Self {
Self {
db_connected: std::sync::atomic::AtomicBool::new(false),
ml_model_loaded: std::sync::atomic::AtomicBool::new(false),
soar_engine_running: std::sync::atomic::AtomicBool::new(false),
ebpf_attached: std::sync::atomic::AtomicBool::new(false),
started_at: std::time::Instant::now(),
}
}
}