mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
chore: Update something
This commit is contained in:
parent
301d6ac1b8
commit
6ca57dbe10
@ -1 +1 @@
|
||||
Subproject commit 00d347c5eae3ed32f595b0a3553601f16bedfa7e
|
||||
Subproject commit 48764e1a3f04b465f158a27785163426e34709a3
|
||||
@ -11,6 +11,7 @@ use tokio::sync::{broadcast, oneshot};
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::domain::data_plane::drop_event::{DropCounters, DropCountersAtomic, DropEventMessage};
|
||||
use crate::interface::port::drop_stats::DropStatsPort;
|
||||
|
||||
pub struct DropMonitor {
|
||||
broadcast_tx: broadcast::Sender<DropEventMessage>,
|
||||
@ -39,9 +40,8 @@ impl DropMonitor {
|
||||
/// so no broadcast event is emitted — `/api/stats/drops` stays correct,
|
||||
/// `/ws/drops` simply does not surface the individual packet. Parse the
|
||||
/// packet upstream if you need a structured event.
|
||||
pub fn record_userspace_drop_count_only(&self, reason: u8) {
|
||||
self.counters.total.fetch_add(1, Ordering::Relaxed);
|
||||
let bucket = match reason {
|
||||
fn bucket_for(&self, reason: u8) -> Option<&std::sync::atomic::AtomicU64> {
|
||||
match reason {
|
||||
DROP_REASON_ACL_BLACKLIST => Some(&self.counters.acl_blacklist),
|
||||
DROP_REASON_RATE_LIMIT_PKT => Some(&self.counters.rate_limit_pkt),
|
||||
DROP_REASON_RATE_LIMIT_SYN => Some(&self.counters.rate_limit_syn),
|
||||
@ -51,28 +51,22 @@ impl DropMonitor {
|
||||
DROP_REASON_DNS_BLACKLIST => Some(&self.counters.dns_blacklist),
|
||||
DROP_REASON_GEO_BLOCK => Some(&self.counters.geo_block),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(counter) = bucket {
|
||||
}
|
||||
}
|
||||
|
||||
fn record_drop(&self, reason: u8) {
|
||||
self.counters.total.fetch_add(1, Ordering::Relaxed);
|
||||
if let Some(counter) = self.bucket_for(reason) {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_userspace_drop_count_only(&self, reason: u8) {
|
||||
self.record_drop(reason);
|
||||
}
|
||||
|
||||
fn process_event(&self, raw: &RawDropEvent) {
|
||||
self.counters.total.fetch_add(1, Ordering::Relaxed);
|
||||
let bucket = match raw.reason {
|
||||
DROP_REASON_ACL_BLACKLIST => Some(&self.counters.acl_blacklist),
|
||||
DROP_REASON_RATE_LIMIT_PKT => Some(&self.counters.rate_limit_pkt),
|
||||
DROP_REASON_RATE_LIMIT_SYN => Some(&self.counters.rate_limit_syn),
|
||||
DROP_REASON_RATE_LIMIT_UDP => Some(&self.counters.rate_limit_udp),
|
||||
DROP_REASON_RATE_LIMIT_DNS => Some(&self.counters.rate_limit_dns),
|
||||
DROP_REASON_PROTOCOL_FILTER => Some(&self.counters.protocol_filter),
|
||||
DROP_REASON_DNS_BLACKLIST => Some(&self.counters.dns_blacklist),
|
||||
DROP_REASON_GEO_BLOCK => Some(&self.counters.geo_block),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(counter) = bucket {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
self.record_drop(raw.reason);
|
||||
|
||||
let reason_str = reason_to_str(raw.reason);
|
||||
|
||||
@ -94,6 +88,12 @@ impl DropMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
impl DropStatsPort for DropMonitor {
|
||||
fn get_counters(&self) -> DropCounters {
|
||||
self.counters.snapshot()
|
||||
}
|
||||
}
|
||||
|
||||
fn format_ips(raw: &RawDropEvent) -> (String, String) {
|
||||
match raw.ip_version {
|
||||
4 => {
|
||||
|
||||
@ -11,6 +11,7 @@ use parking_lot::RwLock;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::data_plane::error::EbpfError;
|
||||
use crate::domain::data_plane::ip_address::NativeConvert;
|
||||
use crate::interface::port::protocol_filter::ProtocolFilterPort;
|
||||
|
||||
pub struct ProtocolFilter {
|
||||
ipv4_http_service: RwLock<HttpServiceWrapper<AddrPortV4>>,
|
||||
@ -176,6 +177,116 @@ impl ProtocolFilter {
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolFilterPort for ProtocolFilter {
|
||||
fn get_ipv4_http_service(&self) -> HashMap<SocketAddrV4, Vec<HttpMethod>> {
|
||||
self.get_ipv4_http_service()
|
||||
}
|
||||
|
||||
fn get_ipv6_http_service(&self) -> HashMap<SocketAddrV6, Vec<HttpMethod>> {
|
||||
self.get_ipv6_http_service()
|
||||
}
|
||||
|
||||
fn add_ipv4_http_service(&self, addr: SocketAddrV4, m: Vec<HttpMethod>) -> Result<(), Error> {
|
||||
self.add_ipv4_http_service(addr, m)
|
||||
}
|
||||
|
||||
fn add_ipv6_http_service(&self, addr: SocketAddrV6, m: Vec<HttpMethod>) -> Result<(), Error> {
|
||||
self.add_ipv6_http_service(addr, m)
|
||||
}
|
||||
|
||||
fn remove_ipv4_http_service(&self, addr: SocketAddrV4, m: Vec<HttpMethod>) -> Result<(), Error> {
|
||||
self.remove_ipv4_http_service(addr, m)
|
||||
}
|
||||
|
||||
fn remove_ipv6_http_service(&self, addr: SocketAddrV6, m: Vec<HttpMethod>) -> Result<(), Error> {
|
||||
self.remove_ipv6_http_service(addr, m)
|
||||
}
|
||||
|
||||
fn is_ssh_white_list_enable(&self) -> bool {
|
||||
self.is_ssh_white_list_enable()
|
||||
}
|
||||
|
||||
fn enable_ssh_white_list(&self) -> Result<(), Error> {
|
||||
self.enable_ssh_white_list()
|
||||
}
|
||||
|
||||
fn disable_ssh_white_list(&self) -> Result<(), Error> {
|
||||
self.disable_ssh_white_list()
|
||||
}
|
||||
|
||||
fn get_ipv4_ssh_service(&self) -> Vec<SocketAddrV4> {
|
||||
self.get_ipv4_ssh_service()
|
||||
}
|
||||
|
||||
fn get_ipv6_ssh_service(&self) -> Vec<SocketAddrV6> {
|
||||
self.get_ipv6_ssh_service()
|
||||
}
|
||||
|
||||
fn add_ipv4_ssh_service(&self, addr: SocketAddrV4) -> Result<(), Error> {
|
||||
self.add_ipv4_ssh_service(addr)
|
||||
}
|
||||
|
||||
fn add_ipv6_ssh_service(&self, addr: SocketAddrV6) -> Result<(), Error> {
|
||||
self.add_ipv6_ssh_service(addr)
|
||||
}
|
||||
|
||||
fn remove_ipv4_ssh_service(&self, addr: SocketAddrV4) -> Result<(), Error> {
|
||||
self.remove_ipv4_ssh_service(addr)
|
||||
}
|
||||
|
||||
fn remove_ipv6_ssh_service(&self, addr: SocketAddrV6) -> Result<(), Error> {
|
||||
self.remove_ipv6_ssh_service(addr)
|
||||
}
|
||||
|
||||
fn get_ipv4_ssh_white_list(&self) -> Vec<Ipv4Addr> {
|
||||
self.get_ipv4_ssh_white_list()
|
||||
}
|
||||
|
||||
fn get_ipv6_ssh_white_list(&self) -> Vec<Ipv6Addr> {
|
||||
self.get_ipv6_ssh_white_list()
|
||||
}
|
||||
|
||||
fn add_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.add_ipv4_ssh_white_list(ip)
|
||||
}
|
||||
|
||||
fn add_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.add_ipv6_ssh_white_list(ip)
|
||||
}
|
||||
|
||||
fn remove_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.remove_ipv4_ssh_white_list(ip)
|
||||
}
|
||||
|
||||
fn remove_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.remove_ipv6_ssh_white_list(ip)
|
||||
}
|
||||
|
||||
fn get_ipv4_ssh_black_list(&self) -> Vec<Ipv4Addr> {
|
||||
self.get_ipv4_ssh_black_list()
|
||||
}
|
||||
|
||||
fn get_ipv6_ssh_black_list(&self) -> Vec<Ipv6Addr> {
|
||||
self.get_ipv6_ssh_black_list()
|
||||
}
|
||||
|
||||
fn add_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.add_ipv4_ssh_black_list(ip)
|
||||
}
|
||||
|
||||
fn add_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.add_ipv6_ssh_black_list(ip)
|
||||
}
|
||||
|
||||
fn remove_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error> {
|
||||
self.remove_ipv4_ssh_black_list(ip)
|
||||
}
|
||||
|
||||
fn remove_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error> {
|
||||
self.remove_ipv6_ssh_black_list(ip)
|
||||
}
|
||||
}
|
||||
|
||||
struct WhiteListControl {
|
||||
map: Option<AyaArray<MapData, PlaceHolder>>,
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ use serde::Deserialize;
|
||||
use crate::adapter::http::response::ok_or_error;
|
||||
use crate::core::identity::extractor::AuthClaims;
|
||||
use crate::core::identity::jwt::JwtService;
|
||||
use crate::domain::identity::auth::{DEFAULT_ADMIN_USERNAME, GROUP_ADMIN, GROUP_VIEWER, ROLE_ADMIN, ROLE_VIEWER};
|
||||
use crate::domain::identity::error::AuthError;
|
||||
use crate::domain::identity::password;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
@ -118,10 +119,10 @@ async fn login(body: web::Json<LoginRequest>, db: web::Data<Repo>, jwt: web::Dat
|
||||
let permissions = db.list_user_permissions(id).unwrap_or_default();
|
||||
|
||||
let groups = db.list_groups_for_user(id).unwrap_or_default();
|
||||
let role = if groups.iter().any(|(_id, name, _desc, _perms)| name == "Administrator") {
|
||||
"admin".to_string()
|
||||
let role = if groups.iter().any(|(_id, name, _desc, _perms)| name == GROUP_ADMIN) {
|
||||
ROLE_ADMIN.to_string()
|
||||
} else {
|
||||
"viewer".to_string()
|
||||
ROLE_VIEWER.to_string()
|
||||
};
|
||||
|
||||
match jwt.create_token(id, &username, &role, permissions) {
|
||||
@ -146,12 +147,12 @@ async fn register(auth: AuthClaims, body: web::Json<RegisterRequest>, db: web::D
|
||||
}
|
||||
|
||||
// Validate role
|
||||
if reg.role != "admin" && reg.role != "viewer" {
|
||||
if reg.role != ROLE_ADMIN && reg.role != ROLE_VIEWER {
|
||||
return HttpResponse::BadRequest().json(serde_json::json!({"error": "Role must be 'admin' or 'viewer'"}));
|
||||
}
|
||||
|
||||
// Only admins can create admin accounts
|
||||
if reg.role == "admin" && auth.role != "admin" {
|
||||
if reg.role == ROLE_ADMIN && auth.role != ROLE_ADMIN {
|
||||
return HttpResponse::Forbidden()
|
||||
.json(serde_json::json!({"error": "Only administrators can create admin accounts"}));
|
||||
}
|
||||
@ -166,7 +167,11 @@ async fn register(auth: AuthClaims, body: web::Json<RegisterRequest>, db: web::D
|
||||
match db.insert_user(®.username, &hash, ®.role, false) {
|
||||
Ok(new_user_id) => {
|
||||
// Auto-assign to default group based on role
|
||||
let default_group_name = if reg.role == "admin" { "Administrator" } else { "Viewer" };
|
||||
let default_group_name = if reg.role == ROLE_ADMIN {
|
||||
GROUP_ADMIN
|
||||
} else {
|
||||
GROUP_VIEWER
|
||||
};
|
||||
if let Ok(groups) = db.list_user_groups()
|
||||
&& let Some((group_id, _, _, _, _)) =
|
||||
groups.into_iter().find(|(_, name, _, _, _)| name == default_group_name)
|
||||
@ -186,10 +191,10 @@ async fn me(auth: AuthClaims, db: web::Data<Repo>) -> impl Responder {
|
||||
.iter()
|
||||
.map(|(_id, name, _desc, _perms)| name.clone())
|
||||
.collect();
|
||||
let role = if group_names.iter().any(|n| n == "Administrator") {
|
||||
"admin"
|
||||
let role = if group_names.iter().any(|n| n == GROUP_ADMIN) {
|
||||
ROLE_ADMIN
|
||||
} else {
|
||||
"viewer"
|
||||
ROLE_VIEWER
|
||||
};
|
||||
let permissions = db.list_user_permissions(auth.sub).unwrap_or_default();
|
||||
HttpResponse::Ok().json(serde_json::json!({
|
||||
@ -257,10 +262,10 @@ async fn list_users(_auth: AuthClaims, db: web::Data<Repo>) -> impl Responder {
|
||||
.iter()
|
||||
.map(|(gid, name)| serde_json::json!({"id": gid, "name": name}))
|
||||
.collect();
|
||||
let role = if user_groups.iter().any(|(_id, name)| name == "Administrator") {
|
||||
"admin"
|
||||
let role = if user_groups.iter().any(|(_id, name)| name == GROUP_ADMIN) {
|
||||
ROLE_ADMIN
|
||||
} else {
|
||||
"viewer"
|
||||
ROLE_VIEWER
|
||||
};
|
||||
serde_json::json!({
|
||||
"id": id,
|
||||
@ -288,7 +293,7 @@ async fn delete_user(_auth: AuthClaims, path: web::Path<i64>, db: web::Data<Repo
|
||||
|
||||
// Protect the built-in admin account
|
||||
match db.find_user_by_id(user_id) {
|
||||
Ok(Some((_, ref username, _, _, _))) if username == "admin" => {
|
||||
Ok(Some((_, ref username, _, _, _))) if username == DEFAULT_ADMIN_USERNAME => {
|
||||
return HttpResponse::Forbidden()
|
||||
.json(serde_json::json!({"error": "Cannot delete the built-in admin account"}));
|
||||
}
|
||||
@ -316,7 +321,7 @@ async fn update_role(
|
||||
}
|
||||
|
||||
let role = match body.get("role").and_then(|v| v.as_str()) {
|
||||
Some(r) if r == "admin" || r == "viewer" => r,
|
||||
Some(r) if r == ROLE_ADMIN || r == ROLE_VIEWER => r,
|
||||
_ => {
|
||||
return HttpResponse::BadRequest().json(serde_json::json!({"error": "Role must be 'admin' or 'viewer'"}));
|
||||
}
|
||||
@ -472,7 +477,7 @@ async fn update_group(
|
||||
let existing = match db.get_user_group(group_id) {
|
||||
Ok(Some(g)) => {
|
||||
// Protect built-in groups
|
||||
if g.1 == "Administrator" || g.1 == "Viewer" {
|
||||
if g.1 == GROUP_ADMIN || g.1 == GROUP_VIEWER {
|
||||
return HttpResponse::Forbidden().json(serde_json::json!({"error": "Cannot modify built-in groups"}));
|
||||
}
|
||||
g
|
||||
@ -508,7 +513,7 @@ async fn delete_group(_auth: AuthClaims, path: web::Path<i64>, db: web::Data<Rep
|
||||
|
||||
// Protect built-in groups
|
||||
match db.get_user_group(group_id) {
|
||||
Ok(Some(g)) if g.1 == "Administrator" || g.1 == "Viewer" => {
|
||||
Ok(Some(g)) if g.1 == GROUP_ADMIN || g.1 == GROUP_VIEWER => {
|
||||
return HttpResponse::Forbidden().json(serde_json::json!({"error": "Cannot delete built-in groups"}));
|
||||
}
|
||||
_ => {}
|
||||
@ -531,7 +536,7 @@ async fn set_user_groups(
|
||||
|
||||
// Protect the default admin account
|
||||
match db.find_user_by_id(user_id) {
|
||||
Ok(Some((_, ref username, _, _, _))) if username == "admin" => {
|
||||
Ok(Some((_, ref username, _, _, _))) if username == DEFAULT_ADMIN_USERNAME => {
|
||||
return HttpResponse::Forbidden()
|
||||
.json(serde_json::json!({"error": "Cannot modify groups for the built-in admin account"}));
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ use actix_web::{HttpResponse, Responder, Scope, web};
|
||||
use common::model::http_method::HttpMethod;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::adapter::ebpf::protocol_filter::ProtocolFilter;
|
||||
use crate::adapter::http::response::ok_or_error;
|
||||
use crate::interface::port::protocol_filter::ProtocolFilterPort;
|
||||
use crate::core::data_plane::dns_filter_service::DnsFilterService;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
@ -16,7 +16,7 @@ pub fn initialize() -> Scope {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DnsDomainsPayload {
|
||||
struct DnsDomainsRequest {
|
||||
domains: Vec<String>,
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ async fn get_dns_blacklist(service: web::Data<DnsFilterService>) -> impl Respond
|
||||
}
|
||||
|
||||
async fn add_dns_blacklist(
|
||||
payload: web::Json<DnsDomainsPayload>,
|
||||
payload: web::Json<DnsDomainsRequest>,
|
||||
service: web::Data<DnsFilterService>,
|
||||
) -> impl Responder {
|
||||
let domains = payload.into_inner().domains;
|
||||
@ -45,7 +45,7 @@ async fn add_dns_blacklist(
|
||||
}
|
||||
|
||||
async fn remove_dns_blacklist(
|
||||
payload: web::Json<DnsDomainsPayload>,
|
||||
payload: web::Json<DnsDomainsRequest>,
|
||||
service: web::Data<DnsFilterService>,
|
||||
) -> impl Responder {
|
||||
let domains = payload.into_inner().domains;
|
||||
@ -102,17 +102,17 @@ fn ssh_blacklist_scope() -> Scope {
|
||||
|
||||
// --- HTTP service handlers ---
|
||||
|
||||
async fn get_ipv4_http_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv4_http_service(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv4_http_service())
|
||||
}
|
||||
|
||||
async fn get_ipv6_http_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv6_http_service(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv6_http_service())
|
||||
}
|
||||
|
||||
async fn add_ipv4_http_service(
|
||||
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
ok_or_error(service.add_ipv4_http_service(addr, methods))
|
||||
@ -120,7 +120,7 @@ async fn add_ipv4_http_service(
|
||||
|
||||
async fn add_ipv6_http_service(
|
||||
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
ok_or_error(service.add_ipv6_http_service(addr, methods))
|
||||
@ -128,7 +128,7 @@ async fn add_ipv6_http_service(
|
||||
|
||||
async fn remove_ipv4_http_service(
|
||||
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
ok_or_error(service.remove_ipv4_http_service(addr, methods))
|
||||
@ -136,7 +136,7 @@ async fn remove_ipv4_http_service(
|
||||
|
||||
async fn remove_ipv6_http_service(
|
||||
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
ok_or_error(service.remove_ipv6_http_service(addr, methods))
|
||||
@ -144,108 +144,108 @@ async fn remove_ipv6_http_service(
|
||||
|
||||
// --- SSH service handlers ---
|
||||
|
||||
async fn get_ipv4_ssh_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv4_ssh_service(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv4_ssh_service())
|
||||
}
|
||||
|
||||
async fn get_ipv6_ssh_service(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv6_ssh_service(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv6_ssh_service())
|
||||
}
|
||||
|
||||
async fn add_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn add_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv4_ssh_service(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn add_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn add_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv6_ssh_service(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn remove_ipv4_ssh_service(
|
||||
ip_addr: web::Json<SocketAddrV4>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv4_ssh_service(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn remove_ipv6_ssh_service(
|
||||
ip_addr: web::Json<SocketAddrV6>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv6_ssh_service(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
// --- SSH whitelist handlers ---
|
||||
|
||||
async fn is_ssh_white_list_enable(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn is_ssh_white_list_enable(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.is_ssh_white_list_enable())
|
||||
}
|
||||
|
||||
async fn enable_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn enable_ssh_white_list(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.enable_ssh_white_list())
|
||||
}
|
||||
|
||||
async fn disable_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn disable_ssh_white_list(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.disable_ssh_white_list())
|
||||
}
|
||||
|
||||
async fn get_ipv4_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv4_ssh_white_list(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv4_ssh_white_list())
|
||||
}
|
||||
|
||||
async fn get_ipv6_ssh_white_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv6_ssh_white_list(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv6_ssh_white_list())
|
||||
}
|
||||
|
||||
async fn add_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn add_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv4_ssh_white_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn add_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn add_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv6_ssh_white_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn remove_ipv4_ssh_white_list(
|
||||
ip_addr: web::Json<Ipv4Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv4_ssh_white_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn remove_ipv6_ssh_white_list(
|
||||
ip_addr: web::Json<Ipv6Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv6_ssh_white_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
// --- SSH blacklist handlers ---
|
||||
|
||||
async fn get_ipv4_ssh_black_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv4_ssh_black_list(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv4_ssh_black_list())
|
||||
}
|
||||
|
||||
async fn get_ipv6_ssh_black_list(service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn get_ipv6_ssh_black_list(service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(service.get_ipv6_ssh_black_list())
|
||||
}
|
||||
|
||||
async fn add_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn add_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv4_ssh_black_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn add_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<ProtocolFilter>) -> impl Responder {
|
||||
async fn add_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<dyn ProtocolFilterPort>) -> impl Responder {
|
||||
ok_or_error(service.add_ipv6_ssh_black_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn remove_ipv4_ssh_black_list(
|
||||
ip_addr: web::Json<Ipv4Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv4_ssh_black_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
async fn remove_ipv6_ssh_black_list(
|
||||
ip_addr: web::Json<Ipv6Addr>,
|
||||
service: web::Data<ProtocolFilter>,
|
||||
service: web::Data<dyn ProtocolFilterPort>,
|
||||
) -> impl Responder {
|
||||
ok_or_error(service.remove_ipv6_ssh_black_list(ip_addr.into_inner()))
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ use crate::adapter::persistence::Database;
|
||||
use crate::core::identity::setup_guard::SetupCompleteFlag;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::system::SystemError;
|
||||
use crate::domain::identity::auth::DEFAULT_ADMIN_USERNAME;
|
||||
use crate::domain::identity::password;
|
||||
use crate::infrastructure::secret_store::SecretStore;
|
||||
use crate::interface::port::secret_store::SecretStorePort;
|
||||
@ -143,7 +144,7 @@ async fn complete_setup(
|
||||
match password::hash_password(&body.admin_password) {
|
||||
Ok(hash) => {
|
||||
// Find admin user and update password
|
||||
if let Ok(Some(user)) = db.find_user("admin") {
|
||||
if let Ok(Some(user)) = db.find_user(DEFAULT_ADMIN_USERNAME) {
|
||||
if let Err(e) = db.update_user_password(user.0, &hash) {
|
||||
log!(SystemError::SetupPasswordUpdateFailed(e));
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_web::{HttpResponse, Scope, web};
|
||||
use arc_swap::ArcSwap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::adapter::http::response::{ok_json_or_error, ok_or_error};
|
||||
use crate::core::identity::extractor::AuthClaims;
|
||||
use crate::core::response::engine::SoarEngine;
|
||||
use crate::core::response::playbook_service::PlaybookService;
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::event::{DetectionSource, ThreatDetectedEvent};
|
||||
use crate::domain::response::playbook_data::{CreateConditionInput, CreatePlaybookInput};
|
||||
|
||||
@ -36,6 +38,47 @@ struct CreateConditionRequest {
|
||||
value2: Option<String>,
|
||||
}
|
||||
|
||||
fn map_request_to_input(body: &CreatePlaybookRequest, fallback_cooldown: i64) -> CreatePlaybookInput {
|
||||
let actions = body
|
||||
.actions
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let params_str = a
|
||||
.params
|
||||
.as_ref()
|
||||
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "{}".into()))
|
||||
.unwrap_or_else(|| "{}".into());
|
||||
(a.action_type.clone(), params_str)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let conditions = body
|
||||
.conditions
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
CreateConditionInput::new(
|
||||
c.condition_type.clone(),
|
||||
c.operator.clone(),
|
||||
c.value.clone(),
|
||||
c.value2.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
CreatePlaybookInput {
|
||||
name: body.name.clone(),
|
||||
trigger_event: body.trigger_event.clone(),
|
||||
condition_threshold: body.condition_threshold,
|
||||
condition_count: body.condition_count,
|
||||
condition_window_secs: body.condition_window_secs,
|
||||
cooldown_secs: body.cooldown_secs.unwrap_or(fallback_cooldown),
|
||||
actions,
|
||||
conditions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/soar")
|
||||
.route("/playbooks", web::get().to(list_playbooks))
|
||||
@ -53,100 +96,16 @@ pub fn initialize() -> Scope {
|
||||
}
|
||||
|
||||
async fn list_playbooks(_auth: AuthClaims, svc: web::Data<PlaybookService>) -> HttpResponse {
|
||||
match svc.list_playbooks() {
|
||||
Ok(playbooks) => {
|
||||
let responses: Vec<serde_json::Value> = playbooks
|
||||
.into_iter()
|
||||
.map(|pb| {
|
||||
let actions: Vec<serde_json::Value> = pb
|
||||
.actions
|
||||
.into_iter()
|
||||
.map(|a| {
|
||||
serde_json::json!({
|
||||
"id": a.id,
|
||||
"action_order": a.action_order,
|
||||
"action_type": a.action_type,
|
||||
"params": a.params,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let conditions: Vec<serde_json::Value> = pb
|
||||
.conditions
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
serde_json::json!({
|
||||
"id": c.id,
|
||||
"condition_type": c.condition_type,
|
||||
"operator": c.operator,
|
||||
"value": c.value,
|
||||
"value2": c.value2,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::json!({
|
||||
"id": pb.id,
|
||||
"name": pb.name,
|
||||
"enabled": pb.enabled,
|
||||
"trigger_event": pb.trigger_event,
|
||||
"condition_threshold": pb.condition_threshold,
|
||||
"condition_count": pb.condition_count,
|
||||
"condition_window_secs": pb.condition_window_secs,
|
||||
"cooldown_secs": pb.cooldown_secs,
|
||||
"actions": actions,
|
||||
"conditions": conditions,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
HttpResponse::Ok().json(responses)
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_json_or_error(svc.list_playbooks())
|
||||
}
|
||||
|
||||
async fn create_playbook(
|
||||
_auth: AuthClaims,
|
||||
svc: web::Data<PlaybookService>,
|
||||
app_config: web::Data<ArcSwap<AppConfig>>,
|
||||
body: web::Json<CreatePlaybookRequest>,
|
||||
) -> HttpResponse {
|
||||
let actions: Vec<(String, String)> = body
|
||||
.actions
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let params_str = a
|
||||
.params
|
||||
.as_ref()
|
||||
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "{}".into()))
|
||||
.unwrap_or_else(|| "{}".into());
|
||||
(a.action_type.clone(), params_str)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let conditions: Vec<CreateConditionInput> = body
|
||||
.conditions
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
CreateConditionInput::new(
|
||||
c.condition_type.clone(),
|
||||
c.operator.clone(),
|
||||
c.value.clone(),
|
||||
c.value2.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let input = CreatePlaybookInput {
|
||||
name: body.name.clone(),
|
||||
trigger_event: body.trigger_event.clone(),
|
||||
condition_threshold: body.condition_threshold,
|
||||
condition_count: body.condition_count,
|
||||
condition_window_secs: body.condition_window_secs,
|
||||
cooldown_secs: body.cooldown_secs.unwrap_or(300),
|
||||
actions,
|
||||
conditions,
|
||||
};
|
||||
|
||||
let input = map_request_to_input(&body, app_config.load().soar.fallback_cooldown_secs);
|
||||
match svc.create_playbook(&input) {
|
||||
Ok(id) => HttpResponse::Created().json(serde_json::json!({"id": id})),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
@ -156,50 +115,12 @@ async fn create_playbook(
|
||||
async fn update_playbook(
|
||||
_auth: AuthClaims,
|
||||
svc: web::Data<PlaybookService>,
|
||||
app_config: web::Data<ArcSwap<AppConfig>>,
|
||||
path: web::Path<i64>,
|
||||
body: web::Json<CreatePlaybookRequest>,
|
||||
) -> HttpResponse {
|
||||
let id = path.into_inner();
|
||||
|
||||
let actions: Vec<(String, String)> = body
|
||||
.actions
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let params_str = a
|
||||
.params
|
||||
.as_ref()
|
||||
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "{}".into()))
|
||||
.unwrap_or_else(|| "{}".into());
|
||||
(a.action_type.clone(), params_str)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let conditions: Vec<CreateConditionInput> = body
|
||||
.conditions
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
CreateConditionInput::new(
|
||||
c.condition_type.clone(),
|
||||
c.operator.clone(),
|
||||
c.value.clone(),
|
||||
c.value2.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let input = CreatePlaybookInput {
|
||||
name: body.name.clone(),
|
||||
trigger_event: body.trigger_event.clone(),
|
||||
condition_threshold: body.condition_threshold,
|
||||
condition_count: body.condition_count,
|
||||
condition_window_secs: body.condition_window_secs,
|
||||
cooldown_secs: body.cooldown_secs.unwrap_or(300),
|
||||
actions,
|
||||
conditions,
|
||||
};
|
||||
|
||||
let input = map_request_to_input(&body, app_config.load().soar.fallback_cooldown_secs);
|
||||
match svc.update_playbook(id, &input) {
|
||||
Ok(true) => HttpResponse::Ok().json(serde_json::json!({"updated": true})),
|
||||
Ok(false) => HttpResponse::NotFound().json(serde_json::json!({"error": "Playbook not found"})),
|
||||
@ -234,23 +155,7 @@ async fn delete_playbook(_auth: AuthClaims, svc: web::Data<PlaybookService>, pat
|
||||
}
|
||||
|
||||
async fn list_active_blocks(_auth: AuthClaims, svc: web::Data<PlaybookService>) -> HttpResponse {
|
||||
match svc.list_active_blocks() {
|
||||
Ok(blocks) => {
|
||||
let responses: Vec<serde_json::Value> = blocks
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
serde_json::json!({
|
||||
"id": b.id,
|
||||
"source_ip": b.source_ip,
|
||||
"playbook_id": b.playbook_id,
|
||||
"expires_at": b.expires_at,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
HttpResponse::Ok().json(responses)
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_json_or_error(svc.list_active_blocks())
|
||||
}
|
||||
|
||||
async fn manual_unblock(_auth: AuthClaims, svc: web::Data<PlaybookService>, path: web::Path<i64>) -> HttpResponse {
|
||||
@ -258,25 +163,7 @@ async fn manual_unblock(_auth: AuthClaims, svc: web::Data<PlaybookService>, path
|
||||
}
|
||||
|
||||
async fn list_executions(_auth: AuthClaims, svc: web::Data<PlaybookService>) -> HttpResponse {
|
||||
match svc.list_executions(100) {
|
||||
Ok(executions) => {
|
||||
let responses: Vec<serde_json::Value> = executions
|
||||
.into_iter()
|
||||
.map(|ex| {
|
||||
serde_json::json!({
|
||||
"id": ex.id,
|
||||
"playbook_id": ex.playbook_id,
|
||||
"source_ip": ex.source_ip,
|
||||
"trigger_event": ex.trigger_event,
|
||||
"actions_executed": ex.actions_executed,
|
||||
"created_at": ex.created_at,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
HttpResponse::Ok().json(responses)
|
||||
}
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
ok_json_or_error(svc.list_executions(100))
|
||||
}
|
||||
|
||||
async fn list_whitelist(_auth: AuthClaims, svc: web::Data<PlaybookService>) -> HttpResponse {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use actix_web::{HttpResponse, Responder, Scope, web};
|
||||
|
||||
use crate::adapter::ebpf::drop_monitor::DropMonitor;
|
||||
use crate::infrastructure::statistics::FlowStatistics;
|
||||
use crate::interface::port::drop_stats::DropStatsPort;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/stats")
|
||||
@ -24,6 +24,6 @@ async fn get_summary(stats: web::Data<FlowStatistics>) -> impl Responder {
|
||||
HttpResponse::Ok().json(stats.get_summary())
|
||||
}
|
||||
|
||||
async fn get_drop_stats(monitor: web::Data<DropMonitor>) -> impl Responder {
|
||||
async fn get_drop_stats(monitor: web::Data<dyn DropStatsPort>) -> impl Responder {
|
||||
HttpResponse::Ok().json(monitor.get_counters())
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ use serde::Deserialize;
|
||||
|
||||
use crate::core::common::config_service::ConfigService;
|
||||
use crate::core::identity::extractor::AuthClaims;
|
||||
use crate::domain::common::config::constants::PERMISSION_SYSTEM_ADMIN;
|
||||
use crate::infrastructure::enforce_mode_handler::EnforceModeHandler;
|
||||
use crate::infrastructure::logger::Logger;
|
||||
use crate::infrastructure::system::{ShutdownHandle, ShutdownMode};
|
||||
@ -135,7 +136,7 @@ async fn update_config(
|
||||
}
|
||||
|
||||
async fn shutdown(auth: AuthClaims, handle: web::Data<ShutdownHandle>) -> impl Responder {
|
||||
if !auth.permissions.iter().any(|p| p == "system:admin") {
|
||||
if !auth.permissions.iter().any(|p| p == PERMISSION_SYSTEM_ADMIN) {
|
||||
return HttpResponse::Forbidden().json(serde_json::json!({"error": "Requires system:admin permission"}));
|
||||
}
|
||||
if handle.trigger(ShutdownMode::Shutdown) {
|
||||
@ -146,7 +147,7 @@ async fn shutdown(auth: AuthClaims, handle: web::Data<ShutdownHandle>) -> impl R
|
||||
}
|
||||
|
||||
async fn restart(auth: AuthClaims, handle: web::Data<ShutdownHandle>) -> impl Responder {
|
||||
if !auth.permissions.iter().any(|p| p == "system:admin") {
|
||||
if !auth.permissions.iter().any(|p| p == PERMISSION_SYSTEM_ADMIN) {
|
||||
return HttpResponse::Forbidden().json(serde_json::json!({"error": "Requires system:admin permission"}));
|
||||
}
|
||||
if handle.trigger(ShutdownMode::Restart) {
|
||||
|
||||
@ -18,6 +18,7 @@ use rusqlite::{self, Connection, params};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::database::DatabaseError;
|
||||
use crate::domain::common::log::misc::MiscLog;
|
||||
use crate::domain::identity::auth::{GROUP_ADMIN, GROUP_VIEWER};
|
||||
|
||||
/// 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).
|
||||
@ -139,11 +140,8 @@ impl Database {
|
||||
conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
|
||||
.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(DatabaseError::QueryFailed)?;
|
||||
conn.execute("ATTACH DATABASE ?1 AS plaintext KEY '';", params![dest_path])
|
||||
.map_err(DatabaseError::QueryFailed)?;
|
||||
conn.query_row("SELECT sqlcipher_export('plaintext')", [], |_| Ok(()))
|
||||
.map_err(DatabaseError::QueryFailed)?;
|
||||
conn.execute_batch("DETACH DATABASE plaintext;")
|
||||
@ -158,12 +156,8 @@ impl Database {
|
||||
// Verify it's readable as plaintext
|
||||
conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))
|
||||
.map_err(|_| DatabaseError::SourceDatabaseNotReadable)?;
|
||||
conn.execute_batch(&format!(
|
||||
"ATTACH DATABASE '{}' AS encrypted KEY '{}';",
|
||||
dest_path.replace('\'', "''"),
|
||||
key.replace('\'', "''"),
|
||||
))
|
||||
.map_err(DatabaseError::QueryFailed)?;
|
||||
conn.execute("ATTACH DATABASE ?1 AS encrypted KEY ?2;", params![dest_path, key])
|
||||
.map_err(DatabaseError::QueryFailed)?;
|
||||
conn.query_row("SELECT sqlcipher_export('encrypted')", [], |_| Ok(()))
|
||||
.map_err(DatabaseError::QueryFailed)?;
|
||||
conn.execute_batch("DETACH DATABASE encrypted;")
|
||||
@ -388,15 +382,11 @@ impl Database {
|
||||
|
||||
conn_ref.execute(
|
||||
"INSERT INTO user_groups (name, description, permissions) VALUES (?1, ?2, ?3)",
|
||||
params![
|
||||
"Administrator",
|
||||
"Full system access with all permissions",
|
||||
&all_permissions
|
||||
],
|
||||
params![GROUP_ADMIN, "Full system access with all permissions", &all_permissions],
|
||||
)?;
|
||||
conn_ref.execute(
|
||||
"INSERT INTO user_groups (name, description, permissions) VALUES (?1, ?2, ?3)",
|
||||
params!["Viewer", "Read-only access to all modules", &viewer_permissions],
|
||||
params![GROUP_VIEWER, "Read-only access to all modules", &viewer_permissions],
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,31 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rusqlite::params;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::Database;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::response::playbook_data::UpdatePlaybookInput;
|
||||
use crate::interface::port::soar::{PlaybookRow, SoarExecutionRow, SoarRepo};
|
||||
use crate::domain::response::playbook_data::{
|
||||
ActionView, ActiveBlockView, ConditionView, ExecutionView, PendingUnblock, PlaybookView, UpdatePlaybookInput,
|
||||
};
|
||||
use crate::interface::port::soar::SoarRepo;
|
||||
|
||||
/// Intermediate row from the playbooks LEFT JOIN playbook_actions query.
|
||||
/// Private to this module; consumed only by `SoarRepo::list_playbooks`.
|
||||
struct PlaybookActionRow {
|
||||
pb_id: i64,
|
||||
name: String,
|
||||
enabled: bool,
|
||||
trigger_event: String,
|
||||
condition_threshold: Option<f64>,
|
||||
condition_count: Option<i64>,
|
||||
condition_window_secs: Option<i64>,
|
||||
cooldown_secs: i64,
|
||||
action_id: Option<i64>,
|
||||
action_order: Option<i64>,
|
||||
action_type: Option<String>,
|
||||
action_params: Option<String>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn insert_playbook(
|
||||
@ -39,26 +61,7 @@ impl Database {
|
||||
}
|
||||
|
||||
/// Load all playbooks with their actions in a single JOIN query (avoids N+1).
|
||||
/// Returns Vec of (playbook fields..., action fields...).
|
||||
pub fn list_playbooks_with_actions(
|
||||
&self,
|
||||
) -> Result<
|
||||
Vec<(
|
||||
i64,
|
||||
String,
|
||||
bool,
|
||||
String,
|
||||
Option<f64>,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
i64,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
)>,
|
||||
Error,
|
||||
> {
|
||||
fn list_playbooks_with_actions(&self) -> Result<Vec<PlaybookActionRow>, Error> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT p.id, p.name, p.enabled, p.trigger_event, p.condition_threshold, \
|
||||
@ -69,20 +72,20 @@ impl Database {
|
||||
ORDER BY p.id, a.action_order",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, bool>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, Option<f64>>(4)?,
|
||||
row.get::<_, Option<i64>>(5)?,
|
||||
row.get::<_, Option<i64>>(6)?,
|
||||
row.get::<_, i64>(7)?,
|
||||
row.get::<_, Option<i64>>(8)?,
|
||||
row.get::<_, Option<i64>>(9)?,
|
||||
row.get::<_, Option<String>>(10)?,
|
||||
row.get::<_, Option<String>>(11)?,
|
||||
))
|
||||
Ok(PlaybookActionRow {
|
||||
pb_id: row.get::<_, i64>(0)?,
|
||||
name: row.get::<_, String>(1)?,
|
||||
enabled: row.get::<_, bool>(2)?,
|
||||
trigger_event: row.get::<_, String>(3)?,
|
||||
condition_threshold: row.get::<_, Option<f64>>(4)?,
|
||||
condition_count: row.get::<_, Option<i64>>(5)?,
|
||||
condition_window_secs: row.get::<_, Option<i64>>(6)?,
|
||||
cooldown_secs: row.get::<_, i64>(7)?,
|
||||
action_id: row.get::<_, Option<i64>>(8)?,
|
||||
action_order: row.get::<_, Option<i64>>(9)?,
|
||||
action_type: row.get::<_, Option<String>>(10)?,
|
||||
action_params: row.get::<_, Option<String>>(11)?,
|
||||
})
|
||||
})?;
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
@ -123,9 +126,7 @@ impl Database {
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn list_all_playbook_conditions(
|
||||
&self,
|
||||
) -> Result<Vec<(i64, i64, String, String, String, Option<String>)>, Error> {
|
||||
fn list_all_playbook_conditions(&self) -> Result<Vec<(i64, ConditionView)>, Error> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, playbook_id, condition_type, operator, value, value2 \
|
||||
@ -133,12 +134,14 @@ impl Database {
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, i64>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, String>(4)?,
|
||||
row.get::<_, Option<String>>(5)?,
|
||||
ConditionView {
|
||||
id: row.get::<_, i64>(0)?,
|
||||
condition_type: row.get::<_, String>(2)?,
|
||||
operator: row.get::<_, String>(3)?,
|
||||
value: row.get::<_, String>(4)?,
|
||||
value2: row.get::<_, Option<String>>(5)?,
|
||||
},
|
||||
))
|
||||
})?;
|
||||
let mut result = Vec::new();
|
||||
@ -163,23 +166,21 @@ impl Database {
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn list_soar_executions(
|
||||
&self,
|
||||
limit: i64,
|
||||
) -> Result<Vec<(i64, i64, Option<String>, String, String, String)>, Error> {
|
||||
pub fn list_soar_executions(&self, limit: i64) -> Result<Vec<ExecutionView>, Error> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, playbook_id, source_ip, trigger_event, actions_executed, executed_at FROM soar_executions ORDER BY executed_at DESC LIMIT ?1"
|
||||
)?;
|
||||
let rows = stmt.query_map(params![limit], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, i64>(1)?,
|
||||
row.get::<_, Option<String>>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
row.get::<_, String>(4)?,
|
||||
row.get::<_, String>(5)?,
|
||||
))
|
||||
let actions_str: String = row.get(4)?;
|
||||
Ok(ExecutionView {
|
||||
id: row.get::<_, i64>(0)?,
|
||||
playbook_id: row.get::<_, i64>(1)?,
|
||||
source_ip: row.get::<_, Option<String>>(2)?,
|
||||
trigger_event: row.get::<_, String>(3)?,
|
||||
actions_executed: serde_json::from_str(&actions_str).unwrap_or(Value::Null),
|
||||
created_at: row.get::<_, String>(5)?,
|
||||
})
|
||||
})?;
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
@ -236,8 +237,72 @@ impl Database {
|
||||
}
|
||||
|
||||
impl SoarRepo for Database {
|
||||
fn list_playbooks_with_actions(&self) -> Result<Vec<PlaybookRow>, Error> {
|
||||
self.list_playbooks_with_actions()
|
||||
fn list_playbooks(&self) -> Result<Vec<PlaybookView>, Error> {
|
||||
let rows = self.list_playbooks_with_actions()?;
|
||||
let mut result: Vec<PlaybookView> = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
let pb = if let Some(last) = result.last_mut() {
|
||||
if last.id == row.pb_id {
|
||||
last
|
||||
} else {
|
||||
result.push(PlaybookView {
|
||||
id: row.pb_id,
|
||||
name: row.name,
|
||||
enabled: row.enabled,
|
||||
trigger_event: row.trigger_event,
|
||||
condition_threshold: row.condition_threshold,
|
||||
condition_count: row.condition_count,
|
||||
condition_window_secs: row.condition_window_secs,
|
||||
cooldown_secs: row.cooldown_secs,
|
||||
actions: Vec::new(),
|
||||
conditions: Vec::new(),
|
||||
});
|
||||
// SAFETY: just pushed above, Vec cannot be empty
|
||||
result.last_mut().unwrap_or_else(|| unreachable!())
|
||||
}
|
||||
} else {
|
||||
result.push(PlaybookView {
|
||||
id: row.pb_id,
|
||||
name: row.name,
|
||||
enabled: row.enabled,
|
||||
trigger_event: row.trigger_event,
|
||||
condition_threshold: row.condition_threshold,
|
||||
condition_count: row.condition_count,
|
||||
condition_window_secs: row.condition_window_secs,
|
||||
cooldown_secs: row.cooldown_secs,
|
||||
actions: Vec::new(),
|
||||
conditions: Vec::new(),
|
||||
});
|
||||
// SAFETY: just pushed above, Vec cannot be empty
|
||||
result.last_mut().unwrap_or_else(|| unreachable!())
|
||||
};
|
||||
|
||||
if let (Some(aid), Some(order), Some(atype), Some(params_str)) =
|
||||
(row.action_id, row.action_order, row.action_type, row.action_params)
|
||||
{
|
||||
pb.actions.push(ActionView {
|
||||
id: aid,
|
||||
action_order: order,
|
||||
action_type: atype,
|
||||
params: serde_json::from_str(¶ms_str).unwrap_or(Value::Null),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load conditions and attach to playbooks
|
||||
let cond_rows = self.list_all_playbook_conditions()?;
|
||||
let mut cond_map: HashMap<i64, Vec<ConditionView>> = HashMap::new();
|
||||
for (pb_id, cond) in cond_rows {
|
||||
cond_map.entry(pb_id).or_default().push(cond);
|
||||
}
|
||||
for pb in &mut result {
|
||||
if let Some(conds) = cond_map.remove(&pb.id) {
|
||||
pb.conditions = conds;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn update_playbook_enabled(&self, id: i64, enabled: bool) -> Result<bool, Error> {
|
||||
@ -252,23 +317,19 @@ impl SoarRepo for Database {
|
||||
self.seed_default_playbooks()
|
||||
}
|
||||
|
||||
fn list_all_playbook_conditions(&self) -> Result<Vec<(i64, i64, String, String, String, Option<String>)>, Error> {
|
||||
self.list_all_playbook_conditions()
|
||||
}
|
||||
|
||||
fn count_active_soar_blocks(&self) -> Result<u32, Error> {
|
||||
self.count_active_soar_blocks()
|
||||
}
|
||||
|
||||
fn list_active_soar_blocks(&self) -> Result<Vec<(i64, String, i64, String)>, Error> {
|
||||
fn list_active_soar_blocks(&self) -> Result<Vec<ActiveBlockView>, Error> {
|
||||
self.list_active_soar_blocks()
|
||||
}
|
||||
|
||||
fn find_soar_block_by_id(&self, id: i64) -> Result<Option<(i64, String, i64, String)>, Error> {
|
||||
fn find_soar_block_by_id(&self, id: i64) -> Result<Option<ActiveBlockView>, Error> {
|
||||
self.find_soar_block_by_id(id)
|
||||
}
|
||||
|
||||
fn list_expired_soar_blocks(&self) -> Result<Vec<(i64, String, i64)>, Error> {
|
||||
fn list_expired_soar_blocks(&self) -> Result<Vec<ActiveBlockView>, Error> {
|
||||
self.list_expired_soar_blocks()
|
||||
}
|
||||
|
||||
@ -280,7 +341,7 @@ impl SoarRepo for Database {
|
||||
self.insert_pending_unblock(source_ip)
|
||||
}
|
||||
|
||||
fn list_pending_unblocks(&self) -> Result<Vec<(i64, String, i64)>, Error> {
|
||||
fn list_pending_unblocks(&self) -> Result<Vec<PendingUnblock>, Error> {
|
||||
self.list_pending_unblocks()
|
||||
}
|
||||
|
||||
@ -302,7 +363,7 @@ impl SoarRepo for Database {
|
||||
self.insert_soar_execution(playbook_id, source_ip, trigger_event, actions_json)
|
||||
}
|
||||
|
||||
fn list_soar_executions(&self, limit: i64) -> Result<Vec<SoarExecutionRow>, Error> {
|
||||
fn list_soar_executions(&self, limit: i64) -> Result<Vec<ExecutionView>, Error> {
|
||||
self.list_soar_executions(limit)
|
||||
}
|
||||
|
||||
@ -402,9 +463,8 @@ mod tests {
|
||||
.insert_playbook_atomic("atom_pb", "threat", Some(0.8), None, None, 300, &actions, &conditions)
|
||||
.unwrap();
|
||||
assert!(id > 0);
|
||||
let loaded = db.list_playbooks_with_actions().unwrap();
|
||||
let loaded = db.list_playbooks().unwrap();
|
||||
assert!(!loaded.is_empty());
|
||||
let cond_rows = db.list_all_playbook_conditions().unwrap();
|
||||
assert_eq!(cond_rows.len(), 1);
|
||||
assert_eq!(loaded[0].conditions.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ use rusqlite::params;
|
||||
|
||||
use super::Database;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::response::playbook_data::{ActiveBlockView, PendingUnblock};
|
||||
use crate::interface::port::db_admin::DbAdminRepo;
|
||||
|
||||
impl Database {
|
||||
@ -28,13 +29,18 @@ impl Database {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub fn list_expired_soar_blocks(&self) -> Result<Vec<(i64, String, i64)>, Error> {
|
||||
pub fn list_expired_soar_blocks(&self) -> Result<Vec<ActiveBlockView>, Error> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, source_ip, playbook_id FROM soar_block_rules WHERE expires_at <= datetime('now') AND unblocked_at IS NULL",
|
||||
"SELECT id, source_ip, playbook_id, expires_at FROM soar_block_rules WHERE expires_at <= datetime('now') AND unblocked_at IS NULL",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?, row.get::<_, i64>(2)?))
|
||||
Ok(ActiveBlockView {
|
||||
id: row.get::<_, i64>(0)?,
|
||||
source_ip: row.get::<_, String>(1)?,
|
||||
playbook_id: row.get::<_, i64>(2)?,
|
||||
expires_at: row.get::<_, String>(3)?,
|
||||
})
|
||||
})?;
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
@ -43,18 +49,18 @@ impl Database {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get a single SOAR block rule by ID, returning (id, source_ip, playbook_id, expires_at).
|
||||
pub fn find_soar_block_by_id(&self, id: i64) -> Result<Option<(i64, String, i64, String)>, Error> {
|
||||
/// Get a single SOAR block rule by ID.
|
||||
pub fn find_soar_block_by_id(&self, id: i64) -> Result<Option<ActiveBlockView>, Error> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT id, source_ip, playbook_id, expires_at FROM soar_block_rules WHERE id = ?1")?;
|
||||
let mut rows = stmt.query_map(params![id], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
))
|
||||
Ok(ActiveBlockView {
|
||||
id: row.get::<_, i64>(0)?,
|
||||
source_ip: row.get::<_, String>(1)?,
|
||||
playbook_id: row.get::<_, i64>(2)?,
|
||||
expires_at: row.get::<_, String>(3)?,
|
||||
})
|
||||
})?;
|
||||
match rows.next() {
|
||||
Some(row) => Ok(Some(row?)),
|
||||
@ -71,18 +77,18 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_active_soar_blocks(&self) -> Result<Vec<(i64, String, i64, String)>, Error> {
|
||||
pub fn list_active_soar_blocks(&self) -> Result<Vec<ActiveBlockView>, Error> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, source_ip, playbook_id, expires_at FROM soar_block_rules WHERE unblocked_at IS NULL AND expires_at > datetime('now')"
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, i64>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)?,
|
||||
row.get::<_, String>(3)?,
|
||||
))
|
||||
Ok(ActiveBlockView {
|
||||
id: row.get::<_, i64>(0)?,
|
||||
source_ip: row.get::<_, String>(1)?,
|
||||
playbook_id: row.get::<_, i64>(2)?,
|
||||
expires_at: row.get::<_, String>(3)?,
|
||||
})
|
||||
})?;
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
@ -100,11 +106,15 @@ impl Database {
|
||||
Ok(conn.last_insert_rowid())
|
||||
}
|
||||
|
||||
pub fn list_pending_unblocks(&self) -> Result<Vec<(i64, String, i64)>, Error> {
|
||||
pub fn list_pending_unblocks(&self) -> Result<Vec<PendingUnblock>, Error> {
|
||||
let conn = self.conn()?;
|
||||
let mut stmt = conn.prepare("SELECT id, source_ip, retry_count FROM pending_unblock ORDER BY id")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?, row.get::<_, i64>(2)?))
|
||||
Ok(PendingUnblock {
|
||||
id: row.get::<_, i64>(0)?,
|
||||
source_ip: row.get::<_, String>(1)?,
|
||||
retry_count: row.get::<_, i64>(2)?,
|
||||
})
|
||||
})?;
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
@ -179,6 +189,7 @@ impl DbAdminRepo for Database {
|
||||
mod tests {
|
||||
use super::super::tests::test_db;
|
||||
use crate::interface::port::db_admin::DbAdminRepo;
|
||||
use crate::interface::port::soar::SoarRepo;
|
||||
|
||||
/// Happy path. Verifies `commit_soar_block_to_db` writes both
|
||||
/// `soar_block_rules` and `acl_rules` atomically.
|
||||
@ -197,9 +208,9 @@ mod tests {
|
||||
assert!(soar_block_id > 0);
|
||||
|
||||
// soar_block_rules has the row
|
||||
let active = db.list_active_soar_blocks().unwrap();
|
||||
let active = SoarRepo::list_active_soar_blocks(&db).unwrap();
|
||||
assert_eq!(active.len(), 1);
|
||||
assert_eq!(active[0].1, "10.0.0.99");
|
||||
assert_eq!(active[0].source_ip, "10.0.0.99");
|
||||
|
||||
// acl_rules has the matching row
|
||||
let rules = db.list_acl_rules().unwrap();
|
||||
@ -224,6 +235,6 @@ mod tests {
|
||||
// acl_rules row gone
|
||||
assert!(db.list_acl_rules().unwrap().is_empty());
|
||||
// soar_block_rules row no longer in "active" view (unblocked_at is set)
|
||||
assert!(db.list_active_soar_blocks().unwrap().is_empty());
|
||||
assert!(SoarRepo::list_active_soar_blocks(&db).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,89 +1,13 @@
|
||||
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 actix_ws::handle;
|
||||
|
||||
use crate::adapter::websocket::ws_bridge;
|
||||
use crate::core::inference::alert::MLAlert;
|
||||
use crate::domain::common::error::http::HttpError;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::log::http::HttpLog;
|
||||
use crate::domain::detection::ml_detection::AlertMessage;
|
||||
|
||||
pub async fn websocket_alert(req: HttpRequest, body: web::Payload, ai: web::Data<MLAlert>) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
let broadcast_rx = ai.subscribe_to_alerts();
|
||||
|
||||
spawn(async move {
|
||||
handle_alert_connection(session, msg_stream, broadcast_rx).await;
|
||||
});
|
||||
|
||||
let rx = ai.subscribe_to_alerts();
|
||||
spawn(ws_bridge::broadcast_json(session, msg_stream, rx));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_alert_connection(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut broadcast_rx: broadcast::Receiver<AlertMessage>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = broadcast_rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(alert) => {
|
||||
if !send_alert(&mut session, &alert).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_alert(session: &mut Session, alert: &AlertMessage) -> bool {
|
||||
match serde_json::to_string(alert) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,9 @@
|
||||
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 actix_ws::handle;
|
||||
|
||||
use super::ws_bridge;
|
||||
use crate::adapter::ebpf::drop_monitor::DropMonitor;
|
||||
use crate::domain::common::error::http::HttpError;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::log::http::HttpLog;
|
||||
use crate::domain::data_plane::drop_event::DropEventMessage;
|
||||
|
||||
pub async fn websocket_drops(
|
||||
req: HttpRequest,
|
||||
@ -18,76 +11,7 @@ pub async fn websocket_drops(
|
||||
monitor: web::Data<DropMonitor>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
let broadcast_rx = monitor.subscribe();
|
||||
|
||||
spawn(async move {
|
||||
handle_drop_connection(session, msg_stream, broadcast_rx).await;
|
||||
});
|
||||
|
||||
let rx = monitor.subscribe();
|
||||
spawn(ws_bridge::broadcast_json(session, msg_stream, rx));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_drop_connection(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut broadcast_rx: broadcast::Receiver<DropEventMessage>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = broadcast_rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(event) => {
|
||||
if !send_drop_event(&mut session, &event).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_drop_event(session: &mut Session, event: &DropEventMessage) -> bool {
|
||||
match serde_json::to_string(event) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,16 +16,13 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use actix_web::rt::spawn;
|
||||
use actix_web::{HttpRequest, HttpResponse, Result, web};
|
||||
use actix_ws::{Message, MessageStream, Session, handle};
|
||||
use futures_util::StreamExt;
|
||||
use actix_ws::handle;
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
|
||||
use crate::domain::common::error::http::HttpError;
|
||||
use super::ws_bridge;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::event::ThreatDetectedEvent;
|
||||
use crate::domain::common::log::http::HttpLog;
|
||||
|
||||
pub async fn websocket_fusion(
|
||||
req: HttpRequest,
|
||||
@ -33,76 +30,12 @@ pub async fn websocket_fusion(
|
||||
threat_tx: web::Data<broadcast::Sender<ThreatDetectedEvent>>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
let broadcast_rx = threat_tx.subscribe();
|
||||
|
||||
spawn(async move {
|
||||
handle_fusion_connection(session, msg_stream, broadcast_rx).await;
|
||||
});
|
||||
|
||||
let rx = threat_tx.subscribe();
|
||||
spawn(ws_bridge::broadcast_loop(session, msg_stream, rx, envelope_with_ts));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_fusion_connection(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut broadcast_rx: broadcast::Receiver<ThreatDetectedEvent>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = broadcast_rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(event) => {
|
||||
if !send_event(&mut session, &event).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap each event in `{ts, ...event_fields}`. The `ts` is a server-stamped
|
||||
/// unix-seconds value so the client can render "5s ago" without inferring
|
||||
/// the time from the audit chain. All declared fields of
|
||||
/// `ThreatDetectedEvent` flow through verbatim via the event's own
|
||||
/// `Serialize` derive — no field whitelist to drift out of date.
|
||||
async fn send_event(session: &mut Session, event: &ThreatDetectedEvent) -> bool {
|
||||
fn envelope_with_ts(event: &ThreatDetectedEvent) -> Option<String> {
|
||||
let ts = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
@ -113,21 +46,18 @@ async fn send_event(session: &mut Session, event: &ThreatDetectedEvent) -> bool
|
||||
map.insert("ts".to_string(), serde_json::Value::from(ts));
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
// The derived Serialize on a struct always produces an Object —
|
||||
// this branch only fires if the type changes shape in a future
|
||||
// refactor. Falling back to the raw value keeps the stream alive.
|
||||
Ok(other) => other,
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match serde_json::to_string(&payload) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Ok(json) => Some(json),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
false
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,14 +100,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn envelope_adds_ts_field_to_event_object() {
|
||||
// The `send_event` wire path inserts `ts` into the event's own
|
||||
// serde object; mirror that here without an actix session so the
|
||||
// wrapping logic stays covered when the orchestrator schema evolves.
|
||||
let event = sample_event();
|
||||
let mut value = serde_json::to_value(&event).expect("serialize event");
|
||||
let object = value.as_object_mut().expect("expected object shape");
|
||||
object.insert("ts".to_string(), serde_json::Value::from(1_700_000_000_u64));
|
||||
assert_eq!(value["ts"], 1_700_000_000_u64);
|
||||
let json = envelope_with_ts(&sample_event()).expect("should serialize");
|
||||
let value: serde_json::Value = serde_json::from_str(&json).expect("valid json");
|
||||
assert!(value["ts"].is_u64());
|
||||
assert_eq!(value["attack_type"], "brute_force");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,8 @@
|
||||
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 actix_ws::handle;
|
||||
|
||||
use crate::domain::common::error::http::HttpError;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::log::http::HttpLog;
|
||||
use crate::domain::common::system::health::SystemHealthMetrics;
|
||||
use super::ws_bridge;
|
||||
use crate::infrastructure::health::SystemHealth;
|
||||
|
||||
pub async fn websocket_system_health(
|
||||
@ -18,76 +11,7 @@ pub async fn websocket_system_health(
|
||||
health: web::Data<SystemHealth>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
let broadcast_rx = health.subscribe_to_metrics();
|
||||
|
||||
spawn(async move {
|
||||
handle_health_connection(session, msg_stream, broadcast_rx).await;
|
||||
});
|
||||
|
||||
let rx = health.subscribe_to_metrics();
|
||||
spawn(ws_bridge::broadcast_json(session, msg_stream, rx));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_health_connection(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut broadcast_rx: broadcast::Receiver<SystemHealthMetrics>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = broadcast_rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(metrics) => {
|
||||
if !send_metrics(&mut session, &metrics).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_metrics(session: &mut Session, metrics: &SystemHealthMetrics) -> bool {
|
||||
match serde_json::to_string(metrics) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,3 +4,4 @@ pub mod flow_websocket;
|
||||
pub mod fusion_websocket;
|
||||
pub mod health_websocket;
|
||||
pub mod routes;
|
||||
pub mod ws_bridge;
|
||||
|
||||
86
net-guardia/src/adapter/websocket/ws_bridge.rs
Normal file
86
net-guardia/src/adapter/websocket/ws_bridge.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use actix_ws::{Message, MessageStream, Session};
|
||||
use futures_util::StreamExt;
|
||||
use macros::log;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
|
||||
use crate::domain::common::error::http::HttpError;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::log::http::HttpLog;
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_json<T: Serialize>(value: &T) -> Option<String> {
|
||||
match serde_json::to_string(value) {
|
||||
Ok(json) => Some(json),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn broadcast_json<T: Serialize + Clone + Send + 'static>(
|
||||
session: Session,
|
||||
msg_stream: MessageStream,
|
||||
rx: broadcast::Receiver<T>,
|
||||
) {
|
||||
broadcast_loop(session, msg_stream, rx, |event| serialize_json(event)).await;
|
||||
}
|
||||
|
||||
pub async fn broadcast_loop<T: Clone + Send + 'static>(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut rx: broadcast::Receiver<T>,
|
||||
to_json: impl Fn(&T) -> Option<String>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(event) => {
|
||||
let Some(json) = to_json(&event) else {
|
||||
break;
|
||||
};
|
||||
if session.text(json).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLagged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
@ -4,7 +4,6 @@ use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use macros::log;
|
||||
use serde_json::Value;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
@ -77,92 +76,61 @@ impl SoarEngine {
|
||||
|
||||
/// Load playbooks and admin whitelist from DB into memory.
|
||||
pub fn reload_cache(&self) -> Result<(), Error> {
|
||||
// Load playbooks via single JOIN query (no N+1)
|
||||
let rows = self.db.list_playbooks_with_actions()?;
|
||||
let views = self.db.list_playbooks()?;
|
||||
let mut playbooks: Vec<Playbook> = Vec::new();
|
||||
|
||||
for (
|
||||
pb_id,
|
||||
name,
|
||||
enabled,
|
||||
trigger_event,
|
||||
threshold,
|
||||
_count,
|
||||
_window,
|
||||
cooldown,
|
||||
_action_id,
|
||||
action_order,
|
||||
action_type,
|
||||
action_params,
|
||||
) in rows
|
||||
{
|
||||
// Check if this row belongs to the same playbook as the last one
|
||||
let needs_new = playbooks.last().is_none_or(|last| last.id != pb_id);
|
||||
if needs_new {
|
||||
let _ = threshold; // persisted for schema stability; runtime gating comes from the Condition rows
|
||||
playbooks.push(Playbook {
|
||||
id: pb_id,
|
||||
name,
|
||||
enabled,
|
||||
trigger_event,
|
||||
cooldown_secs: cooldown,
|
||||
actions: Vec::new(),
|
||||
conditions: Vec::new(),
|
||||
for view in views {
|
||||
let mut actions: Vec<PlaybookAction> = Vec::new();
|
||||
for a in view.actions {
|
||||
actions.push(PlaybookAction {
|
||||
action_order: a.action_order,
|
||||
action_type: a.action_type,
|
||||
params: a.params,
|
||||
});
|
||||
}
|
||||
// Safe: we just pushed if empty, and last() was Some otherwise
|
||||
let Some(pb) = playbooks.last_mut() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let (Some(order), Some(atype), Some(params_str)) = (action_order, action_type, action_params) {
|
||||
pb.actions.push(PlaybookAction {
|
||||
action_order: order,
|
||||
action_type: atype,
|
||||
params: serde_json::from_str(¶ms_str).unwrap_or_else(|e| {
|
||||
log!(SoarLog::PlaybookError(
|
||||
pb.name.clone(),
|
||||
format!("Malformed action params JSON: {e}"),
|
||||
));
|
||||
Value::Object(Default::default())
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load conditions and attach to playbooks
|
||||
let condition_rows = self.db.list_all_playbook_conditions()?;
|
||||
for (_cid, pb_id, ctype_str, operator, value, value2) in condition_rows {
|
||||
if let Ok(ctype) = ctype_str.parse::<ConditionType>()
|
||||
&& let Some(pb) = playbooks.iter_mut().find(|p| p.id == pb_id)
|
||||
{
|
||||
let mut conditions: Vec<PlaybookCondition> = Vec::new();
|
||||
for c in view.conditions {
|
||||
let Ok(ctype) = c.condition_type.parse::<ConditionType>() else {
|
||||
continue;
|
||||
};
|
||||
// Validate operator at load time to prevent silent fallback to defaults
|
||||
let valid = match ctype {
|
||||
ConditionType::Threshold => matches!(operator.as_str(), ">=" | "<="),
|
||||
ConditionType::Threshold => matches!(c.operator.as_str(), ">=" | "<="),
|
||||
ConditionType::SourceCountry | ConditionType::IpPattern => {
|
||||
matches!(operator.as_str(), "in" | "not_in")
|
||||
matches!(c.operator.as_str(), "in" | "not_in")
|
||||
}
|
||||
ConditionType::RepeatOffender => operator == "==",
|
||||
ConditionType::Frequency => operator == ">=",
|
||||
ConditionType::MultiSourceMin => operator == ">=",
|
||||
ConditionType::SingleSourceHigh => operator == ">=",
|
||||
ConditionType::FusedConfidenceAbove => matches!(operator.as_str(), ">=" | "<="),
|
||||
ConditionType::RepeatOffender => c.operator == "==",
|
||||
ConditionType::Frequency => c.operator == ">=",
|
||||
ConditionType::MultiSourceMin => c.operator == ">=",
|
||||
ConditionType::SingleSourceHigh => c.operator == ">=",
|
||||
ConditionType::FusedConfidenceAbove => matches!(c.operator.as_str(), ">=" | "<="),
|
||||
};
|
||||
if !valid {
|
||||
log!(SoarLog::InvalidConditionOperator(
|
||||
pb.name.clone(),
|
||||
ctype_str.clone(),
|
||||
operator.clone(),
|
||||
view.name.clone(),
|
||||
c.condition_type.clone(),
|
||||
c.operator.clone(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
pb.conditions.push(PlaybookCondition {
|
||||
conditions.push(PlaybookCondition {
|
||||
condition_type: ctype,
|
||||
operator,
|
||||
value,
|
||||
value2,
|
||||
operator: c.operator,
|
||||
value: c.value,
|
||||
value2: c.value2,
|
||||
});
|
||||
}
|
||||
|
||||
playbooks.push(Playbook {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
enabled: view.enabled,
|
||||
trigger_event: view.trigger_event,
|
||||
cooldown_secs: view.cooldown_secs,
|
||||
actions,
|
||||
conditions,
|
||||
});
|
||||
}
|
||||
|
||||
// Warn on playbooks whose trigger_event isn't in the canonical
|
||||
@ -270,10 +238,10 @@ impl SoarEngine {
|
||||
let active_blocks = self.db.list_active_soar_blocks()?;
|
||||
let count = active_blocks.len();
|
||||
|
||||
for (_id, source_ip, _playbook_id, _expires_at) in &active_blocks {
|
||||
for block in &active_blocks {
|
||||
// Preserve original error-swallowing behavior during recovery
|
||||
if let Err(e) = self.access_control.block_ip(source_ip) {
|
||||
log!(SoarLog::RecoveryFailed(source_ip.clone(), e.to_string()));
|
||||
if let Err(e) = self.access_control.block_ip(&block.source_ip) {
|
||||
log!(SoarLog::RecoveryFailed(block.source_ip.clone(), e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,32 +265,32 @@ impl SoarEngine {
|
||||
}
|
||||
};
|
||||
|
||||
for (id, source_ip, retry_count) in pending {
|
||||
if retry_count >= self.matcher.config.load().soar.max_pending_unblock_retries {
|
||||
for pu in pending {
|
||||
if pu.retry_count >= self.matcher.config.load().soar.max_pending_unblock_retries {
|
||||
log!(SoarLog::EventHandlingFailed(format!(
|
||||
"Giving up on pending unblock for IP {} after {} retries",
|
||||
source_ip, retry_count
|
||||
pu.source_ip, pu.retry_count
|
||||
)));
|
||||
// Remove from queue to avoid infinite retries
|
||||
let _ = self.db.delete_pending_unblock(id);
|
||||
let _ = self.db.delete_pending_unblock(pu.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.access_control.unblock_ip(&source_ip) {
|
||||
match self.access_control.unblock_ip(&pu.source_ip) {
|
||||
Ok(()) => {
|
||||
let _ = self.db.delete_pending_unblock(id);
|
||||
let _ = self.db.delete_pending_unblock(pu.id);
|
||||
log!(SoarLog::EventHandlingFailed(format!(
|
||||
"Successfully unblocked orphan IP {} on retry #{}",
|
||||
source_ip,
|
||||
retry_count + 1
|
||||
pu.source_ip,
|
||||
pu.retry_count + 1
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = self.db.increment_pending_unblock_retry(id);
|
||||
let _ = self.db.increment_pending_unblock_retry(pu.id);
|
||||
log!(SoarLog::EventHandlingFailed(format!(
|
||||
"Retry #{} failed to unblock orphan IP {}: {}",
|
||||
retry_count + 1,
|
||||
source_ip,
|
||||
pu.retry_count + 1,
|
||||
pu.source_ip,
|
||||
e
|
||||
)));
|
||||
}
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::core::response::engine::SoarEngine;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::response::error::SoarError;
|
||||
use crate::domain::response::playbook_data::{
|
||||
ActionView, ActiveBlockView, ConditionView, CreatePlaybookInput, ExecutionView, PlaybookView, UpdatePlaybookInput,
|
||||
ActiveBlockView, CreatePlaybookInput, ExecutionView, PlaybookView, UpdatePlaybookInput,
|
||||
};
|
||||
use crate::interface::port::access_control::AccessControlPort;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
@ -31,93 +28,7 @@ impl PlaybookService {
|
||||
}
|
||||
|
||||
pub fn list_playbooks(&self) -> Result<Vec<PlaybookView>, Error> {
|
||||
let rows = self.db.list_playbooks_with_actions()?;
|
||||
let mut result: Vec<PlaybookView> = Vec::new();
|
||||
|
||||
for (
|
||||
pb_id,
|
||||
name,
|
||||
enabled,
|
||||
trigger_event,
|
||||
threshold,
|
||||
count,
|
||||
window,
|
||||
cooldown,
|
||||
action_id,
|
||||
action_order,
|
||||
action_type,
|
||||
action_params,
|
||||
) in rows
|
||||
{
|
||||
// Find or create the playbook entry
|
||||
let pb = if let Some(last) = result.last_mut() {
|
||||
if last.id == pb_id {
|
||||
last
|
||||
} else {
|
||||
result.push(PlaybookView {
|
||||
id: pb_id,
|
||||
name,
|
||||
enabled,
|
||||
trigger_event,
|
||||
condition_threshold: threshold,
|
||||
condition_count: count,
|
||||
condition_window_secs: window,
|
||||
cooldown_secs: cooldown,
|
||||
actions: Vec::new(),
|
||||
conditions: Vec::new(),
|
||||
});
|
||||
// SAFETY: just pushed above, Vec cannot be empty
|
||||
result.last_mut().unwrap_or_else(|| unreachable!())
|
||||
}
|
||||
} else {
|
||||
result.push(PlaybookView {
|
||||
id: pb_id,
|
||||
name,
|
||||
enabled,
|
||||
trigger_event,
|
||||
condition_threshold: threshold,
|
||||
condition_count: count,
|
||||
condition_window_secs: window,
|
||||
cooldown_secs: cooldown,
|
||||
actions: Vec::new(),
|
||||
conditions: Vec::new(),
|
||||
});
|
||||
// SAFETY: just pushed above, Vec cannot be empty
|
||||
result.last_mut().unwrap_or_else(|| unreachable!())
|
||||
};
|
||||
|
||||
// Append action if present (LEFT JOIN may yield NULLs)
|
||||
if let (Some(aid), Some(order), Some(atype), Some(params_str)) =
|
||||
(action_id, action_order, action_type, action_params)
|
||||
{
|
||||
pb.actions.push(ActionView {
|
||||
id: aid,
|
||||
action_order: order,
|
||||
action_type: atype,
|
||||
params: serde_json::from_str(¶ms_str).unwrap_or(Value::Null),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Load conditions and attach to playbooks
|
||||
let cond_rows = self.db.list_all_playbook_conditions()?;
|
||||
let mut cond_map: HashMap<i64, Vec<ConditionView>> = HashMap::new();
|
||||
for (cid, pb_id, ctype, operator, value, value2) in cond_rows {
|
||||
cond_map.entry(pb_id).or_default().push(ConditionView {
|
||||
id: cid,
|
||||
condition_type: ctype,
|
||||
operator,
|
||||
value,
|
||||
value2,
|
||||
});
|
||||
}
|
||||
for pb in &mut result {
|
||||
if let Some(conds) = cond_map.remove(&pb.id) {
|
||||
pb.conditions = conds;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
self.db.list_playbooks()
|
||||
}
|
||||
|
||||
pub fn create_playbook(&self, input: &CreatePlaybookInput) -> Result<i64, Error> {
|
||||
@ -207,16 +118,7 @@ impl PlaybookService {
|
||||
}
|
||||
|
||||
pub fn list_active_blocks(&self) -> Result<Vec<ActiveBlockView>, Error> {
|
||||
let blocks = self.db.list_active_soar_blocks()?;
|
||||
Ok(blocks
|
||||
.into_iter()
|
||||
.map(|(id, ip, pb_id, expires)| ActiveBlockView {
|
||||
id,
|
||||
source_ip: ip,
|
||||
playbook_id: pb_id,
|
||||
expires_at: expires,
|
||||
})
|
||||
.collect())
|
||||
self.db.list_active_soar_blocks()
|
||||
}
|
||||
|
||||
/// Manually unblock an IP: remove from eBPF, atomically clear both DB
|
||||
@ -228,7 +130,7 @@ impl PlaybookService {
|
||||
.db
|
||||
.find_soar_block_by_id(id)?
|
||||
.ok_or_else(|| SoarError::UnblockRuleNotFound(id))?;
|
||||
let source_ip = &block.1;
|
||||
let source_ip = &block.source_ip;
|
||||
|
||||
// Remove from eBPF ACL
|
||||
self.access_control.unblock_ip(source_ip)?;
|
||||
@ -245,20 +147,7 @@ impl PlaybookService {
|
||||
}
|
||||
|
||||
pub fn list_executions(&self, limit: i64) -> Result<Vec<ExecutionView>, Error> {
|
||||
let rows = self.db.list_soar_executions(limit)?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(
|
||||
|(id, pb_id, source_ip, trigger_event, actions, created_at)| ExecutionView {
|
||||
id,
|
||||
playbook_id: pb_id,
|
||||
source_ip,
|
||||
trigger_event,
|
||||
actions_executed: serde_json::from_str(&actions).unwrap_or(Value::Null),
|
||||
created_at,
|
||||
},
|
||||
)
|
||||
.collect())
|
||||
self.db.list_soar_executions(limit)
|
||||
}
|
||||
|
||||
pub fn list_whitelist(&self) -> Result<Vec<String>, Error> {
|
||||
|
||||
@ -65,34 +65,34 @@ impl TtlScheduler {
|
||||
let mut removed = 0u32;
|
||||
let mut skipped = 0u32;
|
||||
|
||||
for (id, source_ip, _playbook_id) in &expired {
|
||||
for block in &expired {
|
||||
// Check if a manual ACL rule exists for this IP
|
||||
let has_manual_rule = self.db.has_manual_acl_rule(source_ip)?;
|
||||
let has_manual_rule = self.db.has_manual_acl_rule(&block.source_ip)?;
|
||||
|
||||
if has_manual_rule {
|
||||
// Only mark as unblocked in SOAR records, don't remove from eBPF
|
||||
self.db.mark_soar_block_unblocked(*id)?;
|
||||
self.db.mark_soar_block_unblocked(block.id)?;
|
||||
self.soar_engine.decrement_block_count();
|
||||
skipped += 1;
|
||||
log!(SoarLog::WhitelistSkipped(
|
||||
source_ip.clone(),
|
||||
block.source_ip.clone(),
|
||||
"TTL expired but manual ACL exists".to_string()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove from eBPF ACL via AccessControlPort
|
||||
if let Err(e) = self.access_control.unblock_ip(source_ip) {
|
||||
if let Err(e) = self.access_control.unblock_ip(&block.source_ip) {
|
||||
log!(SoarLog::RecoveryFailed(
|
||||
source_ip.clone(),
|
||||
block.source_ip.clone(),
|
||||
format!("unblock failed: {}", e)
|
||||
));
|
||||
}
|
||||
|
||||
// Atomically drop acl_rules entry AND mark soar_block_rules
|
||||
// unblocked in one transaction.
|
||||
let ip_version = ip_version_from_str(source_ip);
|
||||
self.db.commit_soar_unblock_to_db(*id, ip_version, source_ip)?;
|
||||
let ip_version = ip_version_from_str(&block.source_ip);
|
||||
self.db.commit_soar_unblock_to_db(block.id, ip_version, &block.source_ip)?;
|
||||
self.soar_engine.decrement_block_count();
|
||||
removed += 1;
|
||||
}
|
||||
|
||||
@ -31,5 +31,8 @@ pub const AUDIT_ACTOR_SECURITY_ADMIN_PREFIX: &str = "SecurityAdmin";
|
||||
pub const FLOW_TRACE_FILE_MARKER: &str = "flow-trace-";
|
||||
pub const FLOW_TRACE_FILE_EXT: &str = ".csv";
|
||||
|
||||
// ── Permissions ───────────────────────────────────────────────────
|
||||
pub const PERMISSION_SYSTEM_ADMIN: &str = "system:admin";
|
||||
|
||||
// ── Event Channels ────────────────────────────────────────────────
|
||||
pub const EVENT_CHANNEL_CAPACITY: usize = 256;
|
||||
|
||||
@ -6,6 +6,8 @@ use macros::log;
|
||||
|
||||
use crate::domain::common::config::correlation::CorrelationDetectorParams;
|
||||
use crate::domain::common::event::{DetectionEvent, DetectionSource};
|
||||
use crate::domain::detection::attack_type::CanonicalAttackType;
|
||||
use crate::domain::detection::correlation_cleanup::capped_cleanup;
|
||||
use crate::domain::detection::log::DetectionLog;
|
||||
use crate::domain::detection::ml_detection::AlertMessage;
|
||||
|
||||
@ -86,7 +88,7 @@ impl BotnetDetector {
|
||||
// SOAR blocks source_ip, so we must NOT put the victim here.
|
||||
let event = DetectionEvent {
|
||||
source: DetectionSource::Correlation,
|
||||
attack_type: "threat_detected".to_string(),
|
||||
attack_type: CanonicalAttackType::BotActivity.as_str().to_string(),
|
||||
confidence: 0.85,
|
||||
source_ip: alert.src_ip.clone(),
|
||||
dest_ip: key.clone(),
|
||||
@ -110,25 +112,8 @@ impl BotnetDetector {
|
||||
None
|
||||
}
|
||||
|
||||
/// Remove expired entries. Returns number of entries removed.
|
||||
pub fn cleanup(&self) -> usize {
|
||||
let now = Instant::now();
|
||||
let window = self.window;
|
||||
let before = self.state.len();
|
||||
|
||||
self.state
|
||||
.retain(|_, set| now.duration_since(set.window_start) < window);
|
||||
|
||||
// Enforce max capacity by removing oldest entries if over limit
|
||||
if self.state.len() > self.max_tracked {
|
||||
let excess = self.state.len() - self.max_tracked;
|
||||
let keys_to_remove: Vec<String> = self.state.iter().take(excess).map(|e| e.key().clone()).collect();
|
||||
for key in keys_to_remove {
|
||||
self.state.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
before.saturating_sub(self.state.len())
|
||||
capped_cleanup(&self.state, self.window, self.max_tracked, |s| s.window_start)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
net-guardia/src/domain/detection/correlation_cleanup.rs
Normal file
26
net-guardia/src/domain/detection/correlation_cleanup.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use std::hash::Hash;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
||||
pub fn capped_cleanup<K: Eq + Hash + Clone, V>(
|
||||
map: &DashMap<K, V>,
|
||||
window: Duration,
|
||||
max_tracked: usize,
|
||||
window_start: impl Fn(&V) -> Instant,
|
||||
) -> usize {
|
||||
let now = Instant::now();
|
||||
let before = map.len();
|
||||
|
||||
map.retain(|_, v| now.duration_since(window_start(v)) < window);
|
||||
|
||||
if map.len() > max_tracked {
|
||||
let excess = map.len() - max_tracked;
|
||||
let keys_to_remove: Vec<K> = map.iter().take(excess).map(|e| e.key().clone()).collect();
|
||||
for key in keys_to_remove {
|
||||
map.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
before.saturating_sub(map.len())
|
||||
}
|
||||
@ -53,15 +53,7 @@ traceable! {
|
||||
#[error("Failed to spawn Suricata subprocess")]
|
||||
SpawnFailed => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Suricata subprocess exited: {reason}")]
|
||||
SubprocessExited { reason: String } => tracing::Level::WARN,
|
||||
|
||||
#[error("Failed to open eve.json stream at '{path}'")]
|
||||
EveOpenFailed { path: String } => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Failed to parse eve.json line: {reason}")]
|
||||
EveParseFailed { reason: String } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,6 +307,11 @@ impl FlowTracker {
|
||||
self.active.iter().map(|(_, entry)| entry.lock().clone()).collect()
|
||||
}
|
||||
|
||||
/// Extract scalar stats from all active flows without cloning packet vectors.
|
||||
pub fn get_flow_stats<T>(&self, convert: impl Fn(&FlowData) -> T) -> Vec<T> {
|
||||
self.active.iter().map(|(_, entry)| convert(&entry.lock())).collect()
|
||||
}
|
||||
|
||||
/// Get flows that received new packets since their last inference, and
|
||||
/// mark them as inferred. Used by ML engine.
|
||||
pub fn get_uninferred_flows(&self, limit: usize) -> Vec<FlowData> {
|
||||
|
||||
@ -7,6 +7,8 @@ use macros::log;
|
||||
|
||||
use crate::domain::common::config::correlation::CorrelationDetectorParams;
|
||||
use crate::domain::common::event::{DetectionEvent, DetectionSource};
|
||||
use crate::domain::detection::attack_type::CanonicalAttackType;
|
||||
use crate::domain::detection::correlation_cleanup::capped_cleanup;
|
||||
use crate::domain::detection::log::DetectionLog;
|
||||
use crate::domain::detection::ml_detection::AlertMessage;
|
||||
|
||||
@ -84,7 +86,7 @@ impl LateralMovementDetector {
|
||||
|
||||
let event = DetectionEvent {
|
||||
source: DetectionSource::Correlation,
|
||||
attack_type: "threat_detected".to_string(),
|
||||
attack_type: CanonicalAttackType::LateralMovement.as_str().to_string(),
|
||||
confidence: 0.75,
|
||||
source_ip: key.clone(),
|
||||
dest_ip: alert.dst_ip.clone(),
|
||||
@ -108,24 +110,8 @@ impl LateralMovementDetector {
|
||||
None
|
||||
}
|
||||
|
||||
/// Remove expired entries. Returns number of entries removed.
|
||||
pub fn cleanup(&self) -> usize {
|
||||
let now = Instant::now();
|
||||
let window = self.window;
|
||||
let before = self.state.len();
|
||||
|
||||
self.state
|
||||
.retain(|_, set| now.duration_since(set.window_start) < window);
|
||||
|
||||
if self.state.len() > self.max_tracked {
|
||||
let excess = self.state.len() - self.max_tracked;
|
||||
let keys_to_remove: Vec<String> = self.state.iter().take(excess).map(|e| e.key().clone()).collect();
|
||||
for key in keys_to_remove {
|
||||
self.state.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
before.saturating_sub(self.state.len())
|
||||
capped_cleanup(&self.state, self.window, self.max_tracked, |s| s.window_start)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ pub mod aggregator;
|
||||
pub mod attack_type;
|
||||
pub mod beaconing;
|
||||
pub mod botnet;
|
||||
pub mod correlation_cleanup;
|
||||
pub mod drift;
|
||||
pub mod drift_detector;
|
||||
pub mod error;
|
||||
|
||||
@ -6,6 +6,7 @@ use macros::log;
|
||||
|
||||
use crate::domain::common::config::correlation::CorrelationDetectorParams;
|
||||
use crate::domain::common::event::{DetectionEvent, DetectionSource};
|
||||
use crate::domain::detection::correlation_cleanup::capped_cleanup;
|
||||
use crate::domain::detection::log::DetectionLog;
|
||||
use crate::domain::detection::ml_detection::AlertMessage;
|
||||
|
||||
@ -103,24 +104,8 @@ impl ScanDetector {
|
||||
None
|
||||
}
|
||||
|
||||
/// Remove expired entries. Returns number of entries removed.
|
||||
pub fn cleanup(&self) -> usize {
|
||||
let now = Instant::now();
|
||||
let window = self.window;
|
||||
let before = self.state.len();
|
||||
|
||||
self.state
|
||||
.retain(|_, set| now.duration_since(set.window_start) < window);
|
||||
|
||||
if self.state.len() > self.max_tracked {
|
||||
let excess = self.state.len() - self.max_tracked;
|
||||
let keys_to_remove: Vec<String> = self.state.iter().take(excess).map(|e| e.key().clone()).collect();
|
||||
for key in keys_to_remove {
|
||||
self.state.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
before.saturating_sub(self.state.len())
|
||||
capped_cleanup(&self.state, self.window, self.max_tracked, |s| s.window_start)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const ROLE_ADMIN: &str = "admin";
|
||||
pub const ROLE_VIEWER: &str = "viewer";
|
||||
pub const GROUP_ADMIN: &str = "Administrator";
|
||||
pub const GROUP_VIEWER: &str = "Viewer";
|
||||
pub const DEFAULT_ADMIN_USERNAME: &str = "admin";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub sub: i64,
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use serde::Serialize;
|
||||
|
||||
/// Input for updating a playbook row (without actions/conditions).
|
||||
pub struct UpdatePlaybookInput {
|
||||
pub name: String,
|
||||
@ -50,6 +52,7 @@ pub struct CreatePlaybookInput {
|
||||
}
|
||||
|
||||
/// Persisted condition row for API responses.
|
||||
#[derive(Serialize)]
|
||||
pub struct ConditionView {
|
||||
pub id: i64,
|
||||
pub condition_type: String,
|
||||
@ -59,6 +62,7 @@ pub struct ConditionView {
|
||||
}
|
||||
|
||||
/// Flattened playbook representation for API responses.
|
||||
#[derive(Serialize)]
|
||||
pub struct PlaybookView {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
@ -72,6 +76,7 @@ pub struct PlaybookView {
|
||||
pub conditions: Vec<ConditionView>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ActionView {
|
||||
pub id: i64,
|
||||
pub action_order: i64,
|
||||
@ -80,6 +85,7 @@ pub struct ActionView {
|
||||
}
|
||||
|
||||
/// Execution record from soar_executions table.
|
||||
#[derive(Serialize)]
|
||||
pub struct ExecutionView {
|
||||
pub id: i64,
|
||||
pub playbook_id: i64,
|
||||
@ -90,9 +96,18 @@ pub struct ExecutionView {
|
||||
}
|
||||
|
||||
/// Active block record from soar_block_rules table.
|
||||
#[derive(Serialize)]
|
||||
pub struct ActiveBlockView {
|
||||
pub id: i64,
|
||||
pub source_ip: String,
|
||||
pub playbook_id: i64,
|
||||
pub expires_at: String,
|
||||
}
|
||||
|
||||
/// Pending unblock recovery record.
|
||||
#[derive(Serialize)]
|
||||
pub struct PendingUnblock {
|
||||
pub id: i64,
|
||||
pub source_ip: String,
|
||||
pub retry_count: i64,
|
||||
}
|
||||
|
||||
@ -49,6 +49,8 @@ use crate::infrastructure::system::ShutdownHandle;
|
||||
use crate::interface::port::api_key::ApiKeyRepo;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
use crate::interface::port::audit::AuditRepo;
|
||||
use crate::interface::port::drop_stats::DropStatsPort;
|
||||
use crate::interface::port::protocol_filter::ProtocolFilterPort;
|
||||
|
||||
/// Shared flag: true when all services (eBPF, ML, SOAR) are fully initialized.
|
||||
pub type ReadyFlag = Arc<AtomicBool>;
|
||||
@ -216,7 +218,7 @@ pub fn start_setup_server(
|
||||
/// Run the full HTTP server with all services.
|
||||
pub async fn run(params: HttpServerParams) -> Result<(), Error> {
|
||||
let access_control = params.ebpf_services.access_control.clone();
|
||||
let protocol_filter = params.ebpf_services.protocol_filter.clone();
|
||||
let protocol_filter: Arc<dyn ProtocolFilterPort> = params.ebpf_services.protocol_filter.clone();
|
||||
let dns_filter = params.ebpf_services.dns_filter.clone();
|
||||
let geo_block = params.ebpf_services.geo_block.clone();
|
||||
let rate_limit = params.ebpf_services.rate_limit.clone();
|
||||
@ -227,6 +229,7 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
|
||||
let fusion_metrics = params.app_services.fusion_metrics.clone();
|
||||
let flow_statistics = params.app_services.flow_statistics.clone();
|
||||
let drop_monitor = params.ebpf_services.drop_monitor.clone();
|
||||
let drop_stats: Arc<dyn DropStatsPort> = drop_monitor.clone();
|
||||
let app_config = params.app_config;
|
||||
let inference_config = params.inference_config;
|
||||
let db = params.db;
|
||||
@ -278,6 +281,7 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
|
||||
.app_data(web::Data::from(fusion_metrics.clone()))
|
||||
.app_data(web::Data::from(flow_statistics.clone()))
|
||||
.app_data(web::Data::from(drop_monitor.clone()))
|
||||
.app_data(web::Data::from(drop_stats.clone()))
|
||||
.app_data(web::Data::from(db.clone() as Arc<dyn AppRepo>))
|
||||
.app_data(web::Data::from(db.clone() as Arc<dyn ApiKeyRepo>))
|
||||
.app_data(web::Data::from(db.clone() as Arc<dyn AuditRepo>))
|
||||
|
||||
@ -42,7 +42,7 @@ impl FlowStatistics {
|
||||
pub fn get_all_flows(&self) -> Vec<FlowStatsEntry> {
|
||||
let mut entries = Vec::new();
|
||||
for tracker in self.engine.trackers() {
|
||||
entries.extend(tracker.get_flows().iter().map(FlowStatsEntry::from));
|
||||
entries.extend(tracker.get_flow_stats(|flow| FlowStatsEntry::from(flow)));
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
5
net-guardia/src/interface/port/drop_stats.rs
Normal file
5
net-guardia/src/interface/port/drop_stats.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use crate::domain::data_plane::drop_event::DropCounters;
|
||||
|
||||
pub trait DropStatsPort: Send + Sync {
|
||||
fn get_counters(&self) -> DropCounters;
|
||||
}
|
||||
@ -7,12 +7,14 @@ pub mod audit;
|
||||
pub mod db_admin;
|
||||
pub mod dns_filter_api;
|
||||
pub mod dns_query_filter;
|
||||
pub mod drop_stats;
|
||||
pub mod enforcement;
|
||||
pub mod geo_block_api;
|
||||
pub mod geo_lookup;
|
||||
pub mod identity;
|
||||
pub mod notification;
|
||||
pub mod packet_sink;
|
||||
pub mod protocol_filter;
|
||||
pub mod rate_limit_api;
|
||||
pub mod secret_store;
|
||||
pub mod setting;
|
||||
|
||||
40
net-guardia/src/interface/port/protocol_filter.rs
Normal file
40
net-guardia/src/interface/port/protocol_filter.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use common::model::http_method::HttpMethod;
|
||||
|
||||
use crate::domain::common::error::Error;
|
||||
|
||||
pub trait ProtocolFilterPort: Send + Sync {
|
||||
fn get_ipv4_http_service(&self) -> HashMap<SocketAddrV4, Vec<HttpMethod>>;
|
||||
fn get_ipv6_http_service(&self) -> HashMap<SocketAddrV6, Vec<HttpMethod>>;
|
||||
fn add_ipv4_http_service(&self, address: SocketAddrV4, methods: Vec<HttpMethod>) -> Result<(), Error>;
|
||||
fn add_ipv6_http_service(&self, address: SocketAddrV6, methods: Vec<HttpMethod>) -> Result<(), Error>;
|
||||
fn remove_ipv4_http_service(&self, address: SocketAddrV4, methods: Vec<HttpMethod>) -> Result<(), Error>;
|
||||
fn remove_ipv6_http_service(&self, address: SocketAddrV6, methods: Vec<HttpMethod>) -> Result<(), Error>;
|
||||
|
||||
fn is_ssh_white_list_enable(&self) -> bool;
|
||||
fn enable_ssh_white_list(&self) -> Result<(), Error>;
|
||||
fn disable_ssh_white_list(&self) -> Result<(), Error>;
|
||||
|
||||
fn get_ipv4_ssh_service(&self) -> Vec<SocketAddrV4>;
|
||||
fn get_ipv6_ssh_service(&self) -> Vec<SocketAddrV6>;
|
||||
fn add_ipv4_ssh_service(&self, address: SocketAddrV4) -> Result<(), Error>;
|
||||
fn add_ipv6_ssh_service(&self, address: SocketAddrV6) -> Result<(), Error>;
|
||||
fn remove_ipv4_ssh_service(&self, address: SocketAddrV4) -> Result<(), Error>;
|
||||
fn remove_ipv6_ssh_service(&self, address: SocketAddrV6) -> Result<(), Error>;
|
||||
|
||||
fn get_ipv4_ssh_white_list(&self) -> Vec<Ipv4Addr>;
|
||||
fn get_ipv6_ssh_white_list(&self) -> Vec<Ipv6Addr>;
|
||||
fn add_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error>;
|
||||
fn add_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error>;
|
||||
fn remove_ipv4_ssh_white_list(&self, ip: Ipv4Addr) -> Result<(), Error>;
|
||||
fn remove_ipv6_ssh_white_list(&self, ip: Ipv6Addr) -> Result<(), Error>;
|
||||
|
||||
fn get_ipv4_ssh_black_list(&self) -> Vec<Ipv4Addr>;
|
||||
fn get_ipv6_ssh_black_list(&self) -> Vec<Ipv6Addr>;
|
||||
fn add_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error>;
|
||||
fn add_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error>;
|
||||
fn remove_ipv4_ssh_black_list(&self, ip: Ipv4Addr) -> Result<(), Error>;
|
||||
fn remove_ipv6_ssh_black_list(&self, ip: Ipv6Addr) -> Result<(), Error>;
|
||||
}
|
||||
@ -1,26 +1,7 @@
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::response::playbook_data::UpdatePlaybookInput;
|
||||
|
||||
/// Type alias for playbook+action JOIN rows.
|
||||
/// (id, name, enabled, trigger_event, threshold, count, window, cooldown, action_id, action_order, action_type, params)
|
||||
pub type PlaybookRow = (
|
||||
i64,
|
||||
String,
|
||||
bool,
|
||||
String,
|
||||
Option<f64>,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
i64,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
Option<String>,
|
||||
Option<String>,
|
||||
);
|
||||
|
||||
/// Type alias for SOAR execution log rows.
|
||||
/// (id, playbook_id, source_ip, trigger_event, actions_executed, executed_at)
|
||||
pub type SoarExecutionRow = (i64, i64, Option<String>, String, String, String);
|
||||
use crate::domain::response::playbook_data::{
|
||||
ActiveBlockView, ExecutionView, PendingUnblock, PlaybookView, UpdatePlaybookInput,
|
||||
};
|
||||
|
||||
/// Threat Response BC — SOAR aggregate repository.
|
||||
///
|
||||
@ -31,25 +12,21 @@ pub type SoarExecutionRow = (i64, i64, Option<String>, String, String, String);
|
||||
/// `DbAdminRepo::with_transaction` + `TxRepos`.
|
||||
pub trait SoarRepo: Send + Sync {
|
||||
// --- Playbooks ---
|
||||
fn list_playbooks_with_actions(&self) -> Result<Vec<PlaybookRow>, Error>;
|
||||
fn list_playbooks(&self) -> Result<Vec<PlaybookView>, Error>;
|
||||
fn update_playbook_enabled(&self, id: i64, enabled: bool) -> Result<bool, Error>;
|
||||
fn delete_playbook(&self, id: i64) -> Result<bool, Error>;
|
||||
fn seed_default_playbooks(&self) -> Result<(), Error>;
|
||||
|
||||
// --- Playbook Conditions ---
|
||||
/// Returns: (condition_id, playbook_id, condition_type, operator, value, value2)
|
||||
fn list_all_playbook_conditions(&self) -> Result<Vec<(i64, i64, String, String, String, Option<String>)>, Error>;
|
||||
|
||||
// --- Block Rules ---
|
||||
fn count_active_soar_blocks(&self) -> Result<u32, Error>;
|
||||
fn list_active_soar_blocks(&self) -> Result<Vec<(i64, String, i64, String)>, Error>;
|
||||
fn find_soar_block_by_id(&self, id: i64) -> Result<Option<(i64, String, i64, String)>, Error>;
|
||||
fn list_expired_soar_blocks(&self) -> Result<Vec<(i64, String, i64)>, Error>;
|
||||
fn list_active_soar_blocks(&self) -> Result<Vec<ActiveBlockView>, Error>;
|
||||
fn find_soar_block_by_id(&self, id: i64) -> Result<Option<ActiveBlockView>, Error>;
|
||||
fn list_expired_soar_blocks(&self) -> Result<Vec<ActiveBlockView>, Error>;
|
||||
fn mark_soar_block_unblocked(&self, id: i64) -> Result<(), Error>;
|
||||
|
||||
// --- Pending Unblock Recovery ---
|
||||
fn insert_pending_unblock(&self, source_ip: &str) -> Result<i64, Error>;
|
||||
fn list_pending_unblocks(&self) -> Result<Vec<(i64, String, i64)>, Error>;
|
||||
fn list_pending_unblocks(&self) -> Result<Vec<PendingUnblock>, Error>;
|
||||
fn delete_pending_unblock(&self, id: i64) -> Result<(), Error>;
|
||||
fn increment_pending_unblock_retry(&self, id: i64) -> Result<(), Error>;
|
||||
|
||||
@ -61,7 +38,7 @@ pub trait SoarRepo: Send + Sync {
|
||||
trigger_event: &str,
|
||||
actions_json: &str,
|
||||
) -> Result<i64, Error>;
|
||||
fn list_soar_executions(&self, limit: i64) -> Result<Vec<SoarExecutionRow>, Error>;
|
||||
fn list_soar_executions(&self, limit: i64) -> Result<Vec<ExecutionView>, Error>;
|
||||
|
||||
// --- Intra-aggregate atomic operations ---
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ use crate::domain::common::config::observability::ObservabilityConfig;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::system::SystemError;
|
||||
use crate::domain::common::log::system::SystemLog;
|
||||
use crate::domain::identity::auth::{DEFAULT_ADMIN_USERNAME, GROUP_ADMIN, ROLE_ADMIN};
|
||||
use crate::domain::identity::password;
|
||||
use crate::infrastructure::cli::{Cli, handle_subcommand};
|
||||
use crate::infrastructure::http_server;
|
||||
@ -37,10 +38,10 @@ fn seed_default_admin(database: &Arc<Database>) -> Result<(), Error> {
|
||||
if database.user_count().unwrap_or(0) != 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let hash = password::hash_password("admin")?;
|
||||
let admin_user_id = database.insert_user("admin", &hash, "admin", false)?;
|
||||
let hash = password::hash_password(DEFAULT_ADMIN_USERNAME)?;
|
||||
let admin_user_id = database.insert_user(DEFAULT_ADMIN_USERNAME, &hash, ROLE_ADMIN, false)?;
|
||||
if let Ok(groups) = database.list_user_groups()
|
||||
&& let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == "Administrator")
|
||||
&& let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == GROUP_ADMIN)
|
||||
&& let Err(err) = database.set_user_groups(admin_user_id, &[group_id])
|
||||
{
|
||||
log!(SystemError::SetUserGroupsFailed(err));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user