fix: CI Failed

This commit is contained in:
DaLaw2 2026-04-25 19:18:56 +08:00
parent 965480fb9e
commit c2c4ed5b23
29 changed files with 221 additions and 209 deletions

View File

@ -31,10 +31,6 @@ impl DropMonitor {
self.broadcast_tx.subscribe()
}
pub fn get_counters(&self) -> DropCounters {
self.counters.snapshot()
}
/// Record a userspace drop decision (XSK worker's DNS filter) by the
/// per-reason counter. Callers at this layer haven't parsed src/dst yet,
/// so no broadcast event is emitted — `/api/stats/drops` stays correct,

View File

@ -123,7 +123,6 @@ impl XskManager {
network.clone(),
queue_id,
&network.ingress_ifname,
&network.egress_ifname,
Direction::Ingress,
sink.clone(),
dns_filter.clone(),
@ -134,7 +133,6 @@ impl XskManager {
network.clone(),
queue_id,
&network.egress_ifname,
&network.ingress_ifname,
Direction::Egress,
sink,
None,
@ -194,7 +192,6 @@ impl XskPair {
config: EbpfConfig,
queue_id: u32,
rx_ifname: &str,
_tx_ifname: &str,
direction: Direction,
sink: Option<Arc<dyn PacketSink>>,
dns_filter: Option<Arc<dyn DnsQueryFilter>>,

View File

@ -5,8 +5,8 @@ use common::model::http_method::HttpMethod;
use serde::Deserialize;
use crate::adapter::http::response::ok_or_error;
use crate::interface::port::protocol_filter::ProtocolFilterPort;
use crate::core::data_plane::dns_filter_service::DnsFilterService;
use crate::interface::port::protocol_filter::ProtocolFilterPort;
pub fn initialize() -> Scope {
web::scope("/filter")
@ -152,11 +152,17 @@ async fn get_ipv6_ssh_service(service: web::Data<dyn ProtocolFilterPort>) -> imp
HttpResponse::Ok().json(service.get_ipv6_ssh_service())
}
async fn add_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
async fn add_ipv4_ssh_service(
ip_addr: web::Json<SocketAddrV4>,
service: web::Data<dyn ProtocolFilterPort>,
) -> impl Responder {
ok_or_error(service.add_ipv4_ssh_service(ip_addr.into_inner()))
}
async fn add_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
async fn add_ipv6_ssh_service(
ip_addr: web::Json<SocketAddrV6>,
service: web::Data<dyn ProtocolFilterPort>,
) -> impl Responder {
ok_or_error(service.add_ipv6_ssh_service(ip_addr.into_inner()))
}
@ -196,11 +202,17 @@ async fn get_ipv6_ssh_white_list(service: web::Data<dyn ProtocolFilterPort>) ->
HttpResponse::Ok().json(service.get_ipv6_ssh_white_list())
}
async fn add_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
async fn add_ipv4_ssh_white_list(
ip_addr: web::Json<Ipv4Addr>,
service: web::Data<dyn ProtocolFilterPort>,
) -> impl Responder {
ok_or_error(service.add_ipv4_ssh_white_list(ip_addr.into_inner()))
}
async fn add_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
async fn add_ipv6_ssh_white_list(
ip_addr: web::Json<Ipv6Addr>,
service: web::Data<dyn ProtocolFilterPort>,
) -> impl Responder {
ok_or_error(service.add_ipv6_ssh_white_list(ip_addr.into_inner()))
}
@ -228,11 +240,17 @@ async fn get_ipv6_ssh_black_list(service: web::Data<dyn ProtocolFilterPort>) ->
HttpResponse::Ok().json(service.get_ipv6_ssh_black_list())
}
async fn add_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
async fn add_ipv4_ssh_black_list(
ip_addr: web::Json<Ipv4Addr>,
service: web::Data<dyn ProtocolFilterPort>,
) -> impl Responder {
ok_or_error(service.add_ipv4_ssh_black_list(ip_addr.into_inner()))
}
async fn add_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
async fn add_ipv6_ssh_black_list(
ip_addr: web::Json<Ipv6Addr>,
service: web::Data<dyn ProtocolFilterPort>,
) -> impl Responder {
ok_or_error(service.add_ipv6_ssh_black_list(ip_addr.into_inner()))
}

View File

@ -3,7 +3,7 @@ use tokio::sync::broadcast;
use crate::core::identity::extractor::AuthClaims;
use crate::core::inference::engine::Engine;
use crate::core::inference::inference::Inference;
use crate::core::inference::runner::Inference;
use crate::domain::common::config::constants::AUDIT_ACTOR_SECURITY_ADMIN_PREFIX;
use crate::domain::common::event::AuditEvent;
use crate::domain::detection::model_adapter::ModelSourceState;

View File

@ -36,8 +36,8 @@ use tokio::task;
use uuid::Uuid;
use crate::core::identity::extractor::AuthClaims;
use crate::core::inference::inference::Inference;
use crate::core::inference::model_loader::build_adapter;
use crate::core::inference::runner::Inference;
use crate::domain::common::config::AppConfig;
use crate::domain::common::config::constants::{
AUDIT_ACTOR_SECURITY_ADMIN_PREFIX, MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR,
@ -164,16 +164,16 @@ async fn upload(
return e.into_response();
}
};
let outcome = validate_and_promote(
&staging_dir,
&summary,
inference.get_ref(),
audit_tx.get_ref(),
promote_lock.get_ref(),
&claims.username,
let outcome = validate_and_promote(&PromoteContext {
staging_dir: &staging_dir,
summary: &summary,
inference: inference.get_ref(),
audit_tx: audit_tx.get_ref(),
promote_lock: promote_lock.get_ref(),
actor_username: &claims.username,
batch_size,
onnx_load_timeout,
)
})
.await;
// Always sweep staging — successful promote renames the files out,
@ -512,16 +512,26 @@ pub fn looks_like_onnx(first_chunk: &[u8]) -> bool {
/// logged but does not roll back the rename; the chain prefers a
/// missing audit entry to a rolled-back promote that a downstream
/// subscriber may already have reacted to.
async fn validate_and_promote(
staging_dir: &Path,
summary: &UploadSummary,
inference: &Inference,
audit_tx: &broadcast::Sender<AuditEvent>,
promote_lock: &PromoteGate,
actor_username: &str,
struct PromoteContext<'a> {
staging_dir: &'a Path,
summary: &'a UploadSummary,
inference: &'a Inference,
audit_tx: &'a broadcast::Sender<AuditEvent>,
promote_lock: &'a PromoteGate,
actor_username: &'a str,
batch_size: usize,
onnx_load_timeout: Duration,
) -> Result<PromoteReport, PromoteError> {
}
async fn validate_and_promote(ctx: &PromoteContext<'_>) -> Result<PromoteReport, PromoteError> {
let staging_dir = ctx.staging_dir;
let summary = ctx.summary;
let inference = ctx.inference;
let audit_tx = ctx.audit_tx;
let promote_lock = ctx.promote_lock;
let actor_username = ctx.actor_username;
let batch_size = ctx.batch_size;
let onnx_load_timeout = ctx.onnx_load_timeout;
let staging_manifest = staging_dir.join(MANIFEST_FILENAME);
// Structural manifest validation. The full `build_adapter` pipeline

View File

@ -106,7 +106,7 @@ impl Database {
Ok(conn.last_insert_rowid())
}
pub fn list_api_keys(&self) -> Result<Vec<(i64, String, String, String, Option<String>)>, Error> {
pub fn list_api_keys(&self) -> Result<Vec<ApiKeyListItem>, Error> {
let conn = self.conn()?;
let mut stmt = conn.prepare("SELECT id, name, permission_level, created_at, last_used_at FROM api_keys")?;
let rows = stmt.query_map([], |row| {

View File

@ -26,7 +26,7 @@ impl Database {
Ok(())
}
pub fn get_app_secret(&self, key: &str) -> Result<Option<String>, Error> {
fn get_app_secret(&self, key: &str) -> Result<Option<String>, Error> {
let conn = self.conn()?;
let result = conn.query_row("SELECT value FROM app_secrets WHERE key = ?1", params![key], |row| {
row.get(0)
@ -38,7 +38,7 @@ impl Database {
}
}
pub fn set_app_secret(&self, key: &str, value: &str) -> Result<(), Error> {
fn set_app_secret(&self, key: &str, value: &str) -> Result<(), Error> {
let conn = self.conn()?;
conn.execute(
"INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?1, ?2)",

View File

@ -6,7 +6,8 @@ use serde_json::Value;
use super::Database;
use crate::domain::common::error::Error;
use crate::domain::response::playbook_data::{
ActionView, ActiveBlockView, ConditionView, ExecutionView, PendingUnblock, PlaybookView, UpdatePlaybookInput,
ActionView, ActiveBlockView, ConditionView, CreatePlaybookInput, ExecutionView, PendingUnblock, PlaybookView,
UpdatePlaybookInput,
};
use crate::interface::port::soar::SoarRepo;
@ -369,12 +370,7 @@ impl SoarRepo for Database {
fn insert_playbook_atomic(
&self,
name: &str,
trigger_event: &str,
threshold: Option<f64>,
count: Option<i64>,
window: Option<i64>,
cooldown: i64,
input: &CreatePlaybookInput,
actions: &[(i64, String, String)],
conditions: &[(String, String, String, Option<String>)],
) -> Result<i64, Error> {
@ -382,7 +378,7 @@ impl SoarRepo for Database {
let tx = conn.transaction()?;
tx.execute(
"INSERT INTO playbooks (name, trigger_event, condition_threshold, condition_count, condition_window_secs, cooldown_secs) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![name, trigger_event, threshold, count, window, cooldown],
params![input.name, input.trigger_event, input.condition_threshold, input.condition_count, input.condition_window_secs, input.cooldown_secs],
)?;
let playbook_id = tx.last_insert_rowid();
for (action_order, action_type, params_json) in actions {
@ -457,11 +453,21 @@ mod tests {
use crate::interface::port::soar::SoarRepo;
let db = test_db();
use crate::domain::response::playbook_data::CreatePlaybookInput;
let input = CreatePlaybookInput {
name: "atom_pb".to_string(),
trigger_event: "threat".to_string(),
condition_threshold: Some(0.8),
condition_count: None,
condition_window_secs: None,
cooldown_secs: 300,
actions: vec![("block_ip".to_string(), "{}".to_string())],
conditions: vec![],
};
let actions = vec![(1i64, "block_ip".to_string(), "{}".to_string())];
let conditions = vec![("threshold".to_string(), ">=".to_string(), "0.8".to_string(), None)];
let id = db
.insert_playbook_atomic("atom_pb", "threat", Some(0.8), None, None, 300, &actions, &conditions)
.unwrap();
let id = db.insert_playbook_atomic(&input, &actions, &conditions).unwrap();
assert!(id > 0);
let loaded = db.list_playbooks().unwrap();
assert!(!loaded.is_empty());

View File

@ -14,7 +14,7 @@ use tokio::time::interval;
use super::alert::MLAlert;
use super::drift_detector::DriftDetectorHandle;
use super::inference::Inference;
use super::runner::Inference;
use super::traffic_logger::TrafficLogger;
use crate::domain::data_plane::user_packet::UserPacket;
use crate::domain::detection::aggregator::AttackAggregator;

View File

@ -2,8 +2,8 @@ pub mod alert;
pub mod config_loader;
pub mod drift_detector;
pub mod engine;
pub mod inference;
pub mod manifest;
pub mod model_loader;
pub mod model_watcher;
pub mod runner;
pub mod traffic_logger;

View File

@ -14,8 +14,8 @@ use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc;
use tokio::time::sleep;
use super::inference::Inference;
use super::model_loader::build_adapter;
use super::runner::Inference;
use crate::domain::common::config::AppConfig;
use crate::domain::common::config::constants::{MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR};
use crate::domain::detection::error::MLError;

View File

@ -139,26 +139,7 @@ impl Inference {
/// Dispatch on adapter variant.
fn infer_batch_inner(&self, adapter: &MLModelAdapter, flows: &[FlowData]) -> Vec<DetectionResult> {
match adapter {
MLModelAdapter::MultiTask {
ae,
classifier,
batch_size,
n_ae,
n_cls,
labels,
normal_idx,
c2_idx,
} => self.infer_multitask(
ae,
classifier,
*batch_size,
*n_ae,
*n_cls,
labels,
*normal_idx,
*c2_idx,
flows,
),
MLModelAdapter::MultiTask { .. } => self.infer_multitask(adapter, flows),
MLModelAdapter::AutoencoderOnly {
model,
batch_size,
@ -177,18 +158,22 @@ impl Inference {
/// MultiTask path. Runs the AE batch → computes per-flow MSE → feeds the
/// classifier over (ae_features ++ ae_score) → fires on anomaly OR
/// non-Normal classifier agreement OR elevated C2 head.
fn infer_multitask(
&self,
ae: &RunnableModel,
classifier: &RunnableModel,
batch_size: usize,
n_ae: usize,
n_cls: usize,
labels: &BTreeMap<String, LabelSpec>,
normal_idx: Option<usize>,
c2_idx: Option<usize>,
flows: &[FlowData],
) -> Vec<DetectionResult> {
fn infer_multitask(&self, adapter: &MLModelAdapter, flows: &[FlowData]) -> Vec<DetectionResult> {
let MLModelAdapter::MultiTask {
ae,
classifier,
batch_size,
n_ae,
n_cls,
labels,
normal_idx,
c2_idx,
} = adapter
else {
unreachable!()
};
let (batch_size, n_ae, n_cls) = (*batch_size, *n_ae, *n_cls);
let (normal_idx, c2_idx) = (*normal_idx, *c2_idx);
let n = flows.len();
let all_ae_features: Vec<Vec<f32>> = flows.iter().map(|f| self.preprocess_ae_features(f)).collect();

View File

@ -43,17 +43,29 @@ pub struct SoarEngine {
pub(super) secrets: Option<Arc<dyn SecretStorePort>>,
}
pub struct SoarEngineDeps {
pub db: Arc<dyn AppRepo>,
pub config: Arc<ArcSwap<AppConfig>>,
pub access_control: Arc<dyn AccessControlPort>,
pub alert_notifier: Option<Arc<dyn AlertNotifier>>,
pub geoip: Option<Arc<dyn GeoLookup>>,
pub rate_limit: Option<Arc<dyn RateLimitPort>>,
pub enforce_level_cache: Arc<AtomicU8>,
pub secrets: Option<Arc<dyn SecretStorePort>>,
}
impl SoarEngine {
pub fn new(
db: Arc<dyn AppRepo>,
config: Arc<ArcSwap<AppConfig>>,
access_control: Arc<dyn AccessControlPort>,
alert_notifier: Option<Arc<dyn AlertNotifier>>,
geoip: Option<Arc<dyn GeoLookup>>,
rate_limit: Option<Arc<dyn RateLimitPort>>,
enforce_level_cache: Arc<AtomicU8>,
secrets: Option<Arc<dyn SecretStorePort>>,
) -> Result<Self, Error> {
pub fn new(deps: SoarEngineDeps) -> Result<Self, Error> {
let SoarEngineDeps {
db,
config,
access_control,
alert_notifier,
geoip,
rate_limit,
enforce_level_cache,
secrets,
} = deps;
let soar_cfg = config.load();
let rate_limit_channel = soar_cfg.soar.rate_limit_cmd_channel_capacity;
let freq_max_keys = soar_cfg.soar.frequency_max_tracked_keys;
@ -398,8 +410,17 @@ mod tests {
AppConfig::seed_defaults(&*db).expect("seed config defaults");
let cfg = AppConfig::from_settings(&*db).expect("load config");
let config = Arc::new(ArcSwap::from_pointee(cfg));
SoarEngine::new(db as Arc<dyn AppRepo>, config, ac, None, None, None, cache, None)
.expect("Failed to create SOAR engine")
SoarEngine::new(SoarEngineDeps {
db: db as Arc<dyn AppRepo>,
config,
access_control: ac,
alert_notifier: None,
geoip: None,
rate_limit: None,
enforce_level_cache: cache,
secrets: None,
})
.expect("Failed to create SOAR engine")
}
#[tokio::test]
@ -492,16 +513,16 @@ mod tests {
AppConfig::seed_defaults(&*db).expect("seed config defaults");
let cfg = AppConfig::from_settings(&*db).expect("load config");
let config = Arc::new(ArcSwap::from_pointee(cfg));
let engine = SoarEngine::new(
db as Arc<dyn AppRepo>,
let engine = SoarEngine::new(SoarEngineDeps {
db: db as Arc<dyn AppRepo>,
config,
mock.clone(),
None,
None,
None,
cache,
None,
)
access_control: mock.clone(),
alert_notifier: None,
geoip: None,
rate_limit: None,
enforce_level_cache: cache,
secrets: None,
})
.expect("Failed to create engine");
engine.recover_active_blocks().await.expect("Recovery should succeed");
@ -526,16 +547,16 @@ mod tests {
AppConfig::seed_defaults(&*db).expect("seed config defaults");
let cfg = AppConfig::from_settings(&*db).expect("load config");
let config = Arc::new(ArcSwap::from_pointee(cfg));
let engine = SoarEngine::new(
db as Arc<dyn AppRepo>,
let engine = SoarEngine::new(SoarEngineDeps {
db: db as Arc<dyn AppRepo>,
config,
mock.clone(),
None,
None,
None,
cache,
None,
)
access_control: mock.clone(),
alert_notifier: None,
geoip: None,
rate_limit: None,
enforce_level_cache: cache,
secrets: None,
})
.expect("Failed to create engine");
// Should not panic — errors are logged, not propagated
@ -640,16 +661,16 @@ mod tests {
AppConfig::seed_defaults(&*db).expect("seed config defaults");
let cfg = AppConfig::from_settings(&*db).expect("load config");
let config = Arc::new(ArcSwap::from_pointee(cfg));
let engine = SoarEngine::new(
db as Arc<dyn AppRepo>,
let engine = SoarEngine::new(SoarEngineDeps {
db: db as Arc<dyn AppRepo>,
config,
mock.clone(),
None,
None,
None,
cache,
None,
)
access_control: mock.clone(),
alert_notifier: None,
geoip: None,
rate_limit: None,
enforce_level_cache: cache,
secrets: None,
})
.expect("Failed to create engine");
let event = ThreatDetectedEvent {
@ -715,16 +736,16 @@ mod tests {
AppConfig::seed_defaults(&*db).expect("seed config defaults");
let cfg = AppConfig::from_settings(&*db).expect("load config");
let config = Arc::new(ArcSwap::from_pointee(cfg));
let engine = SoarEngine::new(
db.clone() as Arc<dyn AppRepo>,
let engine = SoarEngine::new(SoarEngineDeps {
db: db.clone() as Arc<dyn AppRepo>,
config,
mock,
None,
None,
None,
cache,
None,
)
access_control: mock,
alert_notifier: None,
geoip: None,
rate_limit: None,
enforce_level_cache: cache,
secrets: None,
})
.expect("Failed to create engine");
// Should have loaded default playbooks

View File

@ -51,16 +51,7 @@ impl PlaybookService {
)
})
.collect();
let playbook_id = self.db.insert_playbook_atomic(
&input.name,
&input.trigger_event,
input.condition_threshold,
input.condition_count,
input.condition_window_secs,
input.cooldown_secs,
&actions,
&conditions,
)?;
let playbook_id = self.db.insert_playbook_atomic(input, &actions, &conditions)?;
self.soar_engine.reload_cache()?;
Ok(playbook_id)
}

View File

@ -92,7 +92,8 @@ impl TtlScheduler {
// Atomically drop acl_rules entry AND mark soar_block_rules
// unblocked in one transaction.
let ip_version = ip_version_from_str(&block.source_ip);
self.db.commit_soar_unblock_to_db(block.id, ip_version, &block.source_ip)?;
self.db
.commit_soar_unblock_to_db(block.id, ip_version, &block.source_ip)?;
self.soar_engine.decrement_block_count();
removed += 1;
}

View File

@ -3,8 +3,6 @@ use std::str::FromStr;
use serde::Serialize;
use crate::interface::communication::event::Event;
// -- Detection Source ---------------------------------------------------------
/// Identifies which detection subsystem produced a detection.
@ -113,8 +111,6 @@ pub struct ThreatDetectedEvent {
pub c2_score: f32,
}
impl Event for ThreatDetectedEvent {}
/// Fired when the ML drift detector finds feature drift beyond 3 sigma.
#[derive(Debug, Clone)]
pub struct DriftDetectedEvent {
@ -122,8 +118,6 @@ pub struct DriftDetectedEvent {
pub max_deviation: f64,
}
impl Event for DriftDetectedEvent {}
// -- Audit Events -------------------------------------------------------------
/// Fired for auditable actions (enforce mode changes, playbook CRUD, etc.).
@ -137,5 +131,3 @@ pub struct AuditEvent {
/// JSON string with action-specific details
pub detail: String,
}
impl Event for AuditEvent {}

View File

@ -18,9 +18,3 @@ pub enum SuricataHealth {
reason: String,
},
}
impl SuricataHealth {
pub fn is_running(&self) -> bool {
matches!(self, SuricataHealth::Running { .. })
}
}

View File

@ -64,12 +64,12 @@ impl BotnetDetector {
set.last_alert = alert.clone();
if set.sources.len() >= self.threshold {
if let Some(last) = set.last_alerted {
if now.duration_since(last) < self.window {
set.sources.clear();
set.window_start = now;
return None;
}
if let Some(last) = set.last_alerted
&& now.duration_since(last) < self.window
{
set.sources.clear();
set.window_start = now;
return None;
}
Some(set.sources.len())
} else {

View File

@ -302,11 +302,6 @@ impl FlowTracker {
entry.lock().add_packet(&packet, &self.limits);
}
/// Get all active flows (clone, no drain). Used by WebSocket.
pub fn get_flows(&self) -> Vec<FlowData> {
self.active.iter().map(|(_, entry)| entry.lock().clone()).collect()
}
/// Extract scalar stats from all active flows without cloning packet vectors.
pub fn get_flow_stats<T>(&self, convert: impl Fn(&FlowData) -> T) -> Vec<T> {
self.active.iter().map(|(_, entry)| convert(&entry.lock())).collect()

View File

@ -64,12 +64,12 @@ impl LateralMovementDetector {
set.dests.insert(alert.dst_ip.clone());
if set.dests.len() >= self.threshold {
if let Some(last) = set.last_alerted {
if now.duration_since(last) < self.window {
set.dests.clear();
set.window_start = now;
return None;
}
if let Some(last) = set.last_alerted
&& now.duration_since(last) < self.window
{
set.dests.clear();
set.window_start = now;
return None;
}
Some(set.dests.len())
} else {

View File

@ -62,12 +62,12 @@ impl ScanDetector {
set.last_dst_ip = alert.dst_ip.clone();
if set.ports.len() >= self.threshold {
if let Some(last) = set.last_alerted {
if now.duration_since(last) < self.window {
set.ports.clear();
set.window_start = now;
return None;
}
if let Some(last) = set.last_alerted
&& now.duration_since(last) < self.window
{
set.ports.clear();
set.window_start = now;
return None;
}
Some((set.ports.len(), set.last_dst_ip.clone()))
} else {

View File

@ -11,8 +11,8 @@ use tokio::sync::oneshot;
use crate::core::inference::alert::MLAlert;
use crate::core::inference::drift_detector::DriftDetectorHandle;
use crate::core::inference::engine::Engine;
use crate::core::inference::inference::Inference;
use crate::core::inference::model_loader::build_adapter;
use crate::core::inference::runner::Inference;
use crate::core::inference::traffic_logger::{RotationPolicy, TrafficLogger};
use crate::domain::common::config::AppConfig;
use crate::domain::common::config::constants::{MANIFEST_FILENAME, MODELS_DIR};

View File

@ -14,6 +14,7 @@ use crate::domain::common::error::Error;
use crate::domain::common::error::crypto::CryptoError;
use crate::domain::common::log::crypto::CryptoLog;
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::setting::SettingRepo;
/// AES-256-GCM envelope encryption for sensitive values stored in `app_secrets`.
pub struct SecretStore {
@ -126,7 +127,8 @@ impl SecretStore {
impl SecretStorePort for SecretStore {
fn get_secret(&self, key: &str) -> Result<Option<String>, Error> {
match self.db.get_app_secret(key)? {
let repo: &dyn SettingRepo = self.db.as_ref();
match repo.get_app_secret(key)? {
Some(envelope_json) => Ok(Some(self.decrypt(&envelope_json)?)),
None => Ok(None),
}
@ -134,7 +136,8 @@ impl SecretStorePort for SecretStore {
fn set_secret(&self, key: &str, plaintext: &str) -> Result<(), Error> {
let envelope = self.encrypt(plaintext)?;
self.db.set_app_secret(key, &envelope)
let repo: &dyn SettingRepo = self.db.as_ref();
repo.set_app_secret(key, &envelope)
}
fn encrypt_envelope(&self, plaintext: &str) -> Result<String, Error> {

View File

@ -26,7 +26,7 @@ use crate::core::data_plane::rate_limit_service::RateLimitService;
use crate::core::identity::jwt::JwtService;
use crate::core::inference::drift_detector::DriftDetectorHandle;
use crate::core::reporting::email_scheduler::ReportScheduler;
use crate::core::response::engine::SoarEngine;
use crate::core::response::engine::{SoarEngine, SoarEngineDeps};
use crate::core::response::playbook_service::PlaybookService;
use crate::core::response::scheduler::TtlScheduler;
use crate::domain::common::config::AppConfig;
@ -109,6 +109,13 @@ fn stage_registry() -> HashMap<&'static str, (&'static str, u32)> {
])
}
struct EbpfBuild {
ingress: Ebpf,
egress: Ebpf,
program_array: ProgramArray<MapData>,
services: EbpfServices,
}
/// Factory responsible for creating and wiring all application services.
pub struct ServiceFactory;
@ -153,7 +160,12 @@ impl ServiceFactory {
// the reason.
let (ingress_ebpf, egress_ebpf, ingress_program_array, ebpf_services) = match Self::try_build_ebpf(&app_config)
{
Ok((ingress, egress, pa, services)) => (Some(ingress), Some(egress), Some(pa), Arc::new(services)),
Ok(build) => (
Some(build.ingress),
Some(build.egress),
Some(build.program_array),
Arc::new(build.services),
),
Err((stage, err)) => {
let health = ebpf_preflight::classify(stage, &err, None);
log!(SystemLog::EbpfBringupFailed(format!("{:?}", health)));
@ -264,16 +276,16 @@ impl ServiceFactory {
// Create SOAR engine
let rate_limit_port: Arc<dyn RateLimitPort> = ebpf_services.rate_limit.clone();
let soar_engine = Arc::new(SoarEngine::new(
db.clone(),
app_config.clone(),
access_control_port.clone(),
alert_notifier.clone(),
geoip.clone(),
Some(rate_limit_port.clone()),
let soar_engine = Arc::new(SoarEngine::new(SoarEngineDeps {
db: db.clone(),
config: app_config.clone(),
access_control: access_control_port.clone(),
alert_notifier: alert_notifier.clone(),
geoip: geoip.clone(),
rate_limit: Some(rate_limit_port.clone()),
enforce_level_cache,
Some(secret_store_port.clone()),
)?);
secrets: Some(secret_store_port.clone()),
})?);
// Create TTL scheduler
let ttl_scheduler = TtlScheduler::new(db.clone(), access_control_port.clone(), soar_engine.clone());
@ -361,11 +373,7 @@ impl ServiceFactory {
/// ingress pipeline, write queue counts, and hand out map handles to the
/// services. Returns the original stage on the first failure so the
/// classifier can render targeted diagnostics.
fn try_build_ebpf(
app_config: &Arc<ArcSwap<AppConfig>>,
) -> Result<(Ebpf, Ebpf, ProgramArray<MapData>, EbpfServices), (EbpfFailStage, Error)> {
use crate::domain::common::system::health::EbpfFailStage;
fn try_build_ebpf(app_config: &Arc<ArcSwap<AppConfig>>) -> Result<EbpfBuild, (EbpfFailStage, Error)> {
let mut ingress = Self::load_ebpf("ingress").map_err(|e| (EbpfFailStage::Load, e))?;
let mut egress = Self::load_ebpf("egress").map_err(|e| (EbpfFailStage::Load, e))?;
@ -381,7 +389,12 @@ impl ServiceFactory {
let services = EbpfServices::new(app_config.clone(), &mut ingress, &mut egress)
.map_err(|e| (EbpfFailStage::MapsBind, e))?;
Ok((ingress, egress, pipeline, services))
Ok(EbpfBuild {
ingress,
egress,
program_array: pipeline,
services,
})
}
fn load_ebpf(name: &str) -> Result<Ebpf, Error> {

View File

@ -1 +0,0 @@
pub trait Event: Send + Clone + 'static {}

View File

@ -1 +0,0 @@
// Types are available via crate::domain::event

View File

@ -1,2 +0,0 @@
pub mod event;
pub mod event_types;

View File

@ -1,3 +1,2 @@
pub mod communication;
pub mod port;
pub mod utils;

View File

@ -1,6 +1,6 @@
use crate::domain::common::error::Error;
use crate::domain::response::playbook_data::{
ActiveBlockView, ExecutionView, PendingUnblock, PlaybookView, UpdatePlaybookInput,
ActiveBlockView, CreatePlaybookInput, ExecutionView, PendingUnblock, PlaybookView, UpdatePlaybookInput,
};
/// Threat Response BC — SOAR aggregate repository.
@ -50,12 +50,7 @@ pub trait SoarRepo: Send + Sync {
/// `conditions` tuples: `(condition_type, operator, value, value2)`.
fn insert_playbook_atomic(
&self,
name: &str,
trigger_event: &str,
threshold: Option<f64>,
count: Option<i64>,
window: Option<i64>,
cooldown: i64,
input: &CreatePlaybookInput,
actions: &[(i64, String, String)],
conditions: &[(String, String, String, Option<String>)],
) -> Result<i64, Error>;