wip: Architecture adjustment

This commit is contained in:
DaLaw2 2026-04-22 16:54:16 +08:00
parent 1282048144
commit 56b80e9d96
224 changed files with 1251 additions and 1341 deletions

View File

@ -2,11 +2,11 @@ use std::net::{IpAddr, SocketAddrV4, SocketAddrV6};
use std::sync::Arc;
use crate::adapter::ebpf::access_control::AccessControl;
use crate::domain::common::error::Error;
use crate::domain::data_plane::direction::FlowDirection;
use crate::domain::data_plane::error::EbpfError;
use crate::domain::data_plane::list_type::ListType;
use crate::interface::port::access_control::AccessControlPort;
use crate::model::access_control::list_type::ListType;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::monitoring::direction::FlowDirection;
/// Adapter that implements AccessControlPort by delegating to the eBPF AccessControl.
pub struct AccessControlAdapter {

View File

@ -7,12 +7,12 @@ use common::model::ip_address::{IPv4, IPv6, Port};
use common::model::port_rule::PortRule;
use parking_lot::RwLock;
use crate::domain::common::error::Error;
use crate::domain::data_plane::direction::FlowDirection;
use crate::domain::data_plane::error::EbpfError;
use crate::domain::data_plane::ip_address::NativeConvert;
use crate::domain::data_plane::list_type::ListType;
use crate::interface::port::access_control_admin::AccessControlAdminPort;
use crate::model::access_control::ip_address::NativeConvert;
use crate::model::access_control::list_type::ListType;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::monitoring::direction::FlowDirection;
pub struct AccessControl {
ipv4_src_whitelist: RwLock<MapWrapper<IPv4>>,

View File

@ -3,10 +3,10 @@ use core::str;
use common::model::dns_name::DnsName;
use dashmap::DashSet;
use crate::domain::common::error::Error;
use crate::domain::common::error::misc::MiscError;
use crate::interface::port::dns_filter_api::DnsFilterPort;
use crate::interface::port::dns_query_filter::DnsQueryFilter;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
pub struct DnsFilter {
blacklist: DashSet<DnsName>,

View File

@ -10,7 +10,7 @@ use common::model::drop_event::DropEvent as RawDropEvent;
use tokio::sync::{broadcast, oneshot};
use tokio::time::interval;
use crate::model::monitoring::drop_event::{DropCounters, DropCountersAtomic, DropEventMessage};
use crate::domain::data_plane::drop_event::{DropCounters, DropCountersAtomic, DropEventMessage};
pub struct DropMonitor {
broadcast_tx: broadcast::Sender<DropEventMessage>,

View File

@ -9,11 +9,11 @@ use ipnetwork::IpNetwork;
use maxminddb::{Reader, geoip2};
use parking_lot::RwLock;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::misc::MiscError;
use crate::domain::data_plane::error::EbpfError;
use crate::interface::port::geo_block_api::GeoBlockPort;
use crate::model::config::AppConfig;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::misc::MiscError;
/// Pre-indexed GeoIP prefix table, built once at startup.
struct GeoIndex {

View File

@ -23,12 +23,12 @@ use crate::adapter::ebpf::geo_block::GeoBlock;
use crate::adapter::ebpf::protocol_filter::ProtocolFilter;
use crate::adapter::ebpf::rate_limit::RateLimitConfig;
use crate::adapter::ebpf::xsk_manager::XskManager;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::system::SystemError;
use crate::domain::data_plane::error::EbpfError;
use crate::interface::port::dns_query_filter::DnsQueryFilter;
use crate::interface::port::packet_sink::PacketSinkFactory;
use crate::model::config::AppConfig;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::system::SystemError;
pub struct EbpfServices {
pub xsk_manager: Arc<XskManager>,

View File

@ -8,9 +8,9 @@ use common::model::ip_address::{AddrPortV4, AddrPortV6, IPv4, IPv6};
use common::model::placeholder::PlaceHolder;
use parking_lot::RwLock;
use crate::model::access_control::ip_address::NativeConvert;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::domain::common::error::Error;
use crate::domain::data_plane::error::EbpfError;
use crate::domain::data_plane::ip_address::NativeConvert;
pub struct ProtocolFilter {
ipv4_http_service: RwLock<HttpServiceWrapper<AddrPortV4>>,

View File

@ -2,9 +2,9 @@ use aya::Ebpf;
use aya::maps::{Array, MapData};
use parking_lot::Mutex;
use crate::domain::common::error::Error;
use crate::domain::data_plane::error::EbpfError;
use crate::interface::port::rate_limit_api::RateLimitPort;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
pub struct RateLimitConfig {
config_map: Mutex<Option<Array<MapData, u64>>>,

View File

@ -19,15 +19,15 @@ use xsk_rs::config::{BindFlags, FrameSize, Interface, LibxdpFlags, QueueSize, So
use xsk_rs::{CompQueue, FillQueue, FrameDesc, RxQueue, Socket, TxQueue, Umem};
use crate::adapter::ebpf::drop_monitor::DropMonitor;
use crate::domain::common::config::AppConfig;
use crate::domain::common::config::ebpf::EbpfConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::system::SystemError;
use crate::domain::data_plane::direction::Direction;
use crate::domain::data_plane::error::EbpfError;
use crate::domain::data_plane::log::EbpfLog;
use crate::interface::port::dns_query_filter::DnsQueryFilter;
use crate::interface::port::packet_sink::{PacketSink, PacketSinkFactory};
use crate::model::config::AppConfig;
use crate::model::config::ebpf::EbpfConfig;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::system::SystemError;
use crate::model::log::ebpf::EbpfLog;
use crate::model::monitoring::direction::Direction;
use crate::utils::packet_parser::parse_packet;
/// Pre-allocated buffer pool to avoid per-packet malloc.

View File

@ -4,9 +4,9 @@ use actix_web::{HttpResponse, Responder, Scope, web};
use serde::Deserialize;
use crate::adapter::http::response::ok_or_error;
use crate::core::acl_service::AclService;
use crate::model::access_control::list_type::ListType;
use crate::model::monitoring::direction::FlowDirection;
use crate::core::data_plane::acl_service::AclService;
use crate::domain::data_plane::direction::FlowDirection;
use crate::domain::data_plane::list_type::ListType;
#[derive(Deserialize)]
struct CountryCodesRequest {

View File

@ -1,7 +1,7 @@
use actix_web::{HttpResponse, Scope, web};
use serde::Deserialize;
use crate::core::auth::extractor::AuthClaims;
use crate::core::identity::extractor::AuthClaims;
use crate::interface::port::api_key::ApiKeyRepo;
pub fn initialize() -> Scope {

View File

@ -1,10 +1,10 @@
use actix_web::{HttpResponse, Scope, web};
use crate::adapter::persistence::Database;
use crate::core::auth::extractor::AuthClaims;
use crate::core::identity::extractor::AuthClaims;
use crate::domain::common::error::Error;
use crate::domain::common::error::database::DatabaseError;
use crate::interface::port::audit::AuditRepo;
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
pub fn initialize() -> Scope {
web::scope("/audit")

View File

@ -3,11 +3,11 @@ use macros::log;
use serde::Deserialize;
use crate::adapter::http::response::ok_or_error;
use crate::core::auth::extractor::AuthClaims;
use crate::core::auth::jwt::JwtService;
use crate::core::auth::password;
use crate::core::identity::extractor::AuthClaims;
use crate::core::identity::jwt::JwtService;
use crate::domain::identity::error::AuthError;
use crate::domain::identity::password;
use crate::interface::port::app_repo::AppRepo;
use crate::model::error::auth::AuthError;
type Repo = dyn AppRepo;

View File

@ -6,8 +6,8 @@
use actix_web::{HttpResponse, Scope, web};
use crate::core::auth::extractor::AuthClaims;
use crate::core::ml::feature_extractor::feature_registry_names;
use crate::core::identity::extractor::AuthClaims;
use crate::domain::detection::feature_extractor::feature_registry_names;
pub fn initialize() -> Scope {
web::scope("/byo").route("/feature-registry", web::get().to(get_feature_registry))

View File

@ -6,7 +6,7 @@ use serde::Deserialize;
use crate::adapter::ebpf::protocol_filter::ProtocolFilter;
use crate::adapter::http::response::ok_or_error;
use crate::core::dns_filter_service::DnsFilterService;
use crate::core::data_plane::dns_filter_service::DnsFilterService;
pub fn initialize() -> Scope {
web::scope("/filter")

View File

@ -10,8 +10,8 @@ use std::path::{Path, PathBuf};
use actix_files::NamedFile;
use actix_web::{HttpRequest, HttpResponse, Responder, Scope, web};
use crate::core::ml::engine::Engine;
use crate::core::ml::traffic_logger::{FLOW_TRACE_FILE_EXT, FLOW_TRACE_FILE_MARKER, list_flow_trace_files};
use crate::core::inference::engine::Engine;
use crate::core::inference::traffic_logger::{FLOW_TRACE_FILE_EXT, FLOW_TRACE_FILE_MARKER, list_flow_trace_files};
pub fn initialize() -> Scope {
web::scope("/flow-trace")

View File

@ -12,9 +12,9 @@ use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, Responder, Scope, web};
use arc_swap::ArcSwap;
use crate::core::detection::metrics::FusionMetrics;
use crate::domain::common::config::AppConfig;
use crate::domain::detection::metrics::FusionMetrics;
use crate::interface::port::audit::{AuditLogEntry, AuditRepo};
use crate::model::config::AppConfig;
/// Stable audit action string the fusion engine emits — kept in sync
/// with `core::detection::orchestrator::FUSION_AUDIT_ACTION`. If that

View File

@ -8,8 +8,8 @@ use actix_web::{HttpResponse, Scope, web};
use arc_swap::ArcSwap;
use serde::{Deserialize, Serialize};
use crate::core::observability::log_buffer::{self, LogEntry};
use crate::model::config::AppConfig;
use crate::core::common::observability::log_buffer::{self, LogEntry};
use crate::domain::common::config::AppConfig;
/// Hardcoded log directory — not configurable via API to prevent directory traversal.
const LOG_DIR: &str = "logs";

View File

@ -1,12 +1,12 @@
use actix_web::{HttpResponse, Responder, Scope, web};
use crate::adapter::http::AUDIT_ACTOR_SECURITY_ADMIN_PREFIX;
use crate::core::auth::extractor::AuthClaims;
use crate::core::ml::adapter::ModelSourceState;
use crate::core::ml::engine::Engine;
use crate::core::ml::inference::Inference;
use crate::core::identity::extractor::AuthClaims;
use crate::core::inference::engine::Engine;
use crate::core::inference::inference::Inference;
use crate::domain::common::event::AuditEvent;
use crate::domain::detection::model_adapter::ModelSourceState;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::model::event::AuditEvent;
/// Permission required to forcibly revert the active ML source to dormant.
/// Mirrors the upload handler's gate so swap-out and revert are symmetric:

View File

@ -34,15 +34,15 @@ use tokio::io::AsyncWriteExt;
use tokio::task;
use uuid::Uuid;
use crate::core::auth::extractor::AuthClaims;
use crate::core::ml::inference::Inference;
use crate::core::ml::manifest::{AdapterKind, ModelManifest};
use crate::core::ml::model_loader::build_adapter;
use crate::core::identity::extractor::AuthClaims;
use crate::core::inference::inference::Inference;
use crate::core::inference::model_loader::build_adapter;
use crate::domain::common::config::AppConfig;
use crate::domain::common::config::constants::{MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR};
use crate::domain::common::event::AuditEvent;
use crate::domain::detection::manifest::{AdapterKind, ModelManifest};
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::model::config::AppConfig;
use crate::model::config::constants::{MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR};
use crate::model::detection::ml_inference_config::MLInferenceConfig;
use crate::model::event::AuditEvent;
/// Multipart field names the client must use. Stable wire contract —
/// the frontend form generator depends on these exact strings.

View File

@ -2,8 +2,8 @@ use actix_web::{HttpResponse, Scope, web};
use serde::Deserialize;
use crate::adapter::http::response::{ok_json_or_error, ok_or_error};
use crate::core::auth::extractor::AuthClaims;
use crate::core::notification_service::NotificationService;
use crate::core::common::notification_service::NotificationService;
use crate::core::identity::extractor::AuthClaims;
pub fn initialize() -> Scope {
web::scope("/notifications")

View File

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

View File

@ -7,15 +7,15 @@ use tokio::task::spawn_blocking;
use crate::adapter::http::response::ok_json_or_error;
use crate::adapter::persistence::Database;
use crate::core::auth::extractor::AuthClaims;
use crate::core::email::report::generate_weekly_report;
use crate::core::email::scheduler::SmtpClient;
use crate::core::report::engine;
use crate::core::identity::extractor::AuthClaims;
use crate::core::reporting::email_report::generate_weekly_report;
use crate::core::reporting::email_scheduler::SmtpClient;
use crate::core::reporting::report_engine;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::misc::MiscError;
use crate::infrastructure::secret_store::SecretStore;
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
use crate::model::config::AppConfig;
use crate::model::error::misc::MiscError;
pub fn initialize() -> Scope {
web::scope("/report")
@ -36,7 +36,7 @@ async fn generate_report(
}));
}
let db_ref = db.get_ref();
match engine::generate_html_report(db_ref as &dyn SettingRepo, &report_dir) {
match report_engine::generate_html_report(db_ref as &dyn SettingRepo, &report_dir) {
Ok(path) => match fs::read(&path) {
Ok(content) => HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
@ -62,7 +62,7 @@ async fn generate_report(
async fn report_data(_auth: AuthClaims, db: web::Data<Database>) -> HttpResponse {
let db_ref = db.get_ref();
ok_json_or_error(engine::generate_report_json(db_ref as &dyn SettingRepo))
ok_json_or_error(report_engine::generate_report_json(db_ref as &dyn SettingRepo))
}
/// Manually trigger: generate the weekly report and send it via SMTP now.

View File

@ -8,12 +8,12 @@ use serde::Deserialize;
use serde_json::Value;
use crate::adapter::persistence::Database;
use crate::core::auth::password;
use crate::core::auth::setup_guard::SetupCompleteFlag;
use crate::core::identity::setup_guard::SetupCompleteFlag;
use crate::domain::common::error::Error;
use crate::domain::common::error::system::SystemError;
use crate::domain::identity::password;
use crate::infrastructure::secret_store::SecretStore;
use crate::interface::port::secret_store::SecretStorePort;
use crate::model::error::Error;
use crate::model::error::system::SystemError;
pub fn initialize() -> Scope {
web::scope("/setup")

View File

@ -4,11 +4,11 @@ use actix_web::{HttpResponse, Scope, web};
use serde::Deserialize;
use crate::adapter::http::response::{ok_json_or_error, ok_or_error};
use crate::core::auth::extractor::AuthClaims;
use crate::core::playbook_service::PlaybookService;
use crate::core::soar::engine::SoarEngine;
use crate::model::event::{DetectionSource, ThreatDetectedEvent};
use crate::model::soar::playbook_data::{CreateConditionInput, CreatePlaybookInput};
use crate::core::identity::extractor::AuthClaims;
use crate::core::response::engine::SoarEngine;
use crate::core::response::playbook_service::PlaybookService;
use crate::domain::common::event::{DetectionSource, ThreatDetectedEvent};
use crate::domain::response::playbook_data::{CreateConditionInput, CreatePlaybookInput};
#[derive(Deserialize)]
struct CreatePlaybookRequest {

View File

@ -1,8 +1,8 @@
use actix_web::{HttpResponse, Responder, Scope, web};
use serde::Deserialize;
use crate::core::auth::extractor::AuthClaims;
use crate::core::config_service::ConfigService;
use crate::core::common::config_service::ConfigService;
use crate::core::identity::extractor::AuthClaims;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::infrastructure::system::{ShutdownHandle, ShutdownMode};
use crate::interface::communication::command_types::ChangeEnforceModeCommand;

View File

@ -1,8 +1,8 @@
use rusqlite::params;
use super::Database;
use crate::domain::common::error::Error;
use crate::interface::port::acl::{AclRepo, AclRuleTuple};
use crate::model::error::Error;
impl Database {
pub fn insert_acl_rule(

View File

@ -5,9 +5,9 @@ use rusqlite::{Error as RusqliteError, params};
use sha2::Sha256;
use super::Database;
use crate::domain::common::error::Error;
use crate::domain::identity::auth::Claims;
use crate::interface::port::api_key::{ApiKeyListItem, ApiKeyRepo};
use crate::model::error::Error;
use crate::model::identity::auth::Claims;
type HmacSha256 = Hmac<Sha256>;

View File

@ -5,9 +5,9 @@ use rusqlite::params;
use sha2::{Digest, Sha256};
use super::Database;
use crate::domain::common::error::Error;
use crate::domain::common::error::database::DatabaseError;
use crate::interface::port::audit::{AuditLogEntry, AuditRepo};
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
/// Compute the row hash for an audit_log entry.
/// Formula: sha256_hex(ts || 0x00 || actor || 0x00 || action || 0x00 || detail || 0x00 || prev_hash)

View File

@ -1,8 +1,8 @@
use rusqlite::params;
use super::Database;
use crate::domain::common::error::Error;
use crate::interface::port::enforcement::EnforcementRepo;
use crate::model::error::Error;
impl Database {
pub fn set_rate_limit(&self, key: &str, value: u64) -> Result<(), Error> {

View File

@ -15,9 +15,9 @@ use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{self, Connection, params};
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
use crate::model::log::misc::MiscLog;
use crate::domain::common::error::Error;
use crate::domain::common::error::database::DatabaseError;
use crate::domain::common::log::misc::MiscLog;
/// Reads the SQLCipher encryption key from the environment variable `NETGUARDIA_DB_KEY`.
/// Returns `Some(key)` if set and non-empty, `None` otherwise (dev / unencrypted mode).

View File

@ -1,8 +1,8 @@
use rusqlite::{Error as RusqliteError, Transaction, params};
use super::Database;
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
impl Database {
pub fn get_setting(&self, key: &str) -> Result<Option<String>, Error> {
@ -189,9 +189,9 @@ impl SettingRepo for SettingTxView<'_> {
#[cfg(test)]
mod tests {
use super::super::tests::test_db;
use crate::domain::common::error::Error;
use crate::domain::common::error::misc::MiscError;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
#[test]
fn test_settings_crud() {

View File

@ -1,9 +1,9 @@
use rusqlite::params;
use super::Database;
use crate::domain::common::error::Error;
use crate::domain::response::playbook_data::UpdatePlaybookInput;
use crate::interface::port::soar::{PlaybookRow, SoarExecutionRow, SoarRepo};
use crate::model::error::Error;
use crate::model::soar::playbook_data::UpdatePlaybookInput;
impl Database {
pub fn insert_playbook(

View File

@ -1,8 +1,8 @@
use rusqlite::params;
use super::Database;
use crate::domain::common::error::Error;
use crate::interface::port::db_admin::DbAdminRepo;
use crate::model::error::Error;
impl Database {
/// Test-only helper: direct insert of a SOAR block rule row. Production

View File

@ -1,8 +1,8 @@
use rusqlite::params;
use super::Database;
use crate::domain::common::error::Error;
use crate::interface::port::stats::StatsRepo;
use crate::model::error::Error;
impl Database {
/// Count SOAR executions in the last N days.

View File

@ -4,9 +4,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rusqlite::{Error as RusqliteError, params};
use super::Database;
use crate::domain::common::error::Error;
use crate::domain::common::error::database::DatabaseError;
use crate::interface::port::identity::{IdentityRepo, UserGroupTuple, UserTuple, UserWithGroups};
use crate::model::error::Error;
use crate::model::error::database::DatabaseError;
impl Database {
pub fn find_user(&self, username: &str) -> Result<Option<UserTuple>, Error> {

View File

@ -8,13 +8,13 @@ use macros::log;
use reqwest::Client;
use tokio::time::sleep;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::notification::NotificationError;
use crate::domain::common::log::system::SystemLog;
use crate::interface::port::notification::{AlertNotifier, AlertNotifierFactory, AlertPayload};
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
use crate::model::config::AppConfig;
use crate::model::error::Error;
use crate::model::error::notification::NotificationError;
use crate::model::log::system::SystemLog;
/// Telegram Bot API adapter implementing AlertNotifier.
pub struct TelegramAdapter {

View File

@ -6,11 +6,11 @@ use macros::log;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use crate::core::ml::alert::MLAlert;
use crate::model::detection::ml_detection::AlertMessage;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;
use crate::core::inference::alert::MLAlert;
use crate::domain::common::error::http::HttpError;
use crate::domain::common::error::misc::MiscError;
use crate::domain::common::log::http::HttpLog;
use crate::domain::detection::ml_detection::AlertMessage;
pub async fn websocket_alert(req: HttpRequest, body: web::Payload, ai: web::Data<MLAlert>) -> Result<HttpResponse> {
let (response, session, msg_stream) = handle(&req, body)?;

View File

@ -7,10 +7,10 @@ use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use crate::adapter::ebpf::drop_monitor::DropMonitor;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;
use crate::model::monitoring::drop_event::DropEventMessage;
use crate::domain::common::error::http::HttpError;
use crate::domain::common::error::misc::MiscError;
use crate::domain::common::log::http::HttpLog;
use crate::domain::data_plane::drop_event::DropEventMessage;
pub async fn websocket_drops(
req: HttpRequest,

View File

@ -6,8 +6,8 @@ use actix_ws::Message;
use futures_util::StreamExt;
use tokio::time::interval;
use crate::domain::data_plane::flow_stats::FlowSubscription;
use crate::infrastructure::statistics::FlowStatistics;
use crate::model::monitoring::flow_stats::FlowSubscription;
/// Default subscription: all flows, no filter, 5 second interval
fn default_subscription() -> FlowSubscription {

View File

@ -22,11 +22,11 @@ use macros::log;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use crate::domain::common::error::http::HttpError;
use crate::domain::common::error::misc::MiscError;
use crate::domain::common::event::ThreatDetectedEvent;
use crate::domain::common::log::http::HttpLog;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::event::ThreatDetectedEvent;
use crate::model::log::http::HttpLog;
pub async fn websocket_fusion(
req: HttpRequest,
@ -144,7 +144,7 @@ async fn send_event(session: &mut Session, event: &ThreatDetectedEvent) -> bool
#[cfg(test)]
mod tests {
use super::*;
use crate::model::event::DetectionSource;
use crate::domain::common::event::DetectionSource;
fn sample_event() -> ThreatDetectedEvent {
ThreatDetectedEvent {

View File

@ -6,11 +6,11 @@ use macros::log;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use crate::domain::common::error::http::HttpError;
use crate::domain::common::error::misc::MiscError;
use crate::domain::common::log::http::HttpLog;
use crate::domain::common::system::health::SystemHealthMetrics;
use crate::infrastructure::health::SystemHealth;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;
use crate::model::system::health::SystemHealthMetrics;
pub async fn websocket_system_health(
req: HttpRequest,

View File

@ -3,8 +3,8 @@ use serde::Deserialize;
use super::{alert_websocket, drop_websocket, flow_websocket, fusion_websocket, health_websocket};
use crate::adapter::ebpf::drop_monitor::DropMonitor;
use crate::core::auth::jwt::JwtService;
use crate::core::ml::alert::MLAlert;
use crate::core::identity::jwt::JwtService;
use crate::core::inference::alert::MLAlert;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::infrastructure::health::SystemHealth;
use crate::infrastructure::statistics::FlowStatistics;

View File

@ -2,11 +2,11 @@ use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::misc::MiscError;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::secret_store::SecretStorePort;
use crate::model::config::AppConfig;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
/// Keys that must be routed through SecretStore instead of plaintext settings.
const SECRET_KEYS: &[&str] = &["smtp_password"];

View File

@ -0,0 +1,3 @@
pub mod config_service;
pub mod notification_service;
pub mod observability;

View File

@ -3,13 +3,13 @@ use std::sync::Arc;
use arc_swap::ArcSwap;
use serde_json::Value;
use crate::core::email::scheduler::SmtpClient;
use crate::core::reporting::email_scheduler::SmtpClient;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::misc::MiscError;
use crate::interface::port::notification::AlertNotifierFactory;
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
use crate::model::config::AppConfig;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
/// Domain service for notification config (Telegram, SMTP).
/// Coordinates DB persistence and external service testing.

View File

@ -7,13 +7,13 @@ use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{broadcast, mpsc};
use tokio::time::interval;
use crate::core::correlation::botnet::BotnetDetector;
use crate::core::correlation::lateral::LateralMovementDetector;
use crate::core::correlation::scan::ScanDetector;
use crate::model::config::AppConfig;
use crate::model::detection::ml_detection::AlertMessage;
use crate::model::event::DetectionEvent;
use crate::model::log::detection::DetectionLog;
use crate::domain::common::config::AppConfig;
use crate::domain::common::event::DetectionEvent;
use crate::domain::detection::botnet::BotnetDetector;
use crate::domain::detection::lateral::LateralMovementDetector;
use crate::domain::detection::log::DetectionLog;
use crate::domain::detection::ml_detection::AlertMessage;
use crate::domain::detection::scan::ScanDetector;
/// Coordinates cross-flow correlation detectors (botnet, scan, lateral movement).
/// Subscribes to ML AlertMessage broadcast and feeds enriched DetectionEvents
@ -73,9 +73,15 @@ impl CorrelationEngine {
}
fn process_alert(&self, alert: &AlertMessage) {
self.botnet.process(alert, &self.detection_tx);
self.scan.process(alert, &self.detection_tx);
self.lateral.process(alert, &self.detection_tx);
if let Some(event) = self.botnet.process(alert) {
let _ = self.detection_tx.try_send(event);
}
if let Some(event) = self.scan.process(alert) {
let _ = self.detection_tx.try_send(event);
}
if let Some(event) = self.lateral.process(alert) {
let _ = self.detection_tx.try_send(event);
}
}
fn cleanup(&self) {

View File

@ -1,4 +1 @@
pub mod botnet;
pub mod engine;
pub mod lateral;
pub mod scan;

View File

@ -3,13 +3,13 @@ use std::sync::Arc;
use macros::log;
use crate::domain::common::error::Error;
use crate::domain::data_plane::direction::FlowDirection;
use crate::domain::data_plane::error::EbpfError;
use crate::domain::data_plane::list_type::ListType;
use crate::interface::port::access_control_admin::AccessControlAdminPort;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::geo_block_api::GeoBlockPort;
use crate::model::access_control::list_type::ListType;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::monitoring::direction::FlowDirection;
/// Domain service that coordinates ACL changes between DB persistence and eBPF data plane.
/// Atomic write: eBPF first, then DB. If DB fails, rollback eBPF.

View File

@ -2,11 +2,11 @@ use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::misc::MiscError;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::dns_filter_api::DnsFilterPort;
use crate::model::config::AppConfig;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
/// Domain service that coordinates DNS filter changes between DB and in-memory service.
/// Write order: eBPF/in-memory first, then DB — if eBPF fails, DB remains clean.

View File

@ -0,0 +1,3 @@
pub mod acl_service;
pub mod dns_filter_service;
pub mod rate_limit_service;

View File

@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::domain::common::error::Error;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::rate_limit_api::RateLimitPort;
use crate::model::error::Error;
/// Domain service that coordinates rate limit config updates between DB and eBPF.
pub struct RateLimitService {
@ -44,4 +44,4 @@ impl RateLimitService {
}
}
use crate::model::system::rate_limit_settings::RateLimitSettings;
use crate::domain::common::system::rate_limit_settings::RateLimitSettings;

View File

@ -1,40 +1,23 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::time::Duration;
use arc_swap::ArcSwap;
use dashmap::DashMap;
use macros::log;
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{broadcast, mpsc};
use tokio::time::interval;
use crate::model::config::AppConfig;
use crate::model::detection::ml_detection::AlertMessage;
use crate::model::event::{DetectionEvent, DetectionSource};
use crate::model::log::detection::DetectionLog;
use crate::domain::common::config::AppConfig;
use crate::domain::common::event::DetectionEvent;
use crate::domain::detection::beaconing::BeaconingState;
use crate::domain::detection::log::DetectionLog;
use crate::domain::detection::ml_detection::AlertMessage;
/// Key for tracking flow timing: (src_ip, dst_ip, dst_port).
type FlowTuple = (String, String, u16);
struct CachedFlow {
timestamps: Vec<Instant>,
last_alerted: Option<Instant>,
}
/// Detects C2 beaconing by analyzing the periodicity of flows between
/// (src_ip, dst_ip, dst_port) tuples. Uses coefficient of variation (CV)
/// of inter-arrival times: CV below `cv_threshold` with sufficient
/// observations = beaconing.
pub struct BeaconingDetector {
flow_cache: DashMap<FlowTuple, CachedFlow>,
detection_tx: mpsc::Sender<DetectionEvent>,
state: BeaconingState,
alert_rx: broadcast::Receiver<AlertMessage>,
detection_tx: mpsc::Sender<DetectionEvent>,
analysis_interval_secs: u64,
min_observations: usize,
cv_threshold: f64,
max_cache_entries: usize,
expiry_secs: u64,
alert_cooldown_secs: u64,
}
impl BeaconingDetector {
@ -46,262 +29,42 @@ impl BeaconingDetector {
let cfg = app_config.load();
let beaconing = &cfg.detection.beaconing;
Self {
flow_cache: DashMap::new(),
detection_tx,
state: BeaconingState::new(
beaconing.min_observations,
beaconing.cv_threshold,
beaconing.max_cache_entries,
beaconing.expiry_secs,
beaconing.alert_cooldown_secs,
),
alert_rx,
detection_tx,
analysis_interval_secs: beaconing.analysis_interval_secs,
min_observations: beaconing.min_observations,
cv_threshold: beaconing.cv_threshold,
max_cache_entries: beaconing.max_cache_entries,
expiry_secs: beaconing.expiry_secs,
alert_cooldown_secs: beaconing.alert_cooldown_secs,
}
}
/// Spawn the beaconing detector as a background task.
pub fn start(self) {
tokio::spawn(async move { self.run().await });
}
async fn run(mut self) {
log!(DetectionLog::BeaconingDetectorStarted);
let mut analysis_interval = interval(Duration::from_secs(self.analysis_interval_secs));
loop {
tokio::select! {
result = self.alert_rx.recv() => {
match result {
Ok(alert) => self.record_flow(&alert),
Ok(alert) => self.state.record_flow(&alert),
Err(RecvError::Lagged(_)) => continue,
Err(RecvError::Closed) => break,
}
}
_ = analysis_interval.tick() => {
self.analyze_and_alert();
self.cleanup();
for event in self.state.analyze() {
let _ = self.detection_tx.try_send(event);
}
self.state.cleanup();
}
}
}
}
fn record_flow(&self, alert: &AlertMessage) {
let key = (alert.src_ip.clone(), alert.dst_ip.clone(), alert.dst_port);
let now = Instant::now();
let mut entry = self.flow_cache.entry(key).or_insert_with(|| CachedFlow {
timestamps: Vec::new(),
last_alerted: None,
});
entry.timestamps.push(now);
// Cap stored timestamps to avoid unbounded growth per entry
if entry.timestamps.len() > 100 {
let excess = entry.timestamps.len() - 100;
entry.timestamps.drain(..excess);
}
}
fn analyze_and_alert(&self) {
let now = Instant::now();
let cooldown = Duration::from_secs(self.alert_cooldown_secs);
// Phase 1: read-lock scan to find beaconing candidates (avoids holding write locks
// across the entire 50K-entry iteration, reducing contention with record_flow).
let mut alerts: Vec<(FlowTuple, f64, usize)> = Vec::new();
for entry in self.flow_cache.iter() {
let flow = entry.value();
if flow.timestamps.len() < self.min_observations {
continue;
}
if let Some(last) = flow.last_alerted
&& now.duration_since(last) < cooldown
{
continue;
}
let cv = compute_cv(&flow.timestamps);
if cv < self.cv_threshold {
alerts.push((entry.key().clone(), cv, flow.timestamps.len()));
}
}
// Phase 2: selective write-lock only for entries that need last_alerted update.
for (key, cv, count) in alerts {
let (src_ip, dst_ip, dst_port) = &key;
log!(DetectionLog::BeaconingDetected(
src_ip.clone(),
dst_ip.clone(),
*dst_port,
cv,
count,
));
let event = DetectionEvent {
source: DetectionSource::Beaconing,
attack_type: "c2_communication".to_string(),
confidence: (1.0 - cv / self.cv_threshold) as f32 * 0.5 + 0.5,
source_ip: src_ip.clone(),
dest_ip: dst_ip.clone(),
protocol: 6,
packet_count: count as u64,
flow_duration_us: 0,
ae_score: 0.0,
anomaly_score: 0.0,
c2_score: 0.0,
};
let _ = self.detection_tx.try_send(event);
if let Some(mut entry) = self.flow_cache.get_mut(&key) {
entry.last_alerted = Some(now);
}
}
}
fn cleanup(&self) {
let now = Instant::now();
let expiry = Duration::from_secs(self.expiry_secs);
self.flow_cache.retain(|_, flow| {
flow.timestamps
.last()
.is_some_and(|last| now.duration_since(*last) < expiry)
});
// Enforce max capacity
if self.flow_cache.len() > self.max_cache_entries {
let excess = self.flow_cache.len() - self.max_cache_entries;
let keys_to_remove: Vec<FlowTuple> = self.flow_cache.iter().take(excess).map(|e| e.key().clone()).collect();
for key in keys_to_remove {
self.flow_cache.remove(&key);
}
}
}
}
/// Compute the coefficient of variation (std / mean) of inter-arrival times.
/// Returns f64::MAX if fewer than 2 timestamps (no intervals to compute).
fn compute_cv(timestamps: &[Instant]) -> f64 {
if timestamps.len() < 2 {
return f64::MAX;
}
let intervals: Vec<f64> = timestamps
.windows(2)
.map(|w| w[1].duration_since(w[0]).as_secs_f64())
.collect();
let n = intervals.len() as f64;
let mean = intervals.iter().sum::<f64>() / n;
if mean <= 0.0 {
return f64::MAX;
}
let variance = intervals.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
let std = variance.sqrt();
std / mean
}
#[cfg(test)]
mod tests {
use super::*;
fn test_app_config() -> Arc<ArcSwap<AppConfig>> {
Arc::new(ArcSwap::from_pointee(AppConfig {
acl: crate::model::config::acl::AclConfig::defaults(),
auth: crate::model::config::auth::AuthConfig::defaults(),
correlation: crate::model::config::correlation::CorrelationConfig::defaults(),
detection: crate::model::config::detection::DetectionConfig::defaults(),
dns_filter: crate::model::config::dns_filter::DnsFilterConfig::defaults(),
ebpf: crate::model::config::ebpf::EbpfConfig::defaults(),
http_server: crate::model::config::http_server::HttpServerConfig::defaults(),
ml: crate::model::config::ml::MlConfig::defaults(),
notification: crate::model::config::notification::NotificationConfig {
telegram: crate::model::config::notification::TelegramConfig::defaults(),
smtp: crate::model::config::notification::SmtpConfig::defaults(),
},
observability: crate::model::config::observability::ObservabilityConfig::defaults(),
pipeline: crate::model::config::pipeline::PipelineConfig::defaults(),
soar: crate::model::config::soar::SoarConfig::defaults(),
suricata: crate::model::config::suricata::SuricataConfig::defaults(),
system: crate::model::config::system::SystemConfig::defaults(),
}))
}
#[test]
fn cv_perfectly_periodic() {
// Perfectly periodic: CV should be ~0
let base = Instant::now();
let timestamps: Vec<Instant> = (0..10).map(|i| base + Duration::from_secs(i * 60)).collect();
let cv = compute_cv(&timestamps);
assert!(cv < 0.01, "Perfectly periodic CV should be ~0, got {cv}");
}
#[test]
fn cv_random_high() {
// Irregular intervals: CV should be high
let base = Instant::now();
let timestamps = vec![
base,
base + Duration::from_secs(1),
base + Duration::from_secs(100),
base + Duration::from_secs(101),
base + Duration::from_secs(500),
base + Duration::from_secs(501),
];
let cv = compute_cv(&timestamps);
assert!(cv > 0.5, "Random intervals CV should be high, got {cv}");
}
#[test]
fn cv_with_slight_jitter() {
// Periodic with small jitter: CV should be low but > 0
let base = Instant::now();
let timestamps = vec![
base,
base + Duration::from_millis(60_000),
base + Duration::from_millis(121_000), // 61s interval
base + Duration::from_millis(179_000), // 58s interval
base + Duration::from_millis(240_000), // 61s interval
base + Duration::from_millis(299_000), // 59s interval
];
let cv = compute_cv(&timestamps);
assert!(cv < 0.3, "Slight jitter CV should be < 0.3, got {cv}");
}
#[test]
fn cv_insufficient_data() {
let base = Instant::now();
assert_eq!(compute_cv(&[base]), f64::MAX);
assert_eq!(compute_cv(&[]), f64::MAX);
}
#[tokio::test]
async fn beaconing_detector_records_and_detects() {
let (alert_tx, alert_rx) = broadcast::channel(64);
let (detection_tx, mut detection_rx) = mpsc::channel(64);
let detector = BeaconingDetector::new(&test_app_config(), alert_rx, detection_tx);
// Manually record periodic flows
let base = Instant::now();
let key = ("10.0.0.1".to_string(), "1.2.3.4".to_string(), 443_u16);
detector.flow_cache.insert(
key,
CachedFlow {
timestamps: (0..10).map(|i| base + Duration::from_secs(i * 60)).collect(),
last_alerted: None,
},
);
detector.analyze_and_alert();
let event = detection_rx.try_recv().expect("Should detect beaconing");
assert_eq!(event.source, DetectionSource::Beaconing);
assert_eq!(event.attack_type, "c2_communication");
drop(alert_tx);
}
}

View File

@ -1,4 +1,2 @@
pub mod beaconing;
pub mod fusion_math;
pub mod metrics;
pub mod orchestrator;

View File

@ -8,15 +8,15 @@ use macros::log;
use tokio::sync::mpsc;
use tokio::time::interval;
use super::fusion_math::{FusionWindowLengths, fused_confidence};
use super::metrics::FusionMetrics;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::system::SystemError;
use crate::domain::common::event::{AuditEvent, DetectionEvent, DetectionSource, ThreatDetectedEvent};
use crate::domain::detection::attack_type::translate;
use crate::domain::detection::fusion_math::{FusionWindowLengths, fused_confidence};
use crate::domain::detection::log::DetectionLog;
use crate::domain::detection::metrics::FusionMetrics;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::interface::port::geo_lookup::GeoLookup;
use crate::model::config::AppConfig;
use crate::model::detection::attack_type::translate;
use crate::model::error::system::SystemError;
use crate::model::event::{AuditEvent, DetectionEvent, DetectionSource, ThreatDetectedEvent};
use crate::model::log::detection::DetectionLog;
/// Actor recorded on every fusion-chain WORM entry. Stable across releases —
/// downstream audit tooling filters on this string.

View File

@ -1,2 +0,0 @@
pub mod report;
pub mod scheduler;

View File

@ -5,7 +5,7 @@ use actix_web::dev::Payload;
use actix_web::error::ErrorUnauthorized;
use actix_web::{Error as ActixError, FromRequest, HttpMessage, HttpRequest};
use crate::model::identity::auth::Claims;
use crate::domain::identity::auth::Claims;
/// Actix-web extractor that pulls `Claims` from request extensions.
///

View File

@ -3,10 +3,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind};
use crate::domain::common::error::Error;
use crate::domain::identity::auth::Claims;
use crate::domain::identity::error::AuthError;
use crate::interface::port::secret_store::SecretStorePort;
use crate::model::error::Error;
use crate::model::error::auth::AuthError;
use crate::model::identity::auth::Claims;
pub struct JwtService {
encoding_key: EncodingKey,

View File

@ -9,10 +9,10 @@ use actix_web::http::Method;
use actix_web::{Error as ActixError, HttpMessage, HttpResponse, web};
use macros::log;
use crate::core::auth::jwt::JwtService;
use crate::core::identity::jwt::JwtService;
use crate::domain::identity::error::AuthError;
use crate::interface::port::api_key::ApiKeyRepo;
use crate::interface::port::app_repo::AppRepo;
use crate::model::error::auth::AuthError;
pub struct AuthMiddleware;

View File

@ -3,5 +3,4 @@ pub mod extractor;
pub mod https_redirect;
pub mod jwt;
pub mod middleware;
pub mod password;
pub mod setup_guard;

View File

@ -1,8 +1,8 @@
use macros::log;
use tokio::sync::broadcast;
use crate::model::detection::ml_detection::{AlertMessage, DetectionResult};
use crate::model::log::ml::MLLog;
use crate::domain::detection::log::MLLog;
use crate::domain::detection::ml_detection::{AlertMessage, DetectionResult};
pub struct MLAlert {
broadcast_tx: broadcast::Sender<AlertMessage>,

View File

@ -2,9 +2,9 @@ use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::path::{Path, PathBuf};
use super::manifest::{AdapterKind, LabelSpec, ModelManifest};
use crate::model::detection::ml_inference_config::MLInferenceConfig;
use crate::model::error::ml::MLError;
use crate::domain::detection::error::MLError;
use crate::domain::detection::manifest::{AdapterKind, LabelSpec, ModelManifest};
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
impl MLInferenceConfig {
pub fn load_file(file: &str) -> Result<Self, MLError> {
@ -145,7 +145,7 @@ mod tests {
use std::io::Write;
use super::*;
use crate::model::detection::ml_detection::ClipParams;
use crate::domain::detection::ml_detection::ClipParams;
/// Integration test: the shipped `models/manifest.yaml` must successfully pair
/// with its scaler sidecar to yield a valid `MLInferenceConfig`. Skipped silently

View File

@ -0,0 +1,53 @@
use std::time::Duration;
use tokio::sync::{mpsc, oneshot};
use crate::domain::detection::drift::{DriftReport, FeatureBaselines};
use crate::domain::detection::drift_detector::DriftDetector;
enum DriftCmd {
Update(Vec<f64>),
CheckDrift {
reply: oneshot::Sender<Option<DriftReport>>,
},
}
#[derive(Clone)]
pub struct DriftDetectorHandle {
tx: mpsc::Sender<DriftCmd>,
}
impl DriftDetectorHandle {
pub fn spawn(
baselines: Option<FeatureBaselines>,
drift_window: Duration,
max_snapshots: usize,
channel_capacity: usize,
) -> Self {
let (tx, mut rx) = mpsc::channel::<DriftCmd>(channel_capacity);
tokio::spawn(async move {
let mut detector = DriftDetector::new(baselines, drift_window, max_snapshots);
while let Some(cmd) = rx.recv().await {
match cmd {
DriftCmd::Update(features) => detector.update(&features),
DriftCmd::CheckDrift { reply } => {
let _ = reply.send(detector.check_drift());
}
}
}
});
Self { tx }
}
pub fn update(&self, features: Vec<f64>) {
let _ = self.tx.try_send(DriftCmd::Update(features));
}
pub async fn check_drift(&self) -> Option<DriftReport> {
let (reply_tx, reply_rx) = oneshot::channel();
if self.tx.send(DriftCmd::CheckDrift { reply: reply_tx }).await.is_err() {
return None;
}
reply_rx.await.unwrap_or(None)
}
}

View File

@ -12,17 +12,17 @@ use tokio::sync::oneshot;
use tokio::task::spawn_blocking;
use tokio::time::interval;
use super::aggregator::AttackAggregator;
use super::alert::MLAlert;
use super::drift_detector::DriftDetectorHandle;
use super::flow_tracker::{FlowData, FlowLimits, FlowTracker};
use super::inference::Inference;
use super::traffic_logger::TrafficLogger;
use crate::domain::data_plane::user_packet::UserPacket;
use crate::domain::detection::aggregator::AttackAggregator;
use crate::domain::detection::flow_features::FlowFeatures;
use crate::domain::detection::flow_tracker::{FlowData, FlowLimits, FlowTracker};
use crate::domain::detection::log::MLLog;
use crate::domain::detection::ml_detection::{EngineConfig, FlowKey, InferenceStats};
use crate::interface::port::packet_sink::{PacketSink, PacketSinkFactory};
use crate::model::detection::flow_features::FlowFeatures;
use crate::model::detection::ml_detection::{EngineConfig, FlowKey, InferenceStats};
use crate::model::log::ml::MLLog;
use crate::model::monitoring::user_packet::UserPacket;
/// Per-queue tracker. With symmetric hash in eBPF, both directions of a flow
/// land on the same queue, so per-queue trackers correctly see bidirectional flows.

View File

@ -20,15 +20,15 @@ use arc_swap::ArcSwap;
use macros::log;
use tract_onnx::prelude::*;
use super::adapter::{MLModelAdapter, ModelSourceState};
use super::flow_tracker::FlowData;
use super::manifest::LabelSpec;
use crate::model::config::AppConfig;
use crate::model::detection::flow_features::FlowFeatures;
use crate::model::detection::ml_detection::{DetectionResult, RunnableModel};
use crate::model::detection::ml_inference_config::MLInferenceConfig;
use crate::model::detection::model_source::ModelSourceStatus;
use crate::model::log::ml::MLLog;
use crate::domain::common::config::AppConfig;
use crate::domain::detection::flow_features::FlowFeatures;
use crate::domain::detection::flow_tracker::FlowData;
use crate::domain::detection::log::MLLog;
use crate::domain::detection::manifest::LabelSpec;
use crate::domain::detection::ml_detection::{DetectionResult, RunnableModel};
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
use crate::domain::detection::model_adapter::{MLModelAdapter, ModelSourceState};
use crate::domain::detection::model_source::ModelSourceStatus;
/// (anomaly_scores, per_class_probs, c2_scores) — MultiTask batch output.
type ClassifierBatchOutput = (Vec<f32>, Vec<Vec<f32>>, Vec<f32>);

View File

@ -0,0 +1,17 @@
use std::fs;
use std::path::Path;
use crate::domain::detection::error::MLError;
use crate::domain::detection::manifest::ModelManifest;
impl ModelManifest {
pub fn load(path: impl AsRef<Path>) -> Result<Self, MLError> {
let path = path.as_ref();
let content = fs::read_to_string(path)
.map_err(|e| MLError::ManifestInvalid(path.to_path_buf(), format!("read failed: {e}")))?;
let manifest: ModelManifest = serde_yaml_ng::from_str(&content)
.map_err(|e| MLError::ManifestInvalid(path.to_path_buf(), format!("YAML parse: {e}")))?;
manifest.validate(path)?;
Ok(manifest)
}
}

View File

@ -1,11 +1,7 @@
pub mod adapter;
pub mod aggregator;
pub mod alert;
pub mod config_loader;
pub mod drift_detector;
pub mod engine;
pub mod feature_extractor;
pub mod flow_tracker;
pub mod inference;
pub mod manifest;
pub mod model_loader;

View File

@ -16,13 +16,13 @@ use tract_onnx::prelude::*;
use tract_onnx::tract_hir::infer::Factoid;
use tract_onnx::tract_hir::internal::DimLike;
use super::adapter::MLModelAdapter;
use super::manifest::{AdapterKind, LabelSpec, ModelManifest};
use crate::model::config::constants::MODELS_DIR;
use crate::model::detection::ml_detection::RunnableModel;
use crate::model::detection::ml_inference_config::MLInferenceConfig;
use crate::model::error::ml::MLError;
use crate::model::log::ml::MLLog;
use crate::domain::common::config::constants::MODELS_DIR;
use crate::domain::detection::error::MLError;
use crate::domain::detection::log::MLLog;
use crate::domain::detection::manifest::{AdapterKind, LabelSpec, ModelManifest};
use crate::domain::detection::ml_detection::RunnableModel;
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
use crate::domain::detection::model_adapter::MLModelAdapter;
/// Build an `MLModelAdapter` by loading the ONNX file(s) the manifest names,
/// validating shape against the inference config's feature counts, and

View File

@ -14,15 +14,15 @@ use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;
use tokio::time::sleep;
use super::adapter::ModelSourceState;
use super::inference::Inference;
use super::model_loader::build_adapter;
use crate::model::config::AppConfig;
use crate::model::config::constants::{MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR};
use crate::model::detection::ml_inference_config::MLInferenceConfig;
use crate::model::detection::model_source::ModelInfo;
use crate::model::error::ml::MLError;
use crate::model::log::ml::MLLog;
use crate::domain::common::config::AppConfig;
use crate::domain::common::config::constants::{MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR};
use crate::domain::detection::error::MLError;
use crate::domain::detection::log::MLLog;
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
use crate::domain::detection::model_adapter::ModelSourceState;
use crate::domain::detection::model_source::ModelInfo;
pub struct ModelWatcher {
inference: Arc<Inference>,

View File

@ -22,10 +22,10 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crossbeam::channel::{Receiver, Sender, TrySendError, bounded};
use macros::log;
use crate::domain::common::event::AuditEvent;
use crate::domain::detection::error::MLError;
use crate::domain::detection::log::MLLog;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::model::error::ml::MLError;
use crate::model::event::AuditEvent;
use crate::model::log::ml::MLLog;
/// Default per-file size cap. A single CSV file won't grow past this
/// before the writer rolls to a fresh one.

View File

@ -1,15 +1,8 @@
pub mod acl_service;
pub mod auth;
pub mod config_service;
pub mod common;
pub mod correlation;
pub mod data_plane;
pub mod detection;
pub mod dns_filter_service;
pub mod email;
pub mod ml;
pub mod notification_service;
pub mod observability;
pub mod playbook_service;
pub mod rate_limit_service;
pub mod report;
pub mod soar;
pub mod stats_aggregator;
pub mod identity;
pub mod inference;
pub mod reporting;
pub mod response;

View File

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

View File

@ -1,7 +1,7 @@
use chrono::Local;
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
/// Generate an HTML weekly report email body.
///

View File

@ -9,14 +9,14 @@ use macros::log;
use tokio::task::{JoinHandle, spawn_blocking};
use tokio::time::{self, Duration};
use super::report;
use super::email_report as report;
use crate::domain::common::config::AppConfig;
use crate::domain::common::config::notification::SmtpConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::notification::NotificationError;
use crate::domain::common::log::system::SystemLog;
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
use crate::model::config::AppConfig;
use crate::model::config::notification::SmtpConfig;
use crate::model::error::Error;
use crate::model::error::notification::NotificationError;
use crate::model::log::system::SystemLog;
pub struct SmtpClient {
host: String,

View File

@ -0,0 +1,4 @@
pub mod email_report;
pub mod email_scheduler;
pub mod report_engine;
pub mod stats_aggregator;

View File

@ -4,12 +4,12 @@ use std::path::PathBuf;
use chrono::Local;
use macros::log;
use crate::domain::common::error::Error;
use crate::domain::common::error::io::IOError;
use crate::domain::common::error::misc::MiscError;
use crate::domain::common::log::system::SystemLog;
use crate::domain::report::data::ReportData;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
use crate::model::error::io::IOError;
use crate::model::error::misc::MiscError;
use crate::model::log::system::SystemLog;
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

@ -5,10 +5,10 @@ use serde_json::Value;
use tokio::task::JoinHandle;
use tokio::time::{self, Duration};
use crate::domain::common::error::Error;
use crate::domain::common::log::system::SystemLog;
use crate::interface::port::setting::SettingRepo;
use crate::interface::port::stats::StatsRepo;
use crate::model::error::Error;
use crate::model::log::system::SystemLog;
/// Background service that periodically aggregates statistics from SOAR/ML tables
/// and writes them to the settings table for the Report engine to consume.

View File

@ -17,17 +17,17 @@ use tokio::net::lookup_host;
use tokio::task::spawn_blocking;
use url::Url;
use crate::core::email::scheduler::SmtpClient;
use crate::core::playbook_service::ip_version_from_str;
use crate::core::soar::engine::SoarEngine;
use crate::core::reporting::email_scheduler::SmtpClient;
use crate::core::response::engine::SoarEngine;
use crate::core::response::playbook_service::ip_version_from_str;
use crate::domain::common::error::Error;
use crate::domain::common::event::ThreatDetectedEvent;
use crate::domain::response::error::SoarError;
use crate::domain::response::log::SoarLog;
use crate::domain::response::playbook::{Playbook, PlaybookAction};
use crate::interface::port::access_control::AccessControlPort;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::notification::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};
/// Lower bound on the rate-limit factor — anything below 1% of current
/// would brick traffic flow.
@ -59,7 +59,7 @@ impl SoarEngine {
}
// Check admin whitelist
if self.admin_whitelist.load().contains(&event.source_ip) {
if self.matcher.admin_whitelist.load().contains(&event.source_ip) {
log!(SoarLog::WhitelistSkipped(
event.source_ip.clone(),
playbook.name.clone()
@ -150,9 +150,9 @@ impl SoarEngine {
.params
.get("ttl_secs")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| self.config.load().soar.default_block_ttl_secs);
.unwrap_or_else(|| self.matcher.config.load().soar.default_block_ttl_secs);
let soar_cfg = self.config.load().soar.clone();
let soar_cfg = self.matcher.config.load().soar.clone();
let max_ttl = soar_cfg.max_ttl_secs;
let max_cap = soar_cfg.max_auto_block_cap;
if ttl_secs > max_ttl {
@ -161,12 +161,13 @@ impl SoarEngine {
// Atomically check cap and reserve a slot using CAS loop.
loop {
let current_count = self.active_block_count.load(Ordering::SeqCst);
let current_count = self.matcher.active_block_count.load(Ordering::SeqCst);
if current_count >= max_cap {
log!(SoarLog::CapReached(current_count, max_cap, event.source_ip.clone()));
Err(SoarError::CapReached(max_cap))?;
}
if self
.matcher
.active_block_count
.compare_exchange(current_count, current_count + 1, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
@ -234,7 +235,7 @@ impl SoarEngine {
) -> Result<String, Error> {
let owner = self.rate_limit.as_ref().ok_or(SoarError::RateLimitUnavailable)?;
let soar_defaults = self.config.load().soar.clone();
let soar_defaults = self.matcher.config.load().soar.clone();
let factor = action
.params
.get("factor")
@ -250,7 +251,7 @@ impl SoarEngine {
Err(SoarError::InvalidRateLimitFactor(factor))?;
}
let max_ttl = self.config.load().soar.max_ttl_secs;
let max_ttl = self.matcher.config.load().soar.max_ttl_secs;
if ttl_secs > max_ttl {
Err(SoarError::InvalidTtl(ttl_secs, max_ttl))?;
}
@ -295,7 +296,7 @@ impl SoarEngine {
/// Send email alert.
async fn action_send_email(&self, event: &ThreatDetectedEvent) -> Result<String, Error> {
let smtp_cfg = self.config.load().notification.smtp.clone();
let smtp_cfg = self.matcher.config.load().notification.smtp.clone();
match SmtpClient::from_config(&smtp_cfg, self.secrets.as_deref())? {
Some(smtp) => {
let subject = format!(
@ -340,7 +341,7 @@ impl SoarEngine {
.params
.get("timeout_secs")
.and_then(|v| v.as_u64())
.unwrap_or_else(|| self.config.load().soar.default_webhook_timeout_secs);
.unwrap_or_else(|| self.matcher.config.load().soar.default_webhook_timeout_secs);
// Parse URL and extract host
let parsed_url = Url::parse(url_str).map_err(|e| SoarError::ActionFailed("webhook", e))?;
@ -360,7 +361,7 @@ impl SoarEngine {
}
for addr in &addrs {
if Self::is_private_ip(&addr.ip()) {
if crate::domain::response::matcher::is_private_ip(&addr.ip()) {
log!(SoarLog::EventHandlingFailed(format!(
"SSRF blocked: webhook URL '{}' resolved to private IP {}",
url_str,
@ -433,7 +434,7 @@ impl SoarEngine {
/// Only fires when source_ip is present.
pub(super) async fn execute_fallback(&self, event: &ThreatDetectedEvent) -> Result<(), Error> {
// Check admin whitelist — never block admin IPs even in fallback
if self.admin_whitelist.load().contains(&event.source_ip) {
if self.matcher.admin_whitelist.load().contains(&event.source_ip) {
log!(SoarLog::WhitelistSkipped(
event.source_ip.clone(),
"fallback".to_string()
@ -442,7 +443,7 @@ impl SoarEngine {
}
// Check cooldown — uses FALLBACK_PLAYBOOK_ID as the synthetic key
let fallback_cfg = self.config.load().soar.clone();
let fallback_cfg = self.matcher.config.load().soar.clone();
if self.is_cooldown_active(
FALLBACK_PLAYBOOK_ID,
&event.source_ip,

View File

@ -1,18 +1,24 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
use std::time::Instant;
use std::sync::atomic::{AtomicU8, Ordering};
use arc_swap::ArcSwap;
use dashmap::DashMap;
use macros::log;
use serde_json::Value;
use tokio::sync::Semaphore;
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use crate::core::soar::frequency::FrequencyTracker;
use crate::core::soar::rate_limit_owner::RateLimitOwnerHandle;
use crate::core::response::rate_limit_owner::RateLimitOwnerHandle;
use crate::domain::common::config::AppConfig;
use crate::domain::common::error::Error;
use crate::domain::common::event::ThreatDetectedEvent;
use crate::domain::detection::attack_type::canonical_from_str;
use crate::domain::response::condition::{ConditionType, PlaybookCondition};
use crate::domain::response::error::SoarError;
use crate::domain::response::log::SoarLog;
use crate::domain::response::matcher::PlaybookMatcher;
use crate::domain::response::playbook::{Playbook, PlaybookAction};
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::interface::port::access_control::AccessControlPort;
use crate::interface::port::app_repo::AppRepo;
@ -20,41 +26,11 @@ use crate::interface::port::geo_lookup::GeoLookup;
use crate::interface::port::notification::AlertNotifier;
use crate::interface::port::rate_limit_api::RateLimitPort;
use crate::interface::port::secret_store::SecretStorePort;
use crate::model::config::AppConfig;
use crate::model::detection::attack_type::canonical_from_str;
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::condition::{ConditionType, PlaybookCondition};
use crate::model::soar::playbook::{Playbook, PlaybookAction};
/// Cooldown key: (playbook_id, source_ip)
type CooldownKey = (i64, String);
/// SOAR Engine — subscribes to ThreatDetectedEvent and executes matching playbooks.
///
/// The engine is intentionally split across three files within `core::soar`:
/// - `engine.rs` (this file) — struct definition, lifecycle (new/start/event_loop),
/// cache reload, recovery, rate-limit-TTL restoration
/// - `matcher.rs` — domain: playbook matching, condition evaluation, cooldowns
/// - `actions.rs` — application: action dispatch + all action_* implementations
///
/// Fields are `pub(super)` so the sibling files can read them; external
/// callers still see the struct via its public methods only.
pub struct SoarEngine {
pub(super) db: Arc<dyn AppRepo>,
pub(super) access_control: Arc<dyn AccessControlPort>,
/// In-memory cache of playbooks (loaded at startup, refreshed on change).
pub(super) playbooks: ArcSwap<Vec<Arc<Playbook>>>,
/// In-memory cache of admin whitelist IPs.
pub(super) admin_whitelist: ArcSwap<HashSet<String>>,
/// Cooldown tracker: maps (playbook_id, source_ip) → last execution time.
pub(super) cooldowns: DashMap<CooldownKey, Instant>,
/// Frequency tracker for frequency-based conditions.
pub(super) frequency_tracker: FrequencyTracker,
/// AtomicU32 counter for active auto-blocks (avoids DB query per event).
pub(super) active_block_count: AtomicU32,
pub(super) matcher: PlaybookMatcher,
/// Optional alert notifier (Telegram, etc.).
pub(super) alert_notifier: Option<Arc<dyn AlertNotifier>>,
/// Optional GeoIP service for country lookups.
@ -68,7 +44,6 @@ pub struct SoarEngine {
pub(super) enforce_level_cache: Arc<AtomicU8>,
/// Secret store for decrypting SMTP passwords etc.
pub(super) secrets: Option<Arc<dyn SecretStorePort>>,
pub(super) config: Arc<ArcSwap<AppConfig>>,
}
impl SoarEngine {
@ -87,20 +62,16 @@ impl SoarEngine {
let freq_max_keys = soar_cfg.soar.frequency_max_tracked_keys;
drop(soar_cfg);
let rate_limit_owner = rate_limit.map(|rl| RateLimitOwnerHandle::spawn(db.clone(), rl, rate_limit_channel));
let matcher = PlaybookMatcher::new(config, freq_max_keys);
let engine = Self {
db,
access_control,
playbooks: ArcSwap::from_pointee(Vec::new()),
admin_whitelist: ArcSwap::from_pointee(HashSet::new()),
cooldowns: DashMap::new(),
frequency_tracker: FrequencyTracker::new(freq_max_keys),
active_block_count: AtomicU32::new(0),
matcher,
alert_notifier,
geoip,
rate_limit: rate_limit_owner,
enforce_level_cache,
secrets,
config,
};
engine.reload_cache()?;
Ok(engine)
@ -213,17 +184,17 @@ impl SoarEngine {
// hand out cheap Arc clones instead of cloning the full Playbook
// (with its nested Vec<PlaybookCondition> / Vec<PlaybookAction>).
let playbooks: Vec<Arc<Playbook>> = playbooks.into_iter().map(Arc::new).collect();
self.playbooks.store(Arc::new(playbooks));
self.matcher.playbooks.store(Arc::new(playbooks));
// Load admin whitelist
let whitelist = self.db.list_admin_whitelist()?;
let whitelist: HashSet<String> = whitelist.into_iter().collect();
let whitelist_count = whitelist.len();
self.admin_whitelist.store(Arc::new(whitelist));
self.matcher.admin_whitelist.store(Arc::new(whitelist));
// Initialize block counter from DB
let count = self.db.count_active_soar_blocks()?;
self.active_block_count.store(count, Ordering::SeqCst);
self.matcher.active_block_count.store(count, Ordering::SeqCst);
log!(SoarLog::CacheLoaded(playbook_count, whitelist_count, count));
@ -248,7 +219,7 @@ impl SoarEngine {
async fn event_loop(self: Arc<Self>, mut rx: broadcast::Receiver<ThreatDetectedEvent>) {
log!(SoarLog::EngineStarted);
let concurrency = self.config.load().soar.handle_concurrency.max(1);
let concurrency = self.matcher.config.load().soar.handle_concurrency.max(1);
let semaphore = Arc::new(Semaphore::new(concurrency));
loop {
match rx.recv().await {
@ -338,7 +309,7 @@ impl SoarEngine {
};
for (id, source_ip, retry_count) in pending {
if retry_count >= self.config.load().soar.max_pending_unblock_retries {
if retry_count >= self.matcher.config.load().soar.max_pending_unblock_retries {
log!(SoarLog::EventHandlingFailed(format!(
"Giving up on pending unblock for IP {} after {} retries",
source_ip, retry_count
@ -378,6 +349,35 @@ impl SoarEngine {
None => Ok(()),
}
}
pub fn find_matching_playbooks(&self, event: &ThreatDetectedEvent) -> Vec<Arc<Playbook>> {
self.matcher.find_matching_playbooks(event)
}
#[cfg(test)]
pub fn evaluate_conditions(&self, pb: &Playbook, event: &ThreatDetectedEvent) -> bool {
self.matcher.evaluate_conditions(pb, event)
}
pub fn is_cooldown_active(&self, playbook_id: i64, source_ip: &str, cooldown_secs: i64) -> bool {
self.matcher.is_cooldown_active(playbook_id, source_ip, cooldown_secs)
}
pub fn record_cooldown(&self, playbook_id: i64, source_ip: &str) {
self.matcher.record_cooldown(playbook_id, source_ip)
}
pub fn cleanup_expired_cooldowns(&self) {
self.matcher.cleanup_expired_cooldowns()
}
pub fn decrement_block_count(&self) {
self.matcher.decrement_block_count()
}
pub fn dry_run(&self, event: &ThreatDetectedEvent) -> Vec<crate::domain::response::dry_run::DryRunMatch> {
self.matcher.dry_run(event)
}
}
#[cfg(test)]
@ -388,8 +388,8 @@ mod tests {
use parking_lot::Mutex;
use super::*;
use crate::model::error::ebpf::EbpfError;
use crate::model::event::DetectionSource;
use crate::domain::common::event::DetectionSource;
use crate::domain::data_plane::error::EbpfError;
/// Mock AccessControlPort that records calls.
struct MockAccessControl {
@ -595,7 +595,7 @@ mod tests {
let engine = test_engine(mock.clone());
// Set counter to max (default cap is 100)
engine.active_block_count.store(100, Ordering::SeqCst);
engine.matcher.active_block_count.store(100, Ordering::SeqCst);
let event = ThreatDetectedEvent {
source_ip: "1.2.3.4".to_string(),
@ -730,22 +730,22 @@ mod tests {
let engine = test_engine(mock);
// Start at 0
assert_eq!(engine.active_block_count.load(Ordering::SeqCst), 0);
assert_eq!(engine.matcher.active_block_count.load(Ordering::SeqCst), 0);
// Decrement should not underflow
engine.decrement_block_count();
assert_eq!(engine.active_block_count.load(Ordering::SeqCst), 0);
assert_eq!(engine.matcher.active_block_count.load(Ordering::SeqCst), 0);
// Set to 2, decrement twice → should be 0
engine.active_block_count.store(2, Ordering::SeqCst);
engine.matcher.active_block_count.store(2, Ordering::SeqCst);
engine.decrement_block_count();
assert_eq!(engine.active_block_count.load(Ordering::SeqCst), 1);
assert_eq!(engine.matcher.active_block_count.load(Ordering::SeqCst), 1);
engine.decrement_block_count();
assert_eq!(engine.active_block_count.load(Ordering::SeqCst), 0);
assert_eq!(engine.matcher.active_block_count.load(Ordering::SeqCst), 0);
// One more decrement should stay at 0
engine.decrement_block_count();
assert_eq!(engine.active_block_count.load(Ordering::SeqCst), 0);
assert_eq!(engine.matcher.active_block_count.load(Ordering::SeqCst), 0);
}
#[test]
@ -771,18 +771,18 @@ mod tests {
.expect("Failed to create engine");
// Should have loaded default playbooks
let count = engine.playbooks.load().len();
let count = engine.matcher.playbooks.load().len();
assert!(count > 0, "Should have loaded default playbooks");
// Add a new playbook directly to DB
db.insert_playbook("test_pb", "port_scan", None, None, None, 60).ok();
// Cache should not have it yet
assert_eq!(engine.playbooks.load().len(), count);
assert_eq!(engine.matcher.playbooks.load().len(), count);
// After reload, should have one more
engine.reload_cache().expect("reload should succeed");
assert_eq!(engine.playbooks.load().len(), count + 1);
assert_eq!(engine.matcher.playbooks.load().len(), count + 1);
}
fn test_event(confidence: f32, country: Option<&str>, ip: &str, repeat: bool) -> ThreatDetectedEvent {

View File

@ -1,6 +1,5 @@
pub mod actions;
pub mod engine;
pub mod frequency;
pub mod matcher;
pub mod playbook_service;
pub mod rate_limit_owner;
pub mod scheduler;

View File

@ -4,14 +4,14 @@ use std::sync::Arc;
use serde_json::Value;
use crate::core::soar::engine::SoarEngine;
use crate::interface::port::access_control::AccessControlPort;
use crate::interface::port::app_repo::AppRepo;
use crate::model::error::Error;
use crate::model::error::soar::SoarError;
use crate::model::soar::playbook_data::{
use crate::core::response::engine::SoarEngine;
use crate::domain::common::error::Error;
use crate::domain::response::error::SoarError;
use crate::domain::response::playbook_data::{
ActionView, ActiveBlockView, ConditionView, CreatePlaybookInput, ExecutionView, PlaybookView, UpdatePlaybookInput,
};
use crate::interface::port::access_control::AccessControlPort;
use crate::interface::port::app_repo::AppRepo;
/// Domain service for SOAR playbook CRUD operations.
/// Coordinates DB reads/writes, SOAR engine cache refresh, and eBPF unblock.

View File

@ -14,11 +14,11 @@ use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
use macros::log;
use tokio::sync::{mpsc, oneshot};
use crate::domain::common::error::Error;
use crate::domain::response::error::SoarError;
use crate::domain::response::log::SoarLog;
use crate::interface::port::app_repo::AppRepo;
use crate::interface::port::rate_limit_api::RateLimitPort;
use crate::model::error::Error;
use crate::model::error::soar::SoarError;
use crate::model::log::soar::SoarLog;
/// Settings keys persisted across restarts so the next process can resume the
/// same TTL window. Stable wire format with the DB.

View File

@ -4,12 +4,12 @@ use macros::log;
use tokio::task::JoinHandle;
use tokio::time::{self, Duration};
use crate::core::playbook_service::ip_version_from_str;
use crate::core::soar::engine::SoarEngine;
use crate::core::response::engine::SoarEngine;
use crate::core::response::playbook_service::ip_version_from_str;
use crate::domain::common::error::Error;
use crate::domain::response::log::SoarLog;
use crate::interface::port::access_control::AccessControlPort;
use crate::interface::port::app_repo::AppRepo;
use crate::model::error::Error;
use crate::model::log::soar::SoarLog;
/// TTL expiry scheduler: runs every 60 seconds, removes expired auto-block rules.
/// Before removing from eBPF, checks if a manual ACL rule exists for the same IP.

View File

@ -1,6 +1,6 @@
use super::helpers::{override_string_nonempty, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct AclConfig {

View File

@ -1,6 +1,6 @@
use super::helpers::{override_parsed, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct AuthConfig {

View File

@ -1,6 +1,6 @@
use super::helpers::{override_parsed, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
/// Shared shape for scan / lateral / botnet correlation detectors.
#[derive(Debug, Clone)]

View File

@ -1,6 +1,6 @@
use super::helpers::{override_parsed, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct DetectionConfig {

View File

@ -1,6 +1,6 @@
use super::helpers::{override_parsed, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct DnsFilterConfig {

View File

@ -1,6 +1,6 @@
use super::helpers::{override_parsed, override_string, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct EbpfConfig {

View File

@ -1,9 +1,13 @@
use std::str::FromStr;
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
pub(super) fn override_parsed<T: FromStr>(target: &mut T, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
pub(in crate::domain) fn override_parsed<T: FromStr>(
target: &mut T,
repo: &dyn SettingRepo,
key: &str,
) -> Result<(), Error> {
if let Some(v) = repo.get_setting(key)?
&& let Ok(parsed) = v.parse()
{
@ -12,21 +16,25 @@ pub(super) fn override_parsed<T: FromStr>(target: &mut T, repo: &dyn SettingRepo
Ok(())
}
pub(super) fn override_bool(target: &mut bool, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
pub(in crate::domain) fn override_bool(target: &mut bool, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
if let Some(v) = repo.get_setting(key)? {
*target = v == "true" || v == "1";
}
Ok(())
}
pub(super) fn override_string(target: &mut String, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
pub(in crate::domain) fn override_string(target: &mut String, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
if let Some(v) = repo.get_setting(key)? {
*target = v;
}
Ok(())
}
pub(super) fn override_string_nonempty(target: &mut String, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
pub(in crate::domain) fn override_string_nonempty(
target: &mut String,
repo: &dyn SettingRepo,
key: &str,
) -> Result<(), Error> {
if let Some(v) = repo.get_setting(key)?
&& !v.is_empty()
{
@ -35,7 +43,11 @@ pub(super) fn override_string_nonempty(target: &mut String, repo: &dyn SettingRe
Ok(())
}
pub(super) fn override_csv(target: &mut Vec<String>, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
pub(in crate::domain) fn override_csv(
target: &mut Vec<String>,
repo: &dyn SettingRepo,
key: &str,
) -> Result<(), Error> {
if let Some(v) = repo.get_setting(key)? {
*target = if v.is_empty() {
Vec::new()
@ -46,7 +58,7 @@ pub(super) fn override_csv(target: &mut Vec<String>, repo: &dyn SettingRepo, key
Ok(())
}
pub(super) fn seed_key(repo: &dyn SettingRepo, key: &str, value: &str) -> Result<(), Error> {
pub(in crate::domain) fn seed_key(repo: &dyn SettingRepo, key: &str, value: &str) -> Result<(), Error> {
if repo.get_setting(key)?.is_none() {
repo.set_setting(key, value)?;
}

View File

@ -1,6 +1,6 @@
use super::helpers::{override_bool, override_csv, override_parsed, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct HttpServerConfig {

View File

@ -1,6 +1,6 @@
use super::helpers::{override_bool, override_parsed, override_string_nonempty, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct MlConfig {

View File

@ -15,23 +15,23 @@ pub mod soar;
pub mod suricata;
pub mod system;
use crate::domain::common::config::acl::AclConfig;
use crate::domain::common::config::auth::AuthConfig;
use crate::domain::common::config::correlation::CorrelationConfig;
use crate::domain::common::config::detection::DetectionConfig;
use crate::domain::common::config::dns_filter::DnsFilterConfig;
use crate::domain::common::config::ebpf::EbpfConfig;
use crate::domain::common::config::http_server::HttpServerConfig;
use crate::domain::common::config::ml::MlConfig;
use crate::domain::common::config::notification::NotificationConfig;
use crate::domain::common::config::observability::ObservabilityConfig;
use crate::domain::common::config::pipeline::PipelineConfig;
use crate::domain::common::config::soar::SoarConfig;
use crate::domain::common::config::suricata::SuricataConfig;
use crate::domain::common::config::system::SystemConfig;
use crate::domain::common::error::Error;
use crate::domain::common::error::system::SystemError;
use crate::interface::port::setting::SettingRepo;
use crate::model::config::acl::AclConfig;
use crate::model::config::auth::AuthConfig;
use crate::model::config::correlation::CorrelationConfig;
use crate::model::config::detection::DetectionConfig;
use crate::model::config::dns_filter::DnsFilterConfig;
use crate::model::config::ebpf::EbpfConfig;
use crate::model::config::http_server::HttpServerConfig;
use crate::model::config::ml::MlConfig;
use crate::model::config::notification::NotificationConfig;
use crate::model::config::observability::ObservabilityConfig;
use crate::model::config::pipeline::PipelineConfig;
use crate::model::config::soar::SoarConfig;
use crate::model::config::suricata::SuricataConfig;
use crate::model::config::system::SystemConfig;
use crate::model::error::Error;
use crate::model::error::system::SystemError;
#[derive(Debug, Clone)]
pub struct AppConfig {

View File

@ -1,6 +1,6 @@
use super::helpers::{override_parsed, override_string, seed_key};
use crate::domain::common::error::Error;
use crate::interface::port::setting::SettingRepo;
use crate::model::error::Error;
#[derive(Debug, Clone)]
pub struct TelegramConfig {

Some files were not shown because too many files have changed in this diff Show More