mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
fix: CI Failed
This commit is contained in:
parent
965480fb9e
commit
c2c4ed5b23
@ -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,
|
||||
|
||||
@ -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>>,
|
||||
|
||||
@ -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()))
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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| {
|
||||
|
||||
@ -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)",
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -18,9 +18,3 @@ pub enum SuricataHealth {
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl SuricataHealth {
|
||||
pub fn is_running(&self) -> bool {
|
||||
matches!(self, SuricataHealth::Running { .. })
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
pub trait Event: Send + Clone + 'static {}
|
||||
@ -1 +0,0 @@
|
||||
// Types are available via crate::domain::event
|
||||
@ -1,2 +0,0 @@
|
||||
pub mod event;
|
||||
pub mod event_types;
|
||||
@ -1,3 +1,2 @@
|
||||
pub mod communication;
|
||||
pub mod port;
|
||||
pub mod utils;
|
||||
|
||||
@ -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>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user