mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
style: CODE_STYLE sweep — magic numbers, task-ref tags, inline paths
* soar/actions.rs: hoist 8 magic numbers into named consts
(DEFAULT_BLOCK_TTL_SECS, DEFAULT_SOAR_MAX_TTL_SECS,
DEFAULT_SOAR_MAX_AUTO_BLOCK_CAP, DEFAULT_RATE_LIMIT_FACTOR,
DEFAULT_RATE_LIMIT_TTL_SECS, RATE_LIMIT_FACTOR_MIN/MAX,
DEFAULT_WEBHOOK_TIMEOUT_SECS, DEFAULT_WEBHOOK_HTTPS_PORT,
FALLBACK_PLAYBOOK_ID, FALLBACK_COOLDOWN_SECS).
* suricata_monitor.rs: hoist severity→confidence map and IANA proto
numbers into named consts (SURICATA_CONFIDENCE_*, SURICATA_SEVERITY_*,
IANA_PROTO_*).
* Drop tx-1/tx-2/tx-3/tx-4/tx-5 / R2-mitigation task-ref tags from doc
comments and inline comments across playbook_service, soar/scheduler,
interface/port/{db_admin,soar}, adapter/persistence/repository.
* Hoist inline `use crate::core:📧:scheduler::SmtpClient` from
function body to top-of-file imports.
* Add // SAFETY: comment to repository.rs HMAC unwrap (HMAC-SHA256
accepts any key length so InvalidLength is unreachable).
* Replace `Arc<dyn crate:🅰️🅱️:c::Trait>` inline 5-segment type
annotations with imported trait names: service_factory.rs (4 sites:
RateLimitPort, AccessControlAdminPort, GeoBlockPort, DnsFilterPort)
and system.rs (PacketSinkFactory).
* fusion.rs: import AuditLogEntry instead of inlining
`crate::interface::port::audit::AuditLogEntry` in the function
signature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7773313f81
commit
1d935bf2da
@ -10,7 +10,7 @@
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, Scope, web};
|
||||
|
||||
use crate::core::detection::metrics::FusionMetrics;
|
||||
use crate::interface::port::audit::AuditRepo;
|
||||
use crate::interface::port::audit::{AuditLogEntry, AuditRepo};
|
||||
|
||||
/// Maximum audit rows scanned per explain request. Caps DB work in
|
||||
/// case the audit chain grows large enough that a naive full-table
|
||||
@ -80,12 +80,8 @@ async fn explain_ip(req: HttpRequest, audit: web::Data<dyn AuditRepo>) -> impl R
|
||||
///
|
||||
/// Extracted as a free function so tests can cover the filter /
|
||||
/// ordering / cap behaviour without an in-memory DB.
|
||||
pub fn filter_fusion_evidence_for_ip(
|
||||
entries: &[crate::interface::port::audit::AuditLogEntry],
|
||||
target_ip: &str,
|
||||
cap: usize,
|
||||
) -> Vec<serde_json::Value> {
|
||||
let mut filtered: Vec<&crate::interface::port::audit::AuditLogEntry> = entries
|
||||
pub fn filter_fusion_evidence_for_ip(entries: &[AuditLogEntry], target_ip: &str, cap: usize) -> Vec<serde_json::Value> {
|
||||
let mut filtered: Vec<&AuditLogEntry> = entries
|
||||
.iter()
|
||||
.filter(|entry| detail_matches_src_ip(&entry.detail, target_ip))
|
||||
.collect();
|
||||
@ -117,7 +113,6 @@ fn detail_matches_src_ip(detail_json: &str, target_ip: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::interface::port::audit::AuditLogEntry;
|
||||
|
||||
fn entry(id: i64, src_ip: &str, attack: &str) -> AuditLogEntry {
|
||||
let detail = serde_json::json!({
|
||||
|
||||
@ -122,6 +122,9 @@ impl Database {
|
||||
use std::fmt::Write;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
// SAFETY: HMAC-SHA256 accepts keys of any length; the only error
|
||||
// `new_from_slice` returns (`InvalidLength`) is unreachable for this
|
||||
// algorithm. The unreachable!() is the correct sentinel.
|
||||
let mut mac = HmacSha256::new_from_slice(&self.api_key_hmac).unwrap_or_else(|_| unreachable!());
|
||||
mac.update(raw_key.as_bytes());
|
||||
let result = mac.finalize().into_bytes();
|
||||
@ -1980,7 +1983,7 @@ impl SoarRepo for Database {
|
||||
self.list_soar_executions(limit)
|
||||
}
|
||||
|
||||
// --- tx-4 / tx-5: intra-aggregate atomic operations ---
|
||||
// --- intra-aggregate atomic operations ---
|
||||
|
||||
fn insert_playbook_atomic(
|
||||
&self,
|
||||
@ -2116,7 +2119,7 @@ impl ApiKeyRepo for Database {
|
||||
}
|
||||
|
||||
impl DbAdminRepo for Database {
|
||||
/// tx-1 — Commit a SOAR-driven block to both `soar_block_rules` and
|
||||
/// Commit a SOAR-driven block to both `soar_block_rules` and
|
||||
/// `acl_rules` in one transaction. Callers must have already installed
|
||||
/// the eBPF block before calling this, and are responsible for removing
|
||||
/// the eBPF block if this returns Err.
|
||||
@ -2142,8 +2145,8 @@ impl DbAdminRepo for Database {
|
||||
Ok(soar_block_id)
|
||||
}
|
||||
|
||||
/// tx-2 / tx-3 — Clear a SOAR-driven block: remove the `acl_rules` entry
|
||||
/// and mark the `soar_block_rules` row as unblocked in one transaction.
|
||||
/// Clear a SOAR-driven block: remove the `acl_rules` entry and mark
|
||||
/// the `soar_block_rules` row as unblocked in one transaction.
|
||||
/// Callers handle eBPF unblock separately.
|
||||
fn commit_soar_unblock_to_db(&self, soar_block_id: i64, ip_version: u8, source_ip: &str) -> Result<(), Error> {
|
||||
let mut conn = self.conn()?;
|
||||
@ -2333,7 +2336,7 @@ mod tests {
|
||||
assert_eq!(identity.user_count().unwrap(), 1);
|
||||
}
|
||||
|
||||
/// tx-1 — happy path. Verifies `commit_soar_block_to_db` writes both
|
||||
/// Happy path. Verifies `commit_soar_block_to_db` writes both
|
||||
/// `soar_block_rules` and `acl_rules` atomically.
|
||||
#[test]
|
||||
fn test_commit_soar_block_happy_path() {
|
||||
@ -2360,8 +2363,8 @@ mod tests {
|
||||
assert_eq!(rules[0].3, "10.0.0.99");
|
||||
}
|
||||
|
||||
/// tx-2 / tx-3 — verifies `commit_soar_unblock_to_db` removes the ACL row
|
||||
/// and marks the SOAR row as unblocked in one transaction.
|
||||
/// Verifies `commit_soar_unblock_to_db` removes the ACL row and marks
|
||||
/// the SOAR row as unblocked in one transaction.
|
||||
#[test]
|
||||
fn test_commit_soar_unblock_clears_both_tables() {
|
||||
let db = test_db();
|
||||
@ -2380,7 +2383,7 @@ mod tests {
|
||||
assert!(db.get_active_soar_blocks().unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// tx-4 — `insert_playbook_atomic` writes playbook + actions + conditions
|
||||
/// Verifies `insert_playbook_atomic` writes playbook + actions + conditions
|
||||
/// atomically.
|
||||
#[test]
|
||||
fn test_insert_playbook_atomic_writes_all_three_tables() {
|
||||
|
||||
@ -121,7 +121,7 @@ impl PlaybookService {
|
||||
}
|
||||
|
||||
pub fn create_playbook(&self, input: &CreatePlaybookInput) -> Result<i64, Error> {
|
||||
// tx-4: single atomic insert (playbook + actions + conditions)
|
||||
// Single atomic insert (playbook + actions + conditions).
|
||||
let actions: Vec<(i64, String, String)> = input
|
||||
.actions
|
||||
.iter()
|
||||
@ -163,7 +163,7 @@ impl PlaybookService {
|
||||
condition_window_secs: input.condition_window_secs,
|
||||
cooldown_secs: input.cooldown_secs,
|
||||
};
|
||||
// tx-5: single atomic update (playbook metadata + replace actions/conditions)
|
||||
// Single atomic update (playbook metadata + replace actions/conditions).
|
||||
let actions: Vec<(i64, String, String)> = input
|
||||
.actions
|
||||
.iter()
|
||||
@ -233,8 +233,8 @@ impl PlaybookService {
|
||||
// Remove from eBPF ACL
|
||||
self.access_control.unblock_ip(source_ip)?;
|
||||
|
||||
// Atomically drop acl_rules entry AND mark soar_block_rules unblocked
|
||||
// in one transaction (R2 mitigation, tx-3 per M2_CARVE_PLAN §4b).
|
||||
// Atomically drop acl_rules entry AND mark soar_block_rules
|
||||
// unblocked in one transaction.
|
||||
let ip_version = ip_version_from_str(source_ip);
|
||||
self.db.commit_soar_unblock_to_db(id, ip_version, source_ip)?;
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//! layer (`matcher.rs`) decided the playbook should fire.
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
@ -16,8 +17,10 @@ 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::interface::port::app_repo::AppRepo;
|
||||
use crate::interface::port::notification::AlertPayload;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::soar::SoarError;
|
||||
@ -25,6 +28,37 @@ use crate::model::event::ThreatDetectedEvent;
|
||||
use crate::model::log::soar::SoarLog;
|
||||
use crate::model::soar::playbook::{Playbook, PlaybookAction};
|
||||
|
||||
/// Default block TTL when a `block_ip` action omits `ttl_secs`.
|
||||
const DEFAULT_BLOCK_TTL_SECS: u64 = 1800;
|
||||
/// DB-overridable cap on per-block TTL — see setting `soar_max_ttl_secs`.
|
||||
const DEFAULT_SOAR_MAX_TTL_SECS: u64 = 86_400;
|
||||
/// DB-overridable cap on concurrent SOAR-driven blocks — see setting
|
||||
/// `soar_max_auto_block_cap`.
|
||||
const DEFAULT_SOAR_MAX_AUTO_BLOCK_CAP: u32 = 100;
|
||||
/// Default rate-limit reduction factor when an `adjust_rate_limit` action
|
||||
/// omits `factor`. 0.5 = halve the current rate.
|
||||
const DEFAULT_RATE_LIMIT_FACTOR: f64 = 0.5;
|
||||
/// Default rate-limit TTL when an `adjust_rate_limit` action omits `ttl_secs`.
|
||||
const DEFAULT_RATE_LIMIT_TTL_SECS: u64 = 600;
|
||||
/// Lower bound on the rate-limit factor — anything below 1% of current
|
||||
/// would brick traffic flow.
|
||||
const RATE_LIMIT_FACTOR_MIN: f64 = 0.01;
|
||||
/// Upper bound on the rate-limit factor — `1.0` is a no-op; values above
|
||||
/// would *raise* the limit, which isn't a SOAR mitigation.
|
||||
const RATE_LIMIT_FACTOR_MAX: f64 = 1.0;
|
||||
/// Default webhook timeout when an action omits `timeout_secs`.
|
||||
const DEFAULT_WEBHOOK_TIMEOUT_SECS: u64 = 10;
|
||||
/// Fallback port when the webhook URL has no explicit port and no
|
||||
/// well-known scheme port.
|
||||
const DEFAULT_WEBHOOK_HTTPS_PORT: u16 = 443;
|
||||
/// Sentinel `playbook_id` used by the no-matching-playbook fallback path.
|
||||
/// `-1` is reserved on the audit / cooldown maps and never assigned to a
|
||||
/// real DB playbook row.
|
||||
const FALLBACK_PLAYBOOK_ID: i64 = -1;
|
||||
/// Cooldown applied to the fallback path so a single noisy IP doesn't
|
||||
/// spam the WORM audit chain on every fused detection.
|
||||
const FALLBACK_COOLDOWN_SECS: i64 = 300;
|
||||
|
||||
impl SoarEngine {
|
||||
/// Check if the system is in enforce mode (as opposed to monitor mode).
|
||||
/// Reads from the in-memory AtomicU8 cache: Monitor=0, MlOnly=1, Enforce=2.
|
||||
@ -128,7 +162,11 @@ impl SoarEngine {
|
||||
event: &ThreatDetectedEvent,
|
||||
playbook_id: i64,
|
||||
) -> Result<String, Error> {
|
||||
let ttl_secs = action.params.get("ttl_secs").and_then(|v| v.as_u64()).unwrap_or(1800);
|
||||
let ttl_secs = action
|
||||
.params
|
||||
.get("ttl_secs")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(DEFAULT_BLOCK_TTL_SECS);
|
||||
|
||||
// Two settings read together off the tokio worker thread — r2d2's
|
||||
// pool.get() and rusqlite are blocking, so back-to-back calls inside
|
||||
@ -208,10 +246,18 @@ impl SoarEngine {
|
||||
) -> Result<String, Error> {
|
||||
let owner = self.rate_limit.as_ref().ok_or(SoarError::RateLimitUnavailable)?;
|
||||
|
||||
let factor = action.params.get("factor").and_then(|v| v.as_f64()).unwrap_or(0.5);
|
||||
let ttl_secs = action.params.get("ttl_secs").and_then(|v| v.as_u64()).unwrap_or(600);
|
||||
let factor = action
|
||||
.params
|
||||
.get("factor")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(DEFAULT_RATE_LIMIT_FACTOR);
|
||||
let ttl_secs = action
|
||||
.params
|
||||
.get("ttl_secs")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(DEFAULT_RATE_LIMIT_TTL_SECS);
|
||||
|
||||
if !(0.01..=1.0).contains(&factor) {
|
||||
if !(RATE_LIMIT_FACTOR_MIN..=RATE_LIMIT_FACTOR_MAX).contains(&factor) {
|
||||
Err(SoarError::InvalidRateLimitFactor(factor))?;
|
||||
}
|
||||
|
||||
@ -263,7 +309,6 @@ impl SoarEngine {
|
||||
|
||||
/// Send email alert.
|
||||
async fn action_send_email(&self, event: &ThreatDetectedEvent) -> Result<String, Error> {
|
||||
use crate::core::email::scheduler::SmtpClient;
|
||||
match SmtpClient::from_soar_port(&*self.db, self.secrets.as_deref())? {
|
||||
Some(smtp) => {
|
||||
let subject = format!(
|
||||
@ -303,7 +348,11 @@ impl SoarEngine {
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| SoarError::WebhookMissingParam("url"))?;
|
||||
|
||||
let timeout_secs = action.params.get("timeout_secs").and_then(|v| v.as_u64()).unwrap_or(10);
|
||||
let timeout_secs = action
|
||||
.params
|
||||
.get("timeout_secs")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(DEFAULT_WEBHOOK_TIMEOUT_SECS);
|
||||
|
||||
// Parse URL and extract host
|
||||
let parsed_url = Url::parse(url_str).map_err(|e| SoarError::ActionFailed("webhook", e))?;
|
||||
@ -311,7 +360,7 @@ impl SoarEngine {
|
||||
let host = parsed_url.host_str().ok_or(SoarError::WebhookUrlNoHost)?;
|
||||
|
||||
// DNS resolve all IPs and verify none are private/loopback/link-local
|
||||
let port = parsed_url.port_or_known_default().unwrap_or(443);
|
||||
let port = parsed_url.port_or_known_default().unwrap_or(DEFAULT_WEBHOOK_HTTPS_PORT);
|
||||
let resolve_target = format!("{}:{}", host, port);
|
||||
let addrs: Vec<SocketAddr> = lookup_host(&resolve_target)
|
||||
.await
|
||||
@ -404,31 +453,31 @@ impl SoarEngine {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check cooldown — use playbook_id=-1 for fallback actions
|
||||
if self.is_cooldown_active(-1, &event.source_ip, 300) {
|
||||
// Check cooldown — uses FALLBACK_PLAYBOOK_ID as the synthetic key
|
||||
if self.is_cooldown_active(FALLBACK_PLAYBOOK_ID, &event.source_ip, FALLBACK_COOLDOWN_SECS) {
|
||||
log!(SoarLog::CooldownActive("fallback".to_string(), event.source_ip.clone()));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Default fallback: block IP for 30 minutes + log
|
||||
// Default fallback: block IP for the default block TTL + log
|
||||
let fake_action = PlaybookAction {
|
||||
action_order: 1,
|
||||
action_type: "block_ip".to_string(),
|
||||
params: serde_json::json!({"ttl_secs": 1800}),
|
||||
params: serde_json::json!({"ttl_secs": DEFAULT_BLOCK_TTL_SECS}),
|
||||
};
|
||||
|
||||
let block_result = self.execute_action(&fake_action, event, -1).await;
|
||||
let block_result = self.execute_action(&fake_action, event, FALLBACK_PLAYBOOK_ID).await;
|
||||
let result_json = match &block_result {
|
||||
Ok(msg) => serde_json::json!({"action": "block_ip", "status": "ok", "message": msg}),
|
||||
Err(e) => serde_json::json!({"action": "block_ip", "status": "error", "message": e.to_string()}),
|
||||
};
|
||||
|
||||
// Record cooldown for fallback
|
||||
self.record_cooldown(-1, &event.source_ip);
|
||||
self.record_cooldown(FALLBACK_PLAYBOOK_ID, &event.source_ip);
|
||||
|
||||
// Audit trail with playbook_id = -1
|
||||
// Audit trail under the fallback synthetic playbook id
|
||||
self.db.insert_soar_execution(
|
||||
-1,
|
||||
FALLBACK_PLAYBOOK_ID,
|
||||
Some(&event.source_ip),
|
||||
&event.attack_type,
|
||||
&serde_json::to_string(&[result_json]).unwrap_or_default(),
|
||||
@ -439,10 +488,6 @@ impl SoarEngine {
|
||||
}
|
||||
}
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
|
||||
/// Read both block-related caps in a single offloaded blocking call so the
|
||||
/// async caller pays one spawn_blocking hop instead of two.
|
||||
async fn read_block_caps(db: Arc<dyn AppRepo>) -> Result<(u64, u32), Error> {
|
||||
@ -452,13 +497,13 @@ async fn read_block_caps(db: Arc<dyn AppRepo>) -> Result<(u64, u32), Error> {
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(86400);
|
||||
.unwrap_or(DEFAULT_SOAR_MAX_TTL_SECS);
|
||||
let max_cap: u32 = db
|
||||
.get_setting("soar_max_auto_block_cap")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(100);
|
||||
.unwrap_or(DEFAULT_SOAR_MAX_AUTO_BLOCK_CAP);
|
||||
Ok::<_, Error>((max_ttl, max_cap))
|
||||
})
|
||||
.await
|
||||
@ -471,10 +516,10 @@ async fn read_max_ttl(db: Arc<dyn AppRepo>) -> u64 {
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(86400)
|
||||
.unwrap_or(DEFAULT_SOAR_MAX_TTL_SECS)
|
||||
})
|
||||
.await
|
||||
.unwrap_or(86400)
|
||||
.unwrap_or(DEFAULT_SOAR_MAX_TTL_SECS)
|
||||
}
|
||||
|
||||
async fn commit_block_blocking(
|
||||
|
||||
@ -89,8 +89,8 @@ impl TtlScheduler {
|
||||
));
|
||||
}
|
||||
|
||||
// Atomically drop acl_rules entry AND mark soar_block_rules unblocked
|
||||
// in one transaction (R2 mitigation, tx-2 per M2_CARVE_PLAN §4b).
|
||||
// Atomically drop acl_rules entry AND mark soar_block_rules
|
||||
// unblocked in one transaction.
|
||||
let ip_version = ip_version_from_str(source_ip);
|
||||
self.db.commit_soar_unblock_to_db(*id, ip_version, source_ip)?;
|
||||
self.soar_engine.decrement_block_count();
|
||||
|
||||
@ -41,8 +41,12 @@ use crate::infrastructure::suricata_manager::SuricataManager;
|
||||
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
|
||||
use crate::interface::communication::query_types::GetEnforceModeQuery;
|
||||
use crate::interface::port::access_control::AccessControlPort;
|
||||
use crate::interface::port::access_control_admin::AccessControlAdminPort;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
use crate::interface::port::dns_filter_api::DnsFilterPort;
|
||||
use crate::interface::port::geo_block_api::GeoBlockPort;
|
||||
use crate::interface::port::notification::AlertNotifier;
|
||||
use crate::interface::port::rate_limit_api::RateLimitPort;
|
||||
use crate::interface::port::secret_store::SecretStorePort;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use crate::interface::port::soar::SoarRepo;
|
||||
@ -259,8 +263,7 @@ impl ServiceFactory {
|
||||
Arc::new(EbpfAccessControlAdapter::new(ebpf_services.access_control.clone()));
|
||||
|
||||
// Create SOAR engine
|
||||
let rate_limit_port: Arc<dyn crate::interface::port::rate_limit_api::RateLimitPort> =
|
||||
ebpf_services.rate_limit.clone();
|
||||
let rate_limit_port: Arc<dyn RateLimitPort> = ebpf_services.rate_limit.clone();
|
||||
let soar_engine = Arc::new(SoarEngine::new(
|
||||
db.clone(),
|
||||
access_control_port.clone(),
|
||||
@ -280,12 +283,9 @@ impl ServiceFactory {
|
||||
|
||||
// Create domain services (Phase 2B) — upcast concrete eBPF services to
|
||||
// their port-layer traits so the core services see only abstract ports.
|
||||
let access_control_admin: Arc<dyn crate::interface::port::access_control_admin::AccessControlAdminPort> =
|
||||
ebpf_services.access_control.clone();
|
||||
let geo_block_port: Arc<dyn crate::interface::port::geo_block_api::GeoBlockPort> =
|
||||
ebpf_services.geo_block.clone();
|
||||
let dns_filter_port: Arc<dyn crate::interface::port::dns_filter_api::DnsFilterPort> =
|
||||
ebpf_services.dns_filter.clone();
|
||||
let access_control_admin: Arc<dyn AccessControlAdminPort> = ebpf_services.access_control.clone();
|
||||
let geo_block_port: Arc<dyn GeoBlockPort> = ebpf_services.geo_block.clone();
|
||||
let dns_filter_port: Arc<dyn DnsFilterPort> = ebpf_services.dns_filter.clone();
|
||||
let acl_service = Arc::new(AclService::new(
|
||||
db.clone() as Arc<dyn AppRepo>,
|
||||
access_control_admin,
|
||||
|
||||
@ -37,6 +37,23 @@ const POLL_INTERVAL: Duration = Duration::from_millis(200);
|
||||
/// How long to wait between checks while the file does not yet exist.
|
||||
const FILE_WAIT_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
/// IANA protocol numbers for Suricata's `proto` strings.
|
||||
const IANA_PROTO_ICMP: u8 = 1;
|
||||
const IANA_PROTO_TCP: u8 = 6;
|
||||
const IANA_PROTO_UDP: u8 = 17;
|
||||
|
||||
/// Suricata severity → SOAR confidence mapping. Higher severity ⇒ higher
|
||||
/// confidence so SOAR thresholds tend to trip on real alerts.
|
||||
const SURICATA_CONFIDENCE_HIGH: f32 = 0.95;
|
||||
const SURICATA_CONFIDENCE_MEDIUM: f32 = 0.80;
|
||||
const SURICATA_CONFIDENCE_LOW: f32 = 0.65;
|
||||
const SURICATA_CONFIDENCE_INFO: f32 = 0.50;
|
||||
|
||||
/// Suricata severity numeric encoding (eve.json `alert.severity`).
|
||||
const SURICATA_SEVERITY_HIGH: u64 = 1;
|
||||
const SURICATA_SEVERITY_MEDIUM: u64 = 2;
|
||||
const SURICATA_SEVERITY_LOW: u64 = 3;
|
||||
|
||||
pub struct SuricataMonitor {
|
||||
config: Arc<AppConfig>,
|
||||
detection_tx: mpsc::Sender<DetectionEvent>,
|
||||
@ -145,9 +162,9 @@ impl SuricataMonitor {
|
||||
let dest_ip = v.get("dest_ip")?.as_str()?.to_string();
|
||||
let proto_str = v.get("proto").and_then(|x| x.as_str()).unwrap_or("");
|
||||
let protocol: u8 = match proto_str {
|
||||
"TCP" => 6,
|
||||
"UDP" => 17,
|
||||
"ICMP" => 1,
|
||||
"TCP" => IANA_PROTO_TCP,
|
||||
"UDP" => IANA_PROTO_UDP,
|
||||
"ICMP" => IANA_PROTO_ICMP,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
@ -161,12 +178,15 @@ impl SuricataMonitor {
|
||||
// Suricata severity: 1=high, 2=medium, 3=low, 4=informational.
|
||||
// Map to confidence in [0.5, 1.0] so high-severity alerts tend to trip
|
||||
// SOAR condition thresholds.
|
||||
let severity = alert.get("severity").and_then(|x| x.as_u64()).unwrap_or(3);
|
||||
let severity = alert
|
||||
.get("severity")
|
||||
.and_then(|x| x.as_u64())
|
||||
.unwrap_or(SURICATA_SEVERITY_LOW);
|
||||
let confidence = match severity {
|
||||
1 => 0.95,
|
||||
2 => 0.80,
|
||||
3 => 0.65,
|
||||
_ => 0.50,
|
||||
SURICATA_SEVERITY_HIGH => SURICATA_CONFIDENCE_HIGH,
|
||||
SURICATA_SEVERITY_MEDIUM => SURICATA_CONFIDENCE_MEDIUM,
|
||||
SURICATA_SEVERITY_LOW => SURICATA_CONFIDENCE_LOW,
|
||||
_ => SURICATA_CONFIDENCE_INFO,
|
||||
};
|
||||
|
||||
log!(SuricataLog::AlertForwarded(
|
||||
|
||||
@ -44,6 +44,7 @@ use crate::infrastructure::service_factory::ServiceFactory;
|
||||
use crate::infrastructure::suricata_manager::SuricataManager;
|
||||
use crate::infrastructure::suricata_monitor::SuricataMonitor;
|
||||
use crate::interface::port::audit::AuditRepo;
|
||||
use crate::interface::port::packet_sink::PacketSinkFactory;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use crate::interface::port::stats::StatsRepo;
|
||||
use crate::model::config::constants::{MODELS_DIR, STAGING_SUBDIR};
|
||||
@ -211,8 +212,7 @@ impl System {
|
||||
// When maps exist but bind fails (e.g. igb on kernel < 6.17), record
|
||||
// the classified reason and continue — the ML engine will see no
|
||||
// packets, same as a network that is simply quiet.
|
||||
let sink_factory: Arc<dyn crate::interface::port::packet_sink::PacketSinkFactory> =
|
||||
app_services.ml_engine.clone();
|
||||
let sink_factory: Arc<dyn PacketSinkFactory> = app_services.ml_engine.clone();
|
||||
if let Err(e) = ebpf_services.run(sink_factory).await {
|
||||
use crate::infrastructure::ebpf_preflight;
|
||||
use crate::model::system::health::EbpfFailStage;
|
||||
|
||||
@ -8,19 +8,8 @@ use crate::model::error::Error;
|
||||
/// primitive (which cannot be used through a trait object because Rust
|
||||
/// forbids generic methods on dyn traits), each cross-aggregate use case
|
||||
/// gets a dedicated business method.
|
||||
///
|
||||
/// The five tx points identified during M2 design (see
|
||||
/// `docs/strategy/debate/v12-architecture-review/M2_CARVE_PLAN.md` §4b):
|
||||
///
|
||||
/// | # | Use case | Method |
|
||||
/// |---|---|---|
|
||||
/// | tx-1 | SOAR block commit (soar_block_rules + acl_rules) | `commit_soar_block_to_db` |
|
||||
/// | tx-2 | TTL unblock (acl_rules delete + soar row mark) | `commit_soar_unblock_to_db` |
|
||||
/// | tx-3 | Manual unblock (same as tx-2) | `commit_soar_unblock_to_db` |
|
||||
/// | tx-4 | Playbook create + conditions + actions | `insert_playbook_atomic` on SoarRepo |
|
||||
/// | tx-5 | Playbook update + replace conditions/actions | `update_playbook_atomic` on SoarRepo |
|
||||
pub trait DbAdminRepo: Send + Sync {
|
||||
/// tx-1 — Atomically record a SOAR-driven IP block to both
|
||||
/// Atomically record a SOAR-driven IP block to both
|
||||
/// `soar_block_rules` and `acl_rules`. Returns the new
|
||||
/// `soar_block_rules.id`. Callers are responsible for eBPF rollback if
|
||||
/// this fails.
|
||||
@ -32,9 +21,9 @@ pub trait DbAdminRepo: Send + Sync {
|
||||
expires_at: &str,
|
||||
) -> Result<i64, Error>;
|
||||
|
||||
/// tx-2 / tx-3 — Atomically clear a SOAR-driven IP block: removes the
|
||||
/// corresponding `acl_rules` row (if present) and marks the
|
||||
/// `soar_block_rules` row as unblocked. Callers handle eBPF unblock
|
||||
/// separately.
|
||||
/// Atomically clear a SOAR-driven IP block: removes the corresponding
|
||||
/// `acl_rules` row (if present) and marks the `soar_block_rules` row as
|
||||
/// unblocked. Callers handle eBPF unblock separately. Used by both the
|
||||
/// TTL-unblock and manual-unblock paths.
|
||||
fn commit_soar_unblock_to_db(&self, soar_block_id: i64, ip_version: u8, source_ip: &str) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
@ -95,9 +95,9 @@ pub trait SoarRepo: Send + Sync {
|
||||
) -> Result<i64, Error>;
|
||||
fn list_soar_executions(&self, limit: i64) -> Result<Vec<SoarExecutionRow>, Error>;
|
||||
|
||||
// --- Intra-aggregate atomic operations (tx-4 / tx-5 per M2_CARVE_PLAN §4b) ---
|
||||
// --- Intra-aggregate atomic operations ---
|
||||
|
||||
/// tx-4 — Atomically create a playbook with its conditions and actions.
|
||||
/// Atomically create a playbook with its conditions and actions.
|
||||
/// All rows (playbook + conditions + actions) commit together; any error
|
||||
/// rolls back the whole insert. Returns the new playbook id.
|
||||
///
|
||||
@ -116,9 +116,9 @@ pub trait SoarRepo: Send + Sync {
|
||||
conditions: &[(String, String, String, Option<String>)],
|
||||
) -> Result<i64, Error>;
|
||||
|
||||
/// tx-5 — Atomically update a playbook's metadata and replace its
|
||||
/// conditions and actions. Returns `Ok(false)` if no playbook with that
|
||||
/// id exists; otherwise `Ok(true)` after the whole update commits.
|
||||
/// Atomically update a playbook's metadata and replace its conditions
|
||||
/// and actions. Returns `Ok(false)` if no playbook with that id exists;
|
||||
/// otherwise `Ok(true)` after the whole update commits.
|
||||
fn update_playbook_atomic(
|
||||
&self,
|
||||
id: i64,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user