diff --git a/net-guardia/src/adapter/access_control_adapter.rs b/net-guardia/src/adapter/access_control_adapter.rs index 6499258..d94fdf3 100644 --- a/net-guardia/src/adapter/access_control_adapter.rs +++ b/net-guardia/src/adapter/access_control_adapter.rs @@ -7,6 +7,7 @@ use crate::core::ebpf::access_control::AccessControl; 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. @@ -25,7 +26,7 @@ impl AccessControlPort for EbpfAccessControlAdapter { async fn block_ip(&self, ip: &str) -> Result<(), Error> { let addr: IpAddr = ip .parse() - .map_err(|_| Error::from(crate::model::error::ebpf::EbpfError::InvalidIpAddress { ip: ip.to_string() }))?; + .map_err(|_| Error::from(EbpfError::InvalidIpAddress { ip: ip.to_string() }))?; match addr { IpAddr::V4(v4) => { let socket = SocketAddrV4::new(v4, 0); @@ -45,7 +46,7 @@ impl AccessControlPort for EbpfAccessControlAdapter { async fn unblock_ip(&self, ip: &str) -> Result<(), Error> { let addr: IpAddr = ip .parse() - .map_err(|_| Error::from(crate::model::error::ebpf::EbpfError::InvalidIpAddress { ip: ip.to_string() }))?; + .map_err(|_| Error::from(EbpfError::InvalidIpAddress { ip: ip.to_string() }))?; match addr { IpAddr::V4(v4) => { let socket = SocketAddrV4::new(v4, 0); diff --git a/net-guardia/src/adapter/http/logs.rs b/net-guardia/src/adapter/http/logs.rs index 1bdcda3..ec59e8d 100644 --- a/net-guardia/src/adapter/http/logs.rs +++ b/net-guardia/src/adapter/http/logs.rs @@ -1,3 +1,8 @@ +use std::fs; +use std::io::ErrorKind; +use std::path::Path; +use std::time::UNIX_EPOCH; + use actix_web::{HttpResponse, Scope, web}; use serde::Serialize; @@ -32,7 +37,7 @@ struct LogFileEntry { async fn list_logs() -> HttpResponse { let log_dir = LOG_DIR; - let entries = match std::fs::read_dir(log_dir) { + let entries = match fs::read_dir(log_dir) { Ok(dir) => dir .filter_map(|e| e.ok()) .filter_map(|e| { @@ -44,7 +49,7 @@ async fn list_logs() -> HttpResponse { let modified = meta .modified() .ok() - .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) .map(|d| d.as_secs()); Some(LogFileEntry { name, @@ -68,10 +73,10 @@ async fn download_log(path: web::Path) -> HttpResponse { })); } - let file_path = std::path::Path::new(LOG_DIR).join(&filename); + let file_path = Path::new(LOG_DIR).join(&filename); // Canonicalize to prevent symlink traversal - let canonical = match std::fs::canonicalize(&file_path) { + let canonical = match fs::canonicalize(&file_path) { Ok(p) => p, Err(_) => { return HttpResponse::NotFound().json(serde_json::json!({ @@ -79,7 +84,7 @@ async fn download_log(path: web::Path) -> HttpResponse { })); } }; - if let Ok(log_dir_canonical) = std::fs::canonicalize(LOG_DIR) + if let Ok(log_dir_canonical) = fs::canonicalize(LOG_DIR) && !canonical.starts_with(&log_dir_canonical) { return HttpResponse::Forbidden().json(serde_json::json!({ @@ -88,13 +93,13 @@ async fn download_log(path: web::Path) -> HttpResponse { } // Check file size before reading to prevent OOM on large logs - match std::fs::metadata(&canonical) { + match fs::metadata(&canonical) { Ok(meta) if meta.len() > MAX_DOWNLOAD_SIZE => { return HttpResponse::PayloadTooLarge().json(serde_json::json!({ "error": format!("Log file exceeds maximum download size ({}MB)", MAX_DOWNLOAD_SIZE / 1024 / 1024) })); } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + Err(e) if e.kind() == ErrorKind::NotFound => { return HttpResponse::NotFound().json(serde_json::json!({ "error": format!("Log file '{}' not found", filename) })); @@ -107,7 +112,7 @@ async fn download_log(path: web::Path) -> HttpResponse { Ok(_) => {} } - let content = match std::fs::read(&canonical) { + let content = match fs::read(&canonical) { Ok(bytes) => bytes, Err(e) => { return HttpResponse::InternalServerError().json(serde_json::json!({ diff --git a/net-guardia/src/adapter/http/report.rs b/net-guardia/src/adapter/http/report.rs index 9ff06e3..26103cd 100644 --- a/net-guardia/src/adapter/http/report.rs +++ b/net-guardia/src/adapter/http/report.rs @@ -1,7 +1,12 @@ +use std::fs; + use actix_web::{HttpResponse, Scope, web}; +use chrono::Local; +use tokio::task::spawn_blocking; 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::infrastructure::secret_store::SecretStore; @@ -20,14 +25,14 @@ async fn generate_report(_auth: AuthClaims, db: web::Data) -> HttpResp .ok() .flatten() .unwrap_or_else(|| "/var/lib/netguardia/reports".to_string()); - if let Err(e) = std::fs::create_dir_all(&report_dir) { + if let Err(e) = fs::create_dir_all(&report_dir) { return HttpResponse::InternalServerError().json(serde_json::json!({ "error": format!("Failed to create report directory: {}", e) })); } let db_ref = db.get_ref(); match engine::generate_html_report(db_ref as &dyn RepositoryPort, &report_dir) { - Ok(path) => match std::fs::read(&path) { + Ok(path) => match fs::read(&path) { Ok(content) => HttpResponse::Ok() .content_type("text/html; charset=utf-8") .insert_header(( @@ -89,7 +94,7 @@ async fn send_report(_auth: AuthClaims, db: web::Data, secrets: web::D } }; - let html = match crate::core::email::report::generate_weekly_report(db_ref) { + let html = match generate_weekly_report(db_ref) { Ok(h) => h, Err(e) => { return HttpResponse::InternalServerError().json(serde_json::json!({ @@ -99,9 +104,9 @@ async fn send_report(_auth: AuthClaims, db: web::Data, secrets: web::D } }; - let subject = format!("NetGuardia Weekly Report — {}", chrono::Local::now().format("%Y-%m-%d")); + let subject = format!("NetGuardia Weekly Report — {}", Local::now().format("%Y-%m-%d")); - let send_result = tokio::task::spawn_blocking(move || smtp.send(&recipient, &subject, &html)).await; + let send_result = spawn_blocking(move || smtp.send(&recipient, &subject, &html)).await; match send_result { Ok(Ok(())) => HttpResponse::Ok().json(serde_json::json!({ diff --git a/net-guardia/src/adapter/http/setup.rs b/net-guardia/src/adapter/http/setup.rs index f41b164..4f39bcb 100644 --- a/net-guardia/src/adapter/http/setup.rs +++ b/net-guardia/src/adapter/http/setup.rs @@ -1,14 +1,18 @@ -use actix_web::{HttpResponse, Scope, web}; -use serde::Deserialize; +use std::fs; +use std::path::Path; use std::sync::atomic::Ordering; +use actix_web::{HttpResponse, Scope, web}; use macros::log; +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::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 { @@ -27,7 +31,7 @@ async fn setup_status(setup_flag: web::Data) -> HttpResponse async fn list_interfaces() -> HttpResponse { // List available network interfaces - let interfaces: Vec = match std::fs::read_dir("/sys/class/net") { + let interfaces: Vec = match fs::read_dir("/sys/class/net") { Ok(entries) => entries .filter_map(|e| e.ok()) .map(|e| { @@ -98,7 +102,7 @@ async fn complete_setup( })); } let iface_path = format!("/sys/class/net/{}", iface); - if !std::path::Path::new(&iface_path).exists() { + if !Path::new(&iface_path).exists() { return HttpResponse::BadRequest().json(serde_json::json!({ "error": format!("Network interface '{}' not found", iface) })); @@ -172,11 +176,7 @@ async fn complete_setup( })) } -fn save_config( - db: &Database, - secrets: &dyn SecretStorePort, - req: &SetupRequest, -) -> Result<(), crate::model::error::Error> { +fn save_config(db: &Database, secrets: &dyn SecretStorePort, req: &SetupRequest) -> Result<(), Error> { // Save network config db.set_setting("ingress_interface", &req.ingress_interface)?; db.set_setting("egress_interface", &req.egress_interface)?; diff --git a/net-guardia/src/adapter/http/system.rs b/net-guardia/src/adapter/http/system.rs index c80069c..7bb1aaa 100644 --- a/net-guardia/src/adapter/http/system.rs +++ b/net-guardia/src/adapter/http/system.rs @@ -8,6 +8,8 @@ use crate::infrastructure::communication_manager::CommunicationManager; use crate::interface::communication::command_types::ChangeEnforceModeCommand; use crate::interface::communication::query_types::GetEnforceModeQuery; use crate::interface::port::repository::RepositoryPort; +use crate::utils::boot_time; +use crate::utils::logging::Logging; type Repo = dyn RepositoryPort; @@ -31,7 +33,7 @@ pub fn initialize() -> Scope { } async fn get_boot_time() -> impl Responder { - HttpResponse::Ok().json(crate::utils::boot_time::boot_time()) + HttpResponse::Ok().json(boot_time::boot_time()) } async fn get_enforce_mode(comm: web::Data) -> impl Responder { @@ -81,7 +83,7 @@ async fn get_config(svc: web::Data) -> impl Responder { async fn get_log_level() -> impl Responder { HttpResponse::Ok().json(serde_json::json!({ - "level": crate::utils::logging::Logging::current_level(), + "level": Logging::current_level(), })) } @@ -91,7 +93,7 @@ struct LogLevelRequest { } async fn set_log_level(body: web::Json) -> impl Responder { - match crate::utils::logging::Logging::set_level(&body.level) { + match Logging::set_level(&body.level) { Ok(new_level) => HttpResponse::Ok().json(serde_json::json!({ "level": new_level, "message": "Log level updated", diff --git a/net-guardia/src/adapter/persistence/repository.rs b/net-guardia/src/adapter/persistence/repository.rs index 6fa3b2f..97168db 100644 --- a/net-guardia/src/adapter/persistence/repository.rs +++ b/net-guardia/src/adapter/persistence/repository.rs @@ -1,18 +1,31 @@ +use std::collections::{HashMap, HashSet}; +use std::env; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use chrono::Utc; +use macros::log; use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::params; -use std::collections::HashMap; - -use macros::log; +use rusqlite::{self, Connection, Error as RusqliteError, params}; +use crate::interface::port::api_key::{ApiKeyListItem, ApiKeyPort}; +use crate::interface::port::audit::AuditPort; +use crate::interface::port::notification::NotificationConfigPort; +use crate::interface::port::repository::{ + AclRuleTuple, RepositoryPort, UserGroupTuple, UserListItem, UserTuple, UserWithGroups, +}; +use crate::interface::port::soar::{PlaybookRow, SoarExecutionRow, SoarPort}; +use crate::interface::port::stats::StatsPort; use crate::model::error::Error; use crate::model::error::database::DatabaseError; +use crate::model::identity::auth::Claims; use crate::model::log::misc::MiscLog; +use crate::model::soar::playbook_data::UpdatePlaybookRow; /// 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). fn db_encryption_key() -> Option { - match std::env::var("NETGUARDIA_DB_KEY") { + match env::var("NETGUARDIA_DB_KEY") { Ok(k) if !k.is_empty() => Some(k), _ => None, } @@ -74,18 +87,14 @@ impl Database { .max_size(if path == ":memory:" { 1 } else { 6 }) .connection_customizer(Box::new(customizer)) .build(manager) - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; // Verify the pool is actually usable (catches wrong key / corrupt DB early). { - let test_conn = pool - .get() - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + let test_conn = pool.get().map_err(DatabaseError::QueryFailed)?; test_conn .query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(())) - .map_err(|_| DatabaseError::QueryFailed { - reason: "Database encryption key is incorrect or database is corrupted".to_string(), - })?; + .map_err(|_| DatabaseError::EncryptionKeyInvalid)?; } let api_key_hmac = Self::derive_api_key_hmac(); @@ -100,10 +109,10 @@ impl Database { use hkdf::Hkdf; use sha2::Sha256; - let root_key = std::env::var("NETGUARDIA_SECRETS_KEY") + let root_key = env::var("NETGUARDIA_SECRETS_KEY") .ok() .filter(|k| !k.is_empty()) - .or_else(|| std::env::var("NETGUARDIA_DB_KEY").ok().filter(|k| !k.is_empty())) + .or_else(|| env::var("NETGUARDIA_DB_KEY").ok().filter(|k| !k.is_empty())) .unwrap_or_else(|| "netguardia-dev-api-key-secret".to_string()); let hk = Hkdf::::new(Some(b"netguardia-v1-salt"), root_key.as_bytes()); @@ -134,55 +143,47 @@ impl Database { /// Export an encrypted database to a plaintext copy. /// The original file is NOT modified. pub fn decrypt_to_file(src_path: &str, key: &str, dest_path: &str) -> Result<(), Error> { - let conn = - rusqlite::Connection::open(src_path).map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + let conn = Connection::open(src_path).map_err(DatabaseError::QueryFailed)?; conn.pragma_update(None, "key", key) - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; // Verify we can read the encrypted DB conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(())) - .map_err(|_| DatabaseError::QueryFailed { - reason: "Cannot read database with provided key — wrong key or not encrypted".to_string(), - })?; + .map_err(|_| DatabaseError::DatabaseNotReadable)?; // Attach a plaintext destination (empty key = no encryption) conn.execute_batch(&format!( "ATTACH DATABASE '{}' AS plaintext KEY '';", dest_path.replace('\'', "''"), )) - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; conn.query_row("SELECT sqlcipher_export('plaintext')", [], |_| Ok(())) - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; conn.execute_batch("DETACH DATABASE plaintext;") - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; Ok(()) } /// Encrypt a plaintext database to a new encrypted copy. /// The original file is NOT modified. pub fn encrypt_to_file(src_path: &str, key: &str, dest_path: &str) -> Result<(), Error> { - let conn = - rusqlite::Connection::open(src_path).map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + let conn = Connection::open(src_path).map_err(DatabaseError::QueryFailed)?; // Verify it's readable as plaintext conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(())) - .map_err(|_| DatabaseError::QueryFailed { - reason: "Cannot read source database — may already be encrypted".to_string(), - })?; + .map_err(|_| DatabaseError::SourceDatabaseNotReadable)?; conn.execute_batch(&format!( "ATTACH DATABASE '{}' AS encrypted KEY '{}';", dest_path.replace('\'', "''"), key.replace('\'', "''"), )) - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; conn.query_row("SELECT sqlcipher_export('encrypted')", [], |_| Ok(())) - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; conn.execute_batch("DETACH DATABASE encrypted;") - .map_err(|e| DatabaseError::QueryFailed { reason: e.to_string() })?; + .map_err(DatabaseError::QueryFailed)?; Ok(()) } fn conn(&self) -> Result, Error> { - self.pool - .get() - .map_err(|e| -> Error { DatabaseError::QueryFailed { reason: e.to_string() }.into() }) + self.pool.get().map_err(|e| DatabaseError::QueryFailed(e).into()) } fn create_tables(&self) -> Result<(), Error> { @@ -440,7 +441,7 @@ impl Database { Ok(()) } - pub fn load_acl_rules(&self) -> Result, Error> { + pub fn load_acl_rules(&self) -> Result, Error> { let conn = self.conn()?; let mut stmt = conn.prepare("SELECT ip_version, direction, list_type, ip_address, port FROM acl_rules")?; let rows = stmt.query_map([], |row| { @@ -545,8 +546,8 @@ impl Database { }); match result { Ok(val) => Ok(Some(val)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.into()), + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e)?, } } @@ -568,8 +569,8 @@ impl Database { }); match result { Ok(val) => Ok(Some(val)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.into()), + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e)?, } } @@ -583,7 +584,7 @@ impl Database { } // --- Users --- - pub fn find_user(&self, username: &str) -> Result, Error> { + pub fn find_user(&self, username: &str) -> Result, Error> { let conn = self.conn()?; let result = conn.query_row( "SELECT id, username, password_hash, role, force_password_change FROM users WHERE username = ?1", @@ -600,8 +601,8 @@ impl Database { ); match result { Ok(user) => Ok(Some(user)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.into()), + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e)?, } } @@ -644,7 +645,7 @@ impl Database { Ok(conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?) } - pub fn list_users(&self) -> Result, Error> { + pub fn list_users(&self) -> Result, Error> { let conn = self.conn()?; let mut stmt = conn.prepare("SELECT id, username, role, force_password_change, created_at FROM users ORDER BY id")?; @@ -664,7 +665,7 @@ impl Database { Ok(results) } - pub fn list_users_with_groups(&self) -> Result, Error> { + pub fn list_users_with_groups(&self) -> Result, Error> { let conn = self.conn()?; let mut stmt = conn.prepare( "SELECT u.id, u.username, u.role, u.force_password_change, u.created_at, \ @@ -686,7 +687,7 @@ impl Database { )) })?; - let mut user_map: HashMap = HashMap::new(); + let mut user_map: HashMap = HashMap::new(); let mut order: Vec = Vec::new(); for row in rows { @@ -725,10 +726,7 @@ impl Database { Ok(()) } - pub fn find_user_by_id( - &self, - user_id: i64, - ) -> Result, Error> { + pub fn find_user_by_id(&self, user_id: i64) -> Result, Error> { let conn = self.conn()?; let result = conn.query_row( "SELECT id, username, password_hash, role, force_password_change FROM users WHERE id = ?1", @@ -745,13 +743,13 @@ impl Database { ); match result { Ok(user) => Ok(Some(user)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.into()), + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e)?, } } // --- User Groups --- - pub fn list_user_groups(&self) -> Result, Error> { + pub fn list_user_groups(&self) -> Result, Error> { let conn = self.conn()?; let mut stmt = conn.prepare("SELECT id, name, description, permissions, created_at FROM user_groups ORDER BY id")?; @@ -779,10 +777,7 @@ impl Database { ) .map_err(|e| -> Error { if e.to_string().contains("UNIQUE constraint") { - DatabaseError::QueryFailed { - reason: format!("Group '{}' already exists", name), - } - .into() + DatabaseError::GroupAlreadyExists(name).into() } else { e.into() } @@ -806,7 +801,7 @@ impl Database { Ok(affected > 0) } - pub fn get_user_group(&self, id: i64) -> Result, Error> { + pub fn get_user_group(&self, id: i64) -> Result, Error> { let conn = self.conn()?; let result = conn.query_row( "SELECT id, name, description, permissions, created_at FROM user_groups WHERE id = ?1", @@ -823,8 +818,8 @@ impl Database { ); match result { Ok(group) => Ok(Some(group)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.into()), + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e)?, } } @@ -865,7 +860,7 @@ impl Database { pub fn get_user_permissions(&self, user_id: i64) -> Result, Error> { let groups = self.get_user_groups(user_id)?; - let mut all_perms = std::collections::HashSet::new(); + let mut all_perms = HashSet::new(); for (_id, _name, _desc, perms_json) in groups { if let Ok(perms) = serde_json::from_str::>(&perms_json) { for p in perms { @@ -922,9 +917,9 @@ impl Database { self.set_setting(&key_count, &count.to_string())?; if count >= 5 { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(std::time::Duration::ZERO) + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) .as_secs(); let locked_until = now + 900; // 15 minutes self.set_setting(&key_locked, &locked_until.to_string())?; @@ -939,9 +934,9 @@ impl Database { if let Some(locked_str) = self.get_setting(&key_locked)? && let Ok(locked_until) = locked_str.parse::() { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(std::time::Duration::ZERO) + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) .as_secs(); if now < locked_until { return Ok(Some(locked_until - now)); @@ -969,7 +964,7 @@ impl Database { /// Validate an API key and return Claims if valid. /// Computes HMAC-SHA256 of the key and looks it up in api_keys table. - pub fn validate_api_key(&self, api_key: &str) -> Result, Error> { + pub fn validate_api_key(&self, api_key: &str) -> Result, Error> { let digest = self.hmac_api_key(api_key); let conn = self.conn()?; @@ -1023,7 +1018,7 @@ impl Database { ], }; - Ok(Some(crate::model::identity::auth::Claims { + Ok(Some(Claims { sub: -id, // negative ID to distinguish from user IDs username: format!("api:{}", name), role: level, @@ -1031,8 +1026,8 @@ impl Database { exp: usize::MAX, // API keys don't expire (revocation via DB deletion) })) } - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.into()), + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e)?, } } @@ -1259,11 +1254,7 @@ impl Database { Ok(conn.last_insert_rowid()) } - pub fn update_playbook( - &self, - id: i64, - row: &crate::model::soar::playbook_data::UpdatePlaybookRow, - ) -> Result { + pub fn update_playbook(&self, id: i64, row: &UpdatePlaybookRow) -> Result { let conn = self.conn()?; let rows = conn.execute( "UPDATE playbooks SET name = ?2, trigger_event = ?3, condition_threshold = ?4, \ @@ -1429,8 +1420,8 @@ impl Database { |row| row.get::<_, String>(0), ) { Ok(json) => Ok(Some(json)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e.into()), + Err(RusqliteError::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e)?, } } @@ -1611,7 +1602,7 @@ impl Database { /// lookup, row_hash compute, insert) sequence is atomic and serializable. pub fn insert_audit_log(&self, actor: &str, action: &str, detail: &str) -> Result<(), Error> { let mut conn = self.conn()?; - let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let ts = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); let tx = conn.transaction()?; let prev_hash: String = tx @@ -1670,19 +1661,11 @@ impl Database { let row_hash: String = row.get(6)?; if prev_hash != expected_prev { - return Err(DatabaseError::AuditChainBroken { - id, - reason: format!("prev_hash mismatch (expected {}, found {})", expected_prev, prev_hash), - } - .into()); + return Err(DatabaseError::AuditPrevHashMismatch(id, expected_prev, prev_hash).into()); } let computed = Self::audit_row_hash(&ts, &actor, &action, &detail, &prev_hash); if computed != row_hash { - return Err(DatabaseError::AuditChainBroken { - id, - reason: format!("row_hash mismatch (computed {}, stored {})", computed, row_hash), - } - .into()); + return Err(DatabaseError::AuditRowHashMismatch(id, computed, row_hash).into()); } expected_prev = row_hash; count += 1; @@ -1693,7 +1676,7 @@ impl Database { /// Implement the RepositoryPort trait, proving Database satisfies the port contract. /// This enables adapter-level testing with mock implementations. -impl crate::interface::port::repository::RepositoryPort for Database { +impl RepositoryPort for Database { fn insert_acl_rule( &self, ip_version: u8, @@ -1714,7 +1697,7 @@ impl crate::interface::port::repository::RepositoryPort for Database { ) -> Result<(), Error> { self.delete_acl_rule(ip_version, direction, list_type, ip_address, port) } - fn load_acl_rules(&self) -> Result, Error> { + fn load_acl_rules(&self) -> Result, Error> { self.load_acl_rules() } fn set_rate_limit(&self, key: &str, value: u64) -> Result<(), Error> { @@ -1747,7 +1730,7 @@ impl crate::interface::port::repository::RepositoryPort for Database { fn set_setting(&self, key: &str, value: &str) -> Result<(), Error> { self.set_setting(key, value) } - fn find_user(&self, username: &str) -> Result, Error> { + fn find_user(&self, username: &str) -> Result, Error> { self.find_user(username) } fn insert_user( @@ -1765,10 +1748,10 @@ impl crate::interface::port::repository::RepositoryPort for Database { fn user_count(&self) -> Result { self.user_count() } - fn list_users(&self) -> Result, Error> { + fn list_users(&self) -> Result, Error> { self.list_users() } - fn list_users_with_groups(&self) -> Result, Error> { + fn list_users_with_groups(&self) -> Result, Error> { self.list_users_with_groups() } fn delete_user(&self, user_id: i64) -> Result { @@ -1780,10 +1763,10 @@ impl crate::interface::port::repository::RepositoryPort for Database { fn reset_user_password(&self, user_id: i64, password_hash: &str) -> Result<(), Error> { self.reset_user_password(user_id, password_hash) } - fn find_user_by_id(&self, user_id: i64) -> Result, Error> { + fn find_user_by_id(&self, user_id: i64) -> Result, Error> { self.find_user_by_id(user_id) } - fn list_user_groups(&self) -> Result, Error> { + fn list_user_groups(&self) -> Result, Error> { self.list_user_groups() } fn create_user_group(&self, name: &str, description: &str, permissions: &str) -> Result { @@ -1795,7 +1778,7 @@ impl crate::interface::port::repository::RepositoryPort for Database { fn delete_user_group(&self, id: i64) -> Result { self.delete_user_group(id) } - fn get_user_group(&self, id: i64) -> Result, Error> { + fn get_user_group(&self, id: i64) -> Result, Error> { self.get_user_group(id) } fn get_user_groups(&self, user_id: i64) -> Result, Error> { @@ -1827,7 +1810,7 @@ impl crate::interface::port::repository::RepositoryPort for Database { } } -impl crate::interface::port::soar::SoarPort for Database { +impl SoarPort for Database { fn get_setting(&self, key: &str) -> Result, Error> { self.get_setting(key) } @@ -1874,14 +1857,10 @@ impl crate::interface::port::soar::SoarPort for Database { ) -> Result { self.insert_playbook_action(playbook_id, action_order, action_type, params_json) } - fn load_playbooks_with_actions(&self) -> Result, Error> { + fn load_playbooks_with_actions(&self) -> Result, Error> { self.load_playbooks_with_actions() } - fn update_playbook( - &self, - id: i64, - row: &crate::model::soar::playbook_data::UpdatePlaybookRow, - ) -> Result { + fn update_playbook(&self, id: i64, row: &UpdatePlaybookRow) -> Result { self.update_playbook(id, row) } fn update_playbook_enabled(&self, id: i64, enabled: bool) -> Result { @@ -1954,7 +1933,7 @@ impl crate::interface::port::soar::SoarPort for Database { ) -> Result { self.insert_soar_execution(playbook_id, source_ip, trigger_event, actions_json) } - fn list_soar_executions(&self, limit: i64) -> Result, Error> { + fn list_soar_executions(&self, limit: i64) -> Result, Error> { self.list_soar_executions(limit) } fn load_admin_whitelist(&self) -> Result, Error> { @@ -1968,7 +1947,7 @@ impl crate::interface::port::soar::SoarPort for Database { } } -impl crate::interface::port::stats::StatsPort for Database { +impl StatsPort for Database { fn count_weekly_executions(&self, days: i64) -> Result { self.count_weekly_executions(days) } @@ -1989,7 +1968,7 @@ impl crate::interface::port::stats::StatsPort for Database { } } -impl crate::interface::port::notification::NotificationConfigPort for Database { +impl NotificationConfigPort for Database { fn get_notification_config(&self, channel: &str) -> Result, Error> { self.get_notification_config(channel) } @@ -1998,14 +1977,14 @@ impl crate::interface::port::notification::NotificationConfigPort for Database { } } -impl crate::interface::port::audit::AuditPort for Database { +impl AuditPort for Database { fn insert_audit_log(&self, actor: &str, action: &str, detail: &str) -> Result<(), Error> { self.insert_audit_log(actor, action, detail) } } -impl crate::interface::port::api_key::ApiKeyPort for Database { - fn validate_api_key(&self, api_key: &str) -> Result, Error> { +impl ApiKeyPort for Database { + fn validate_api_key(&self, api_key: &str) -> Result, Error> { self.validate_api_key(api_key) } fn hmac_api_key(&self, raw_key: &str) -> String { @@ -2014,7 +1993,7 @@ impl crate::interface::port::api_key::ApiKeyPort for Database { fn insert_api_key(&self, key_hash: &str, name: &str, permission_level: &str) -> Result { self.insert_api_key(key_hash, name, permission_level) } - fn list_api_keys(&self) -> Result, Error> { + fn list_api_keys(&self) -> Result, Error> { self.list_api_keys() } fn delete_api_key(&self, id: i64) -> Result { diff --git a/net-guardia/src/adapter/telegram/mod.rs b/net-guardia/src/adapter/telegram/mod.rs index b46365f..0958555 100644 --- a/net-guardia/src/adapter/telegram/mod.rs +++ b/net-guardia/src/adapter/telegram/mod.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use async_trait::async_trait; +use macros::log; use parking_lot::Mutex; use reqwest::Client; -use tracing::{debug, warn}; +use tokio::time::sleep; use crate::interface::port::notification::{AlertNotifier, AlertPayload, NotificationConfigPort}; use crate::interface::port::repository::RepositoryPort; @@ -12,6 +13,7 @@ use crate::interface::port::secret_store::SecretStorePort; use crate::model::config::constants::TELEGRAM_MAX_RETRIES; 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 { @@ -32,9 +34,7 @@ impl TelegramAdapter { let client = Client::builder() .timeout(Duration::from_secs(10)) .build() - .map_err(|e| NotificationError::TelegramApiError { - reason: format!("Failed to create HTTP client: {}", e), - })?; + .map_err(NotificationError::TelegramRequestFailed)?; Ok(Self { client, @@ -51,9 +51,7 @@ impl TelegramAdapter { match self.notif.get_notification_config("telegram")? { Some(json_str) => { let config: serde_json::Value = - serde_json::from_str(&json_str).map_err(|e| NotificationError::TelegramApiError { - reason: format!("Invalid telegram config JSON: {}", e), - })?; + serde_json::from_str(&json_str).map_err(NotificationError::TelegramRequestFailed)?; let mut token = config.get("bot_token").and_then(|v| v.as_str()).map(|s| s.to_string()); let chat_id = config.get("chat_id").and_then(|v| v.as_str()).map(|s| s.to_string()); @@ -119,7 +117,7 @@ impl TelegramAdapter { if e.is_timeout() { NotificationError::Timeout } else { - NotificationError::TelegramApiError { reason: e.to_string() } + NotificationError::TelegramRequestFailed(e) } })?; @@ -150,13 +148,12 @@ impl TelegramAdapter { .unwrap_or(5); if attempt < TELEGRAM_MAX_RETRIES { - warn!( - "Telegram rate limited, retrying after {}s (attempt {}/{})", + log!(SystemLog::TelegramRateLimitedRetry( retry_after, attempt + 1, - TELEGRAM_MAX_RETRIES - ); - tokio::time::sleep(Duration::from_secs(retry_after)).await; + TELEGRAM_MAX_RETRIES, + )); + sleep(Duration::from_secs(retry_after)).await; continue; } else { return Err(NotificationError::TelegramRateLimited { @@ -168,10 +165,7 @@ impl TelegramAdapter { // Other error let body = resp.text().await.unwrap_or_default(); - return Err(NotificationError::TelegramApiError { - reason: format!("HTTP {}: {}", status, body), - } - .into()); + Err(NotificationError::TelegramHttpError(status.as_u16(), body))?; } unreachable!() @@ -205,7 +199,7 @@ impl AlertNotifier for TelegramAdapter { let (bot_token, chat_id) = match self.get_config()? { Some(config) => config, None => { - debug!("Telegram not configured, skipping alert"); + log!(SystemLog::TelegramNotConfiguredSkipped); return Ok(()); } }; @@ -218,10 +212,10 @@ impl AlertNotifier for TelegramAdapter { .flatten() .and_then(|v| v.parse().ok()) .unwrap_or(20); - warn!( - "Telegram rate limit reached ({}/min), dropping alert for IP {}", - max_per_min, payload.source_ip - ); + log!(SystemLog::TelegramLocalRateLimitDropped( + max_per_min, + payload.source_ip.clone(), + )); return Ok(()); } @@ -232,12 +226,7 @@ impl AlertNotifier for TelegramAdapter { async fn send_test_message(&self) -> Result<(), Error> { let (bot_token, chat_id) = match self.get_config()? { Some(config) => config, - None => { - return Err(NotificationError::NotConfigured { - channel: "telegram".to_string(), - } - .into()); - } + None => Err(NotificationError::NotConfigured("telegram"))?, }; self.send_message( diff --git a/net-guardia/src/adapter/websocket/alert_websocket.rs b/net-guardia/src/adapter/websocket/alert_websocket.rs index 555f235..c5aaa61 100644 --- a/net-guardia/src/adapter/websocket/alert_websocket.rs +++ b/net-guardia/src/adapter/websocket/alert_websocket.rs @@ -1,8 +1,10 @@ +use actix_web::rt::spawn; use actix_web::{HttpRequest, HttpResponse, Result, web}; use actix_ws::{Message, MessageStream, Session, handle}; use futures_util::StreamExt; 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; @@ -15,7 +17,7 @@ pub async fn websocket_alert(req: HttpRequest, body: web::Payload, ai: web::Data let broadcast_rx = ai.subscribe_to_alerts(); - actix_web::rt::spawn(async move { + spawn(async move { handle_alert_connection(session, msg_stream, broadcast_rx).await; }); @@ -41,11 +43,11 @@ async fn handle_alert_connection( break; } } - Err(broadcast::error::RecvError::Lagged(skipped)) => { + Err(RecvError::Lagged(skipped)) => { log!(HttpLog::WebSocketLagged(skipped)); continue; } - Err(broadcast::error::RecvError::Closed) => { + Err(RecvError::Closed) => { break; } } diff --git a/net-guardia/src/adapter/websocket/drop_websocket.rs b/net-guardia/src/adapter/websocket/drop_websocket.rs index 9a390d7..cbc09f7 100644 --- a/net-guardia/src/adapter/websocket/drop_websocket.rs +++ b/net-guardia/src/adapter/websocket/drop_websocket.rs @@ -1,8 +1,10 @@ +use actix_web::rt::spawn; use actix_web::{HttpRequest, HttpResponse, Result, web}; use actix_ws::{Message, MessageStream, Session, handle}; use futures_util::StreamExt; use macros::log; use tokio::sync::broadcast; +use tokio::sync::broadcast::error::RecvError; use crate::core::ebpf::drop_monitor::DropMonitor; use crate::model::error::http::HttpError; @@ -19,7 +21,7 @@ pub async fn websocket_drops( let broadcast_rx = monitor.subscribe(); - actix_web::rt::spawn(async move { + spawn(async move { handle_drop_connection(session, msg_stream, broadcast_rx).await; }); @@ -45,11 +47,11 @@ async fn handle_drop_connection( break; } } - Err(broadcast::error::RecvError::Lagged(skipped)) => { + Err(RecvError::Lagged(skipped)) => { log!(HttpLog::WebSocketLagged(skipped)); continue; } - Err(broadcast::error::RecvError::Closed) => { + Err(RecvError::Closed) => { break; } } diff --git a/net-guardia/src/adapter/websocket/flow_websocket.rs b/net-guardia/src/adapter/websocket/flow_websocket.rs index 83961ac..e818f47 100644 --- a/net-guardia/src/adapter/websocket/flow_websocket.rs +++ b/net-guardia/src/adapter/websocket/flow_websocket.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use actix_web::rt::spawn; use actix_web::{HttpRequest, HttpResponse, web}; use actix_ws::Message; use futures_util::StreamExt; @@ -31,7 +32,7 @@ pub async fn flow_stats_ws( ) -> Result { let (response, mut session, mut msg_stream) = actix_ws::handle(&req, body)?; - actix_web::rt::spawn(async move { + spawn(async move { let mut subscription = default_subscription(); let mut ticker = interval(Duration::from_secs(subscription.interval_secs.unwrap_or(5))); diff --git a/net-guardia/src/adapter/websocket/health_websocket.rs b/net-guardia/src/adapter/websocket/health_websocket.rs index feb9755..b2d7876 100644 --- a/net-guardia/src/adapter/websocket/health_websocket.rs +++ b/net-guardia/src/adapter/websocket/health_websocket.rs @@ -1,8 +1,10 @@ +use actix_web::rt::spawn; use actix_web::{HttpRequest, HttpResponse, Result, web}; use actix_ws::{Message, MessageStream, Session, handle}; use futures_util::StreamExt; use macros::log; use tokio::sync::broadcast; +use tokio::sync::broadcast::error::RecvError; use crate::infrastructure::health::SystemHealth; use crate::model::error::http::HttpError; @@ -19,7 +21,7 @@ pub async fn websocket_system_health( let broadcast_rx = health.subscribe_to_metrics(); - actix_web::rt::spawn(async move { + spawn(async move { handle_health_connection(session, msg_stream, broadcast_rx).await; }); @@ -45,11 +47,11 @@ async fn handle_health_connection( break; } } - Err(broadcast::error::RecvError::Lagged(skipped)) => { + Err(RecvError::Lagged(skipped)) => { log!(HttpLog::WebSocketLagged(skipped)); continue; } - Err(broadcast::error::RecvError::Closed) => { + Err(RecvError::Closed) => { break; } } diff --git a/net-guardia/src/core/auth/extractor.rs b/net-guardia/src/core/auth/extractor.rs index 52394bd..e1efe53 100644 --- a/net-guardia/src/core/auth/extractor.rs +++ b/net-guardia/src/core/auth/extractor.rs @@ -1,7 +1,9 @@ use std::future::{Ready, ready}; +use std::ops::Deref; use actix_web::dev::Payload; -use actix_web::{FromRequest, HttpMessage, HttpRequest}; +use actix_web::error::ErrorUnauthorized; +use actix_web::{Error as ActixError, FromRequest, HttpMessage, HttpRequest}; use crate::model::identity::auth::Claims; @@ -19,7 +21,7 @@ use crate::model::identity::auth::Claims; /// ``` pub struct AuthClaims(pub Claims); -impl std::ops::Deref for AuthClaims { +impl Deref for AuthClaims { type Target = Claims; fn deref(&self) -> &Self::Target { &self.0 @@ -27,15 +29,13 @@ impl std::ops::Deref for AuthClaims { } impl FromRequest for AuthClaims { - type Error = actix_web::Error; + type Error = ActixError; type Future = Ready>; fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { match req.extensions().get::().cloned() { Some(claims) => ready(Ok(AuthClaims(claims))), - None => ready(Err(actix_web::error::ErrorUnauthorized( - serde_json::json!({"error": "Unauthorized"}), - ))), + None => ready(Err(ErrorUnauthorized(serde_json::json!({"error": "Unauthorized"})))), } } } diff --git a/net-guardia/src/core/auth/https_redirect.rs b/net-guardia/src/core/auth/https_redirect.rs index 20a05bc..06ab469 100644 --- a/net-guardia/src/core/auth/https_redirect.rs +++ b/net-guardia/src/core/auth/https_redirect.rs @@ -1,7 +1,10 @@ use std::future::{Future, Ready, ready}; +use std::net::IpAddr; use std::pin::Pin; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::task::{Context, Poll}; use actix_web::body::EitherBody; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; @@ -40,13 +43,13 @@ fn is_safe_redirect_host(host: &str) -> bool { } // Try parsing as IP — allow private ranges only - if let Ok(ip) = hostname.parse::() { + if let Ok(ip) = hostname.parse::() { return match ip { - std::net::IpAddr::V4(v4) => { + IpAddr::V4(v4) => { let o = v4.octets(); o[0] == 10 || (o[0] == 172 && (16..=31).contains(&o[1])) || (o[0] == 192 && o[1] == 168) || o[0] == 127 } - std::net::IpAddr::V6(v6) => v6.is_loopback() || (v6.segments()[0] & 0xfe00) == 0xfc00, + IpAddr::V6(v6) => v6.is_loopback() || (v6.segments()[0] & 0xfe00) == 0xfc00, }; } @@ -68,13 +71,13 @@ where fn new_transform(&self, service: S) -> Self::Future { ready(Ok(HttpsRedirectService { - service: std::rc::Rc::new(service), + service: Rc::new(service), })) } } pub struct HttpsRedirectService { - service: std::rc::Rc, + service: Rc, } impl Service for HttpsRedirectService @@ -86,12 +89,12 @@ where type Error = ActixError; type Future = Pin>>>; - fn poll_ready(&self, ctx: &mut core::task::Context<'_>) -> std::task::Poll> { + fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll> { self.service.poll_ready(ctx) } fn call(&self, req: ServiceRequest) -> Self::Future { - let service = std::rc::Rc::clone(&self.service); + let service = Rc::clone(&self.service); Box::pin(async move { // Check if force_https is enabled diff --git a/net-guardia/src/core/auth/jwt.rs b/net-guardia/src/core/auth/jwt.rs index a4390b4..f069456 100644 --- a/net-guardia/src/core/auth/jwt.rs +++ b/net-guardia/src/core/auth/jwt.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind}; @@ -34,9 +35,9 @@ impl JwtService { role: &str, permissions: Vec, ) -> Result { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(std::time::Duration::ZERO) + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) .as_secs(); let claims = Claims { diff --git a/net-guardia/src/core/auth/middleware.rs b/net-guardia/src/core/auth/middleware.rs index 6cb53e6..c3cd6b1 100644 --- a/net-guardia/src/core/auth/middleware.rs +++ b/net-guardia/src/core/auth/middleware.rs @@ -1,9 +1,11 @@ use std::future::{Future, Ready, ready}; use std::pin::Pin; use std::rc::Rc; +use std::task::{Context, Poll}; use actix_web::body::EitherBody; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; +use actix_web::http::Method; use actix_web::{Error as ActixError, HttpMessage, HttpResponse, web}; use macros::log; @@ -37,7 +39,7 @@ pub struct AuthMiddlewareService { service: Rc, } -fn required_permission(path: &str, method: &actix_web::http::Method) -> Option { +fn required_permission(path: &str, method: &Method) -> Option { let resource = if path == "/api/auth/login" || path == "/api/auth/me" || path == "/api/auth/change-password" { return None; // Public auth endpoints: login (no auth), me/change-password (auth-only, no RBAC) } else if path.starts_with("/api/auth/") { @@ -75,7 +77,7 @@ fn required_permission(path: &str, method: &actix_web::http::Method) -> Option "read", + Method::GET => "read", _ => "write", }; @@ -91,7 +93,7 @@ where type Error = ActixError; type Future = Pin>>>; - fn poll_ready(&self, ctx: &mut core::task::Context<'_>) -> std::task::Poll> { + fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll> { self.service.poll_ready(ctx) } diff --git a/net-guardia/src/core/auth/setup_guard.rs b/net-guardia/src/core/auth/setup_guard.rs index 1767cb0..e280823 100644 --- a/net-guardia/src/core/auth/setup_guard.rs +++ b/net-guardia/src/core/auth/setup_guard.rs @@ -1,7 +1,9 @@ use std::future::{Future, Ready, ready}; use std::pin::Pin; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::task::{Context, Poll}; use actix_web::body::EitherBody; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; @@ -26,13 +28,13 @@ where fn new_transform(&self, service: S) -> Self::Future { ready(Ok(SetupGuardService { - service: std::rc::Rc::new(service), + service: Rc::new(service), })) } } pub struct SetupGuardService { - service: std::rc::Rc, + service: Rc, } impl Service for SetupGuardService @@ -44,12 +46,12 @@ where type Error = ActixError; type Future = Pin>>>; - fn poll_ready(&self, ctx: &mut core::task::Context<'_>) -> std::task::Poll> { + fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll> { self.service.poll_ready(ctx) } fn call(&self, req: ServiceRequest) -> Self::Future { - let service = std::rc::Rc::clone(&self.service); + let service = Rc::clone(&self.service); Box::pin(async move { let path = req.path().to_string(); diff --git a/net-guardia/src/core/config_service.rs b/net-guardia/src/core/config_service.rs index fc3487b..145a526 100644 --- a/net-guardia/src/core/config_service.rs +++ b/net-guardia/src/core/config_service.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use serde_json::Value; + use crate::interface::port::repository::RepositoryPort; use crate::interface::port::secret_store::SecretStorePort; use crate::model::error::Error; @@ -187,14 +189,11 @@ impl ConfigService { let stages: Vec<&str> = val.split(',').map(|s| s.trim()).collect(); for stage in &stages { if !stage.is_empty() && !VALID_PIPELINE_STAGES.contains(stage) { - return Err(MiscError::ValidationError { - message: format!( - "Invalid pipeline stage '{}'. Valid stages: {}", - stage, - VALID_PIPELINE_STAGES.join(", ") - ), - } - .into()); + Err(MiscError::ValidationError(format!( + "Invalid pipeline stage '{}'. Valid stages: {}", + stage, + VALID_PIPELINE_STAGES.join(", ") + )))?; } } } @@ -211,9 +210,9 @@ impl ConfigService { /// Extract a JSON value as a non-empty string, handling string, boolean, and number types. fn json_value_as_string(v: &serde_json::Value) -> Option { match v { - serde_json::Value::String(s) if !s.is_empty() => Some(s.clone()), - serde_json::Value::Bool(b) => Some(b.to_string()), - serde_json::Value::Number(n) => Some(n.to_string()), + Value::String(s) if !s.is_empty() => Some(s.clone()), + Value::Bool(b) => Some(b.to_string()), + Value::Number(n) => Some(n.to_string()), _ => None, } } diff --git a/net-guardia/src/core/correlation/botnet.rs b/net-guardia/src/core/correlation/botnet.rs index 63cc2bf..a287b0d 100644 --- a/net-guardia/src/core/correlation/botnet.rs +++ b/net-guardia/src/core/correlation/botnet.rs @@ -130,6 +130,8 @@ impl BotnetDetector { #[cfg(test)] mod tests { + use std::thread; + use super::*; fn make_alert(src_ip: &str, dst_ip: &str) -> AlertMessage { @@ -187,7 +189,7 @@ mod tests { detector.process(&alert, &tx); assert_eq!(detector.state.len(), 1); - std::thread::sleep(Duration::from_millis(20)); + thread::sleep(Duration::from_millis(20)); let removed = detector.cleanup(); assert_eq!(removed, 1); assert_eq!(detector.state.len(), 0); diff --git a/net-guardia/src/core/correlation/engine.rs b/net-guardia/src/core/correlation/engine.rs index fc6a34a..5be24de 100644 --- a/net-guardia/src/core/correlation/engine.rs +++ b/net-guardia/src/core/correlation/engine.rs @@ -1,7 +1,9 @@ use std::time::Duration; use macros::log; +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; @@ -43,15 +45,15 @@ impl CorrelationEngine { async fn run(mut self) { log!(DetectionLog::CorrelationEngineStarted); - let mut cleanup_interval = tokio::time::interval(Duration::from_secs(CLEANUP_INTERVAL_SECS)); + let mut cleanup_interval = interval(Duration::from_secs(CLEANUP_INTERVAL_SECS)); loop { tokio::select! { result = self.alert_rx.recv() => { match result { Ok(alert) => self.process_alert(&alert), - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => break, + Err(RecvError::Lagged(_)) => continue, + Err(RecvError::Closed) => break, } } _ = cleanup_interval.tick() => { diff --git a/net-guardia/src/core/detection/beaconing.rs b/net-guardia/src/core/detection/beaconing.rs index 01277d5..153853b 100644 --- a/net-guardia/src/core/detection/beaconing.rs +++ b/net-guardia/src/core/detection/beaconing.rs @@ -2,7 +2,9 @@ use std::time::{Duration, Instant}; use dashmap::DashMap; use macros::log; +use tokio::sync::broadcast::error::RecvError; use tokio::sync::{broadcast, mpsc}; +use tokio::time::interval; use crate::model::detection::ml_detection::AlertMessage; use crate::model::event::{DetectionEvent, DetectionSource}; @@ -61,15 +63,15 @@ impl BeaconingDetector { async fn run(mut self) { log!(DetectionLog::BeaconingDetectorStarted); - let mut analysis_interval = tokio::time::interval(Duration::from_secs(ANALYSIS_INTERVAL_SECS)); + let mut analysis_interval = interval(Duration::from_secs(ANALYSIS_INTERVAL_SECS)); loop { tokio::select! { result = self.alert_rx.recv() => { match result { Ok(alert) => self.record_flow(&alert), - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => break, + Err(RecvError::Lagged(_)) => continue, + Err(RecvError::Closed) => break, } } _ = analysis_interval.tick() => { diff --git a/net-guardia/src/core/detection/orchestrator.rs b/net-guardia/src/core/detection/orchestrator.rs index 18a7948..18ac685 100644 --- a/net-guardia/src/core/detection/orchestrator.rs +++ b/net-guardia/src/core/detection/orchestrator.rs @@ -2,8 +2,10 @@ use std::num::NonZero; use std::sync::Arc; use std::time::{Duration, Instant}; +use lru::LruCache; use macros::log; use tokio::sync::mpsc; +use tokio::time::interval; use crate::infrastructure::communication_manager::CommunicationManager; use crate::infrastructure::geoip::GeoIpService; @@ -55,9 +57,9 @@ impl DetectionOrchestrator { comm, geoip, // SAFETY: NonZero::new on a non-zero literal is infallible. - src_ip_counts: lru::LruCache::new(NonZero::new(10_000).unwrap()), - repeat_tracker: lru::LruCache::new(NonZero::new(5_000).unwrap()), - dedup: lru::LruCache::new(NonZero::new(MAX_DEDUP_ENTRIES).unwrap()), + src_ip_counts: LruCache::new(NonZero::new(10_000).unwrap()), + repeat_tracker: LruCache::new(NonZero::new(5_000).unwrap()), + dedup: LruCache::new(NonZero::new(MAX_DEDUP_ENTRIES).unwrap()), dedup_window: Duration::from_secs(DEDUP_WINDOW_SECS), } } @@ -70,7 +72,7 @@ impl DetectionOrchestrator { async fn run(mut self) { log!(DetectionLog::OrchestratorStarted); - let mut cleanup_interval = tokio::time::interval(Duration::from_secs(CLEANUP_INTERVAL_SECS)); + let mut cleanup_interval = interval(Duration::from_secs(CLEANUP_INTERVAL_SECS)); loop { tokio::select! { diff --git a/net-guardia/src/core/dns_filter_service.rs b/net-guardia/src/core/dns_filter_service.rs index 0d24a20..01de1a6 100644 --- a/net-guardia/src/core/dns_filter_service.rs +++ b/net-guardia/src/core/dns_filter_service.rs @@ -30,10 +30,10 @@ impl DnsFilterService { .and_then(|v| v.parse().ok()) .unwrap_or(1000); if domains.len() > max_domains { - return Err(MiscError::ValidationError { - message: format!("too many domains (max {})", max_domains), - } - .into()); + Err(MiscError::ValidationError(format!( + "too many domains (max {})", + max_domains + )))?; } // eBPF first for domain in domains { diff --git a/net-guardia/src/core/ebpf/dns_filter.rs b/net-guardia/src/core/ebpf/dns_filter.rs index 0567aaa..981c250 100644 --- a/net-guardia/src/core/ebpf/dns_filter.rs +++ b/net-guardia/src/core/ebpf/dns_filter.rs @@ -1,3 +1,4 @@ +use core::str; use std::collections::HashSet; use common::model::dns_name::DnsName; @@ -187,16 +188,10 @@ fn domain_to_wire_format(domain: &str) -> Result { let label_bytes = label.as_bytes(); let label_len = label_bytes.len(); if label_len == 0 || label_len >= 64 { - return Err(MiscError::InvalidDnsName { - reason: format!("invalid label length: {}", label_len), - } - .into()); + return Err(MiscError::DnsLabelOutOfRange(label_len).into()); } if pos + 1 + label_len >= 128 { - return Err(MiscError::InvalidDnsName { - reason: format!("domain name too long: {}", domain), - } - .into()); + return Err(MiscError::DnsDomainTooLong(domain).into()); } name.data[pos] = label_len as u8; pos += 1; @@ -229,7 +224,7 @@ fn wire_format_to_domain(name: &DnsName) -> Option { return None; } pos += 1; - let label = core::str::from_utf8(&name.data[pos..pos + label_len]).ok()?; + let label = str::from_utf8(&name.data[pos..pos + label_len]).ok()?; labels.push(label.to_string()); pos += label_len; } diff --git a/net-guardia/src/core/ebpf/drop_monitor.rs b/net-guardia/src/core/ebpf/drop_monitor.rs index 7da97db..03434c4 100644 --- a/net-guardia/src/core/ebpf/drop_monitor.rs +++ b/net-guardia/src/core/ebpf/drop_monitor.rs @@ -1,9 +1,11 @@ use std::mem; +use std::net::Ipv6Addr; use std::sync::Arc; use std::time::Duration; use aya::maps::{MapData, RingBuf}; use tokio::sync::{broadcast, oneshot}; +use tokio::time::interval; use common::define::drop_reason::*; use common::model::drop_event::DropEvent as RawDropEvent; @@ -101,7 +103,7 @@ fn format_ips(raw: &RawDropEvent) -> (String, String) { } fn format_ipv6(bytes: &[u8; 16]) -> String { - std::net::Ipv6Addr::from(*bytes).to_string() + Ipv6Addr::from(*bytes).to_string() } fn reason_to_str(reason: u8) -> &'static str { @@ -124,7 +126,7 @@ pub async fn start_consumer(ring_buf: RingBuf, monitor: Arc (v4, v6), - _ => return Err(EbpfError::NotLoaded.into()), + _ => Err(EbpfError::NotLoaded)?, }; Self::clear_trie_v4(v4_trie); Self::clear_trie_v6(v6_trie); diff --git a/net-guardia/src/core/ebpf/xsk_manager.rs b/net-guardia/src/core/ebpf/xsk_manager.rs index d5c1b12..65d20a6 100644 --- a/net-guardia/src/core/ebpf/xsk_manager.rs +++ b/net-guardia/src/core/ebpf/xsk_manager.rs @@ -1,5 +1,5 @@ use std::ffi::CString; -use std::io::Write; +use std::io::{ErrorKind, Write}; use std::num::NonZero; use std::os::fd::AsRawFd; use std::sync::Arc; @@ -8,11 +8,11 @@ use std::time::Duration; use aya::Ebpf; use aya::maps::{MapData, XskMap}; -use crossbeam::channel::{Receiver, Sender, bounded}; +use crossbeam::channel::{Receiver, Sender, TrySendError, bounded}; use crossbeam::queue::SegQueue; use macros::log; use parking_lot::Mutex; -use tokio::sync::oneshot; +use tokio::sync::oneshot::{self, error::TryRecvError}; use xsk_rs::config::{BindFlags, FrameSize, Interface, LibxdpFlags, QueueSize, SocketConfig, UmemConfig}; use xsk_rs::{CompQueue, FillQueue, FrameDesc, RxQueue, Socket, TxQueue, Umem}; @@ -227,7 +227,7 @@ impl XskPair { let submitted = unsafe { fill_queue.produce(&fill_frames) }; if submitted != fill_frames.len() { - return Err(EbpfError::FillQueueInitFailed.into()); + Err(EbpfError::FillQueueInitFailed)?; } let pool_frames: Vec = frame_descs.iter().skip(fill_frames_count).copied().collect(); @@ -270,10 +270,10 @@ impl XskPair { loop { if let Some(ref mut rx) = shutdown_rx { match rx.try_recv() { - Ok(_) | Err(oneshot::error::TryRecvError::Closed) => { + Ok(_) | Err(TryRecvError::Closed) => { break; } - Err(oneshot::error::TryRecvError::Empty) => {} + Err(TryRecvError::Empty) => {} } } @@ -375,11 +375,11 @@ impl XskPair { buf.extend_from_slice(raw); if let Err(e) = forward_tx.try_send(buf) { match e { - crossbeam::channel::TrySendError::Full(returned) => { + TrySendError::Full(returned) => { buffer_pool.put(returned); log!(EbpfLog::ForwardChannelFull); } - crossbeam::channel::TrySendError::Disconnected(returned) => { + TrySendError::Disconnected(returned) => { buffer_pool.put(returned); log!(EbpfLog::ForwardChannelDisconnected); } @@ -462,7 +462,7 @@ impl XskPair { } if let Err(e) = self.tx.wakeup() - && e.kind() != std::io::ErrorKind::WouldBlock + && e.kind() != ErrorKind::WouldBlock { log!(EbpfLog::TXWakeupFailed(e.to_string())); } diff --git a/net-guardia/src/core/email/report.rs b/net-guardia/src/core/email/report.rs index 8f2e1b1..8c529c4 100644 --- a/net-guardia/src/core/email/report.rs +++ b/net-guardia/src/core/email/report.rs @@ -1,3 +1,5 @@ +use chrono::Local; + use crate::interface::port::repository::RepositoryPort; use crate::model::error::Error; @@ -77,7 +79,7 @@ pub fn generate_weekly_report(db: &dyn RepositoryPort) -> Result let mem = health["memory_percent"].as_f64().unwrap_or(0.0); let disk = health["disk_percent"].as_f64().unwrap_or(0.0); - let now = chrono::Local::now().format("%Y-%m-%d %H:%M"); + let now = Local::now().format("%Y-%m-%d %H:%M"); let html = format!( r#" diff --git a/net-guardia/src/core/email/scheduler.rs b/net-guardia/src/core/email/scheduler.rs index 22b412f..7f48cff 100644 --- a/net-guardia/src/core/email/scheduler.rs +++ b/net-guardia/src/core/email/scheduler.rs @@ -1,13 +1,20 @@ -use crate::interface::port::repository::RepositoryPort; -use crate::interface::port::secret_store::SecretStorePort; -use crate::model::error::Error; -use crate::model::error::notification::NotificationError; +use std::sync::Arc; + +use chrono::{Local, Weekday}; use lettre::message::header::ContentType; use lettre::transport::smtp::authentication::Credentials; use lettre::{Message, SmtpTransport, Transport}; -use std::sync::Arc; +use macros::log; +use tokio::task::{JoinHandle, spawn_blocking}; use tokio::time::{self, Duration}; -use tracing::{error, info, warn}; + +use super::report; +use crate::interface::port::repository::RepositoryPort; +use crate::interface::port::secret_store::SecretStorePort; +use crate::interface::port::soar::SoarPort; +use crate::model::error::Error; +use crate::model::error::notification::NotificationError; +use crate::model::log::system::SystemLog; /// SMTP client wrapper that builds a `lettre::SmtpTransport` from Database /// settings and sends an email. @@ -74,10 +81,7 @@ impl SmtpClient { /// Try to construct an `SmtpClient` from a SOAR port (which also provides `get_setting`). /// Same logic as `from_database`, but accepts `&dyn SoarPort` instead of `&dyn RepositoryPort`. - pub fn from_soar_port( - db: &dyn crate::interface::port::soar::SoarPort, - secrets: Option<&dyn SecretStorePort>, - ) -> Result, Error> { + pub fn from_soar_port(db: &dyn SoarPort, secrets: Option<&dyn SecretStorePort>) -> Result, Error> { let host = match db.get_setting("smtp_host")? { Some(v) if !v.is_empty() => v, _ => return Ok(None), @@ -126,12 +130,11 @@ impl SmtpClient { /// Send an HTML email using the configured SMTP transport. pub fn send(&self, to: &str, subject: &str, html_body: &str) -> Result<(), Error> { - let from_addr = self.sender.parse().map_err(|e| NotificationError::InvalidAddress { - reason: format!("invalid from address: {e}"), - })?; - let to_addr = to.parse().map_err(|e| NotificationError::InvalidAddress { - reason: format!("invalid to address: {e}"), - })?; + let from_addr = self + .sender + .parse() + .map_err(|e| NotificationError::InvalidAddress("from", e))?; + let to_addr = to.parse().map_err(|e| NotificationError::InvalidAddress("to", e))?; let email = Message::builder() .from(from_addr) @@ -139,7 +142,7 @@ impl SmtpClient { .subject(subject) .header(ContentType::TEXT_HTML) .body(html_body.to_string()) - .map_err(|e| NotificationError::MessageBuildFailed { reason: e.to_string() })?; + .map_err(NotificationError::MessageBuildFailed)?; let creds = Credentials::new(self.username.clone(), self.password.clone()); @@ -147,7 +150,7 @@ impl SmtpClient { 465 => { // Implicit TLS (SMTPS) SmtpTransport::relay(&self.host) - .map_err(|e| NotificationError::SmtpConnectionFailed { reason: e.to_string() })? + .map_err(NotificationError::SmtpConnectionFailed)? .port(self.port) .credentials(creds) .build() @@ -155,7 +158,7 @@ impl SmtpClient { 25 | 587 => { // STARTTLS (standard submission ports) SmtpTransport::starttls_relay(&self.host) - .map_err(|e| NotificationError::SmtpConnectionFailed { reason: e.to_string() })? + .map_err(NotificationError::SmtpConnectionFailed)? .port(self.port) .credentials(creds) .build() @@ -169,9 +172,7 @@ impl SmtpClient { } }; - mailer - .send(&email) - .map_err(|e| NotificationError::SmtpSendFailed { reason: e.to_string() })?; + mailer.send(&email).map_err(NotificationError::SmtpSendFailed)?; Ok(()) } @@ -190,11 +191,11 @@ impl ReportScheduler { } /// Spawn a background tokio task that runs the weekly check loop. - pub fn run(&self) -> tokio::task::JoinHandle<()> { + pub fn run(&self) -> JoinHandle<()> { let db = Arc::clone(&self.db); let secrets = self.secrets.clone(); tokio::spawn(async move { - info!("Weekly report scheduler started"); + log!(SystemLog::WeeklyReportSchedulerStarted); let mut interval = time::interval(Duration::from_secs(3600)); loop { interval.tick().await; @@ -203,19 +204,16 @@ impl ReportScheduler { continue; } - info!("Weekly report window reached — preparing report"); + log!(SystemLog::WeeklyReportWindowReached); let smtp = match SmtpClient::from_database(&*db, secrets.as_deref()) { Ok(Some(client)) => client, Ok(None) => { - warn!( - "SMTP is not configured (missing smtp_host/port/username/password). \ - Skipping weekly report." - ); + log!(SystemLog::SmtpNotConfigured); continue; } Err(e) => { - error!("Failed to read SMTP settings: {e}"); + log!(SystemLog::SmtpSettingsReadFailed(e.to_string())); continue; } }; @@ -223,26 +221,26 @@ impl ReportScheduler { let recipient = match db.get_setting("smtp_recipient") { Ok(Some(r)) if !r.is_empty() => r, _ => { - warn!("No smtp_recipient configured. Skipping weekly report."); + log!(SystemLog::SmtpRecipientMissing); continue; } }; - let html = match super::report::generate_weekly_report(&*db) { + let html = match report::generate_weekly_report(&*db) { Ok(h) => h, Err(e) => { - error!("Failed to generate weekly report: {e}"); + log!(SystemLog::WeeklyReportGenerationFailed(e.to_string())); continue; } }; - let subject = format!("NetGuardia Weekly Report — {}", chrono::Local::now().format("%Y-%m-%d")); - let send_result = tokio::task::spawn_blocking(move || smtp.send(&recipient, &subject, &html)).await; + let subject = format!("NetGuardia Weekly Report — {}", Local::now().format("%Y-%m-%d")); + let send_result = spawn_blocking(move || smtp.send(&recipient, &subject, &html)).await; match send_result { - Ok(Ok(())) => info!("Weekly report sent successfully"), - Ok(Err(e)) => error!("Failed to send weekly report: {e}"), - Err(e) => error!("Send task panicked: {e}"), + Ok(Ok(())) => log!(SystemLog::WeeklyReportSent), + Ok(Err(e)) => log!(SystemLog::WeeklyReportSendFailed(e.to_string())), + Err(e) => log!(SystemLog::WeeklyReportSendPanicked(e.to_string())), } } }) @@ -253,6 +251,6 @@ impl ReportScheduler { /// hour (i.e. Monday, hour == 8). fn is_send_window() -> bool { use chrono::{Datelike, Timelike}; - let now = chrono::Local::now(); - now.weekday() == chrono::Weekday::Mon && now.hour() == 8 + let now = Local::now(); + now.weekday() == Weekday::Mon && now.hour() == 8 } diff --git a/net-guardia/src/core/ml/config_loader.rs b/net-guardia/src/core/ml/config_loader.rs index 909c621..dbe42df 100644 --- a/net-guardia/src/core/ml/config_loader.rs +++ b/net-guardia/src/core/ml/config_loader.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fs; use std::path::{Path, PathBuf}; @@ -13,7 +13,8 @@ impl MLInferenceConfig { } fn load_file_at(path: &Path) -> Result { - let content = fs::read_to_string(path).map_err(|_| MLError::ConfigLoadFailed(path.to_path_buf()))?; + let content = fs::read_to_string(path) + .map_err(|e| MLError::ConfigLoadFailed(path.to_path_buf(), format!("read failed: {e}")))?; let config: MLInferenceConfig = serde_json::from_str(&content).map_err(|e| MLError::ConfigParseFailed(e.to_string()))?; validate(&config)?; @@ -114,13 +115,17 @@ fn apply_manifest_overrides(manifest: &ModelManifest, config: &mut MLInferenceCo } } -fn manifest_labels_to_map(labels: &std::collections::BTreeMap) -> HashMap { +fn manifest_labels_to_map(labels: &BTreeMap) -> HashMap { labels.iter().map(|(k, v)| (k.clone(), v.name.clone())).collect() } #[cfg(test)] mod tests { + use std::env; + use std::io::Write; + use super::*; + use crate::model::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 @@ -144,9 +149,6 @@ mod tests { #[test] fn feature_mismatch_between_manifest_and_sidecar_is_rejected() { - use crate::model::detection::ml_detection::ClipParams; - use std::io::Write; - // Build a minimal sidecar JSON with 2 features. let sidecar = MLInferenceConfig { ae_feature_names: vec!["flow_duration".into(), "fwd_packets".into()], @@ -169,11 +171,11 @@ mod tests { ae_feature_weights: HashMap::new(), }; - let tmp = std::env::temp_dir().join("netguardia-m1-mismatch-test"); - std::fs::create_dir_all(&tmp).unwrap(); + let tmp = env::temp_dir().join("netguardia-m1-mismatch-test"); + fs::create_dir_all(&tmp).unwrap(); let sidecar_path = tmp.join("sidecar.json"); let manifest_path = tmp.join("manifest.yaml"); - let mut f = std::fs::File::create(&sidecar_path).unwrap(); + let mut f = fs::File::create(&sidecar_path).unwrap(); f.write_all(serde_json::to_string(&sidecar).unwrap().as_bytes()) .unwrap(); @@ -191,7 +193,7 @@ features: preprocessing: scaler_sidecar: sidecar.json "#; - std::fs::write(&manifest_path, manifest_yaml).unwrap(); + fs::write(&manifest_path, manifest_yaml).unwrap(); let err = MLInferenceConfig::from_manifest_with_sidecar(&manifest_path).expect_err("should reject count mismatch"); assert!(matches!(err, MLError::ManifestInvalid { .. }), "got {err:?}"); diff --git a/net-guardia/src/core/ml/engine.rs b/net-guardia/src/core/ml/engine.rs index f7e1aaf..f632312 100644 --- a/net-guardia/src/core/ml/engine.rs +++ b/net-guardia/src/core/ml/engine.rs @@ -1,23 +1,23 @@ use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use macros::log; use parking_lot::Mutex; 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::DriftDetector; use super::flow_tracker::{FlowData, FlowTracker}; use super::inference::Inference; use super::model_loader::MLModels; use super::traffic_logger::TrafficLogger; use crate::model::detection::flow_features::FlowFeatures; -use crate::model::system::config::MLInferenceConfig; - -use super::alert::MLAlert; -use crate::model::detection::ml_detection::{EngineConfig, InferenceStats}; +use crate::model::detection::ml_detection::{EngineConfig, FlowKey, InferenceStats}; use crate::model::log::ml::MLLog; +use crate::model::system::config::MLInferenceConfig; /// 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. @@ -95,7 +95,7 @@ impl Engine { /// Protocol/port-aware min_packets: some traffic patterns are meaningful /// at very low packet counts and would be invisible to ML at the global threshold. - fn effective_min_packets(flow_key: &crate::model::detection::ml_detection::FlowKey, global: usize) -> usize { + fn effective_min_packets(flow_key: &FlowKey, global: usize) -> usize { match flow_key.protocol { // ICMP: single-packet SYN scans, ping sweeps 1 => 1, @@ -142,7 +142,7 @@ impl Engine { // Move CPU-bound ML inference off the tokio executor let engine = Arc::clone(&self); - let _ = tokio::task::spawn_blocking(move || { + let _ = spawn_blocking(move || { engine.run_inference_tick(); }) .await; @@ -152,8 +152,8 @@ impl Engine { fn run_inference_tick(&self) { let mut all_flows = Vec::new(); let mut total_count = 0; - let now_us = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + let now_us = SystemTime::now() + .duration_since(UNIX_EPOCH) .map(|d| d.as_micros() as u64) .unwrap_or(0); diff --git a/net-guardia/src/core/ml/inference.rs b/net-guardia/src/core/ml/inference.rs index ec69762..132fb85 100644 --- a/net-guardia/src/core/ml/inference.rs +++ b/net-guardia/src/core/ml/inference.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering as CmpOrdering; +use std::panic::{self, AssertUnwindSafe}; use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -71,7 +73,7 @@ impl Inference { return Vec::new(); } - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.infer_batch_inner(flows))) { + match panic::catch_unwind(AssertUnwindSafe(|| self.infer_batch_inner(flows))) { Ok(results) => { // Success: reset failure counter self.failure_count.store(0, Ordering::Relaxed); @@ -258,7 +260,7 @@ impl Inference { let (predicted_class, class_confidence) = class_probs .iter() .enumerate() - .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(CmpOrdering::Equal)) .map(|(i, &p)| (i, p)) .unwrap_or((0, 0.0)); diff --git a/net-guardia/src/core/ml/model_loader.rs b/net-guardia/src/core/ml/model_loader.rs index 39d73e2..aa749bd 100644 --- a/net-guardia/src/core/ml/model_loader.rs +++ b/net-guardia/src/core/ml/model_loader.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::Instant; use macros::log; @@ -61,34 +61,34 @@ impl MLModels { } fn loader_inner( - model_path: &PathBuf, + model_path: &Path, model: &str, features: usize, batch_size: usize, ) -> Result { let mut onnx_model = onnx() .model_for_path(model_path) - .map_err(|_| MLError::ModelLoadFailed(model_path.clone()))?; + .map_err(|e| MLError::ModelLoadFailed(model_path.to_path_buf(), format!("parse ONNX: {e}")))?; // Introspect the ONNX input fact and cross-check its last dim against the // expected feature count. A dynamic/symbolic dim is skipped — we only fail // when the ONNX graph declares a concrete integer that disagrees. if let Some(onnx_dim) = introspect_input_features(&onnx_model) { let matched = onnx_dim == features; - log!(MLLog::OnnxShapeChecked(model.to_string(), features, onnx_dim, matched,)); + log!(MLLog::OnnxShapeChecked(model.to_string(), features, onnx_dim, matched)); if !matched { - return Err(MLError::FeatureMismatch(model_path.clone(), features, onnx_dim)); + return Err(MLError::FeatureMismatch(model_path.to_path_buf(), features, onnx_dim)); } } onnx_model .set_input_fact(0, f32::fact([batch_size, features]).into()) - .map_err(|_| MLError::ModelLoadFailed(model_path.clone()))?; + .map_err(|e| MLError::ModelLoadFailed(model_path.to_path_buf(), format!("set_input_fact: {e}")))?; onnx_model .into_optimized() .and_then(|m| m.into_runnable()) - .map_err(|_| MLError::ModelLoadFailed(model_path.clone())) + .map_err(|e| MLError::ModelLoadFailed(model_path.to_path_buf(), format!("optimize/runnable: {e}"))) } pub fn get_model_info(&self, name: &str) -> String { @@ -153,7 +153,7 @@ mod tests { /// names + sidecar-derived inference config must succeed without shape mismatch. #[test] fn v10_load_named_with_manifest_paths_succeeds() { - let manifest_path = std::path::Path::new("models/manifest.yaml"); + let manifest_path = Path::new("models/manifest.yaml"); if !manifest_path.exists() { eprintln!("skipping: models/manifest.yaml absent"); return; diff --git a/net-guardia/src/core/ml/model_watcher.rs b/net-guardia/src/core/ml/model_watcher.rs index f66f2ae..9745636 100644 --- a/net-guardia/src/core/ml/model_watcher.rs +++ b/net-guardia/src/core/ml/model_watcher.rs @@ -5,10 +5,12 @@ use std::time::Duration; use macros::log; use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use tokio::sync::mpsc; +use tokio::time::sleep; use super::inference::Inference; use super::model_loader::MLModels; use crate::infrastructure::app_config::AppConfig; +use crate::model::error::ml::MLError; use crate::model::log::ml::MLLog; use crate::model::system::config::MLInferenceConfig; @@ -47,7 +49,7 @@ impl ModelWatcher { }); } - async fn run(self) -> Result<(), Box> { + async fn run(self) -> Result<(), MLError> { let models_dir = PathBuf::from("models"); if !models_dir.exists() { log!(MLLog::InferenceFailed( @@ -71,7 +73,7 @@ impl ModelWatcher { } // Debounce: drain any additional events within the window - tokio::time::sleep(Duration::from_secs(DEBOUNCE_SECS)).await; + sleep(Duration::from_secs(DEBOUNCE_SECS)).await; while rx.try_recv().is_ok() {} // Attempt reload @@ -81,7 +83,7 @@ impl ModelWatcher { Ok(()) } - fn spawn_watcher(models_dir: PathBuf, tx: mpsc::Sender<()>) -> Result { + fn spawn_watcher(models_dir: PathBuf, tx: mpsc::Sender<()>) -> Result { let mut watcher = notify::recommended_watcher(move |res: Result| { if let Ok(event) = res { let dominated = matches!(event.kind, EventKind::Create(_) | EventKind::Modify(_)); @@ -93,9 +95,12 @@ impl ModelWatcher { let _ = tx.blocking_send(()); } } - })?; + }) + .map_err(MLError::ModelWatcherFailed)?; - watcher.watch(&models_dir, RecursiveMode::NonRecursive)?; + watcher + .watch(&models_dir, RecursiveMode::NonRecursive) + .map_err(MLError::ModelWatcherFailed)?; Ok(watcher) } diff --git a/net-guardia/src/core/ml/traffic_logger.rs b/net-guardia/src/core/ml/traffic_logger.rs index d509737..c8dc391 100644 --- a/net-guardia/src/core/ml/traffic_logger.rs +++ b/net-guardia/src/core/ml/traffic_logger.rs @@ -1,5 +1,5 @@ use std::fs::OpenOptions; -use std::io::{BufWriter, Write}; +use std::io::{self, BufWriter, Write}; use std::thread; use crossbeam::channel::{Sender, TrySendError, bounded}; @@ -13,7 +13,7 @@ pub struct TrafficLogger { } impl TrafficLogger { - pub fn new(csv_path: &str, header: Vec) -> Result { + pub fn new(csv_path: &str, header: Vec) -> Result { let file = OpenOptions::new() .create(true) .write(true) diff --git a/net-guardia/src/core/notification_service.rs b/net-guardia/src/core/notification_service.rs index cf47b44..e3ebd5f 100644 --- a/net-guardia/src/core/notification_service.rs +++ b/net-guardia/src/core/notification_service.rs @@ -1,5 +1,9 @@ use std::sync::Arc; +use serde_json::Value; + +use crate::adapter::telegram::TelegramAdapter; +use crate::core::email::scheduler::SmtpClient; use crate::interface::port::notification::{AlertNotifier, NotificationConfigPort}; use crate::interface::port::repository::RepositoryPort; use crate::interface::port::secret_store::SecretStorePort; @@ -39,10 +43,10 @@ impl NotificationService { && t.len() > 8 { let redacted = format!("{}...{}", &t[..4], &t[t.len() - 4..]); - config["bot_token_redacted"] = serde_json::Value::String(redacted); + config["bot_token_redacted"] = Value::String(redacted); } config.as_object_mut().map(|obj| obj.remove("bot_token")); - config["configured"] = serde_json::Value::Bool(true); + config["configured"] = Value::Bool(true); Ok(config) } Err(_) => Ok(serde_json::json!({"configured": false})), @@ -66,18 +70,13 @@ impl NotificationService { /// Send a test Telegram message using current config. pub async fn test_telegram(&self) -> Result<(), Error> { - let adapter = crate::adapter::telegram::TelegramAdapter::new( - self.notif.clone(), - self.repo.clone(), - Some(self.secrets.clone()), - )?; + let adapter = TelegramAdapter::new(self.notif.clone(), self.repo.clone(), Some(self.secrets.clone()))?; adapter.send_test_message().await } /// Send a test email using current SMTP config. pub fn test_smtp(&self) -> Result { - let smtp_client = - crate::core::email::scheduler::SmtpClient::from_database(self.repo.as_ref(), Some(self.secrets.as_ref()))?; + let smtp_client = SmtpClient::from_database(self.repo.as_ref(), Some(self.secrets.as_ref()))?; let smtp = smtp_client.ok_or_else(|| MiscError::ValidationError { message: "SMTP not configured. Set smtp_host, smtp_port, smtp_username, smtp_password first. \ If smtp_username is not an email address, also set smtp_sender." diff --git a/net-guardia/src/core/playbook_service.rs b/net-guardia/src/core/playbook_service.rs index acfe682..5074b9a 100644 --- a/net-guardia/src/core/playbook_service.rs +++ b/net-guardia/src/core/playbook_service.rs @@ -1,18 +1,19 @@ +use std::collections::HashMap; +use std::net::IpAddr; use std::sync::Arc; +use macros::log; +use serde_json::Value; + use crate::core::soar::engine::SoarEngine; use crate::interface::port::access_control::AccessControlPort; use crate::interface::port::soar::SoarPort; -use macros::log; - use crate::model::error::Error; use crate::model::error::soar::SoarError; use crate::model::soar::playbook_data::{ ActionData, ActiveBlockData, ConditionData, CreatePlaybookInput, ExecutionData, PlaybookData, UpdatePlaybookRow, }; -use std::collections::HashMap; - /// Domain service for SOAR playbook CRUD operations. /// Coordinates DB reads/writes, SOAR engine cache refresh, and eBPF unblock. pub struct PlaybookService { @@ -98,7 +99,7 @@ impl PlaybookService { id: aid, action_order: order, action_type: atype, - params: serde_json::from_str(¶ms_str).unwrap_or(serde_json::Value::Null), + params: serde_json::from_str(¶ms_str).unwrap_or(Value::Null), }); } } @@ -220,10 +221,7 @@ impl PlaybookService { let block = self .db .get_soar_block_by_id(id)? - .ok_or_else(|| SoarError::ActionFailed { - action_type: "manual_unblock".to_string(), - reason: format!("Block rule {} not found", id), - })?; + .ok_or_else(|| SoarError::UnblockRuleNotFound(id))?; let source_ip = &block.1; // Remove from eBPF ACL @@ -254,7 +252,7 @@ impl PlaybookService { playbook_id: pb_id, source_ip, trigger_event, - actions_executed: serde_json::from_str(&actions).unwrap_or(serde_json::Value::Null), + actions_executed: serde_json::from_str(&actions).unwrap_or(Value::Null), created_at, }, ) @@ -280,9 +278,9 @@ impl PlaybookService { /// Determine IP version from a string address using proper parsing. pub fn ip_version_from_str(ip: &str) -> u8 { - match ip.parse::() { - Ok(std::net::IpAddr::V4(_)) => 4, - Ok(std::net::IpAddr::V6(_)) => 6, + match ip.parse::() { + Ok(IpAddr::V4(_)) => 4, + Ok(IpAddr::V6(_)) => 6, Err(_) => { if ip.contains(':') { 6 diff --git a/net-guardia/src/core/report/engine.rs b/net-guardia/src/core/report/engine.rs index 7a5d771..25c97ca 100644 --- a/net-guardia/src/core/report/engine.rs +++ b/net-guardia/src/core/report/engine.rs @@ -1,9 +1,14 @@ +use std::fs; use std::path::PathBuf; -use tracing::info; + +use chrono::Local; +use macros::log; use crate::interface::port::repository::RepositoryPort; use crate::model::error::Error; -use crate::model::error::notification::NotificationError; +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. @@ -14,18 +19,14 @@ pub fn generate_html_report(db: &dyn RepositoryPort, output_dir: &str) -> Result let html_path = PathBuf::from(output_dir).join(format!( "netguardia-report-{}.html", - chrono::Local::now().format("%Y%m%d-%H%M%S") + Local::now().format("%Y%m%d-%H%M%S") )); - std::fs::create_dir_all(output_dir).map_err(|e| NotificationError::TelegramApiError { - reason: format!("Failed to create report directory: {}", e), - })?; + fs::create_dir_all(output_dir).map_err(|e| IOError::CreateDirectoryFailed(PathBuf::from(output_dir), e))?; - std::fs::write(&html_path, &html).map_err(|e| NotificationError::TelegramApiError { - reason: format!("Failed to write HTML report: {}", e), - })?; + fs::write(&html_path, &html).map_err(|e| IOError::WriteFileFailed(html_path.clone(), e))?; - info!("HTML report generated at {:?}", html_path); + log!(SystemLog::HtmlReportGenerated(format!("{html_path:?}"))); Ok(html_path) } @@ -203,10 +204,5 @@ fn html_escape(s: &str) -> String { /// Generate report data and format as JSON (for API responses). pub fn generate_report_json(db: &dyn RepositoryPort) -> Result { let data = ReportData::from_database(db)?; - serde_json::to_value(&data).map_err(|e| { - NotificationError::TelegramApiError { - reason: format!("Failed to serialize report: {}", e), - } - .into() - }) + serde_json::to_value(&data).map_err(|e| MiscError::SerializeError(e).into()) } diff --git a/net-guardia/src/core/soar/engine.rs b/net-guardia/src/core/soar/engine.rs index 39bb018..f01a9a7 100644 --- a/net-guardia/src/core/soar/engine.rs +++ b/net-guardia/src/core/soar/engine.rs @@ -1,13 +1,23 @@ use std::collections::HashSet; -use std::net::IpAddr; +use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::sync::atomic::{AtomicU8, AtomicU32, Ordering}; +use std::time::{Duration, Instant}; +use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; use dashmap::DashMap; use macros::log; -use tokio::sync::broadcast; +use parking_lot::RwLock; +use reqwest::Client; +use serde_json::Value; +use tokio::net::lookup_host; +use tokio::sync::broadcast::error::RecvError; +use tokio::sync::{Mutex as TokioMutex, broadcast}; +use tokio::task::spawn_blocking; +use url::Url; use crate::core::ebpf::rate_limit::RateLimitConfig; +use crate::core::playbook_service::ip_version_from_str; use crate::core::soar::frequency::FrequencyTracker; use crate::infrastructure::communication_manager::CommunicationManager; use crate::infrastructure::geoip::GeoIpService; @@ -31,11 +41,11 @@ pub struct SoarEngine { db: Arc, access_control: Arc, /// In-memory cache of playbooks (loaded at startup, refreshed on change). - playbooks: parking_lot::RwLock>, + playbooks: RwLock>, /// In-memory cache of admin whitelist IPs. - admin_whitelist: parking_lot::RwLock>, + admin_whitelist: RwLock>, /// Cooldown tracker: maps (playbook_id, source_ip) → last execution time. - cooldowns: DashMap, + cooldowns: DashMap, /// Frequency tracker for frequency-based conditions. frequency_tracker: FrequencyTracker, /// AtomicU32 counter for active auto-blocks (avoids DB query per event). @@ -47,7 +57,7 @@ pub struct SoarEngine { /// Optional rate limit config for adjust_rate_limit action. rate_limit: Option>, /// Lock to serialize rate limit read-save-write sequences (Item 6: atomicity). - rate_limit_lock: tokio::sync::Mutex<()>, + rate_limit_lock: TokioMutex<()>, /// Cached enforce level: Monitor=0, MlOnly=1, Enforce=2. enforce_level_cache: Arc, /// Secret store for decrypting SMTP passwords etc. @@ -67,15 +77,15 @@ impl SoarEngine { let engine = Self { db, access_control, - playbooks: parking_lot::RwLock::new(Vec::new()), - admin_whitelist: parking_lot::RwLock::new(HashSet::new()), + playbooks: RwLock::new(Vec::new()), + admin_whitelist: RwLock::new(HashSet::new()), cooldowns: DashMap::new(), frequency_tracker: FrequencyTracker::new(), active_block_count: AtomicU32::new(0), alert_notifier, geoip, rate_limit, - rate_limit_lock: tokio::sync::Mutex::new(()), + rate_limit_lock: TokioMutex::new(()), enforce_level_cache, secrets, }; @@ -132,7 +142,7 @@ impl SoarEngine { name: pb.name.clone(), error: format!("Malformed action params JSON: {}", e), }); - serde_json::Value::Object(Default::default()) + Value::Object(Default::default()) }), }); } @@ -197,10 +207,7 @@ impl SoarEngine { "CRITICAL: SOAR engine failed to subscribe — automated threat response is DISABLED: {}", e ))); - SoarError::ActionFailed { - action_type: "subscribe".to_string(), - reason: e.to_string(), - } + SoarError::ActionFailed("subscribe", e) })?; tokio::spawn(async move { Self::event_loop(self, rx).await; @@ -220,10 +227,10 @@ impl SoarEngine { } }); } - Err(broadcast::error::RecvError::Lagged(n)) => { + Err(RecvError::Lagged(n)) => { log!(SoarLog::ReceiverLagged(n)); } - Err(broadcast::error::RecvError::Closed) => { + Err(RecvError::Closed) => { log!(SoarLog::ChannelClosed); break; } @@ -400,7 +407,7 @@ impl SoarEngine { /// Record cooldown for a playbook + source IP combination. fn record_cooldown(&self, playbook_id: i64, source_ip: &str) { let key = (playbook_id, source_ip.to_string()); - self.cooldowns.insert(key, std::time::Instant::now()); + self.cooldowns.insert(key, Instant::now()); } /// Execute a single playbook against an event. @@ -494,11 +501,7 @@ impl SoarEngine { "send_email" => self.action_send_email(event).await, "webhook" => self.action_webhook(action, event).await, "log" => self.action_log(action, event), - other => Err(SoarError::ActionFailed { - action_type: other.to_string(), - reason: "Unknown action type".to_string(), - } - .into()), + other => Err(SoarError::UnknownActionType(other))?, } } @@ -520,11 +523,7 @@ impl SoarEngine { .and_then(|v| v.parse().ok()) .unwrap_or(86400); if ttl_secs > max_ttl { - return Err(SoarError::InvalidTtl { - ttl_secs, - max_secs: max_ttl, - } - .into()); + Err(SoarError::InvalidTtl(ttl_secs, max_ttl))?; } // Atomically check cap and reserve a slot using CAS loop (runtime-configurable via DB) @@ -539,7 +538,7 @@ impl SoarEngine { let current_count = self.active_block_count.load(Ordering::SeqCst); if current_count >= max_cap { log!(SoarLog::CapReached(current_count, max_cap, event.source_ip.clone())); - return Err(SoarError::CapReached { max_cap }.into()); + Err(SoarError::CapReached(max_cap))?; } if self .active_block_count @@ -557,7 +556,7 @@ impl SoarEngine { } // Calculate expiry time - let expires_at = chrono::Utc::now() + chrono::Duration::seconds(ttl_secs as i64); + let expires_at = Utc::now() + ChronoDuration::seconds(ttl_secs as i64); let expires_str = expires_at.format("%Y-%m-%d %H:%M:%S").to_string(); // Record in soar_block_rules @@ -584,7 +583,7 @@ impl SoarEngine { } // Also persist to acl_rules for consistency - let ip_version = crate::core::playbook_service::ip_version_from_str(&event.source_ip); + let ip_version = ip_version_from_str(&event.source_ip); self.db .insert_acl_rule(ip_version, "source", "blacklist", &event.source_ip, 0)?; @@ -599,20 +598,13 @@ impl SoarEngine { action: &PlaybookAction, event: &ThreatDetectedEvent, ) -> Result { - let rate_limit = self.rate_limit.as_ref().ok_or_else(|| SoarError::ActionFailed { - action_type: "adjust_rate_limit".to_string(), - reason: "Rate limit config not available".to_string(), - })?; + let rate_limit = 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); if !(0.01..=1.0).contains(&factor) { - return Err(SoarError::ActionFailed { - action_type: "adjust_rate_limit".to_string(), - reason: format!("factor must be 0.01..1.0, got {}", factor), - } - .into()); + Err(SoarError::InvalidRateLimitFactor(factor))?; } let max_ttl: u64 = self @@ -623,11 +615,7 @@ impl SoarEngine { .and_then(|v| v.parse().ok()) .unwrap_or(86400); if ttl_secs > max_ttl { - return Err(SoarError::InvalidTtl { - ttl_secs, - max_secs: max_ttl, - } - .into()); + Err(SoarError::InvalidTtl(ttl_secs, max_ttl))?; } // Acquire lock to serialize rate limit read-save-write (Item 6: atomicity) @@ -652,7 +640,7 @@ impl SoarEngine { } // Store TTL for restoration - let expires_at = chrono::Utc::now() + chrono::Duration::seconds(ttl_secs as i64); + let expires_at = Utc::now() + ChronoDuration::seconds(ttl_secs as i64); self.db.set_setting( "soar_rate_limit_expires", &expires_at.format("%Y-%m-%d %H:%M:%S").to_string(), @@ -697,7 +685,7 @@ impl SoarEngine { async fn action_send_telegram(&self, event: &ThreatDetectedEvent) -> Result { if let Some(notifier) = &self.alert_notifier { let country = if let Some(geoip) = &self.geoip { - if let Ok(ip_addr) = event.source_ip.parse::() { + if let Ok(ip_addr) = event.source_ip.parse::() { match geoip.lookup(ip_addr).await { Ok(Some(loc)) => loc.country, _ => None, @@ -719,7 +707,7 @@ impl SoarEngine { "SOAR auto-response triggered (hits: {}{})", event.flow_count, repeat_tag, ), - timestamp: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), + timestamp: Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), }; notifier.send_alert(&payload).await?; Ok("Telegram notification sent".to_string()) @@ -748,15 +736,12 @@ impl SoarEngine { event.source_ip, event.attack_type, event.confidence * 100.0, - chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"), + Utc::now().format("%Y-%m-%d %H:%M:%S UTC"), ); if let Some(recipient) = self.db.get_setting("smtp_recipient")? { - tokio::task::spawn_blocking(move || smtp.send(&recipient, &subject, &body)) + spawn_blocking(move || smtp.send(&recipient, &subject, &body)) .await - .map_err(|e| SoarError::ActionFailed { - action_type: "send_email".to_string(), - reason: e.to_string(), - })??; + .map_err(|e| SoarError::ActionFailed("send_email", e))??; Ok("Email alert sent".to_string()) } else { Ok("No SMTP recipient configured, skipped".to_string()) @@ -773,41 +758,25 @@ impl SoarEngine { .params .get("url") .and_then(|v| v.as_str()) - .ok_or_else(|| SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: "Missing 'url' parameter".to_string(), - })?; + .ok_or_else(|| SoarError::WebhookMissingParam("url"))?; let timeout_secs = action.params.get("timeout_secs").and_then(|v| v.as_u64()).unwrap_or(10); // Parse URL and extract host - let parsed_url = url::Url::parse(url_str).map_err(|e| SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: format!("Invalid URL: {}", e), - })?; + let parsed_url = Url::parse(url_str).map_err(|e| SoarError::ActionFailed("webhook", e))?; - let host = parsed_url.host_str().ok_or_else(|| SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: "URL has no host".to_string(), - })?; + 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 resolve_target = format!("{}:{}", host, port); - let addrs: Vec = tokio::net::lookup_host(&resolve_target) + let addrs: Vec = lookup_host(&resolve_target) .await - .map_err(|e| SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: format!("DNS resolution failed for '{}': {}", host, e), - })? + .map_err(|e| SoarError::ActionFailed(format!("webhook (DNS for {})", host), e))? .collect(); if addrs.is_empty() { - return Err(SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: format!("DNS resolution returned no addresses for '{}'", host), - } - .into()); + Err(SoarError::WebhookDnsEmpty(host))?; } for addr in &addrs { @@ -817,11 +786,7 @@ impl SoarEngine { url_str, addr.ip() ))); - return Err(SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: format!("SSRF blocked: host '{}' resolves to private IP {}", host, addr.ip()), - } - .into()); + Err(SoarError::WebhookSsrfBlocked(host, addr.ip().to_string()))?; } } @@ -838,40 +803,32 @@ impl SoarEngine { "geoip_country": event.geoip_country, "is_repeat_offender": event.is_repeat_offender, "detection_sources": sources_str, - "timestamp": chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), + "timestamp": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), }); // Pin resolved IPs to prevent DNS rebinding: the DNS check above verified // all resolved addresses are public, so we force reqwest to use those same // addresses instead of re-resolving (which could return a private IP on TTL expiry). - let mut client_builder = reqwest::Client::builder().timeout(std::time::Duration::from_secs(timeout_secs)); + let mut client_builder = Client::builder().timeout(Duration::from_secs(timeout_secs)); for addr in &addrs { client_builder = client_builder.resolve(host, *addr); } - let client = client_builder.build().map_err(|e| SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: format!("HTTP client error: {}", e), - })?; + let client = client_builder + .build() + .map_err(|e| SoarError::ActionFailed("webhook", e))?; let resp = client .post(url_str) .json(&payload) .send() .await - .map_err(|e| SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: format!("HTTP request failed: {}", e), - })?; + .map_err(|e| SoarError::ActionFailed("webhook", e))?; let status = resp.status(); if status.is_success() { Ok(format!("Webhook sent to {} (status {})", url_str, status)) } else { - Err(SoarError::ActionFailed { - action_type: "webhook".to_string(), - reason: format!("Webhook returned HTTP {}", status), - } - .into()) + Err(SoarError::WebhookHttpStatus(status.as_u16()))? } } @@ -1042,11 +999,11 @@ impl SoarEngine { None => return Ok(()), // No active adjustment }; - let expires = chrono::NaiveDateTime::parse_from_str(&expires_str, "%Y-%m-%d %H:%M:%S") + let expires = NaiveDateTime::parse_from_str(&expires_str, "%Y-%m-%d %H:%M:%S") .map(|dt| dt.and_utc()) - .unwrap_or_else(|_| chrono::Utc::now()); + .unwrap_or_else(|_| Utc::now()); - if chrono::Utc::now() < expires { + if Utc::now() < expires { return Ok(()); // Not yet expired } @@ -1110,7 +1067,7 @@ impl SoarEngine { let playbooks = self.playbooks.read(); playbooks.iter().map(|p| p.cooldown_secs as u64).max().unwrap_or(3600) }; - let expiry = std::time::Duration::from_secs(max_cooldown_secs.saturating_mul(2).max(3600)); + let expiry = Duration::from_secs(max_cooldown_secs.saturating_mul(2).max(3600)); let before = self.cooldowns.len(); self.cooldowns.retain(|_, instant| instant.elapsed() < expiry); let removed = before.saturating_sub(self.cooldowns.len()); @@ -1148,6 +1105,7 @@ impl SoarEngine { mod tests { use super::*; use crate::model::error::ebpf::EbpfError; + use crate::model::event::DetectionSource; use parking_lot::Mutex; use std::sync::atomic::AtomicBool; @@ -1169,10 +1127,10 @@ mod tests { } #[async_trait::async_trait] - impl crate::interface::port::access_control::AccessControlPort for MockAccessControl { + impl AccessControlPort for MockAccessControl { async fn block_ip(&self, ip: &str) -> Result<(), Error> { if self.should_fail.load(Ordering::SeqCst) { - return Err(EbpfError::UnknownError.into()); + Err(EbpfError::UnknownError)?; } self.blocked_ips.lock().push(ip.to_string()); Ok(()) @@ -1180,7 +1138,7 @@ mod tests { async fn unblock_ip(&self, ip: &str) -> Result<(), Error> { if self.should_fail.load(Ordering::SeqCst) { - return Err(EbpfError::UnknownError.into()); + Err(EbpfError::UnknownError)?; } self.unblocked_ips.lock().push(ip.to_string()); Ok(()) @@ -1192,7 +1150,7 @@ mod tests { Arc::new(Database::new(":memory:").expect("Failed to create test database")) as Arc } - fn test_engine(ac: Arc) -> SoarEngine { + fn test_engine(ac: Arc) -> SoarEngine { let db = test_db(); db.seed_default_playbooks().ok(); // Tests expect enforce mode to be active so block_ip actions execute @@ -1217,7 +1175,7 @@ mod tests { protocol: 6, geoip_country: None, is_repeat_offender: false, - sources: vec![crate::model::event::DetectionSource::ML], + sources: vec![DetectionSource::ML], ae_score: 0.0, anomaly_score: 0.0, c2_score: 0.0, @@ -1252,7 +1210,7 @@ mod tests { protocol: 6, geoip_country: None, is_repeat_offender: false, - sources: vec![crate::model::event::DetectionSource::ML], + sources: vec![DetectionSource::ML], ae_score: 0.0, anomaly_score: 0.0, c2_score: 0.0, @@ -1279,7 +1237,7 @@ mod tests { db.seed_default_playbooks().ok(); // Insert a fake active block - let expires = (chrono::Utc::now() + chrono::Duration::hours(1)) + let expires = (Utc::now() + ChronoDuration::hours(1)) .format("%Y-%m-%d %H:%M:%S") .to_string(); db.insert_soar_block_rule("192.168.1.100", 1, &expires).ok(); @@ -1300,7 +1258,7 @@ mod tests { let db = test_db(); db.seed_default_playbooks().ok(); - let expires = (chrono::Utc::now() + chrono::Duration::hours(1)) + let expires = (Utc::now() + ChronoDuration::hours(1)) .format("%Y-%m-%d %H:%M:%S") .to_string(); db.insert_soar_block_rule("10.0.0.1", 1, &expires).ok(); @@ -1334,7 +1292,7 @@ mod tests { protocol: 6, geoip_country: None, is_repeat_offender: false, - sources: vec![crate::model::event::DetectionSource::ML], + sources: vec![DetectionSource::ML], ae_score: 0.0, anomaly_score: 0.0, c2_score: 0.0, @@ -1369,7 +1327,7 @@ mod tests { protocol: 6, geoip_country: None, is_repeat_offender: false, - sources: vec![crate::model::event::DetectionSource::ML], + sources: vec![DetectionSource::ML], ae_score: 0.0, anomaly_score: 0.0, c2_score: 0.0, @@ -1414,7 +1372,7 @@ mod tests { protocol: 6, geoip_country: None, is_repeat_offender: false, - sources: vec![crate::model::event::DetectionSource::ML], + sources: vec![DetectionSource::ML], ae_score: 0.0, anomaly_score: 0.0, c2_score: 0.0, @@ -1490,7 +1448,7 @@ mod tests { protocol: 6, geoip_country: country.map(|s| s.to_string()), is_repeat_offender: repeat, - sources: vec![crate::model::event::DetectionSource::ML], + sources: vec![DetectionSource::ML], ae_score: 0.0, anomaly_score: 0.0, c2_score: 0.0, diff --git a/net-guardia/src/core/soar/scheduler.rs b/net-guardia/src/core/soar/scheduler.rs index 88efd52..77f3682 100644 --- a/net-guardia/src/core/soar/scheduler.rs +++ b/net-guardia/src/core/soar/scheduler.rs @@ -1,8 +1,10 @@ use std::sync::Arc; 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::interface::port::access_control::AccessControlPort; use crate::interface::port::soar::SoarPort; @@ -32,7 +34,7 @@ impl TtlScheduler { } /// Spawn a background tokio task that runs the TTL sweep every 60 seconds. - pub fn start(self) -> tokio::task::JoinHandle<()> { + pub fn start(self) -> JoinHandle<()> { tokio::spawn(async move { log!(SoarLog::EngineStarted); // TTL scheduler uses same log channel let mut interval = time::interval(Duration::from_secs(60)); @@ -93,7 +95,7 @@ impl TtlScheduler { } // Also remove from acl_rules DB table (the auto-added entry) - let ip_version = crate::core::playbook_service::ip_version_from_str(source_ip); + let ip_version = ip_version_from_str(source_ip); if let Err(e) = self.db.delete_acl_rule(ip_version, "source", "blacklist", source_ip, 0) { log!(SoarError::AclCleanupFailed(e)); } diff --git a/net-guardia/src/core/stats_aggregator.rs b/net-guardia/src/core/stats_aggregator.rs index e6efc0b..0241a2a 100644 --- a/net-guardia/src/core/stats_aggregator.rs +++ b/net-guardia/src/core/stats_aggregator.rs @@ -1,11 +1,14 @@ use std::sync::Arc; +use macros::log; +use serde_json::Value; +use tokio::task::JoinHandle; use tokio::time::{self, Duration}; -use tracing::{error, info}; use crate::interface::port::repository::RepositoryPort; use crate::interface::port::stats::StatsPort; 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. @@ -20,18 +23,18 @@ impl StatsAggregator { } /// Spawn a background task that runs aggregation every hour. - pub fn start(self) -> tokio::task::JoinHandle<()> { + pub fn start(self) -> JoinHandle<()> { tokio::spawn(async move { - info!("Stats aggregator started (1h interval)"); + log!(SystemLog::StatsAggregatorStarted); // Run immediately on startup if let Err(e) = self.aggregate() { - error!("Initial stats aggregation failed: {}", e); + log!(SystemLog::InitialStatsAggregationFailed(e.to_string())); } let mut interval = time::interval(Duration::from_secs(3600)); loop { interval.tick().await; if let Err(e) = self.aggregate() { - error!("Stats aggregation failed: {}", e); + log!(SystemLog::StatsAggregationFailed(e.to_string())); } } }) @@ -62,7 +65,7 @@ impl StatsAggregator { let breakdown = self.stats.weekly_threat_breakdown(days)?; let breakdown_json: serde_json::Map = breakdown .into_iter() - .map(|(k, v)| (k, serde_json::Value::Number(v.into()))) + .map(|(k, v)| (k, Value::Number(v.into()))) .collect(); self.repo.set_setting( "weekly_threat_breakdown", @@ -133,10 +136,12 @@ impl StatsAggregator { self.repo.set_setting("weekly_geo_distribution", "[]")?; } - info!( - "Stats aggregated: {} threats, {} blocks, {} unblocks, {} active rules", - threats_count, blocks_count, unblocks_count, active_rules - ); + log!(SystemLog::StatsAggregated( + threats_count, + blocks_count, + unblocks_count, + active_rules, + )); Ok(()) } diff --git a/net-guardia/src/core/system.rs b/net-guardia/src/core/system.rs index d700177..1d247d9 100644 --- a/net-guardia/src/core/system.rs +++ b/net-guardia/src/core/system.rs @@ -1,30 +1,49 @@ use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; use aya::Ebpf; use aya::maps::{MapData, ProgramArray}; use macros::log; +use parking_lot::{Mutex, RwLock}; +use sd_notify::NotifyState; +use tokio::signal::ctrl_c; +use tokio::sync::broadcast::{Receiver, error::RecvError}; +use tokio::sync::mpsc::{self, Sender}; +use tokio::sync::oneshot; +use tokio::time::{interval, sleep}; use crate::adapter::persistence::Database; use crate::core::acl_service::AclService; use crate::core::auth::jwt::JwtService; use crate::core::config_service::ConfigService; +use crate::core::correlation::engine::CorrelationEngine; +use crate::core::detection::beaconing::BeaconingDetector; +use crate::core::detection::orchestrator::DetectionOrchestrator; use crate::core::dns_filter_service::DnsFilterService; use crate::core::ebpf::EbpfServices; use crate::core::email::scheduler::ReportScheduler; use crate::core::ml::drift_detector::DriftDetector; +use crate::core::ml::model_watcher::ModelWatcher; use crate::core::notification_service::NotificationService; use crate::core::playbook_service::PlaybookService; use crate::core::rate_limit_service::RateLimitService; use crate::core::soar::engine::SoarEngine; use crate::core::soar::scheduler::TtlScheduler; +use crate::core::stats_aggregator::StatsAggregator; use crate::infrastructure::app_config::AppConfig; use crate::infrastructure::app_services::AppServices; use crate::infrastructure::audit_logger::AuditLogger; use crate::infrastructure::communication_manager::CommunicationManager; use crate::infrastructure::geoip::GeoIpService; -use crate::infrastructure::http_server::HttpServerParams; +use crate::infrastructure::http_server::{self, HttpServerParams}; use crate::infrastructure::secret_store::SecretStore; use crate::infrastructure::service_factory::ServiceFactory; +use crate::infrastructure::suricata_manager::SuricataManager; +use crate::infrastructure::suricata_monitor::SuricataMonitor; +use crate::interface::port::audit::AuditPort; +use crate::interface::port::repository::RepositoryPort; +use crate::interface::port::stats::StatsPort; use crate::model::detection::ml_detection::AlertMessage; use crate::model::error::Error; use crate::model::error::system::SystemError; @@ -33,6 +52,7 @@ use crate::model::log::detection::DetectionLog; use crate::model::log::ml::MLLog; use crate::model::log::system::SystemLog; use crate::model::system::config::MLInferenceConfig; +use crate::model::system::health::EbpfHealth; use crate::model::system::readiness::ReadinessState; /// API-triggered shutdown mode. @@ -45,13 +65,13 @@ pub enum ShutdownMode { /// Handle for triggering shutdown from HTTP endpoints. /// Uses a parking_lot::Mutex> so it can be shared as app_data. pub struct ShutdownHandle { - tx: parking_lot::Mutex>>, + tx: Mutex>>, } impl ShutdownHandle { - fn new(tx: tokio::sync::oneshot::Sender) -> Self { + fn new(tx: oneshot::Sender) -> Self { Self { - tx: parking_lot::Mutex::new(Some(tx)), + tx: Mutex::new(Some(tx)), } } @@ -92,9 +112,9 @@ pub struct System { pub drift_detector: Arc>, pub shutdown_handle: Option>, _ingress_program_array: Option>, - pub ebpf_health: Arc>, - pub suricata_manager: Arc, - suricata_shutdown: Option>, + pub ebpf_health: Arc>, + pub suricata_manager: Arc, + suricata_shutdown: Option>, } impl System { @@ -200,15 +220,13 @@ impl System { } // Start audit logger (subscribe to AuditEvent + DriftDetectedEvent, persist to DB) - let audit_logger = Arc::new(AuditLogger::new( - self.db.clone() as Arc - )); + let audit_logger = Arc::new(AuditLogger::new(self.db.clone() as Arc)); audit_logger.start(&self.comm); // Start stats aggregator (writes weekly_* settings for Report engine) - let stats_aggregator = crate::core::stats_aggregator::StatsAggregator::new( - self.db.clone() as Arc, - self.db.clone() as Arc, + let stats_aggregator = StatsAggregator::new( + self.db.clone() as Arc, + self.db.clone() as Arc, ); stats_aggregator.start(); @@ -222,12 +240,8 @@ impl System { } // Start detection orchestrator (dedup + enrichment + source attribution) - let (detection_tx, detection_rx) = tokio::sync::mpsc::channel::(1024); - let orchestrator = crate::core::detection::orchestrator::DetectionOrchestrator::new( - detection_rx, - self.comm.clone(), - self.geoip.clone(), - ); + let (detection_tx, detection_rx) = mpsc::channel::(1024); + let orchestrator = DetectionOrchestrator::new(detection_rx, self.comm.clone(), self.geoip.clone()); orchestrator.start(); // Clone detection_tx for correlation engine and beaconing detector @@ -237,14 +251,12 @@ impl System { // Start cross-flow correlation engine (botnet, scan, lateral movement detection) let correlation_alert_rx = self.app_services.ml_alert.subscribe_to_alerts(); - let correlation_engine = - crate::core::correlation::engine::CorrelationEngine::new(correlation_alert_rx, correlation_detection_tx); + let correlation_engine = CorrelationEngine::new(correlation_alert_rx, correlation_detection_tx); correlation_engine.start(); // Start temporal beaconing detector (CV-based C2 periodicity detection) let beaconing_alert_rx = self.app_services.ml_alert.subscribe_to_alerts(); - let beaconing_detector = - crate::core::detection::beaconing::BeaconingDetector::new(beaconing_alert_rx, beaconing_detection_tx); + let beaconing_detector = BeaconingDetector::new(beaconing_alert_rx, beaconing_detection_tx); beaconing_detector.start(); // Bridge ML alerts → DetectionEvent (thin adapter, no enrichment) @@ -253,7 +265,7 @@ impl System { }); // Start model hot-reload watcher (monitors models/ for .onnx changes) - let model_watcher = crate::core::ml::model_watcher::ModelWatcher::new( + let model_watcher = ModelWatcher::new( self.app_services.ml_engine.inference_pipeline().clone(), self.app_config.clone(), self.inference_config.clone(), @@ -261,7 +273,7 @@ impl System { model_watcher.start(); // Initialize force_https flag from DB setting - let force_https = Arc::new(std::sync::atomic::AtomicBool::new( + let force_https = Arc::new(AtomicBool::new( self.db .get_setting("force_https") .ok() @@ -273,29 +285,21 @@ impl System { // Build per-subsystem readiness flags for /health/ready let readiness_state = Arc::new(ReadinessState::new()); // DB is connected (System::new succeeded), ML models loaded (AppServices::new succeeded) - readiness_state - .db_connected - .store(true, std::sync::atomic::Ordering::SeqCst); - readiness_state - .ml_model_loaded - .store(true, std::sync::atomic::Ordering::SeqCst); + readiness_state.db_connected.store(true, Ordering::SeqCst); + readiness_state.ml_model_loaded.store(true, Ordering::SeqCst); // eBPF was attached above (self.attach_ebpf succeeded) - readiness_state - .ebpf_attached - .store(true, std::sync::atomic::Ordering::SeqCst); + readiness_state.ebpf_attached.store(true, Ordering::SeqCst); // SOAR engine started above (self.soar_engine.start succeeded) - readiness_state - .soar_engine_running - .store(true, std::sync::atomic::Ordering::SeqCst); + readiness_state.soar_engine_running.store(true, Ordering::SeqCst); // Create shutdown channel for API-triggered shutdown/restart - let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::(); + let (shutdown_tx, shutdown_rx) = oneshot::channel::(); let shutdown_handle = Arc::new(ShutdownHandle::new(shutdown_tx)); self.shutdown_handle = Some(shutdown_handle.clone()); // Start HTTP server in background (!Send, use actix::spawn) - let setup_flag = Arc::new(std::sync::atomic::AtomicBool::new(true)); - let ready_flag = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let setup_flag = Arc::new(AtomicBool::new(true)); + let ready_flag = Arc::new(AtomicBool::new(false)); let ready_flag_for_set = ready_flag.clone(); let params = HttpServerParams { app_config: self.app_config.clone(), @@ -321,33 +325,33 @@ impl System { }; let ready_for_http = ready_flag_for_set.clone(); actix::spawn(async move { - if let Err(e) = crate::infrastructure::http_server::run(params).await { + if let Err(e) = http_server::run(params).await { // HTTP server failed — mark system as NOT ready so health checks fail - ready_for_http.store(false, std::sync::atomic::Ordering::SeqCst); + ready_for_http.store(false, Ordering::SeqCst); log!(SystemError::HttpServerError(e)); } }); // Brief delay to catch immediate bind failures before reporting ready - tokio::time::sleep(std::time::Duration::from_millis(100)).await; + sleep(Duration::from_millis(100)).await; // Mark system as ready — /api/health/ready will now return {"ready": true} - ready_flag_for_set.store(true, std::sync::atomic::Ordering::SeqCst); + ready_flag_for_set.store(true, Ordering::SeqCst); // Notify systemd that we are ready (Type=notify) - let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]); + let _ = sd_notify::notify(true, &[NotifyState::Ready]); log!(SystemLog::FullInitComplete); // Start systemd watchdog keepalive task { let mut usec: u64 = 0; if sd_notify::watchdog_enabled(false, &mut usec) && usec > 0 { - let notify_interval = std::time::Duration::from_micros(usec / 2); + let notify_interval = Duration::from_micros(usec / 2); tokio::spawn(async move { - let mut tick = tokio::time::interval(notify_interval); + let mut tick = interval(notify_interval); loop { tick.tick().await; - let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Watchdog]); + let _ = sd_notify::notify(false, &[NotifyState::Watchdog]); } }); } @@ -359,12 +363,11 @@ impl System { // Start Suricata eve.json monitor — tails the log file, translates // alert events into DetectionEvent on the shared mpsc. No-op if the // bridge is disabled in config. - crate::infrastructure::suricata_monitor::SuricataMonitor::new(self.app_config.clone(), suricata_detection_tx) - .start(); + SuricataMonitor::new(self.app_config.clone(), suricata_detection_tx).start(); // Wait for shutdown signal (ctrl-c OR API-triggered) tokio::select! { - _ = tokio::signal::ctrl_c() => { + _ = ctrl_c() => { Ok(ShutdownMode::Shutdown) } mode = shutdown_rx => { @@ -409,7 +412,7 @@ impl System { drift_detector: Arc>, comm: Arc, ) { - let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); + let mut interval = interval(Duration::from_secs(60)); loop { interval.tick().await; let report = drift_detector.lock().check_drift(); @@ -431,10 +434,7 @@ impl System { /// Thin ML bridge: converts AlertMessage → DetectionEvent and sends to orchestrator. /// Enrichment (GeoIP, hit count, repeat offender) is handled by the DetectionOrchestrator. - async fn bridge_ml_to_detection( - mut rx: tokio::sync::broadcast::Receiver, - tx: tokio::sync::mpsc::Sender, - ) { + async fn bridge_ml_to_detection(mut rx: Receiver, tx: Sender) { log!(DetectionLog::MlBridgeStarted); loop { @@ -465,10 +465,10 @@ impl System { break; // Orchestrator dropped } } - Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + Err(RecvError::Lagged(n)) => { log!(DetectionLog::MlBridgeLagged(n)); } - Err(tokio::sync::broadcast::error::RecvError::Closed) => { + Err(RecvError::Closed) => { log!(DetectionLog::MlAlertChannelClosed); break; } diff --git a/net-guardia/src/infrastructure/audit_logger.rs b/net-guardia/src/infrastructure/audit_logger.rs index 92e37cd..f10fb21 100644 --- a/net-guardia/src/infrastructure/audit_logger.rs +++ b/net-guardia/src/infrastructure/audit_logger.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use macros::log; +use tokio::sync::broadcast::error::RecvError; use crate::infrastructure::communication_manager::CommunicationManager; use crate::interface::port::audit::AuditPort; @@ -30,10 +31,10 @@ impl AuditLogger { Ok(event) => { this.handle_audit_event(&event); } - Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + Err(RecvError::Lagged(n)) => { log!(AuditLog::AuditLagged { count: n }); } - Err(tokio::sync::broadcast::error::RecvError::Closed) => { + Err(RecvError::Closed) => { log!(AuditLog::AuditChannelClosed); break; } @@ -53,10 +54,10 @@ impl AuditLogger { Ok(event) => { this.handle_drift_event(&event); } - Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + Err(RecvError::Lagged(n)) => { log!(AuditLog::AuditLagged { count: n }); } - Err(tokio::sync::broadcast::error::RecvError::Closed) => { + Err(RecvError::Closed) => { log!(AuditLog::AuditChannelClosed); break; } diff --git a/net-guardia/src/infrastructure/communication_manager.rs b/net-guardia/src/infrastructure/communication_manager.rs index d8ee5d7..3bfc1ec 100644 --- a/net-guardia/src/infrastructure/communication_manager.rs +++ b/net-guardia/src/infrastructure/communication_manager.rs @@ -164,6 +164,8 @@ impl ServiceRegistrar { #[cfg(test)] mod tests { + use std::sync::Mutex; + use super::*; use crate::interface::communication::command::Command; use crate::interface::communication::event::Event; @@ -183,7 +185,7 @@ mod tests { impl Command for TestCommand {} struct TestCommandHandler { - received: Arc>>, + received: Arc>>, } #[async_trait] @@ -226,7 +228,7 @@ mod tests { #[tokio::test] async fn test_command_dispatch() { - let received = Arc::new(std::sync::Mutex::new(Vec::new())); + let received = Arc::new(Mutex::new(Vec::new())); let handler = Arc::new(TestCommandHandler { received: received.clone(), }); @@ -305,7 +307,7 @@ mod tests { #[tokio::test] async fn test_service_registrar() { - let received = Arc::new(std::sync::Mutex::new(Vec::new())); + let received = Arc::new(Mutex::new(Vec::new())); let handler = Arc::new(TestCommandHandler { received: received.clone(), }); diff --git a/net-guardia/src/infrastructure/enforce_mode_handler.rs b/net-guardia/src/infrastructure/enforce_mode_handler.rs index d8dd9fa..5935bec 100644 --- a/net-guardia/src/infrastructure/enforce_mode_handler.rs +++ b/net-guardia/src/infrastructure/enforce_mode_handler.rs @@ -85,7 +85,7 @@ mod tests { let db = Arc::new(Database::new(":memory:").unwrap()) as Arc; let cache = Arc::new(AtomicU8::new(0)); let comm = Arc::new(CommunicationManager::new()); - comm.register_event_type::(); + comm.register_event_type::(); let handler = Arc::new(EnforceModeHandler::new(db, comm.clone(), cache)); let _ = comm .clone() diff --git a/net-guardia/src/infrastructure/health.rs b/net-guardia/src/infrastructure/health.rs index 844d681..ddb15a8 100644 --- a/net-guardia/src/infrastructure/health.rs +++ b/net-guardia/src/infrastructure/health.rs @@ -1,5 +1,6 @@ +use std::env::consts; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use macros::log; use sysinfo::{Components, Networks, System}; @@ -102,8 +103,8 @@ impl SystemHealth { egress_interface: &str, ebpf: EbpfHealth, ) -> SystemHealthMetrics { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); @@ -162,7 +163,7 @@ impl SystemHealth { kernel_version: System::kernel_version(), os_name: System::name(), os_version: System::os_version(), - architecture: std::env::consts::ARCH.to_string(), + architecture: consts::ARCH.to_string(), total_processes: system.processes().len(), } } diff --git a/net-guardia/src/infrastructure/http_server.rs b/net-guardia/src/infrastructure/http_server.rs index 23b6959..8a3601e 100644 --- a/net-guardia/src/infrastructure/http_server.rs +++ b/net-guardia/src/infrastructure/http_server.rs @@ -1,7 +1,12 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use actix_cors::Cors; +use actix_web::dev::ServerHandle; use actix_web::web::route; -use actix_web::{App, HttpServer, web}; +use actix_web::{App, HttpResponse, HttpServer, web}; +use macros::log; use crate::adapter::http::{ acl, api_keys, audit as audit_api, auth, default, filter, health as health_api, logs as logs_api, ml, @@ -13,6 +18,7 @@ use crate::adapter::websocket::routes as ws; use crate::core::acl_service::AclService; use crate::core::auth::https_redirect::{ForceHttpsFlag, HttpsRedirect}; use crate::core::auth::jwt::JwtService; +use crate::core::auth::middleware::AuthMiddleware; use crate::core::auth::setup_guard::{SetupCompleteFlag, SetupGuard}; use crate::core::config_service::ConfigService; use crate::core::dns_filter_service::DnsFilterService; @@ -25,6 +31,7 @@ use crate::infrastructure::app_config::AppConfig; use crate::infrastructure::app_services::AppServices; use crate::infrastructure::communication_manager::CommunicationManager; use crate::infrastructure::secret_store::SecretStore; +use crate::infrastructure::suricata_manager::SuricataManager; use crate::interface::port::api_key::ApiKeyPort; use crate::interface::port::repository::RepositoryPort; use crate::model::config::constants::HTTP_FALLBACK_PORT; @@ -32,12 +39,10 @@ use crate::model::error::Error; use crate::model::error::http::HttpError; use crate::model::log::http::HttpLog; use crate::model::system::config::MLInferenceConfig; -use macros::log; +use crate::model::system::readiness::ReadinessState; /// Shared flag: true when all services (eBPF, ML, SOAR) are fully initialized. -pub type ReadyFlag = Arc; - -use crate::model::system::readiness::ReadinessState; +pub type ReadyFlag = Arc; /// Parameters for starting the HTTP server, avoiding `#[cfg]` on function params. pub struct HttpServerParams { @@ -60,7 +65,7 @@ pub struct HttpServerParams { pub rate_limit_service: Arc, pub force_https: ForceHttpsFlag, pub shutdown_handle: Arc, - pub suricata_manager: Arc, + pub suricata_manager: Arc, } /// CORS configuration shared by both full and setup servers. @@ -72,7 +77,7 @@ pub struct HttpServerParams { /// The host is parsed as an IP address — domain names like "10.malware.net" /// are rejected because they fail IP parsing. fn cors(allowed_origins: Vec) -> actix_cors::Cors { - actix_cors::Cors::default() + Cors::default() .allowed_origin_fn(move |origin, _req_head| { let origin_str = origin.to_str().unwrap_or(""); if !allowed_origins.is_empty() { @@ -117,7 +122,7 @@ fn is_private_origin(origin: &str) -> bool { } // Try parsing as IPv4 - if let Ok(ipv4) = host.parse::() { + if let Ok(ipv4) = host.parse::() { let octets = ipv4.octets(); return octets[0] == 127 // 127.0.0.0/8 || octets[0] == 10 // 10.0.0.0/8 @@ -126,7 +131,7 @@ fn is_private_origin(origin: &str) -> bool { } // Try parsing as IPv6 - if let Ok(ipv6) = host.parse::() { + if let Ok(ipv6) = host.parse::() { return ipv6.is_loopback(); } @@ -143,7 +148,7 @@ pub fn start_setup_server( jwt_service: Arc, setup_complete: SetupCompleteFlag, port: u16, -) -> Result { +) -> Result { let make_app = move || { App::new() .wrap(cors(vec![])) @@ -155,7 +160,7 @@ pub fn start_setup_server( .app_data(web::Data::new(setup_complete.clone())) .service( web::scope("/api") - .wrap(crate::core::auth::middleware::AuthMiddleware) + .wrap(AuthMiddleware) .service(auth::initialize()) .service(setup_api::initialize()) .service(health_api::initialize()), @@ -177,7 +182,7 @@ pub fn start_setup_server( .bind(format!("0.0.0.0:{}", HTTP_FALLBACK_PORT)) .map_err(HttpError::BindPortError)? } - Err(e) => return Err(HttpError::BindPortError(e).into()), + Err(e) => Err(HttpError::BindPortError(e))?, } .run(); @@ -262,7 +267,7 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> { app.wrap(SetupGuard) .service( web::scope("/api") - .wrap(crate::core::auth::middleware::AuthMiddleware) + .wrap(AuthMiddleware) .service(auth::initialize()) .service(acl::initialize()) .service(filter::initialize()) @@ -293,13 +298,13 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> { Ok(()) } -async fn health_ready(ready: web::Data, state: web::Data) -> actix_web::HttpResponse { +async fn health_ready(ready: web::Data, state: web::Data) -> HttpResponse { use std::sync::atomic::Ordering::SeqCst; let is_ready = ready.load(SeqCst); let uptime_secs = state.started_at.elapsed().as_secs(); - actix_web::HttpResponse::Ok().json(serde_json::json!({ + HttpResponse::Ok().json(serde_json::json!({ "ready": is_ready, "subsystems": { "db_connected": state.db_connected.load(SeqCst), diff --git a/net-guardia/src/infrastructure/secret_store.rs b/net-guardia/src/infrastructure/secret_store.rs index 5bb9cad..92597c7 100644 --- a/net-guardia/src/infrastructure/secret_store.rs +++ b/net-guardia/src/infrastructure/secret_store.rs @@ -1,3 +1,4 @@ +use std::env; use std::sync::Arc; use aes_gcm::aead::{Aead, KeyInit, OsRng}; @@ -24,10 +25,10 @@ pub struct SecretStore { impl SecretStore { pub fn new(db: Arc) -> Self { - let raw_key = std::env::var("NETGUARDIA_SECRETS_KEY") + let raw_key = env::var("NETGUARDIA_SECRETS_KEY") .ok() .filter(|k| !k.is_empty()) - .or_else(|| std::env::var("NETGUARDIA_DB_KEY").ok().filter(|k| !k.is_empty())); + .or_else(|| env::var("NETGUARDIA_DB_KEY").ok().filter(|k| !k.is_empty())); let cipher = raw_key.map(|key| { let hk = Hkdf::::new(Some(b"netguardia-v1-salt"), key.as_bytes()); @@ -53,7 +54,7 @@ impl SecretStore { let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let ciphertext = cipher .encrypt(&nonce, plaintext.as_bytes()) - .map_err(|e| CryptoError::EncryptionFailed { reason: e.to_string() })?; + .map_err(CryptoError::EncryptionFailed)?; let envelope = serde_json::json!({ "v": 1, "alg": "aes-256-gcm", @@ -76,24 +77,18 @@ impl SecretStore { } fn decrypt(&self, envelope_json: &str) -> Result { - let env: serde_json::Value = - serde_json::from_str(envelope_json).map_err(|e| CryptoError::InvalidEnvelope { reason: e.to_string() })?; + let env: serde_json::Value = serde_json::from_str(envelope_json).map_err(CryptoError::EnvelopeParseFailed)?; let version = env.get("v").and_then(|v| v.as_u64()).unwrap_or(0); if version != 1 { - return Err(CryptoError::InvalidEnvelope { - reason: format!("unsupported envelope version: {version}"), - } - .into()); + Err(CryptoError::UnsupportedEnvelopeVersion(version))?; } let alg = env.get("alg").and_then(|v| v.as_str()).unwrap_or(""); let ct_b64 = env .get("ct") .and_then(|v| v.as_str()) - .ok_or_else(|| CryptoError::InvalidEnvelope { - reason: "missing ct field".to_string(), - })?; + .ok_or_else(|| CryptoError::MissingEnvelopeField("ct"))?; match alg { "none" => { @@ -101,50 +96,31 @@ impl SecretStore { // Prevents downgrade attack where attacker replaces encrypted envelope // with alg:none + attacker-controlled plaintext. if self.cipher.is_some() { - return Err(CryptoError::InvalidEnvelope { - reason: "alg:none rejected in production mode (encryption key is set)".to_string(), - } - .into()); + Err(CryptoError::AlgNoneRejected)?; } - let plaintext_bytes = B64 - .decode(ct_b64) - .map_err(|e| CryptoError::DecryptionFailed { reason: e.to_string() })?; - String::from_utf8(plaintext_bytes) - .map_err(|e| CryptoError::DecryptionFailed { reason: e.to_string() }.into()) + let plaintext_bytes = B64.decode(ct_b64).map_err(CryptoError::DecryptionFailed)?; + Ok(String::from_utf8(plaintext_bytes).map_err(CryptoError::DecryptionFailed)?) } "aes-256-gcm" => { let cipher = self.cipher.as_ref().ok_or(CryptoError::MasterKeyUnavailable)?; - let nonce_b64 = - env.get("nonce") - .and_then(|v| v.as_str()) - .ok_or_else(|| CryptoError::InvalidEnvelope { - reason: "missing nonce field".to_string(), - })?; + let nonce_b64 = env + .get("nonce") + .and_then(|v| v.as_str()) + .ok_or_else(|| CryptoError::MissingEnvelopeField("nonce"))?; - let nonce_bytes = B64 - .decode(nonce_b64) - .map_err(|e| CryptoError::DecryptionFailed { reason: e.to_string() })?; - let nonce = - Nonce::from_exact_iter(nonce_bytes.into_iter()).ok_or_else(|| CryptoError::DecryptionFailed { - reason: "invalid nonce length".to_string(), - })?; + let nonce_bytes = B64.decode(nonce_b64).map_err(CryptoError::DecryptionFailed)?; + let nonce = Nonce::from_exact_iter(nonce_bytes.into_iter()).ok_or(CryptoError::InvalidNonceLength)?; - let ciphertext = B64 - .decode(ct_b64) - .map_err(|e| CryptoError::DecryptionFailed { reason: e.to_string() })?; + let ciphertext = B64.decode(ct_b64).map_err(CryptoError::DecryptionFailed)?; let plaintext_bytes = cipher .decrypt(&nonce, ciphertext.as_ref()) - .map_err(|e| CryptoError::DecryptionFailed { reason: e.to_string() })?; + .map_err(CryptoError::DecryptionFailed)?; - String::from_utf8(plaintext_bytes) - .map_err(|e| CryptoError::DecryptionFailed { reason: e.to_string() }.into()) + Ok(String::from_utf8(plaintext_bytes).map_err(CryptoError::DecryptionFailed)?) } - other => Err(CryptoError::InvalidEnvelope { - reason: format!("unsupported algorithm: {other}"), - } - .into()), + other => Err(CryptoError::UnsupportedAlgorithm(other))?, } } } diff --git a/net-guardia/src/infrastructure/service_factory.rs b/net-guardia/src/infrastructure/service_factory.rs index 9b32f9c..4969e93 100644 --- a/net-guardia/src/infrastructure/service_factory.rs +++ b/net-guardia/src/infrastructure/service_factory.rs @@ -1,7 +1,11 @@ use std::collections::HashMap; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; +use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::AtomicU8; +use std::time::Duration; + +use parking_lot::{Mutex, RwLock}; use aya::Ebpf; use aya::maps::{Array, MapData, ProgramArray}; @@ -29,9 +33,11 @@ use crate::core::soar::scheduler::TtlScheduler; use crate::infrastructure::app_config::AppConfig; use crate::infrastructure::app_services::AppServices; use crate::infrastructure::communication_manager::CommunicationManager; +use crate::infrastructure::ebpf_preflight; use crate::infrastructure::enforce_mode_handler::EnforceModeHandler; use crate::infrastructure::geoip::GeoIpService; use crate::infrastructure::secret_store::SecretStore; +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; @@ -44,11 +50,13 @@ use crate::model::detection::drift::FeatureBaselines; use crate::model::error::Error; use crate::model::error::ebpf::EbpfError; use crate::model::error::misc::MiscError; +use crate::model::event::{AuditEvent, DriftDetectedEvent, ThreatDetectedEvent}; use crate::model::log::ebpf::EbpfLog; use crate::model::log::ml::MLLog; use crate::model::log::system::SystemLog; use crate::model::monitoring::direction::FlowDirection; use crate::model::system::config::MLInferenceConfig; +use crate::model::system::health::EbpfFailStage; use crate::model::system::health::EbpfHealth; use macros::log; @@ -72,7 +80,7 @@ pub struct AppState { pub playbook_service: Arc, pub rate_limit_service: Arc, pub geoip: Option>, - pub drift_detector: Arc>, + pub drift_detector: Arc>, pub ingress_ebpf: Option, pub egress_ebpf: Option, /// Held to keep the eBPF program array map FD alive. `None` when eBPF @@ -82,8 +90,8 @@ pub struct AppState { /// or to `Unavailable { stage, category, reason }` when any stage fails. /// Read by `SystemHealth` for the metrics broadcast and by HTTP handlers /// that render runtime status to the frontend. - pub ebpf_health: Arc>, - pub suricata_manager: Arc, + pub ebpf_health: Arc>, + pub suricata_manager: Arc, } /// Maps stage name (from config.toml) to (function_name, stage_id). @@ -109,7 +117,7 @@ impl ServiceFactory { // Prefer `models/manifest.yaml` when present (v12 BYO-model path). The manifest // is the user-authored source of truth for features, labels, thresholds, and // model filenames; the legacy JSON-only path is the fallback. - let manifest_path = std::path::PathBuf::from("models/manifest.yaml"); + let manifest_path = PathBuf::from("models/manifest.yaml"); let (inference_config, ml_manifest): (Arc, Option) = if manifest_path.exists() { let (cfg, manifest) = MLInferenceConfig::from_manifest_with_sidecar(&manifest_path)?; @@ -129,7 +137,7 @@ impl ServiceFactory { // Shared eBPF health handle. Initialized Healthy; downgraded to // Unavailable with a classified reason if any stage below fails. - let ebpf_health = Arc::new(parking_lot::RwLock::new(EbpfHealth::Healthy)); + let ebpf_health = Arc::new(RwLock::new(EbpfHealth::Healthy)); // Attempt full eBPF bring-up. On any failure we classify the error, // write it into `ebpf_health`, and fall back to an `EbpfServices` @@ -141,7 +149,7 @@ impl ServiceFactory { { Ok((ingress, egress, pa, services)) => (Some(ingress), Some(egress), Some(pa), Arc::new(services)), Err((stage, err)) => { - let health = crate::infrastructure::ebpf_preflight::classify(stage, &err, None); + let health = ebpf_preflight::classify(stage, &err, None); log!(SystemLog::EbpfBringupFailed(format!("{:?}", health))); *ebpf_health.write() = health; ( @@ -171,9 +179,9 @@ impl ServiceFactory { .flatten() .and_then(|v| v.parse().ok()) .unwrap_or(3600); - let drift_detector = Arc::new(parking_lot::Mutex::new(DriftDetector::new( + let drift_detector = Arc::new(Mutex::new(DriftDetector::new( baselines, - std::time::Duration::from_secs(drift_window_secs), + Duration::from_secs(drift_window_secs), ))); let app_services = Arc::new(AppServices::new( @@ -206,9 +214,9 @@ impl ServiceFactory { .build(); // Register event type channels - comm.register_event_type::(); - comm.register_event_type::(); - comm.register_event_type::(); + comm.register_event_type::(); + comm.register_event_type::(); + comm.register_event_type::(); // Seed default SOAR playbooks if empty (db.as_ref() as &dyn SoarPort).seed_default_playbooks()?; @@ -294,7 +302,7 @@ impl ServiceFactory { secret_store_port, )); - let suricata_manager = crate::infrastructure::suricata_manager::SuricataManager::new(app_config.clone()); + let suricata_manager = SuricataManager::new(app_config.clone()); Ok(AppState { app_config, @@ -333,8 +341,7 @@ impl ServiceFactory { #[allow(clippy::type_complexity)] fn try_build_ebpf( app_config: &Arc, - ) -> Result<(Ebpf, Ebpf, ProgramArray, EbpfServices), (crate::model::system::health::EbpfFailStage, Error)> - { + ) -> Result<(Ebpf, Ebpf, ProgramArray, EbpfServices), (EbpfFailStage, Error)> { use crate::model::system::health::EbpfFailStage; let mut ingress = Self::load_ebpf("ingress").map_err(|e| (EbpfFailStage::Load, e))?; @@ -357,7 +364,7 @@ impl ServiceFactory { let bytes = match name { "ingress" => aya::include_bytes_aligned!(concat!(env!("OUT_DIR"), "/net-guardia-ingress")), "egress" => aya::include_bytes_aligned!(concat!(env!("OUT_DIR"), "/net-guardia-egress")), - _ => return Err(EbpfError::ProgramNotFound.into()), + _ => Err(EbpfError::ProgramNotFound)?, }; Ok(Ebpf::load(bytes).map_err(EbpfError::EbpfNotFound)?) } @@ -426,7 +433,7 @@ impl ServiceFactory { .try_into() .map_err(EbpfError::MapOperationError)?; program.load().map_err(EbpfError::AttachProgramFailed)?; - let fd = program.fd().map_err(|_| EbpfError::UnknownError)?; + let fd = program.fd().map_err(EbpfError::ProgramFdFailed)?; program_array.set(slot, fd, 0).map_err(EbpfError::MapOperationError)?; Ok(()) } @@ -479,7 +486,7 @@ impl ServiceFactory { } Err(skb_err) => { log!(EbpfLog::XdpAttachFailed(ifname.to_string(), skb_err.to_string())); - Err(EbpfError::AttachProgramFailed(skb_err).into()) + Err(EbpfError::AttachProgramFailed(skb_err))? } } } diff --git a/net-guardia/src/infrastructure/statistics.rs b/net-guardia/src/infrastructure/statistics.rs index dd6299a..1135b95 100644 --- a/net-guardia/src/infrastructure/statistics.rs +++ b/net-guardia/src/infrastructure/statistics.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use std::time; +use std::time::{SystemTime, UNIX_EPOCH}; use crate::core::ml::engine::Engine; use crate::core::ml::flow_tracker::FlowData; @@ -49,8 +49,8 @@ impl FlowStatistics { } pub fn get_filtered_flows(&self, sub: &FlowSubscription) -> Vec { - let now_us = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) + let now_us = SystemTime::now() + .duration_since(UNIX_EPOCH) .map(|d| d.as_micros() as u64) .unwrap_or(0); @@ -102,8 +102,8 @@ impl FlowStatistics { } } - let now_ms = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) + let now_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0); diff --git a/net-guardia/src/infrastructure/suricata_manager.rs b/net-guardia/src/infrastructure/suricata_manager.rs index 65b3afe..6a06c79 100644 --- a/net-guardia/src/infrastructure/suricata_manager.rs +++ b/net-guardia/src/infrastructure/suricata_manager.rs @@ -16,8 +16,10 @@ use std::sync::Arc; use std::time::Duration; use macros::log; +use parking_lot::RwLock; use tokio::process::{Child, Command}; use tokio::sync::oneshot; +use tokio::time::{sleep, timeout}; use crate::infrastructure::app_config::AppConfig; use crate::model::error::Error; @@ -27,7 +29,7 @@ use crate::model::system::suricata::SuricataHealth; pub struct SuricataManager { config: Arc, - health: Arc>, + health: Arc>, } impl SuricataManager { @@ -41,12 +43,12 @@ impl SuricataManager { }; Arc::new(Self { config, - health: Arc::new(parking_lot::RwLock::new(initial)), + health: Arc::new(RwLock::new(initial)), }) } /// Shared handle for HTTP handlers and the health broadcast. - pub fn health(&self) -> Arc> { + pub fn health(&self) -> Arc> { self.health.clone() } @@ -100,7 +102,7 @@ impl SuricataManager { backoff, }); *self.health.write() = SuricataHealth::Stopped { reason }; - tokio::time::sleep(Duration::from_secs(backoff)).await; + sleep(Duration::from_secs(backoff)).await; continue; } else { log!(SuricataLog::Stopped { reason: reason.clone() }); @@ -123,11 +125,11 @@ impl SuricataManager { fn preflight(config: &AppConfig) -> Result<(), Error> { let bin = &config.suricata.binary_path; if !Path::new(bin).exists() { - return Err(SuricataError::BinaryNotFound { path: bin.clone() }.into()); + Err(SuricataError::BinaryNotFound(bin.clone()))?; } let cfg = &config.suricata.config_path; if !Path::new(cfg).exists() { - return Err(SuricataError::ConfigNotFound { path: cfg.clone() }.into()); + Err(SuricataError::ConfigNotFound(cfg.clone()))?; } Ok(()) } @@ -169,7 +171,7 @@ impl SuricataManager { libc::kill(pid as libc::pid_t, libc::SIGTERM); } } - match tokio::time::timeout(Duration::from_secs(5), child.wait()).await { + match timeout(Duration::from_secs(5), child.wait()).await { Ok(_) => {} Err(_) => { let _ = child.kill().await; diff --git a/net-guardia/src/infrastructure/suricata_monitor.rs b/net-guardia/src/infrastructure/suricata_monitor.rs index ff5c185..2ca3e19 100644 --- a/net-guardia/src/infrastructure/suricata_monitor.rs +++ b/net-guardia/src/infrastructure/suricata_monitor.rs @@ -20,9 +20,10 @@ use std::sync::Arc; use std::time::Duration; use macros::log; -use tokio::fs::File; +use tokio::fs::{self, File}; use tokio::io::{AsyncBufReadExt, AsyncSeekExt, BufReader}; use tokio::sync::mpsc; +use tokio::time::sleep; use crate::infrastructure::app_config::AppConfig; use crate::model::event::{DetectionEvent, DetectionSource}; @@ -64,14 +65,14 @@ impl SuricataMonitor { if !Path::new(&path).exists() { log!(SuricataLog::MonitorWaitingForFile { path: path.clone() }); while !Path::new(&path).exists() { - tokio::time::sleep(FILE_WAIT_INTERVAL).await; + sleep(FILE_WAIT_INTERVAL).await; } } let mut file = match File::open(&path).await { Ok(f) => f, Err(_) => { - tokio::time::sleep(FILE_WAIT_INTERVAL).await; + sleep(FILE_WAIT_INTERVAL).await; continue; } }; @@ -88,13 +89,13 @@ impl SuricataMonitor { match reader.read_line(&mut line).await { Ok(0) => { // EOF — check for rotation (file truncated or replaced). - if let Ok(meta) = tokio::fs::metadata(&path).await + if let Ok(meta) = fs::metadata(&path).await && meta.len() < pos { log!(SuricataLog::MonitorFileRotated); break; // reopen } - tokio::time::sleep(POLL_INTERVAL).await; + sleep(POLL_INTERVAL).await; } Ok(n) => { pos += n as u64; @@ -102,7 +103,7 @@ impl SuricataMonitor { } Err(_) => { // Read error — treat as rotation and reopen. - tokio::time::sleep(POLL_INTERVAL).await; + sleep(POLL_INTERVAL).await; break; } } diff --git a/net-guardia/src/main.rs b/net-guardia/src/main.rs index 347c0b0..603565a 100644 --- a/net-guardia/src/main.rs +++ b/net-guardia/src/main.rs @@ -5,15 +5,23 @@ mod interface; mod model; mod utils; +use std::env; +use std::path::PathBuf; +use std::process; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread; +use std::time::Duration; use macros::log; +use sd_notify::NotifyState; +use tokio::{signal, time}; use crate::adapter::persistence::Database; use crate::core::auth::jwt::JwtService; use crate::core::auth::password; -use crate::core::system::System; +use crate::core::system::{ShutdownMode, System}; +use crate::infrastructure::http_server; use crate::infrastructure::secret_store::SecretStore; use crate::interface::port::secret_store::SecretStorePort; use crate::model::error::Error; @@ -44,16 +52,16 @@ async fn main() -> Result<(), Error> { Logging::initialize()?; // Handle DB encrypt/decrypt subcommands before full startup - let args: Vec = std::env::args().collect(); + let args: Vec = env::args().collect(); if args.len() >= 2 { - let db_path = std::env::var("NETGUARDIA_DB_PATH").unwrap_or_else(|_| "net-guardia.db".to_string()); + let db_path = env::var("NETGUARDIA_DB_PATH").unwrap_or_else(|_| "net-guardia.db".to_string()); match args[1].as_str() { "--decrypt-db" => { - let key = match std::env::var("NETGUARDIA_DB_KEY") { + let key = match env::var("NETGUARDIA_DB_KEY") { Ok(k) if !k.is_empty() => k, _ => { eprintln!("Error: NETGUARDIA_DB_KEY must be set for decrypt"); - std::process::exit(1); + process::exit(1); } }; let dest = args.get(2).map(|s| s.as_str()).unwrap_or("net-guardia-decrypted.db"); @@ -63,11 +71,11 @@ async fn main() -> Result<(), Error> { return Ok(()); } "--encrypt-db" => { - let key = match std::env::var("NETGUARDIA_DB_KEY") { + let key = match env::var("NETGUARDIA_DB_KEY") { Ok(k) if !k.is_empty() => k, _ => { eprintln!("Error: NETGUARDIA_DB_KEY must be set for encrypt"); - std::process::exit(1); + process::exit(1); } }; let dest = args.get(2).map(|s| s.as_str()).unwrap_or("net-guardia-encrypted.db"); @@ -86,7 +94,7 @@ async fn main() -> Result<(), Error> { } Err(e) => { eprintln!("FAIL: {}", e); - std::process::exit(2); + process::exit(2); } } } @@ -95,7 +103,7 @@ async fn main() -> Result<(), Error> { } // Phase 1: Create DB (fast — needed for setup check and setup server) - let db_path = std::env::var("NETGUARDIA_DB_PATH").unwrap_or_else(|_| "net-guardia.db".to_string()); + let db_path = env::var("NETGUARDIA_DB_PATH").unwrap_or_else(|_| "net-guardia.db".to_string()); let db = Arc::new(Database::new(&db_path)?); // Seed default admin user if no users exist @@ -123,19 +131,13 @@ async fn main() -> Result<(), Error> { let setup_flag = Arc::new(AtomicBool::new(false)); // Start setup server — returns handle for graceful shutdown - let handle = infrastructure::http_server::start_setup_server( - db.clone(), - secret_store, - jwt_service, - setup_flag.clone(), - 8080, - )?; + let handle = http_server::start_setup_server(db.clone(), secret_store, jwt_service, setup_flag.clone(), 8080)?; // Wait for setup completion or shutdown signal let flag = setup_flag.clone(); let setup_done = async move { loop { - tokio::time::sleep(std::time::Duration::from_millis(500)).await; + time::sleep(Duration::from_millis(500)).await; if flag.load(Ordering::SeqCst) { return; } @@ -146,7 +148,7 @@ async fn main() -> Result<(), Error> { _ = setup_done => { log!(SystemLog::SetupCompleted); } - _ = tokio::signal::ctrl_c() => { + _ = signal::ctrl_c() => { log!(SystemLog::ShutdownDuringSetup); handle.stop(true).await; return Ok(()); @@ -164,24 +166,24 @@ async fn main() -> Result<(), Error> { system.terminate().await?; match mode { - crate::core::system::ShutdownMode::Restart => { + ShutdownMode::Restart => { log!(SystemLog::ApiRestart); - let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Reloading]); + let _ = sd_notify::notify(false, &[NotifyState::Reloading]); // Drop System to detach eBPF XDP programs before re-exec drop(system); // Brief delay for kernel to release XDP/AF_XDP resources - std::thread::sleep(std::time::Duration::from_millis(500)); + thread::sleep(Duration::from_millis(500)); // Re-exec self — works with or without systemd use std::os::unix::process::CommandExt; - let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("net-guardia")); - let err = std::process::Command::new(exe).args(std::env::args().skip(1)).exec(); // replaces current process + let exe = env::current_exe().unwrap_or_else(|_| PathBuf::from("net-guardia")); + let err = process::Command::new(exe).args(env::args().skip(1)).exec(); // replaces current process // If exec fails, fall through to exit log!(SystemError::UnexpectedError(err)); - std::process::exit(1); + process::exit(1); } - crate::core::system::ShutdownMode::Shutdown => { + ShutdownMode::Shutdown => { log!(SystemLog::ApiShutdown); - let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Stopping]); + let _ = sd_notify::notify(false, &[NotifyState::Stopping]); } } Ok(()) diff --git a/net-guardia/src/model/detection/ml_detection.rs b/net-guardia/src/model/detection/ml_detection.rs index 4c1f120..fbebfae 100644 --- a/net-guardia/src/model/detection/ml_detection.rs +++ b/net-guardia/src/model/detection/ml_detection.rs @@ -1,4 +1,5 @@ use std::net::{Ipv4Addr, Ipv6Addr}; +use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use tract_onnx::prelude::{Graph, SimplePlan, TypedFact, TypedOp}; @@ -160,8 +161,8 @@ pub struct AlertMessage { impl AlertMessage { pub fn from_detection_result(result: &DetectionResult) -> Self { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); diff --git a/net-guardia/src/model/error/crypto.rs b/net-guardia/src/model/error/crypto.rs index d6a39a1..f24eed3 100644 --- a/net-guardia/src/model/error/crypto.rs +++ b/net-guardia/src/model/error/crypto.rs @@ -2,17 +2,34 @@ use macros::traceable; traceable! { CryptoError { - #[no_source] - #[error("Encryption failed: {reason}")] - EncryptionFailed { reason: String } => tracing::Level::ERROR, + #[error("Encryption failed: {err}")] + EncryptionFailed => tracing::Level::ERROR, + + #[error("Decryption failed: {err}")] + DecryptionFailed => tracing::Level::ERROR, + + #[error("Failed to parse secret envelope: {err}")] + EnvelopeParseFailed => tracing::Level::ERROR, #[no_source] - #[error("Decryption failed: {reason}")] - DecryptionFailed { reason: String } => tracing::Level::ERROR, + #[error("Unsupported envelope version: {version}")] + UnsupportedEnvelopeVersion { version: u64 } => tracing::Level::ERROR, #[no_source] - #[error("Invalid secret envelope: {reason}")] - InvalidEnvelope { reason: String } => tracing::Level::ERROR, + #[error("Missing envelope field: {field}")] + MissingEnvelopeField { field: String } => tracing::Level::ERROR, + + #[no_source] + #[error("Envelope algorithm 'none' rejected in production mode (encryption key is set)")] + AlgNoneRejected => tracing::Level::ERROR, + + #[no_source] + #[error("Invalid envelope nonce length")] + InvalidNonceLength => tracing::Level::ERROR, + + #[no_source] + #[error("Unsupported envelope algorithm: {alg}")] + UnsupportedAlgorithm { alg: String } => tracing::Level::ERROR, #[no_source] #[error("Master key not available")] diff --git a/net-guardia/src/model/error/database.rs b/net-guardia/src/model/error/database.rs index 5904d14..060e8d6 100644 --- a/net-guardia/src/model/error/database.rs +++ b/net-guardia/src/model/error/database.rs @@ -2,9 +2,8 @@ use macros::traceable; traceable! { DatabaseError { - #[no_source] - #[error("Database error: {reason}")] - QueryFailed { reason: String } => tracing::Level::ERROR, + #[error("Database error: {err}")] + QueryFailed => tracing::Level::ERROR, #[error("Database connection failed")] ConnectionFailed => tracing::Level::ERROR, @@ -14,14 +13,34 @@ traceable! { UserAlreadyExists { username: String } => tracing::Level::WARN, #[no_source] - #[error("Audit log chain broken at id {id}: {reason}")] - AuditChainBroken { id: i64, reason: String } => tracing::Level::ERROR, + #[error("User group '{name}' already exists")] + GroupAlreadyExists { name: String } => tracing::Level::WARN, + + #[no_source] + #[error("Database encryption key is incorrect or database is corrupted")] + EncryptionKeyInvalid => tracing::Level::ERROR, + + #[no_source] + #[error("Cannot read database with provided key — wrong key or not encrypted")] + DatabaseNotReadable => tracing::Level::ERROR, + + #[no_source] + #[error("Cannot read source database — may already be encrypted")] + SourceDatabaseNotReadable => tracing::Level::ERROR, + + #[no_source] + #[error("Audit log prev_hash mismatch at id {id}: expected {expected}, found {found}")] + AuditPrevHashMismatch { id: i64, expected: String, found: String } => tracing::Level::ERROR, + + #[no_source] + #[error("Audit log row_hash mismatch at id {id}: computed {computed}, stored {stored}")] + AuditRowHashMismatch { id: i64, computed: String, stored: String } => tracing::Level::ERROR, } } impl From for DatabaseError { fn from(e: rusqlite::Error) -> Self { - DatabaseError::QueryFailed { reason: e.to_string() } + DatabaseError::QueryFailed(e) } } diff --git a/net-guardia/src/model/error/ebpf.rs b/net-guardia/src/model/error/ebpf.rs index 507db7e..cf2187a 100644 --- a/net-guardia/src/model/error/ebpf.rs +++ b/net-guardia/src/model/error/ebpf.rs @@ -22,6 +22,9 @@ traceable! { #[error("Failed to attach XDP program")] AttachProgramFailed => tracing::Level::ERROR, + #[error("Failed to obtain eBPF program FD")] + ProgramFdFailed => tracing::Level::ERROR, + #[error("Failed to set umem")] UmemSetFailed => tracing::Level::ERROR, diff --git a/net-guardia/src/model/error/io.rs b/net-guardia/src/model/error/io.rs index e46cc3c..150283b 100644 --- a/net-guardia/src/model/error/io.rs +++ b/net-guardia/src/model/error/io.rs @@ -6,5 +6,8 @@ traceable! { IOError { #[error("Failed to create directory: {path}")] CreateDirectoryFailed { path: PathBuf } => tracing::Level::ERROR, + + #[error("Failed to write file: {path}")] + WriteFileFailed { path: PathBuf } => tracing::Level::ERROR, } } diff --git a/net-guardia/src/model/error/misc.rs b/net-guardia/src/model/error/misc.rs index 9d8764e..b007c29 100644 --- a/net-guardia/src/model/error/misc.rs +++ b/net-guardia/src/model/error/misc.rs @@ -19,17 +19,19 @@ traceable! { #[error("Network interface '{interface}' not found")] NetworkInterfaceNotFound { interface: String } => tracing::Level::ERROR, - #[no_source] - #[error("Failed to open GeoIP database '{path}': {reason}")] - GeoIPDatabaseError { path: String, reason: String } => tracing::Level::ERROR, + #[error("Failed to open GeoIP database '{path}': {err}")] + GeoIPDatabaseError { path: String } => tracing::Level::ERROR, + + #[error("Failed to create traffic log file '{path}': {err}")] + TrafficLogCreateError { path: String } => tracing::Level::ERROR, #[no_source] - #[error("Failed to create traffic log file '{path}': {reason}")] - TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR, + #[error("DNS label length out of range: {len} (must be 1..64)")] + DnsLabelOutOfRange { len: usize } => tracing::Level::WARN, #[no_source] - #[error("Invalid DNS domain name: {reason}")] - InvalidDnsName { reason: String } => tracing::Level::WARN, + #[error("DNS domain name too long: '{domain}'")] + DnsDomainTooLong { domain: String } => tracing::Level::WARN, #[no_source] #[error("Type mismatch during message dispatch")] diff --git a/net-guardia/src/model/error/ml.rs b/net-guardia/src/model/error/ml.rs index 180fcd1..004a7c9 100644 --- a/net-guardia/src/model/error/ml.rs +++ b/net-guardia/src/model/error/ml.rs @@ -8,24 +8,20 @@ traceable! { #[error("Initialize Machine Learning detection failed")] InitializeFailed => tracing::Level::ERROR, - #[no_source] - #[error("Failed to load ONNX model from: {path:?}")] + #[error("Failed to load ONNX model from {path:?}: {err}")] ModelLoadFailed { path: PathBuf } => tracing::Level::ERROR, - #[no_source] - #[error("Failed to load inference configuration from: {path:?}")] + #[error("Failed to load inference configuration from {path:?}: {err}")] ConfigLoadFailed { path: PathBuf } => tracing::Level::ERROR, - #[no_source] - #[error("Failed to parse inference configuration: {reason}")] - ConfigParseFailed { reason: String } => tracing::Level::ERROR, + #[error("Failed to parse inference configuration: {err}")] + ConfigParseFailed => tracing::Level::ERROR, #[error("Failed to flush traffic log: {err}")] TrafficLogFlushFailed => tracing::Level::ERROR, - #[no_source] - #[error("Model manifest at {path:?} is invalid: {reason}")] - ManifestInvalid { path: PathBuf, reason: String } => tracing::Level::ERROR, + #[error("Model manifest at {path:?} is invalid: {err}")] + ManifestInvalid { path: PathBuf } => tracing::Level::ERROR, #[no_source] #[error("Feature count mismatch for {model:?}: manifest declares {declared}, ONNX input expects {onnx_dim}")] @@ -34,5 +30,8 @@ traceable! { #[no_source] #[error("Unknown feature '{name}' — not registered in FEATURE_REGISTRY")] UnknownFeature { name: String } => tracing::Level::ERROR, + + #[error("Model watcher failed: {err}")] + ModelWatcherFailed => tracing::Level::ERROR, } } diff --git a/net-guardia/src/model/error/notification.rs b/net-guardia/src/model/error/notification.rs index 5d634de..fcb46db 100644 --- a/net-guardia/src/model/error/notification.rs +++ b/net-guardia/src/model/error/notification.rs @@ -2,29 +2,27 @@ use macros::traceable; traceable! { NotificationError { - #[no_source] - #[error("SMTP connection failed: {reason}")] - SmtpConnectionFailed { reason: String } => tracing::Level::ERROR, + #[error("SMTP connection failed: {err}")] + SmtpConnectionFailed => tracing::Level::ERROR, + + #[error("SMTP authentication failed: {err}")] + SmtpAuthFailed => tracing::Level::ERROR, + + #[error("Failed to send email: {err}")] + SmtpSendFailed => tracing::Level::ERROR, + + #[error("Invalid {field} email address: {err}")] + InvalidAddress { field: String } => tracing::Level::WARN, + + #[error("Failed to build email message: {err}")] + MessageBuildFailed => tracing::Level::ERROR, + + #[error("Telegram notification error: {err}")] + TelegramRequestFailed => tracing::Level::ERROR, #[no_source] - #[error("SMTP authentication failed: {reason}")] - SmtpAuthFailed { reason: String } => tracing::Level::ERROR, - - #[no_source] - #[error("Failed to send email: {reason}")] - SmtpSendFailed { reason: String } => tracing::Level::ERROR, - - #[no_source] - #[error("Invalid email address: {reason}")] - InvalidAddress { reason: String } => tracing::Level::WARN, - - #[no_source] - #[error("Failed to build email message: {reason}")] - MessageBuildFailed { reason: String } => tracing::Level::ERROR, - - #[no_source] - #[error("Telegram API error: {reason}")] - TelegramApiError { reason: String } => tracing::Level::ERROR, + #[error("Telegram HTTP {status}: {body}")] + TelegramHttpError { status: u16, body: String } => tracing::Level::ERROR, #[no_source] #[error("Telegram authentication failed (invalid bot token)")] diff --git a/net-guardia/src/model/error/soar.rs b/net-guardia/src/model/error/soar.rs index 0d4e488..c0926c3 100644 --- a/net-guardia/src/model/error/soar.rs +++ b/net-guardia/src/model/error/soar.rs @@ -10,15 +10,50 @@ traceable! { #[error("Invalid TTL: {ttl_secs}s exceeds maximum of {max_secs}s")] InvalidTtl { ttl_secs: u64, max_secs: u64 } => tracing::Level::WARN, + #[error("SOAR action failed: {action_type} — {err}")] + ActionFailed { action_type: String } => tracing::Level::ERROR, + #[no_source] - #[error("SOAR action failed: {action_type} — {reason}")] - ActionFailed { action_type: String, reason: String } => tracing::Level::ERROR, + #[error("Unknown SOAR action type: {action_type}")] + UnknownActionType { action_type: String } => tracing::Level::WARN, + + #[no_source] + #[error("Rate limit config not available for SOAR action")] + RateLimitUnavailable => tracing::Level::WARN, + + #[no_source] + #[error("Invalid rate limit factor: {factor} (must be 0.01..=1.0)")] + InvalidRateLimitFactor { factor: f64 } => tracing::Level::WARN, + + #[no_source] + #[error("Webhook action missing required parameter: {param}")] + WebhookMissingParam { param: String } => tracing::Level::WARN, + + #[no_source] + #[error("Webhook URL has no host")] + WebhookUrlNoHost => tracing::Level::WARN, + + #[no_source] + #[error("Webhook DNS resolution returned no addresses for '{host}'")] + WebhookDnsEmpty { host: String } => tracing::Level::WARN, + + #[no_source] + #[error("Webhook SSRF blocked: host '{host}' resolves to private IP {ip}")] + WebhookSsrfBlocked { host: String, ip: String } => tracing::Level::WARN, + + #[no_source] + #[error("Webhook returned non-success HTTP status: {status}")] + WebhookHttpStatus { status: u16 } => tracing::Level::WARN, + + #[no_source] + #[error("Manual unblock failed: block rule {id} not found")] + UnblockRuleNotFound { id: i64 } => tracing::Level::WARN, #[error("Failed to clean up ACL rule after unblock: {err}")] AclCleanupFailed => tracing::Level::WARN, #[no_source] - #[error("Invalid playbook condition: {condition_type} — {reason}")] - InvalidCondition { condition_type: String, reason: String } => tracing::Level::WARN, + #[error("Unknown SOAR condition type: {condition_type}")] + UnknownConditionType { condition_type: String } => tracing::Level::WARN, } } diff --git a/net-guardia/src/model/log/system.rs b/net-guardia/src/model/log/system.rs index 437039c..2a105b7 100644 --- a/net-guardia/src/model/log/system.rs +++ b/net-guardia/src/model/log/system.rs @@ -104,5 +104,56 @@ loggable! { #[error("eBPF bring-up failed — continuing without data plane: {details}")] EbpfBringupFailed { details: String } => tracing::Level::ERROR, + + #[error("Telegram rate limited, retrying after {retry_after}s (attempt {attempt}/{max})")] + TelegramRateLimitedRetry { retry_after: u64, attempt: u32, max: u32 } => tracing::Level::WARN, + + #[error("Telegram not configured, skipping alert")] + TelegramNotConfiguredSkipped => tracing::Level::DEBUG, + + #[error("Telegram rate limit reached ({max_per_min}/min), dropping alert for IP {source_ip}")] + TelegramLocalRateLimitDropped { max_per_min: u32, source_ip: String } => tracing::Level::WARN, + + #[error("Stats aggregator started (1h interval)")] + StatsAggregatorStarted => tracing::Level::INFO, + + #[error("Initial stats aggregation failed: {error}")] + InitialStatsAggregationFailed { error: String } => tracing::Level::ERROR, + + #[error("Stats aggregation failed: {error}")] + StatsAggregationFailed { error: String } => tracing::Level::ERROR, + + #[error("Stats aggregated: {threats} threats, {blocks} blocks, {unblocks} unblocks, {rules} active rules")] + StatsAggregated { threats: u64, blocks: u64, unblocks: u64, rules: u64 } => tracing::Level::INFO, + + #[error("Weekly report scheduler started")] + WeeklyReportSchedulerStarted => tracing::Level::INFO, + + #[error("Weekly report window reached — preparing report")] + WeeklyReportWindowReached => tracing::Level::INFO, + + #[error("SMTP is not configured (missing smtp_host/port/username/password). Skipping weekly report.")] + SmtpNotConfigured => tracing::Level::WARN, + + #[error("Failed to read SMTP settings: {error}")] + SmtpSettingsReadFailed { error: String } => tracing::Level::ERROR, + + #[error("No smtp_recipient configured. Skipping weekly report.")] + SmtpRecipientMissing => tracing::Level::WARN, + + #[error("Failed to generate weekly report: {error}")] + WeeklyReportGenerationFailed { error: String } => tracing::Level::ERROR, + + #[error("Weekly report sent successfully")] + WeeklyReportSent => tracing::Level::INFO, + + #[error("Failed to send weekly report: {error}")] + WeeklyReportSendFailed { error: String } => tracing::Level::ERROR, + + #[error("Send task panicked: {error}")] + WeeklyReportSendPanicked { error: String } => tracing::Level::ERROR, + + #[error("HTML report generated at {path}")] + HtmlReportGenerated { path: String } => tracing::Level::INFO, } } diff --git a/net-guardia/src/model/monitoring/direction.rs b/net-guardia/src/model/monitoring/direction.rs index 1b52cc3..e502401 100644 --- a/net-guardia/src/model/monitoring/direction.rs +++ b/net-guardia/src/model/monitoring/direction.rs @@ -1,3 +1,5 @@ +use std::fmt; + use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)] @@ -7,8 +9,8 @@ pub enum Direction { Egress, } -impl std::fmt::Display for Direction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Direction::Ingress => write!(f, "Ingress"), Direction::Egress => write!(f, "Egress"), diff --git a/net-guardia/src/model/report/data.rs b/net-guardia/src/model/report/data.rs index 10198ae..cd92cc2 100644 --- a/net-guardia/src/model/report/data.rs +++ b/net-guardia/src/model/report/data.rs @@ -1,3 +1,4 @@ +use chrono::{Duration as ChronoDuration, Local}; use serde::{Deserialize, Serialize}; use crate::interface::port::repository::RepositoryPort; @@ -63,10 +64,10 @@ pub struct SystemHealthSummary { impl ReportData { /// Build report data from database settings (aggregated by the ML pipeline). pub fn from_database(db: &dyn RepositoryPort) -> Result { - let now = chrono::Local::now(); + let now = Local::now(); let period = format!( "{} — {}", - (now - chrono::Duration::days(7)).format("%Y-%m-%d"), + (now - ChronoDuration::days(7)).format("%Y-%m-%d"), now.format("%Y-%m-%d") ); diff --git a/net-guardia/src/model/soar/condition.rs b/net-guardia/src/model/soar/condition.rs index ebb810e..8bd7709 100644 --- a/net-guardia/src/model/soar/condition.rs +++ b/net-guardia/src/model/soar/condition.rs @@ -40,10 +40,7 @@ impl FromStr for ConditionType { "ip_pattern" => Ok(Self::IpPattern), "repeat_offender" => Ok(Self::RepeatOffender), "frequency" => Ok(Self::Frequency), - other => Err(SoarError::InvalidCondition { - condition_type: other.to_string(), - reason: "unknown condition type".to_string(), - }), + other => Err(SoarError::UnknownConditionType(other)), } } } diff --git a/net-guardia/src/model/system/readiness.rs b/net-guardia/src/model/system/readiness.rs index 7312fcc..f5c801e 100644 --- a/net-guardia/src/model/system/readiness.rs +++ b/net-guardia/src/model/system/readiness.rs @@ -1,20 +1,23 @@ +use std::sync::atomic::AtomicBool; +use std::time::Instant; + /// Per-subsystem readiness state exposed by `/health/ready`. pub struct ReadinessState { - pub db_connected: std::sync::atomic::AtomicBool, - pub ml_model_loaded: std::sync::atomic::AtomicBool, - pub soar_engine_running: std::sync::atomic::AtomicBool, - pub ebpf_attached: std::sync::atomic::AtomicBool, - pub started_at: std::time::Instant, + pub db_connected: AtomicBool, + pub ml_model_loaded: AtomicBool, + pub soar_engine_running: AtomicBool, + pub ebpf_attached: AtomicBool, + pub started_at: Instant, } impl ReadinessState { pub fn new() -> Self { Self { - db_connected: std::sync::atomic::AtomicBool::new(false), - ml_model_loaded: std::sync::atomic::AtomicBool::new(false), - soar_engine_running: std::sync::atomic::AtomicBool::new(false), - ebpf_attached: std::sync::atomic::AtomicBool::new(false), - started_at: std::time::Instant::now(), + db_connected: AtomicBool::new(false), + ml_model_loaded: AtomicBool::new(false), + soar_engine_running: AtomicBool::new(false), + ebpf_attached: AtomicBool::new(false), + started_at: Instant::now(), } } } diff --git a/net-guardia/src/utils/logging.rs b/net-guardia/src/utils/logging.rs index c3829ba..73cdd33 100644 --- a/net-guardia/src/utils/logging.rs +++ b/net-guardia/src/utils/logging.rs @@ -1,9 +1,11 @@ +use std::env; use std::fs; use std::sync::OnceLock; use tracing::Level; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::filter::EnvFilter; +use tracing_subscriber::fmt::layer as fmt_layer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::reload; use tracing_subscriber::util::SubscriberInitExt; @@ -41,14 +43,14 @@ impl Logging { let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "NetGuardia"); - let stdout_layer = tracing_subscriber::fmt::layer() + let stdout_layer = fmt_layer() .with_file(true) .with_line_number(true) .with_thread_ids(true) .with_target(false) .with_ansi(true); - let file_layer = tracing_subscriber::fmt::layer() + let file_layer = fmt_layer() .with_file(false) .with_line_number(false) .with_thread_ids(false) @@ -56,7 +58,7 @@ impl Logging { .with_ansi(false) .with_writer(file_appender); - let level = std::env::var("RUST_LOG") + let level = env::var("RUST_LOG") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(if cfg!(debug_assertions) { diff --git a/net-guardia/src/utils/packet_parser.rs b/net-guardia/src/utils/packet_parser.rs index 5344a78..9804244 100644 --- a/net-guardia/src/utils/packet_parser.rs +++ b/net-guardia/src/utils/packet_parser.rs @@ -1,4 +1,4 @@ -use std::time; +use std::time::{SystemTime, UNIX_EPOCH}; use crate::model::monitoring::user_packet::UserPacket; @@ -9,10 +9,7 @@ pub fn parse_packet(packet_data: &[u8]) -> Option<(UserPacket, usize)> { let eth_type = u16::from_be_bytes([packet_data[12], packet_data[13]]); - let timestamp_us = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) - .ok()? - .as_micros() as u64; + let timestamp_us = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_micros() as u64; match eth_type { 0x0800 => parse_ipv4(packet_data, timestamp_us),