mirror of
https://github.com/DaLaw2/NetGuardia.git
synced 2026-08-24 14:10:28 +09:00
Compare commits
2 Commits
56b80e9d96
...
301d6ac1b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 301d6ac1b8 | |||
| 94d366bcbe |
517
macros/src/config.rs
Normal file
517
macros/src/config.rs
Normal file
@ -0,0 +1,517 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{Fields, Ident, ItemStruct, LitBool, LitStr, Result, Token, Type};
|
||||
|
||||
// ── Attribute parsing ──────────────────────────────────────────────
|
||||
|
||||
struct StructAttr {
|
||||
default_section: Option<String>,
|
||||
}
|
||||
|
||||
impl Parse for StructAttr {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let mut section = None;
|
||||
while !input.is_empty() {
|
||||
let key: Ident = input.parse()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
let val: LitStr = input.parse()?;
|
||||
if key == "section" {
|
||||
section = Some(val.value());
|
||||
}
|
||||
if !input.is_empty() {
|
||||
input.parse::<Token![,]>()?;
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
default_section: section,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Field model ────────────────────────────────────────────────────
|
||||
|
||||
enum ConfigField {
|
||||
Setting(SettingField),
|
||||
Flatten(FlattenField),
|
||||
MappedParent(MappedParent),
|
||||
}
|
||||
|
||||
struct SettingField {
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
key: String,
|
||||
default: String,
|
||||
default_debug: Option<String>,
|
||||
section: Option<String>,
|
||||
api: bool,
|
||||
}
|
||||
|
||||
struct FlattenField {
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
struct MappedSetting {
|
||||
key: String,
|
||||
default: String,
|
||||
default_debug: Option<String>,
|
||||
parent: String,
|
||||
sub_field: String,
|
||||
section: Option<String>,
|
||||
api: bool,
|
||||
}
|
||||
|
||||
struct MappedParent {
|
||||
ident: Ident,
|
||||
ty: Type,
|
||||
settings: Vec<MappedSetting>,
|
||||
}
|
||||
|
||||
// ── Parsing ────────────────────────────────────────────────────────
|
||||
|
||||
fn parse_struct_mapped_settings(input: &mut ItemStruct, default_section: &Option<String>) -> Vec<MappedSetting> {
|
||||
let mut mapped = Vec::new();
|
||||
input.attrs.retain(|attr| {
|
||||
if !attr.path().is_ident("setting") {
|
||||
return true;
|
||||
}
|
||||
let mut key = None;
|
||||
let mut default = None;
|
||||
let mut default_debug = None;
|
||||
let mut path = None;
|
||||
let mut section = None;
|
||||
let mut api = true;
|
||||
|
||||
let _ = attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("key") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
key = Some(val.value());
|
||||
} else if meta.path.is_ident("default") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default = Some(val.value());
|
||||
} else if meta.path.is_ident("default_debug") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default_debug = Some(val.value());
|
||||
} else if meta.path.is_ident("path") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
path = Some(val.value());
|
||||
} else if meta.path.is_ident("section") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
section = Some(val.value());
|
||||
} else if meta.path.is_ident("api") {
|
||||
let val: LitBool = meta.value()?.parse()?;
|
||||
api = val.value();
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if let (Some(key), Some(default), Some(path)) = (key, default, path) {
|
||||
let (parent, sub_field) = path
|
||||
.split_once('.')
|
||||
.expect("#[setting] `path` must be `parent.sub_field`");
|
||||
mapped.push(MappedSetting {
|
||||
key,
|
||||
default,
|
||||
default_debug,
|
||||
parent: parent.to_string(),
|
||||
sub_field: sub_field.to_string(),
|
||||
section: section.or_else(|| default_section.clone()),
|
||||
api,
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
mapped
|
||||
}
|
||||
|
||||
fn parse_field(field: &mut syn::Field, default_section: &Option<String>) -> Option<ConfigField> {
|
||||
let idx = field.attrs.iter().position(|a| a.path().is_ident("setting"))?;
|
||||
let attr = field.attrs.remove(idx);
|
||||
|
||||
let mut is_flatten = false;
|
||||
let mut key = None;
|
||||
let mut default = None;
|
||||
let mut default_debug = None;
|
||||
let mut section = None;
|
||||
let mut api = true;
|
||||
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("flatten") {
|
||||
is_flatten = true;
|
||||
} else if meta.path.is_ident("key") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
key = Some(val.value());
|
||||
} else if meta.path.is_ident("default") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default = Some(val.value());
|
||||
} else if meta.path.is_ident("default_debug") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
default_debug = Some(val.value());
|
||||
} else if meta.path.is_ident("section") {
|
||||
let val: LitStr = meta.value()?.parse()?;
|
||||
section = Some(val.value());
|
||||
} else if meta.path.is_ident("api") {
|
||||
let val: LitBool = meta.value()?.parse()?;
|
||||
api = val.value();
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or_else(|e| panic!("invalid #[setting]: {e}"));
|
||||
|
||||
let ident = field.ident.clone().expect("named field");
|
||||
let ty = field.ty.clone();
|
||||
|
||||
if is_flatten {
|
||||
return Some(ConfigField::Flatten(FlattenField { ident, ty }));
|
||||
}
|
||||
|
||||
Some(ConfigField::Setting(SettingField {
|
||||
ident,
|
||||
ty,
|
||||
key: key.expect("#[setting] requires `key`"),
|
||||
default: default.expect("#[setting] requires `default`"),
|
||||
default_debug,
|
||||
section: section.or_else(|| default_section.clone()),
|
||||
api,
|
||||
}))
|
||||
}
|
||||
|
||||
// ── Type detection ─────────────────────────────────────────────────
|
||||
|
||||
fn is_type(ty: &Type, name: &str) -> bool {
|
||||
matches!(ty, Type::Path(tp) if tp.path.is_ident(name))
|
||||
}
|
||||
|
||||
fn is_vec_string(ty: &Type) -> bool {
|
||||
if let Type::Path(tp) = ty
|
||||
&& let Some(seg) = tp.path.segments.last()
|
||||
{
|
||||
return seg.ident == "Vec";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// ── Code generation: defaults() ────────────────────────────────────
|
||||
|
||||
fn make_default_val(ty: &Type, default: &str) -> TokenStream2 {
|
||||
if is_type(ty, "String") {
|
||||
quote! { #default.to_string() }
|
||||
} else if is_type(ty, "bool") {
|
||||
let val = default == "true" || default == "1";
|
||||
quote! { #val }
|
||||
} else if is_vec_string(ty) {
|
||||
if default.is_empty() {
|
||||
quote! { Vec::new() }
|
||||
} else {
|
||||
let items: Vec<&str> = default.split(',').map(|v| v.trim()).collect();
|
||||
quote! { vec![#(#items.to_string()),*] }
|
||||
}
|
||||
} else {
|
||||
// SAFETY: literal default, validated by tests
|
||||
quote! { #default.parse().unwrap() }
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_default(f: &SettingField) -> TokenStream2 {
|
||||
let ident = &f.ident;
|
||||
let ty = &f.ty;
|
||||
|
||||
match &f.default_debug {
|
||||
Some(dbg) => {
|
||||
let release_val = make_default_val(ty, &f.default);
|
||||
let debug_val = make_default_val(ty, dbg);
|
||||
quote! { #ident: if cfg!(debug_assertions) { #debug_val } else { #release_val } }
|
||||
}
|
||||
None => {
|
||||
let val = make_default_val(ty, &f.default);
|
||||
quote! { #ident: #val }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_flatten_default(f: &FlattenField) -> TokenStream2 {
|
||||
let ident = &f.ident;
|
||||
let ty = &f.ty;
|
||||
quote! { #ident: #ty::defaults() }
|
||||
}
|
||||
|
||||
fn gen_mapped_default(mp: &MappedParent) -> TokenStream2 {
|
||||
let ident = &mp.ident;
|
||||
let ty = &mp.ty;
|
||||
let sub_fields: Vec<_> = mp
|
||||
.settings
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let sub = format_ident!("{}", s.sub_field);
|
||||
let val: TokenStream2 = match &s.default_debug {
|
||||
Some(dbg) => {
|
||||
let release = &s.default;
|
||||
// SAFETY: literal default, validated by tests
|
||||
quote! { if cfg!(debug_assertions) { #dbg.parse().unwrap() } else { #release.parse().unwrap() } }
|
||||
}
|
||||
None => {
|
||||
let default = &s.default;
|
||||
// SAFETY: literal default, validated by tests
|
||||
quote! { #default.parse().unwrap() }
|
||||
}
|
||||
};
|
||||
quote! { #sub: #val }
|
||||
})
|
||||
.collect();
|
||||
quote! { #ident: #ty { #(#sub_fields,)* } }
|
||||
}
|
||||
|
||||
// ── Code generation: from_settings() ───────────────────────────────
|
||||
|
||||
fn gen_override(f: &SettingField) -> TokenStream2 {
|
||||
let ident = &f.ident;
|
||||
let key = &f.key;
|
||||
let ty = &f.ty;
|
||||
|
||||
if is_type(ty, "String") {
|
||||
quote! {
|
||||
crate::domain::common::config::helpers::override_string_nonempty(
|
||||
&mut cfg.#ident, repo, #key,
|
||||
)?;
|
||||
}
|
||||
} else if is_type(ty, "bool") {
|
||||
quote! {
|
||||
crate::domain::common::config::helpers::override_bool(
|
||||
&mut cfg.#ident, repo, #key,
|
||||
)?;
|
||||
}
|
||||
} else if is_vec_string(ty) {
|
||||
quote! {
|
||||
crate::domain::common::config::helpers::override_csv(
|
||||
&mut cfg.#ident, repo, #key,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
crate::domain::common::config::helpers::override_parsed(
|
||||
&mut cfg.#ident, repo, #key,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_flatten_override(f: &FlattenField) -> TokenStream2 {
|
||||
let ident = &f.ident;
|
||||
let ty = &f.ty;
|
||||
quote! { cfg.#ident = #ty::from_settings(repo)?; }
|
||||
}
|
||||
|
||||
fn gen_mapped_overrides(mp: &MappedParent) -> TokenStream2 {
|
||||
let parent = &mp.ident;
|
||||
let calls: Vec<_> = mp
|
||||
.settings
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let sub = format_ident!("{}", s.sub_field);
|
||||
let key = &s.key;
|
||||
quote! {
|
||||
crate::domain::common::config::helpers::override_parsed(
|
||||
&mut cfg.#parent.#sub, repo, #key,
|
||||
)?;
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
quote! { #(#calls)* }
|
||||
}
|
||||
|
||||
// ── Code generation: seed_defaults() ───────────────────────────────
|
||||
|
||||
fn gen_seed(f: &SettingField) -> TokenStream2 {
|
||||
let key = &f.key;
|
||||
let default = &f.default;
|
||||
|
||||
match &f.default_debug {
|
||||
Some(dbg) => quote! {
|
||||
crate::domain::common::config::helpers::seed_key(
|
||||
repo, #key,
|
||||
if cfg!(debug_assertions) { #dbg } else { #default },
|
||||
)?;
|
||||
},
|
||||
None => quote! {
|
||||
crate::domain::common::config::helpers::seed_key(repo, #key, #default)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_flatten_seed(f: &FlattenField) -> TokenStream2 {
|
||||
let ty = &f.ty;
|
||||
quote! { #ty::seed_defaults(repo)?; }
|
||||
}
|
||||
|
||||
fn gen_mapped_seeds(mp: &MappedParent) -> TokenStream2 {
|
||||
let calls: Vec<_> = mp
|
||||
.settings
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let key = &s.key;
|
||||
let default = &s.default;
|
||||
match &s.default_debug {
|
||||
Some(dbg) => quote! {
|
||||
crate::domain::common::config::helpers::seed_key(
|
||||
repo, #key,
|
||||
if cfg!(debug_assertions) { #dbg } else { #default },
|
||||
)?;
|
||||
},
|
||||
None => quote! {
|
||||
crate::domain::common::config::helpers::seed_key(repo, #key, #default)?;
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
quote! { #(#calls)* }
|
||||
}
|
||||
|
||||
// ── Code generation: API_KEYS ──────────────────────────────────────
|
||||
|
||||
fn collect_api_keys(fields: &[ConfigField]) -> Vec<(&str, &str)> {
|
||||
let mut keys = Vec::new();
|
||||
for f in fields {
|
||||
match f {
|
||||
ConfigField::Setting(s) if s.api => {
|
||||
let sec = s.section.as_deref().unwrap_or("default");
|
||||
keys.push((sec, s.key.as_str()));
|
||||
}
|
||||
ConfigField::MappedParent(mp) => {
|
||||
for s in &mp.settings {
|
||||
if s.api {
|
||||
let sec = s.section.as_deref().unwrap_or("default");
|
||||
keys.push((sec, s.key.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn gen_keys_consts(fields: &[ConfigField]) -> TokenStream2 {
|
||||
let api_keys = collect_api_keys(fields);
|
||||
if api_keys.is_empty() {
|
||||
return quote! {};
|
||||
}
|
||||
|
||||
let mut sections: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
||||
for (sec, key) in &api_keys {
|
||||
sections.entry(sec).or_default().push(key);
|
||||
}
|
||||
|
||||
let single = sections.len() == 1;
|
||||
sections
|
||||
.iter()
|
||||
.map(|(section, keys)| {
|
||||
let name = if single {
|
||||
format_ident!("API_KEYS")
|
||||
} else {
|
||||
format_ident!("{}_KEYS", section.to_uppercase())
|
||||
};
|
||||
quote! { pub const #name: &[&str] = &[#(#keys),*]; }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Entry point ────────────────────────────────────────────────────
|
||||
|
||||
pub fn config_settings_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let struct_attr = syn::parse_macro_input!(attr as StructAttr);
|
||||
let mut input = syn::parse_macro_input!(item as ItemStruct);
|
||||
|
||||
let mapped_settings = parse_struct_mapped_settings(&mut input, &struct_attr.default_section);
|
||||
|
||||
let mut mapped_groups: BTreeMap<String, Vec<MappedSetting>> = BTreeMap::new();
|
||||
for ms in mapped_settings {
|
||||
mapped_groups.entry(ms.parent.clone()).or_default().push(ms);
|
||||
}
|
||||
|
||||
let fields = match &mut input.fields {
|
||||
Fields::Named(f) => f,
|
||||
_ => panic!("config_settings only supports named fields"),
|
||||
};
|
||||
|
||||
let mut config_fields = Vec::new();
|
||||
for field in &mut fields.named {
|
||||
let field_name = field.ident.as_ref().expect("named field").to_string();
|
||||
|
||||
if let Some(settings) = mapped_groups.remove(&field_name) {
|
||||
config_fields.push(ConfigField::MappedParent(MappedParent {
|
||||
ident: field.ident.clone().unwrap(),
|
||||
ty: field.ty.clone(),
|
||||
settings,
|
||||
}));
|
||||
} else if let Some(cf) = parse_field(field, &struct_attr.default_section) {
|
||||
config_fields.push(cf);
|
||||
}
|
||||
}
|
||||
|
||||
let struct_name = &input.ident;
|
||||
let keys_consts = gen_keys_consts(&config_fields);
|
||||
|
||||
let default_fields: Vec<_> = config_fields
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
ConfigField::Setting(s) => gen_default(s),
|
||||
ConfigField::Flatten(s) => gen_flatten_default(s),
|
||||
ConfigField::MappedParent(mp) => gen_mapped_default(mp),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let override_calls: Vec<_> = config_fields
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
ConfigField::Setting(s) => gen_override(s),
|
||||
ConfigField::Flatten(s) => gen_flatten_override(s),
|
||||
ConfigField::MappedParent(mp) => gen_mapped_overrides(mp),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let seed_calls: Vec<_> = config_fields
|
||||
.iter()
|
||||
.map(|f| match f {
|
||||
ConfigField::Setting(s) => gen_seed(s),
|
||||
ConfigField::Flatten(s) => gen_flatten_seed(s),
|
||||
ConfigField::MappedParent(mp) => gen_mapped_seeds(mp),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let expanded = quote! {
|
||||
#input
|
||||
|
||||
impl #struct_name {
|
||||
#keys_consts
|
||||
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
#(#default_fields,)*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(
|
||||
repo: &dyn crate::interface::port::setting::SettingRepo,
|
||||
) -> Result<Self, crate::domain::common::error::Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
#(#override_calls)*
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(
|
||||
repo: &dyn crate::interface::port::setting::SettingRepo,
|
||||
) -> Result<(), crate::domain::common::error::Error> {
|
||||
#(#seed_calls)*
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
mod config;
|
||||
mod error_enum;
|
||||
mod log;
|
||||
mod loggable;
|
||||
@ -5,6 +6,11 @@ mod traceable;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn config_settings(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
config::config_settings_impl(attr, item)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn log(input: TokenStream) -> TokenStream {
|
||||
log::log_impl(input)
|
||||
|
||||
@ -185,6 +185,8 @@ pub struct XskPair {
|
||||
drop_monitor: Option<Arc<DropMonitor>>,
|
||||
packet_buffer_size: usize,
|
||||
buffer_pool_capacity: usize,
|
||||
tx_packet_buf: Vec<Vec<u8>>,
|
||||
tx_frame_buf: Vec<FrameDesc>,
|
||||
}
|
||||
|
||||
impl XskPair {
|
||||
@ -256,6 +258,8 @@ impl XskPair {
|
||||
drop_monitor,
|
||||
packet_buffer_size: config.packet_buffer_size,
|
||||
buffer_pool_capacity: config.buffer_pool_capacity,
|
||||
tx_packet_buf: Vec::with_capacity(64),
|
||||
tx_frame_buf: Vec::with_capacity(64),
|
||||
};
|
||||
|
||||
Ok(xsk_pair)
|
||||
@ -312,13 +316,14 @@ impl XskPair {
|
||||
idle_count = 0;
|
||||
}
|
||||
|
||||
let sleep_us = match idle_count {
|
||||
0..=10 => 1,
|
||||
11..=100 => 10,
|
||||
_ => 100,
|
||||
};
|
||||
|
||||
thread::sleep(Duration::from_micros(sleep_us));
|
||||
if idle_count > 0 {
|
||||
let sleep_us = match idle_count {
|
||||
1..=10 => 1,
|
||||
11..=100 => 10,
|
||||
_ => 100,
|
||||
};
|
||||
thread::sleep(Duration::from_micros(sleep_us));
|
||||
}
|
||||
}
|
||||
|
||||
log!(EbpfLog::XSKShutdown);
|
||||
@ -421,15 +426,15 @@ impl XskPair {
|
||||
buffer_pool: &mut BufferPool,
|
||||
comp_descs: &mut [FrameDesc],
|
||||
) -> Result<usize, EbpfError> {
|
||||
let mut packets_to_send = Vec::with_capacity(64);
|
||||
self.tx_packet_buf.clear();
|
||||
while let Ok(packet) = forward_rx.try_recv() {
|
||||
packets_to_send.push(packet);
|
||||
if packets_to_send.len() >= 64 {
|
||||
self.tx_packet_buf.push(packet);
|
||||
if self.tx_packet_buf.len() >= 64 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if packets_to_send.is_empty() {
|
||||
if self.tx_packet_buf.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
@ -437,10 +442,10 @@ impl XskPair {
|
||||
log!(EbpfLog::CompQueueError(format!("{:?}", e)));
|
||||
}
|
||||
|
||||
let total_packets = packets_to_send.len();
|
||||
let total_packets = self.tx_packet_buf.len();
|
||||
|
||||
if self.frame_pool.is_empty() {
|
||||
for pkt in packets_to_send {
|
||||
for pkt in self.tx_packet_buf.drain(..) {
|
||||
buffer_pool.put(pkt);
|
||||
}
|
||||
log!(EbpfLog::FramePoolExhausted(total_packets));
|
||||
@ -448,17 +453,19 @@ impl XskPair {
|
||||
}
|
||||
|
||||
let available = self.frame_pool.len().min(total_packets);
|
||||
let mut frames: Vec<FrameDesc> = self.frame_pool.drain(self.frame_pool.len() - available..).collect();
|
||||
self.tx_frame_buf.clear();
|
||||
self.tx_frame_buf
|
||||
.extend(self.frame_pool.drain(self.frame_pool.len() - available..));
|
||||
|
||||
if frames.is_empty() {
|
||||
for pkt in packets_to_send {
|
||||
if self.tx_frame_buf.is_empty() {
|
||||
for pkt in self.tx_packet_buf.drain(..) {
|
||||
buffer_pool.put(pkt);
|
||||
}
|
||||
log!(EbpfLog::NoFramesAvailable);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
for (frame, packet) in frames.iter_mut().zip(packets_to_send.iter()) {
|
||||
for (frame, packet) in self.tx_frame_buf.iter_mut().zip(self.tx_packet_buf.iter()) {
|
||||
unsafe {
|
||||
self.umem
|
||||
.data_mut(frame)
|
||||
@ -468,11 +475,11 @@ impl XskPair {
|
||||
}
|
||||
}
|
||||
|
||||
let nb_submitted = unsafe { self.tx.produce(&frames) };
|
||||
let nb_submitted = unsafe { self.tx.produce(&self.tx_frame_buf) };
|
||||
|
||||
// Return unsubmitted frames to pool to prevent frame leak
|
||||
if nb_submitted < frames.len() {
|
||||
for frame in frames[nb_submitted..].iter() {
|
||||
if nb_submitted < self.tx_frame_buf.len() {
|
||||
for frame in self.tx_frame_buf[nb_submitted..].iter() {
|
||||
self.frame_pool.push(*frame);
|
||||
}
|
||||
}
|
||||
@ -494,7 +501,7 @@ impl XskPair {
|
||||
}
|
||||
|
||||
// Return all buffers to pool
|
||||
for pkt in packets_to_send {
|
||||
for pkt in self.tx_packet_buf.drain(..) {
|
||||
buffer_pool.put(pkt);
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,8 @@ use actix_files::NamedFile;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, Scope, web};
|
||||
|
||||
use crate::core::inference::engine::Engine;
|
||||
use crate::core::inference::traffic_logger::{FLOW_TRACE_FILE_EXT, FLOW_TRACE_FILE_MARKER, list_flow_trace_files};
|
||||
use crate::core::inference::traffic_logger::list_flow_trace_files;
|
||||
use crate::domain::common::config::constants::{FLOW_TRACE_FILE_EXT, FLOW_TRACE_FILE_MARKER};
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/flow-trace")
|
||||
|
||||
@ -13,15 +13,10 @@ use actix_web::{HttpRequest, HttpResponse, Responder, Scope, web};
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::config::constants::FUSION_AUDIT_ACTION;
|
||||
use crate::domain::detection::metrics::FusionMetrics;
|
||||
use crate::interface::port::audit::{AuditLogEntry, AuditRepo};
|
||||
|
||||
/// Stable audit action string the fusion engine emits — kept in sync
|
||||
/// with `core::detection::orchestrator::FUSION_AUDIT_ACTION`. If that
|
||||
/// constant changes, the explain endpoint silently returns nothing, so
|
||||
/// keep this updated at the same time.
|
||||
const FUSION_AUDIT_ACTION: &str = "fused_threat_emitted";
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/fusion")
|
||||
.route("/metrics", web::get().to(get_metrics))
|
||||
|
||||
@ -8,7 +8,7 @@ use actix_web::{HttpResponse, Scope, web};
|
||||
use arc_swap::ArcSwap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::common::observability::log_buffer::{self, LogEntry};
|
||||
use crate::core::common::log_buffer::{self, LogBuffer, LogEntry};
|
||||
use crate::domain::common::config::AppConfig;
|
||||
|
||||
/// Hardcoded log directory — not configurable via API to prevent directory traversal.
|
||||
@ -49,7 +49,11 @@ struct LiveResponse {
|
||||
dropped_oldest: bool,
|
||||
}
|
||||
|
||||
async fn live_logs(query: web::Query<LiveQuery>, app_config: web::Data<Arc<ArcSwap<AppConfig>>>) -> HttpResponse {
|
||||
async fn live_logs(
|
||||
query: web::Query<LiveQuery>,
|
||||
app_config: web::Data<Arc<ArcSwap<AppConfig>>>,
|
||||
buf: web::Data<LogBuffer>,
|
||||
) -> HttpResponse {
|
||||
let since_id = query.since_id.unwrap_or(0);
|
||||
let obs = app_config.load().observability.clone();
|
||||
let limit = query
|
||||
@ -62,7 +66,7 @@ async fn live_logs(query: web::Query<LiveQuery>, app_config: web::Data<Arc<ArcSw
|
||||
.map(|s| log_buffer::level_severity(&s.to_ascii_uppercase()))
|
||||
.unwrap_or(log_buffer::level_severity("TRACE"));
|
||||
|
||||
let snap = log_buffer::snapshot(since_id, min_severity, limit);
|
||||
let snap = buf.snapshot(since_id, min_severity, limit);
|
||||
// Signal to the UI that it lagged enough for the ring to evict rows
|
||||
// between polls. Frontend can warn "older entries dropped" without
|
||||
// silently skipping a gap.
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
use actix_web::{HttpResponse, Responder, Scope, web};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::adapter::http::AUDIT_ACTOR_SECURITY_ADMIN_PREFIX;
|
||||
use crate::core::identity::extractor::AuthClaims;
|
||||
use crate::core::inference::engine::Engine;
|
||||
use crate::core::inference::inference::Inference;
|
||||
use crate::domain::common::config::constants::AUDIT_ACTOR_SECURITY_ADMIN_PREFIX;
|
||||
use crate::domain::common::event::AuditEvent;
|
||||
use crate::domain::detection::model_adapter::ModelSourceState;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
|
||||
/// Permission required to forcibly revert the active ML source to dormant.
|
||||
/// Mirrors the upload handler's gate so swap-out and revert are symmetric:
|
||||
@ -65,7 +65,7 @@ async fn get_current_model(inference: web::Data<Inference>) -> impl Responder {
|
||||
/// the upload path's `model_swap` so both swap-in and revert are auditable.
|
||||
async fn delete_current_model(
|
||||
inference: web::Data<Inference>,
|
||||
comm: web::Data<CommunicationManager>,
|
||||
audit_tx: web::Data<broadcast::Sender<AuditEvent>>,
|
||||
claims: AuthClaims,
|
||||
) -> impl Responder {
|
||||
if !claims.permissions.iter().any(|p| p == DORMANT_REQUIRED_PERMISSION) {
|
||||
@ -87,13 +87,11 @@ async fn delete_current_model(
|
||||
"before": serde_json::to_value(&before_status).unwrap_or(serde_json::Value::Null),
|
||||
})
|
||||
.to_string();
|
||||
let _ = comm
|
||||
.publish_event(AuditEvent {
|
||||
actor: format!("{AUDIT_ACTOR_SECURITY_ADMIN_PREFIX}@{}", claims.username),
|
||||
action: AUDIT_ACTION_MODEL_DORMANT.to_string(),
|
||||
detail: audit_detail,
|
||||
})
|
||||
.await;
|
||||
let _ = audit_tx.send(AuditEvent {
|
||||
actor: format!("{AUDIT_ACTOR_SECURITY_ADMIN_PREFIX}@{}", claims.username),
|
||||
action: AUDIT_ACTION_MODEL_DORMANT.to_string(),
|
||||
detail: audit_detail,
|
||||
});
|
||||
|
||||
HttpResponse::Ok().json(serde_json::json!({
|
||||
"already_dormant": false,
|
||||
|
||||
@ -19,8 +19,3 @@ pub mod setup;
|
||||
pub mod soar;
|
||||
pub mod stats;
|
||||
pub mod system;
|
||||
|
||||
/// Shared audit actor prefix for security-admin triggered events. Stable
|
||||
/// wire string — WORM consumers filter on `SecurityAdmin@<username>` so
|
||||
/// do not rename without coordinating audit-chain readers.
|
||||
pub const AUDIT_ACTOR_SECURITY_ADMIN_PREFIX: &str = "SecurityAdmin";
|
||||
|
||||
@ -31,6 +31,7 @@ use serde_json::Value as JsonValue;
|
||||
use sha2::{Digest, Sha256};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -38,11 +39,12 @@ use crate::core::identity::extractor::AuthClaims;
|
||||
use crate::core::inference::inference::Inference;
|
||||
use crate::core::inference::model_loader::build_adapter;
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::config::constants::{MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR};
|
||||
use crate::domain::common::config::constants::{
|
||||
AUDIT_ACTOR_SECURITY_ADMIN_PREFIX, MANIFEST_FILENAME, MODELS_DIR, STAGING_SUBDIR,
|
||||
};
|
||||
use crate::domain::common::event::AuditEvent;
|
||||
use crate::domain::detection::manifest::{AdapterKind, ModelManifest};
|
||||
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
|
||||
/// Multipart field names the client must use. Stable wire contract —
|
||||
/// the frontend form generator depends on these exact strings.
|
||||
@ -62,14 +64,6 @@ const ONNX_SNIFF_BYTES: usize = 16;
|
||||
/// swap the ML source.
|
||||
const PROMOTE_REQUIRED_PERMISSION: &str = "users:admin";
|
||||
|
||||
/// Actor prefix on the WORM `model_swap` audit entry. Administrators
|
||||
/// that trigger the upload endpoint land on the chain as
|
||||
/// `SecurityAdmin@<username>` so downstream filters can separate
|
||||
/// system-driven entries (actor="system") from human-driven ones
|
||||
/// without parsing free-form text. Stable across releases — renaming
|
||||
/// breaks downstream audit tooling that filters on this prefix.
|
||||
use crate::adapter::http::AUDIT_ACTOR_SECURITY_ADMIN_PREFIX;
|
||||
|
||||
/// Action recorded on the WORM chain when a promote succeeds. Stable
|
||||
/// wire string — fusion-explain tooling and future "who swapped the
|
||||
/// model" views filter on it, so the rename must go through the audit
|
||||
@ -139,7 +133,7 @@ pub fn initialize() -> Scope {
|
||||
async fn upload(
|
||||
app_config: web::Data<ArcSwap<AppConfig>>,
|
||||
inference: web::Data<Inference>,
|
||||
comm: web::Data<CommunicationManager>,
|
||||
audit_tx: web::Data<broadcast::Sender<AuditEvent>>,
|
||||
promote_lock: web::Data<PromoteGate>,
|
||||
claims: AuthClaims,
|
||||
payload: Multipart,
|
||||
@ -174,7 +168,7 @@ async fn upload(
|
||||
&staging_dir,
|
||||
&summary,
|
||||
inference.get_ref(),
|
||||
comm.get_ref(),
|
||||
audit_tx.get_ref(),
|
||||
promote_lock.get_ref(),
|
||||
&claims.username,
|
||||
batch_size,
|
||||
@ -522,7 +516,7 @@ async fn validate_and_promote(
|
||||
staging_dir: &Path,
|
||||
summary: &UploadSummary,
|
||||
inference: &Inference,
|
||||
comm: &CommunicationManager,
|
||||
audit_tx: &broadcast::Sender<AuditEvent>,
|
||||
promote_lock: &PromoteGate,
|
||||
actor_username: &str,
|
||||
batch_size: usize,
|
||||
@ -611,13 +605,11 @@ async fn validate_and_promote(
|
||||
"before": serde_json::to_value(&before_status).unwrap_or(JsonValue::Null),
|
||||
})
|
||||
.to_string();
|
||||
let _ = comm
|
||||
.publish_event(AuditEvent {
|
||||
actor: format!("{AUDIT_ACTOR_SECURITY_ADMIN_PREFIX}@{actor_username}"),
|
||||
action: AUDIT_ACTION_MODEL_SWAP.to_string(),
|
||||
detail: audit_detail,
|
||||
})
|
||||
.await;
|
||||
let _ = audit_tx.send(AuditEvent {
|
||||
actor: format!("{AUDIT_ACTOR_SECURITY_ADMIN_PREFIX}@{actor_username}"),
|
||||
action: AUDIT_ACTION_MODEL_SWAP.to_string(),
|
||||
detail: audit_detail,
|
||||
});
|
||||
|
||||
Ok(PromoteReport {
|
||||
manifest_name: manifest.name,
|
||||
|
||||
@ -3,13 +3,11 @@ use serde::Deserialize;
|
||||
|
||||
use crate::core::common::config_service::ConfigService;
|
||||
use crate::core::identity::extractor::AuthClaims;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::infrastructure::enforce_mode_handler::EnforceModeHandler;
|
||||
use crate::infrastructure::logger::Logger;
|
||||
use crate::infrastructure::system::{ShutdownHandle, ShutdownMode};
|
||||
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
|
||||
use crate::interface::communication::query_types::GetEnforceModeQuery;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
use crate::utils::boot_time;
|
||||
use crate::utils::logging::Logging;
|
||||
|
||||
type Repo = dyn AppRepo;
|
||||
|
||||
@ -36,8 +34,8 @@ async fn get_boot_time() -> impl Responder {
|
||||
HttpResponse::Ok().json(boot_time::boot_time())
|
||||
}
|
||||
|
||||
async fn get_enforce_mode(comm: web::Data<CommunicationManager>) -> impl Responder {
|
||||
match comm.send_query(GetEnforceModeQuery).await {
|
||||
async fn get_enforce_mode(handler: web::Data<EnforceModeHandler>) -> impl Responder {
|
||||
match handler.get_mode() {
|
||||
Ok(mode) => HttpResponse::Ok().json(serde_json::json!({"mode": mode})),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
@ -45,7 +43,7 @@ async fn get_enforce_mode(comm: web::Data<CommunicationManager>) -> impl Respond
|
||||
|
||||
async fn set_enforce_mode(
|
||||
body: web::Json<EnforceModeRequest>,
|
||||
comm: web::Data<CommunicationManager>,
|
||||
handler: web::Data<EnforceModeHandler>,
|
||||
) -> impl Responder {
|
||||
let mode = &body.mode;
|
||||
if mode != "monitor" && mode != "ml_only" && mode != "enforce" {
|
||||
@ -53,7 +51,7 @@ async fn set_enforce_mode(
|
||||
.json(serde_json::json!({"error": "Mode must be 'monitor', 'ml_only', or 'enforce'"}));
|
||||
}
|
||||
|
||||
match comm.send_command(ChangeEnforceModeCommand { mode: mode.clone() }).await {
|
||||
match handler.change_mode(mode.clone()) {
|
||||
Ok(_) => HttpResponse::Ok().json(serde_json::json!({"mode": mode})),
|
||||
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})),
|
||||
}
|
||||
@ -81,9 +79,9 @@ async fn get_config(svc: web::Data<ConfigService>) -> impl Responder {
|
||||
HttpResponse::Ok().json(svc.get_config())
|
||||
}
|
||||
|
||||
async fn get_log_level() -> impl Responder {
|
||||
async fn get_log_level(logging: web::Data<Logger>) -> impl Responder {
|
||||
HttpResponse::Ok().json(serde_json::json!({
|
||||
"level": Logging::current_level(),
|
||||
"level": logging.current_level(),
|
||||
}))
|
||||
}
|
||||
|
||||
@ -92,8 +90,8 @@ struct LogLevelRequest {
|
||||
level: String,
|
||||
}
|
||||
|
||||
async fn set_log_level(body: web::Json<LogLevelRequest>) -> impl Responder {
|
||||
match Logging::set_level(&body.level) {
|
||||
async fn set_log_level(body: web::Json<LogLevelRequest>, logging: web::Data<Logger>) -> impl Responder {
|
||||
match logging.set_level(&body.level) {
|
||||
Ok(new_level) => HttpResponse::Ok().json(serde_json::json!({
|
||||
"level": new_level,
|
||||
"message": "Log level updated",
|
||||
|
||||
@ -59,7 +59,13 @@ impl Database {
|
||||
pub fn has_manual_acl_rule(&self, ip_address: &str) -> Result<bool, Error> {
|
||||
let conn = self.conn()?;
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM acl_rules WHERE ip_address = ?1 AND list_type = 'blacklist'",
|
||||
"SELECT COUNT(*) FROM acl_rules
|
||||
WHERE ip_address = ?1 AND list_type = 'blacklist'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM soar_block_rules
|
||||
WHERE soar_block_rules.source_ip = acl_rules.ip_address
|
||||
AND soar_block_rules.unblocked_at IS NULL
|
||||
)",
|
||||
params![ip_address],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
@ -31,7 +31,7 @@ impl Database {
|
||||
pub fn list_expired_soar_blocks(&self) -> Result<Vec<(i64, String, i64)>, 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 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)?))
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
//! WebSocket bridge for post-fusion threat events.
|
||||
//!
|
||||
//! `/ws/fusion` subscribes to the `ThreatDetectedEvent` broadcast that the
|
||||
//! `DetectionOrchestrator` already publishes through `CommunicationManager`
|
||||
//! (the same stream SOAR consumes). Each event is wrapped with a server-side
|
||||
//! `/ws/fusion` subscribes to the `ThreatDetectedEvent` broadcast channel
|
||||
//! that the `DetectionOrchestrator` publishes to (the same stream SOAR
|
||||
//! consumes). Each event is wrapped with a server-side
|
||||
//! `ts` (unix seconds) so the dashboard can render relative timestamps
|
||||
//! without doing the conversion itself.
|
||||
//!
|
||||
@ -26,24 +26,15 @@ use crate::domain::common::error::http::HttpError;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::event::ThreatDetectedEvent;
|
||||
use crate::domain::common::log::http::HttpLog;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
|
||||
pub async fn websocket_fusion(
|
||||
req: HttpRequest,
|
||||
body: web::Payload,
|
||||
comm: web::Data<CommunicationManager>,
|
||||
threat_tx: web::Data<broadcast::Sender<ThreatDetectedEvent>>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
let broadcast_rx = match comm.subscribe_event::<ThreatDetectedEvent>() {
|
||||
Ok(rx) => rx,
|
||||
Err(e) => {
|
||||
log!(HttpLog::FusionSubscribeFailed(e.to_string()));
|
||||
return Ok(HttpResponse::InternalServerError().json(serde_json::json!({
|
||||
"error": "fusion event channel not registered",
|
||||
})));
|
||||
}
|
||||
};
|
||||
let broadcast_rx = threat_tx.subscribe();
|
||||
|
||||
spawn(async move {
|
||||
handle_fusion_connection(session, msg_stream, broadcast_rx).await;
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, Scope, web};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::{alert_websocket, drop_websocket, flow_websocket, fusion_websocket, health_websocket};
|
||||
use crate::adapter::ebpf::drop_monitor::DropMonitor;
|
||||
use crate::core::identity::jwt::JwtService;
|
||||
use crate::core::inference::alert::MLAlert;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::domain::common::event::ThreatDetectedEvent;
|
||||
use crate::infrastructure::health::SystemHealth;
|
||||
use crate::infrastructure::statistics::FlowStatistics;
|
||||
|
||||
@ -41,10 +42,7 @@ fn validate_ws_token(
|
||||
Some(ref t) => jwt
|
||||
.validate_token(t)
|
||||
.map(|_| ())
|
||||
.map_err(|_| {
|
||||
HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Invalid or expired token"}))
|
||||
}),
|
||||
.map_err(|_| HttpResponse::Unauthorized().json(serde_json::json!({"error": "Invalid or expired token"}))),
|
||||
None => Err(HttpResponse::Unauthorized()
|
||||
.json(serde_json::json!({"error": "Missing authentication: provide Authorization header or token query parameter"}))),
|
||||
}
|
||||
@ -89,14 +87,14 @@ async fn alerts_ws(
|
||||
async fn fusion_ws(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
comm: web::Data<CommunicationManager>,
|
||||
threat_tx: web::Data<broadcast::Sender<ThreatDetectedEvent>>,
|
||||
query: web::Query<WsQuery>,
|
||||
jwt: web::Data<JwtService>,
|
||||
) -> impl Responder {
|
||||
if let Err(resp) = validate_ws_token(&req, &query, &jwt) {
|
||||
return resp;
|
||||
}
|
||||
match fusion_websocket::websocket_fusion(req, stream, comm).await {
|
||||
match fusion_websocket::websocket_fusion(req, stream, threat_tx).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": format!("WebSocket error: {}", err)}))
|
||||
|
||||
@ -3,192 +3,16 @@ use std::sync::Arc;
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::config::section::ConfigSection;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
use crate::interface::port::secret_store::SecretStorePort;
|
||||
|
||||
/// Keys that must be routed through SecretStore instead of plaintext settings.
|
||||
const SECRET_KEYS: &[&str] = &["smtp_password"];
|
||||
|
||||
/// Valid eBPF pipeline stage names.
|
||||
const VALID_PIPELINE_STAGES: &[&str] = &["access_control", "rate_limit", "service"];
|
||||
|
||||
/// All configurable settings grouped by section.
|
||||
const SETTINGS_MAP: &[(&str, &[&str])] = &[
|
||||
(
|
||||
"network",
|
||||
&["ingress_interface", "egress_interface", "refresh_interval"],
|
||||
),
|
||||
("http", &["http_port", "jwt_expiry_hours", "force_https"]),
|
||||
(
|
||||
"inference",
|
||||
&[
|
||||
"max_concurrent_flows",
|
||||
"min_packets_for_inference",
|
||||
"inference_interval_secs",
|
||||
"aggregator_window_secs",
|
||||
"inference_batch_size",
|
||||
"traffic_logging_mode",
|
||||
"traffic_log_csv_path",
|
||||
],
|
||||
),
|
||||
(
|
||||
"xdp",
|
||||
&[
|
||||
"combined_queue_count",
|
||||
"channel_size",
|
||||
"fill_queue_size",
|
||||
"comp_queue_size",
|
||||
"tx_queue_size",
|
||||
"rx_queue_size",
|
||||
"frame_size",
|
||||
"frame_count",
|
||||
"packet_buffer_size",
|
||||
"buffer_pool_capacity",
|
||||
],
|
||||
),
|
||||
(
|
||||
"models",
|
||||
&["deep_autoencoder_name", "classifier_name", "models_config_name"],
|
||||
),
|
||||
// report_dir and log_dir intentionally NOT configurable via API to prevent
|
||||
// arbitrary directory write/read. They use hardcoded safe defaults.
|
||||
("misc", &["geoip_db_name"]),
|
||||
(
|
||||
"soar",
|
||||
&[
|
||||
"soar_max_auto_block_cap",
|
||||
"soar_max_ttl_secs",
|
||||
"soar_handle_concurrency",
|
||||
"soar_max_pending_unblock_retries",
|
||||
"soar_default_block_ttl_secs",
|
||||
"soar_default_rate_limit_factor",
|
||||
"soar_default_rate_limit_ttl_secs",
|
||||
"soar_default_webhook_timeout_secs",
|
||||
"soar_default_frequency_window_secs",
|
||||
"soar_default_single_source_high_min_confidence",
|
||||
"soar_default_cooldown_expiry_secs",
|
||||
"soar_rate_limit_cmd_channel_capacity",
|
||||
"soar_frequency_max_tracked_keys",
|
||||
"soar_fallback_cooldown_secs",
|
||||
],
|
||||
),
|
||||
(
|
||||
"ml",
|
||||
&[
|
||||
"ml_drift_window_secs",
|
||||
"ml_min_packets_floor",
|
||||
"ml_confirmation_window_fraction",
|
||||
"ml_drift_max_snapshots",
|
||||
"ml_drift_channel_capacity",
|
||||
"ml_alert_channel_capacity",
|
||||
"ml_circuit_breaker_threshold",
|
||||
"ml_circuit_breaker_window_secs",
|
||||
"ml_circuit_breaker_cooldown_secs",
|
||||
"ml_onnx_load_timeout_secs",
|
||||
"ml_model_watcher_debounce_secs",
|
||||
"ml_flow_max_packets_per_direction",
|
||||
"ml_flow_max_periods",
|
||||
"ml_flow_idle_threshold_us",
|
||||
"ml_flow_bulk_min_packets",
|
||||
"ml_flow_bulk_min_bytes",
|
||||
"ml_flow_idle_timeout_us",
|
||||
"ml_flow_terminated_timeout_us",
|
||||
],
|
||||
),
|
||||
(
|
||||
"flow_trace",
|
||||
&[
|
||||
"flow_trace_max_file_bytes",
|
||||
"flow_trace_max_file_age_secs",
|
||||
"flow_trace_total_budget_bytes",
|
||||
"traffic_logger_channel_capacity",
|
||||
],
|
||||
),
|
||||
(
|
||||
"model_upload",
|
||||
&[
|
||||
"model_upload_max_onnx_bytes",
|
||||
"model_upload_max_manifest_bytes",
|
||||
"model_upload_max_scaler_bytes",
|
||||
],
|
||||
),
|
||||
(
|
||||
"telegram",
|
||||
&[
|
||||
"telegram_rate_limit_max_messages",
|
||||
"telegram_rate_limit_window_secs",
|
||||
"telegram_max_retries",
|
||||
],
|
||||
),
|
||||
("dns", &["dns_max_domains_per_request"]),
|
||||
("smtp", &["smtp_host", "smtp_port", "smtp_username", "smtp_recipient"]),
|
||||
(
|
||||
"suricata",
|
||||
&[
|
||||
"suricata_enabled",
|
||||
"suricata_binary_path",
|
||||
"suricata_config_path",
|
||||
"suricata_eve_log_path",
|
||||
"suricata_auto_restart_on_crash",
|
||||
"suricata_restart_backoff_secs",
|
||||
"suricata_poll_interval_ms",
|
||||
"suricata_file_wait_interval_secs",
|
||||
"suricata_confidence_high",
|
||||
"suricata_confidence_medium",
|
||||
"suricata_confidence_low",
|
||||
"suricata_confidence_info",
|
||||
],
|
||||
),
|
||||
("detection", &["detection_cleanup_interval_secs"]),
|
||||
(
|
||||
"fusion",
|
||||
&[
|
||||
"fusion_dedup_window_secs",
|
||||
"fusion_repeat_offender_window_secs",
|
||||
"fusion_max_dedup_entries",
|
||||
],
|
||||
),
|
||||
(
|
||||
"beaconing",
|
||||
&[
|
||||
"beaconing_analysis_interval_secs",
|
||||
"beaconing_min_observations",
|
||||
"beaconing_cv_threshold",
|
||||
"beaconing_max_cache_entries",
|
||||
"beaconing_expiry_secs",
|
||||
"beaconing_alert_cooldown_secs",
|
||||
],
|
||||
),
|
||||
(
|
||||
"correlation",
|
||||
&[
|
||||
"correlation_scan_window_secs",
|
||||
"correlation_scan_threshold",
|
||||
"correlation_lateral_window_secs",
|
||||
"correlation_lateral_threshold",
|
||||
"correlation_botnet_window_secs",
|
||||
"correlation_botnet_threshold",
|
||||
"correlation_max_tracked_entries",
|
||||
],
|
||||
),
|
||||
(
|
||||
"observability",
|
||||
&[
|
||||
"log_buffer_capacity",
|
||||
"log_buffer_max_message_bytes",
|
||||
"log_max_download_size",
|
||||
"log_live_default_limit",
|
||||
"log_live_max_limit",
|
||||
"fusion_explain_scan_limit",
|
||||
"fusion_explain_response_cap",
|
||||
"default_event_channel_capacity",
|
||||
"drop_channel_capacity",
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
pub struct ConfigService {
|
||||
db: Arc<dyn AppRepo>,
|
||||
secrets: Option<Arc<dyn SecretStorePort>>,
|
||||
@ -213,12 +37,12 @@ impl ConfigService {
|
||||
let get = |key: &str| -> String { self.db.get_setting(key).ok().flatten().unwrap_or_default() };
|
||||
|
||||
let mut root = serde_json::Map::new();
|
||||
for (section, keys) in SETTINGS_MAP {
|
||||
for section in ConfigSection::ALL {
|
||||
let mut section_obj = serde_json::Map::new();
|
||||
for key in *keys {
|
||||
for key in section.keys() {
|
||||
section_obj.insert(key.to_string(), serde_json::Value::String(get(key)));
|
||||
}
|
||||
root.insert(section.to_string(), serde_json::Value::Object(section_obj));
|
||||
root.insert(section.name().to_string(), serde_json::Value::Object(section_obj));
|
||||
}
|
||||
root.insert(
|
||||
"pipeline".to_string(),
|
||||
@ -235,9 +59,9 @@ impl ConfigService {
|
||||
let mut new_cfg: Option<AppConfig> = None;
|
||||
|
||||
self.db.transaction(&mut |repo| {
|
||||
for (section, keys) in SETTINGS_MAP {
|
||||
if let Some(section_obj) = body.get(section).and_then(|v| v.as_object()) {
|
||||
for key in *keys {
|
||||
for section in ConfigSection::ALL {
|
||||
if let Some(section_obj) = body.get(section.name()).and_then(|v| v.as_object()) {
|
||||
for key in section.keys() {
|
||||
if let Some(val) = section_obj.get(*key).and_then(json_value_as_string) {
|
||||
repo.set_setting(key, &val)?;
|
||||
updated.push(key.to_string());
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::{Arguments, Debug, Write as _};
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@ -11,12 +11,6 @@ use tracing::{Event, Level, Subscriber};
|
||||
use tracing_subscriber::Layer;
|
||||
use tracing_subscriber::layer::Context;
|
||||
|
||||
/// Monotonic id allocator. Clients use `since_id` to resume tailing.
|
||||
/// u64 never wraps in practice (2^64 events at 1 µs/event ≈ 584 000 years).
|
||||
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
static BUFFER: OnceLock<LogRingBuffer> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct LogEntry {
|
||||
pub id: u64,
|
||||
@ -39,6 +33,7 @@ struct LogRingBuffer {
|
||||
entries: Mutex<VecDeque<LogEntry>>,
|
||||
capacity: usize,
|
||||
max_message_bytes: usize,
|
||||
next_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl LogRingBuffer {
|
||||
@ -48,6 +43,7 @@ impl LogRingBuffer {
|
||||
entries: Mutex::new(VecDeque::with_capacity(cap)),
|
||||
capacity: cap,
|
||||
max_message_bytes: max_message_bytes.max(64),
|
||||
next_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,20 +79,14 @@ pub struct Snapshot {
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
/// Get a snapshot for the `/api/logs/live` endpoint.
|
||||
///
|
||||
/// `min_severity` follows tracing level numeric ordering (ERROR=1…TRACE=5);
|
||||
/// an entry at level L is included when `level_severity(L) <= min_severity`.
|
||||
/// Returns an empty snapshot when the buffer has not been installed yet
|
||||
/// (tests, dry-runs).
|
||||
pub fn snapshot(since_id: u64, min_severity: u8, limit: usize) -> Snapshot {
|
||||
match BUFFER.get() {
|
||||
Some(buf) => buf.snapshot(since_id, min_severity, limit),
|
||||
None => Snapshot {
|
||||
entries: Vec::new(),
|
||||
latest_id: 0,
|
||||
total: 0,
|
||||
},
|
||||
/// Handle for reading from the log ring buffer.
|
||||
pub struct LogBuffer {
|
||||
inner: Arc<LogRingBuffer>,
|
||||
}
|
||||
|
||||
impl LogBuffer {
|
||||
pub fn snapshot(&self, since_id: u64, min_severity: u8, limit: usize) -> Snapshot {
|
||||
self.inner.snapshot(since_id, min_severity, limit)
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,36 +121,36 @@ fn now_unix_ms() -> u64 {
|
||||
|
||||
/// [`Layer`] that appends each formatted event into the in-memory ring
|
||||
/// buffer so the UI can tail logs without round-tripping the filesystem.
|
||||
pub struct LogBufferLayer;
|
||||
pub struct LogBufferLayer {
|
||||
inner: Arc<LogRingBuffer>,
|
||||
}
|
||||
|
||||
impl LogBufferLayer {
|
||||
pub fn new(capacity: usize, max_message_bytes: usize) -> Self {
|
||||
let _ = BUFFER.set(LogRingBuffer::new(capacity, max_message_bytes));
|
||||
Self
|
||||
pub fn new(capacity: usize, max_message_bytes: usize) -> (Self, LogBuffer) {
|
||||
let inner = Arc::new(LogRingBuffer::new(capacity, max_message_bytes));
|
||||
let handle = LogBuffer { inner: inner.clone() };
|
||||
(Self { inner }, handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Subscriber> Layer<S> for LogBufferLayer {
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
let Some(buf) = BUFFER.get() else {
|
||||
return;
|
||||
};
|
||||
let metadata = event.metadata();
|
||||
let mut visitor = MessageVisitor::default();
|
||||
event.record(&mut visitor);
|
||||
let mut message = visitor.into_message();
|
||||
if message.len() > buf.max_message_bytes {
|
||||
message.truncate(buf.max_message_bytes);
|
||||
if message.len() > self.inner.max_message_bytes {
|
||||
message.truncate(self.inner.max_message_bytes);
|
||||
message.push_str("…[truncated]");
|
||||
}
|
||||
let entry = LogEntry {
|
||||
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
|
||||
id: self.inner.next_id.fetch_add(1, Ordering::Relaxed),
|
||||
ts_ms: now_unix_ms(),
|
||||
level: level_str(metadata.level()),
|
||||
target: metadata.target().to_string(),
|
||||
message,
|
||||
};
|
||||
buf.push(entry);
|
||||
self.inner.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
pub mod config_service;
|
||||
pub mod log_buffer;
|
||||
pub mod notification_service;
|
||||
pub mod observability;
|
||||
|
||||
@ -1 +0,0 @@
|
||||
pub mod log_buffer;
|
||||
@ -74,13 +74,13 @@ impl CorrelationEngine {
|
||||
|
||||
fn process_alert(&self, alert: &AlertMessage) {
|
||||
if let Some(event) = self.botnet.process(alert) {
|
||||
let _ = self.detection_tx.try_send(event);
|
||||
send_or_log(&self.detection_tx, event);
|
||||
}
|
||||
if let Some(event) = self.scan.process(alert) {
|
||||
let _ = self.detection_tx.try_send(event);
|
||||
send_or_log(&self.detection_tx, event);
|
||||
}
|
||||
if let Some(event) = self.lateral.process(alert) {
|
||||
let _ = self.detection_tx.try_send(event);
|
||||
send_or_log(&self.detection_tx, event);
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,3 +91,13 @@ impl CorrelationEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_or_log(tx: &mpsc::Sender<DetectionEvent>, event: DetectionEvent) {
|
||||
if let Err(mpsc::error::TrySendError::Full(dropped)) = tx.try_send(event) {
|
||||
log!(DetectionLog::DetectionChannelDrop(
|
||||
format!("{:?}", dropped.source),
|
||||
dropped.attack_type,
|
||||
dropped.source_ip,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,13 @@ impl BeaconingDetector {
|
||||
}
|
||||
_ = analysis_interval.tick() => {
|
||||
for event in self.state.analyze() {
|
||||
let _ = self.detection_tx.try_send(event);
|
||||
if let Err(mpsc::error::TrySendError::Full(d)) = self.detection_tx.try_send(event) {
|
||||
log!(DetectionLog::DetectionChannelDrop(
|
||||
format!("{:?}", d.source),
|
||||
d.attack_type,
|
||||
d.source_ip,
|
||||
));
|
||||
}
|
||||
}
|
||||
self.state.cleanup();
|
||||
}
|
||||
|
||||
@ -5,25 +5,19 @@ use std::time::{Duration, Instant};
|
||||
use arc_swap::ArcSwap;
|
||||
use lru::LruCache;
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::error::system::SystemError;
|
||||
use crate::domain::common::config::constants::{FUSION_AUDIT_ACTION, FUSION_AUDIT_ACTOR};
|
||||
use crate::domain::common::event::{AuditEvent, DetectionEvent, DetectionSource, ThreatDetectedEvent};
|
||||
use crate::domain::detection::attack_type::translate;
|
||||
use crate::domain::detection::fusion_math::{FusionWindowLengths, fused_confidence};
|
||||
use crate::domain::detection::log::DetectionLog;
|
||||
use crate::domain::detection::metrics::FusionMetrics;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::interface::port::geo_lookup::GeoLookup;
|
||||
|
||||
/// Actor recorded on every fusion-chain WORM entry. Stable across releases —
|
||||
/// downstream audit tooling filters on this string.
|
||||
const FUSION_AUDIT_ACTOR: &str = "FusionEngine";
|
||||
/// Action recorded on every fusion-chain WORM entry. Stable across releases.
|
||||
const FUSION_AUDIT_ACTION: &str = "fused_threat_emitted";
|
||||
|
||||
/// Per-source record within an in-flight dedup entry. Keeps the strongest
|
||||
/// confidence per source so multi-hit from one source doesn't inflate the
|
||||
/// fused policy. `local_attack_type` is the raw source-specific label seen
|
||||
@ -55,7 +49,8 @@ struct DedupEntry {
|
||||
/// re-emit with the combined confidence `1 − ∏(1 − c_i)`.
|
||||
pub struct DetectionOrchestrator {
|
||||
rx: mpsc::Receiver<DetectionEvent>,
|
||||
comm: Arc<CommunicationManager>,
|
||||
threat_tx: broadcast::Sender<ThreatDetectedEvent>,
|
||||
audit_tx: broadcast::Sender<AuditEvent>,
|
||||
geoip: Option<Arc<dyn GeoLookup>>,
|
||||
metrics: Arc<FusionMetrics>,
|
||||
// Enrichment state
|
||||
@ -75,7 +70,8 @@ impl DetectionOrchestrator {
|
||||
pub fn new(
|
||||
app_config: &Arc<ArcSwap<AppConfig>>,
|
||||
rx: mpsc::Receiver<DetectionEvent>,
|
||||
comm: Arc<CommunicationManager>,
|
||||
threat_tx: broadcast::Sender<ThreatDetectedEvent>,
|
||||
audit_tx: broadcast::Sender<AuditEvent>,
|
||||
geoip: Option<Arc<dyn GeoLookup>>,
|
||||
metrics: Arc<FusionMetrics>,
|
||||
) -> Self {
|
||||
@ -84,7 +80,8 @@ impl DetectionOrchestrator {
|
||||
let max_dedup = NonZero::new(fusion.max_dedup_entries.max(1)).unwrap_or(NonZero::<usize>::MIN);
|
||||
Self {
|
||||
rx,
|
||||
comm,
|
||||
threat_tx,
|
||||
audit_tx,
|
||||
geoip,
|
||||
metrics,
|
||||
// SAFETY: NonZero::new on non-zero literals.
|
||||
@ -256,9 +253,7 @@ impl DetectionOrchestrator {
|
||||
self.publish_fusion_audit(trigger_event, fused, &per_source_samples)
|
||||
.await;
|
||||
|
||||
if let Err(e) = self.comm.publish_event(threat_event).await {
|
||||
log!(SystemError::MlSoarBridgeFailed(e));
|
||||
}
|
||||
let _ = self.threat_tx.send(threat_event);
|
||||
}
|
||||
|
||||
/// Emit a WORM AuditEvent so the eventual "why was this IP blocked?"
|
||||
@ -272,9 +267,7 @@ impl DetectionOrchestrator {
|
||||
detail: build_fusion_audit_detail(&trigger_event.source_ip, &trigger_event.attack_type, fused, per_source),
|
||||
};
|
||||
|
||||
if let Err(e) = self.comm.publish_event(audit).await {
|
||||
log!(DetectionLog::FusionAuditPublishFailed(e.to_string()));
|
||||
}
|
||||
let _ = self.audit_tx.send(audit);
|
||||
}
|
||||
|
||||
async fn enrich(&mut self, event: &DetectionEvent) -> ThreatDetectedEvent {
|
||||
@ -363,7 +356,7 @@ impl DetectionOrchestrator {
|
||||
|
||||
/// Serialize the WORM audit evidence payload for a fused threat emission.
|
||||
/// Extracted as a free function so tests can cover schema shape without a
|
||||
/// live CommunicationManager harness.
|
||||
/// live broadcast harness.
|
||||
fn build_fusion_audit_detail(src_ip: &str, attack_type: &str, fused: f32, per_source: &[SourceSample]) -> String {
|
||||
let per_source_json: Vec<serde_json::Value> = per_source
|
||||
.iter()
|
||||
@ -386,8 +379,7 @@ fn build_fusion_audit_detail(src_ip: &str, attack_type: &str, fused: f32, per_so
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Orchestrator integration tests require an in-memory CommunicationManager
|
||||
//! harness. Until then, the fusion math lives in `fusion_math::tests`,
|
||||
//! Orchestrator unit tests. The fusion math lives in `fusion_math::tests`,
|
||||
//! canonical translation in `model::detection::attack_type::tests`, and
|
||||
//! the audit evidence schema is covered below.
|
||||
|
||||
|
||||
@ -212,16 +212,22 @@ impl Engine {
|
||||
|
||||
for tracker in &self.trackers {
|
||||
total_count += tracker.flow_count();
|
||||
all_flows.extend(tracker.get_uninferred_flows().into_iter().filter(|flow| {
|
||||
let total_packets = flow.packet_count();
|
||||
total_packets >= Self::effective_min_packets(&flow.flow_key, self.min_packets, self.min_packets_floor)
|
||||
&& !Self::is_strong_benign(
|
||||
&flow.flow_key,
|
||||
flow.fwd_packets.len(),
|
||||
flow.bwd_packets.len(),
|
||||
flow.fwd_total_bytes + flow.bwd_total_bytes,
|
||||
)
|
||||
}));
|
||||
all_flows.extend(
|
||||
tracker
|
||||
.get_uninferred_flows(self.batch_size)
|
||||
.into_iter()
|
||||
.filter(|flow| {
|
||||
let total_packets = flow.packet_count();
|
||||
total_packets
|
||||
>= Self::effective_min_packets(&flow.flow_key, self.min_packets, self.min_packets_floor)
|
||||
&& !Self::is_strong_benign(
|
||||
&flow.flow_key,
|
||||
flow.fwd_packets.len(),
|
||||
flow.bwd_packets.len(),
|
||||
flow.fwd_total_bytes + flow.bwd_total_bytes,
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
log!(MLLog::FlowStats(
|
||||
|
||||
@ -235,7 +235,7 @@ impl Inference {
|
||||
}
|
||||
});
|
||||
|
||||
match run_classifier_batch(classifier, &cls_input, actual) {
|
||||
match run_classifier_batch(classifier, cls_input, actual) {
|
||||
Ok((anomaly, class_probs, c2)) => {
|
||||
all_anomaly.extend_from_slice(&anomaly);
|
||||
all_class_probs.extend(class_probs);
|
||||
@ -295,7 +295,11 @@ impl Inference {
|
||||
}
|
||||
|
||||
results.push(DetectionResult {
|
||||
flow_key: build_flow_key_label(flow),
|
||||
flow_key: if is_attack {
|
||||
build_flow_key_label(flow)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
flow_key_raw: flow.flow_key.clone(),
|
||||
direction: flow.direction,
|
||||
is_attack,
|
||||
@ -323,18 +327,17 @@ impl Inference {
|
||||
flows: &[FlowData],
|
||||
) -> Vec<DetectionResult> {
|
||||
let n = flows.len();
|
||||
let all_features: Vec<Vec<f32>> = flows.iter().map(|f| self.preprocess_ae_features(f)).collect();
|
||||
let mut scores = Vec::with_capacity(n);
|
||||
|
||||
for chunk_start in (0..n).step_by(batch_size) {
|
||||
let chunk_end = (chunk_start + batch_size).min(n);
|
||||
let actual = chunk_end - chunk_start;
|
||||
let chunk_features: Vec<Vec<f32>> = flows[chunk_start..chunk_end]
|
||||
.iter()
|
||||
.map(|f| self.preprocess_ae_features(f))
|
||||
.collect();
|
||||
let input = tract_ndarray::Array2::<f32>::from_shape_fn((batch_size, n_features), |(i, j)| {
|
||||
if i < actual {
|
||||
all_features[chunk_start + i][j]
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if i < actual { chunk_features[i][j] } else { 0.0 }
|
||||
});
|
||||
match run_ae_batch(model, &input, actual, n_features) {
|
||||
Ok(s) => scores.extend_from_slice(&s),
|
||||
@ -353,7 +356,11 @@ impl Inference {
|
||||
.map(|(flow, &score)| {
|
||||
let is_attack = score > thr;
|
||||
DetectionResult {
|
||||
flow_key: build_flow_key_label(flow),
|
||||
flow_key: if is_attack {
|
||||
build_flow_key_label(flow)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
flow_key_raw: flow.flow_key.clone(),
|
||||
direction: flow.direction,
|
||||
is_attack,
|
||||
@ -383,21 +390,20 @@ impl Inference {
|
||||
flows: &[FlowData],
|
||||
) -> Vec<DetectionResult> {
|
||||
let n = flows.len();
|
||||
let all_features: Vec<Vec<f32>> = flows.iter().map(|f| self.preprocess_ae_features(f)).collect();
|
||||
let class_min_conf = self.config.class_min_confidence;
|
||||
let mut results = Vec::with_capacity(n);
|
||||
|
||||
for chunk_start in (0..n).step_by(batch_size) {
|
||||
let chunk_end = (chunk_start + batch_size).min(n);
|
||||
let actual = chunk_end - chunk_start;
|
||||
let chunk_features: Vec<Vec<f32>> = flows[chunk_start..chunk_end]
|
||||
.iter()
|
||||
.map(|f| self.preprocess_ae_features(f))
|
||||
.collect();
|
||||
let input = tract_ndarray::Array2::<f32>::from_shape_fn((batch_size, n_features), |(i, j)| {
|
||||
if i < actual {
|
||||
all_features[chunk_start + i][j]
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if i < actual { chunk_features[i][j] } else { 0.0 }
|
||||
});
|
||||
let class_probs = match run_classifier_only_batch(model, &input, actual) {
|
||||
let class_probs = match run_classifier_only_batch(model, input, actual) {
|
||||
Ok(cp) => cp,
|
||||
Err(e) => {
|
||||
log!(MLLog::InferenceFailed("ClassifierOnly".to_string(), e.to_string()));
|
||||
@ -419,7 +425,11 @@ impl Inference {
|
||||
"Normal".to_string()
|
||||
};
|
||||
results.push(DetectionResult {
|
||||
flow_key: build_flow_key_label(flow),
|
||||
flow_key: if is_attack {
|
||||
build_flow_key_label(flow)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
flow_key_raw: flow.flow_key.clone(),
|
||||
direction: flow.direction,
|
||||
is_attack,
|
||||
@ -548,10 +558,10 @@ fn run_ae_batch(
|
||||
/// Run a 3-output multi-task classifier: (anomaly, class_probs, c2_score).
|
||||
fn run_classifier_batch(
|
||||
model: &RunnableModel,
|
||||
input: &tract_ndarray::Array2<f32>,
|
||||
input: tract_ndarray::Array2<f32>,
|
||||
actual: usize,
|
||||
) -> TractResult<ClassifierBatchOutput> {
|
||||
let result = model.run(tvec![input.clone().into_tensor().into()])?;
|
||||
let result = model.run(tvec![input.into_tensor().into()])?;
|
||||
|
||||
let anomaly_view = result[0].to_array_view::<f32>()?;
|
||||
let anomaly: Vec<f32> = (0..actual)
|
||||
@ -576,10 +586,10 @@ fn run_classifier_batch(
|
||||
/// Run a single-output classifier (ClassifierOnly adapter).
|
||||
fn run_classifier_only_batch(
|
||||
model: &RunnableModel,
|
||||
input: &tract_ndarray::Array2<f32>,
|
||||
input: tract_ndarray::Array2<f32>,
|
||||
actual: usize,
|
||||
) -> TractResult<Vec<Vec<f32>>> {
|
||||
let result = model.run(tvec![input.clone().into_tensor().into()])?;
|
||||
let result = model.run(tvec![input.into_tensor().into()])?;
|
||||
let view = result[0]
|
||||
.to_array_view::<f32>()?
|
||||
.into_dimensionality::<tract_ndarray::Ix2>()?;
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
//!
|
||||
//! On FIFO failure (permissions, I/O error) the writer shuts down
|
||||
//! cleanly, leaves inference untouched, logs through `MLLog`, and
|
||||
//! (when a `CommunicationManager` is wired in) also publishes a WORM
|
||||
//! (when an `audit_tx` sender is wired in) also publishes a WORM
|
||||
//! `AuditEvent` so the chain records an auditor-visible reason the
|
||||
//! recording stopped, not just a tracing line that may be lost.
|
||||
//! Callers see the channel disconnect and stop sending rows.
|
||||
@ -21,31 +21,17 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crossbeam::channel::{Receiver, Sender, TrySendError, bounded};
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::domain::common::config::constants::{FLOW_TRACE_FILE_EXT, FLOW_TRACE_FILE_MARKER};
|
||||
use crate::domain::common::event::AuditEvent;
|
||||
use crate::domain::detection::error::MLError;
|
||||
use crate::domain::detection::log::MLLog;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
|
||||
/// Default per-file size cap. A single CSV file won't grow past this
|
||||
/// before the writer rolls to a fresh one.
|
||||
pub const DEFAULT_MAX_FILE_BYTES: u64 = 500 * 1024 * 1024;
|
||||
|
||||
/// Default per-file age cap. Forces a roll even if the size cap
|
||||
/// hasn't been hit so analysts have bounded-age shards to download.
|
||||
pub const DEFAULT_MAX_FILE_AGE: Duration = Duration::from_secs(3600);
|
||||
|
||||
/// Default retained-bytes budget across every `flow-trace-*.csv` in
|
||||
/// the directory. When the total exceeds this, the writer FIFO-deletes
|
||||
/// the oldest files to bring the sum back under the cap.
|
||||
pub const DEFAULT_TOTAL_BUDGET_BYTES: u64 = 10 * 1024 * 1024 * 1024;
|
||||
|
||||
/// Prefix literal baked into every rotated file's name so the HTTP
|
||||
/// file-list handler can recognize ours and skip unrelated files.
|
||||
pub const FLOW_TRACE_FILE_MARKER: &str = "flow-trace-";
|
||||
/// Suffix literal appended to every rotated file.
|
||||
pub const FLOW_TRACE_FILE_EXT: &str = ".csv";
|
||||
|
||||
/// Actor recorded on the WORM `flow_trace_stopped` audit entry. Stable
|
||||
/// wire string — auditors filter on it to separate system-internal
|
||||
/// recording stoppages from administrator-initiated actions. Matches
|
||||
@ -83,7 +69,7 @@ pub struct TrafficLogger {
|
||||
impl TrafficLogger {
|
||||
/// Build a rotating writer rooted at `base_path`'s parent. Any
|
||||
/// existing `flow-trace-*.csv` in that directory participates in
|
||||
/// the FIFO budget. `comm` is optional so tests (and paths where
|
||||
/// the FIFO budget. `audit_tx` is optional so tests (and paths where
|
||||
/// the bus isn't wired yet) can exercise the rotation logic without
|
||||
/// the event-bus dependency; production code always passes `Some`.
|
||||
pub fn new(
|
||||
@ -91,7 +77,7 @@ impl TrafficLogger {
|
||||
header: Vec<String>,
|
||||
policy: RotationPolicy,
|
||||
channel_capacity: usize,
|
||||
comm: Option<Arc<CommunicationManager>>,
|
||||
audit_tx: Option<broadcast::Sender<AuditEvent>>,
|
||||
) -> Result<Self, io::Error> {
|
||||
let directory = base_path
|
||||
.parent()
|
||||
@ -107,7 +93,7 @@ impl TrafficLogger {
|
||||
thread::Builder::new()
|
||||
.name("traffic-logger".to_string())
|
||||
.spawn(move || {
|
||||
writer_loop(receiver, writer_dir, writer_header, writer_policy, comm);
|
||||
writer_loop(receiver, writer_dir, writer_header, writer_policy, audit_tx);
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
@ -202,14 +188,14 @@ fn writer_loop(
|
||||
directory: PathBuf,
|
||||
header: Vec<String>,
|
||||
policy: RotationPolicy,
|
||||
comm: Option<Arc<CommunicationManager>>,
|
||||
audit_tx: Option<broadcast::Sender<AuditEvent>>,
|
||||
) {
|
||||
let mut active = match open_new_file(&directory, &header) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let reason = e.to_string();
|
||||
log!(MLLog::FlowTraceStopped(reason.clone()));
|
||||
emit_flow_trace_stop_audit(comm.as_ref(), &reason, &directory);
|
||||
emit_flow_trace_stop_audit(audit_tx.as_ref(), &reason, &directory);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -227,7 +213,7 @@ fn writer_loop(
|
||||
// see the disconnect and stop trying.
|
||||
let reason = format!("FIFO sweep failed: {e}");
|
||||
log!(MLLog::FlowTraceStopped(reason.clone()));
|
||||
emit_flow_trace_stop_audit(comm.as_ref(), &reason, &directory);
|
||||
emit_flow_trace_stop_audit(audit_tx.as_ref(), &reason, &directory);
|
||||
return;
|
||||
}
|
||||
active = match open_new_file(&directory, &header) {
|
||||
@ -235,7 +221,7 @@ fn writer_loop(
|
||||
Err(e) => {
|
||||
let reason = format!("rotate failed: {e}");
|
||||
log!(MLLog::FlowTraceStopped(reason.clone()));
|
||||
emit_flow_trace_stop_audit(comm.as_ref(), &reason, &directory);
|
||||
emit_flow_trace_stop_audit(audit_tx.as_ref(), &reason, &directory);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@ -256,14 +242,14 @@ fn writer_loop(
|
||||
|
||||
/// Publish a WORM `flow_trace_stopped` audit entry so an auditor can
|
||||
/// later see why Flow Trace recording went dormant without grepping
|
||||
/// process logs. A `None` bus (tests, or pre-wiring paths) is a
|
||||
/// process logs. A `None` sender (tests, or pre-wiring paths) is a
|
||||
/// deliberate no-op — the sibling `MLLog::FlowTraceStopped` tracing
|
||||
/// line still fires in both cases.
|
||||
///
|
||||
/// Extracted as a free function so tests can cover the emit path
|
||||
/// without driving a full writer-loop + failing-disk fixture.
|
||||
fn emit_flow_trace_stop_audit(comm: Option<&Arc<CommunicationManager>>, reason: &str, directory: &Path) {
|
||||
let Some(c) = comm else {
|
||||
fn emit_flow_trace_stop_audit(audit_tx: Option<&broadcast::Sender<AuditEvent>>, reason: &str, directory: &Path) {
|
||||
let Some(tx) = audit_tx else {
|
||||
return;
|
||||
};
|
||||
let detail = serde_json::json!({
|
||||
@ -271,7 +257,7 @@ fn emit_flow_trace_stop_audit(comm: Option<&Arc<CommunicationManager>>, reason:
|
||||
"directory": directory.display().to_string(),
|
||||
})
|
||||
.to_string();
|
||||
let _ = c.publish_event_sync(AuditEvent {
|
||||
let _ = tx.send(AuditEvent {
|
||||
actor: AUDIT_ACTOR_SYSTEM.to_string(),
|
||||
action: AUDIT_ACTION_FLOW_TRACE_STOPPED.to_string(),
|
||||
detail,
|
||||
@ -415,13 +401,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_audit_reaches_subscriber_when_comm_provided() {
|
||||
let comm = Arc::new(CommunicationManager::new(256));
|
||||
comm.register_event_type::<AuditEvent>();
|
||||
let mut rx = comm.subscribe_event::<AuditEvent>().unwrap();
|
||||
async fn stop_audit_reaches_subscriber_when_sender_provided() {
|
||||
let (tx, mut rx) = broadcast::channel::<AuditEvent>(256);
|
||||
|
||||
let dir = scratch_dir("audit-emit");
|
||||
emit_flow_trace_stop_audit(Some(&comm), "FIFO sweep failed: perm denied", &dir);
|
||||
emit_flow_trace_stop_audit(Some(&tx), "FIFO sweep failed: perm denied", &dir);
|
||||
|
||||
let event = rx.recv().await.expect("audit event must be delivered");
|
||||
assert_eq!(event.actor, "system");
|
||||
@ -433,7 +417,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_audit_noop_when_comm_absent() {
|
||||
fn stop_audit_noop_when_sender_absent() {
|
||||
// Passing None is the explicit test-mode path — must not panic.
|
||||
emit_flow_trace_stop_audit(None, "any reason", Path::new("/tmp/anywhere"));
|
||||
}
|
||||
|
||||
@ -15,11 +15,9 @@ use crate::domain::common::error::Error;
|
||||
use crate::domain::common::event::ThreatDetectedEvent;
|
||||
use crate::domain::detection::attack_type::canonical_from_str;
|
||||
use crate::domain::response::condition::{ConditionType, PlaybookCondition};
|
||||
use crate::domain::response::error::SoarError;
|
||||
use crate::domain::response::log::SoarLog;
|
||||
use crate::domain::response::matcher::PlaybookMatcher;
|
||||
use crate::domain::response::playbook::{Playbook, PlaybookAction};
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::interface::port::access_control::AccessControlPort;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
use crate::interface::port::geo_lookup::GeoLookup;
|
||||
@ -61,7 +59,7 @@ impl SoarEngine {
|
||||
let rate_limit_channel = soar_cfg.soar.rate_limit_cmd_channel_capacity;
|
||||
let freq_max_keys = soar_cfg.soar.frequency_max_tracked_keys;
|
||||
drop(soar_cfg);
|
||||
let rate_limit_owner = rate_limit.map(|rl| RateLimitOwnerHandle::spawn(db.clone(), rl, rate_limit_channel));
|
||||
let rate_limit_owner = rate_limit.map(|rl| RateLimitOwnerHandle::spawn(rl, config.clone(), rate_limit_channel));
|
||||
let matcher = PlaybookMatcher::new(config, freq_max_keys);
|
||||
let engine = Self {
|
||||
db,
|
||||
@ -202,19 +200,10 @@ impl SoarEngine {
|
||||
}
|
||||
|
||||
/// Subscribe to ThreatDetectedEvent and start processing.
|
||||
/// Returns an error if subscription fails — caller must handle this as a critical failure.
|
||||
pub fn start(self: Arc<Self>, comm: Arc<CommunicationManager>) -> Result<(), Error> {
|
||||
let rx = comm.subscribe_event::<ThreatDetectedEvent>().map_err(|e| {
|
||||
log!(SoarLog::EventHandlingFailed(format!(
|
||||
"CRITICAL: SOAR engine failed to subscribe — automated threat response is DISABLED: {}",
|
||||
e
|
||||
)));
|
||||
SoarError::ActionFailed("subscribe", e)
|
||||
})?;
|
||||
pub fn start(self: Arc<Self>, threat_rx: broadcast::Receiver<ThreatDetectedEvent>) {
|
||||
tokio::spawn(async move {
|
||||
Self::event_loop(self, rx).await;
|
||||
Self::event_loop(self, threat_rx).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn event_loop(self: Arc<Self>, mut rx: broadcast::Receiver<ThreatDetectedEvent>) {
|
||||
|
||||
@ -1,29 +1,20 @@
|
||||
//! Owner task that serializes SOAR rate-limit adjustments.
|
||||
//!
|
||||
//! The "adjust" and "restore" sequences each touch two systems back-to-back
|
||||
//! (the `soar_rate_limit_*` settings rows and the eBPF `RATE_LIMIT_CONFIG`
|
||||
//! map). The atomicity that the original `TokioMutex<()>` was protecting is
|
||||
//! exactly "no other adjust/restore interleaves between the read and the
|
||||
//! write" — a SQLite transaction can't cover the eBPF half, so we move the
|
||||
//! read-modify-write inside a single tokio task that owns both ports. All
|
||||
//! callers dispatch over an mpsc channel and wait on a one-shot reply.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc};
|
||||
use arc_swap::ArcSwap;
|
||||
use macros::log;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::response::error::SoarError;
|
||||
use crate::domain::response::log::SoarLog;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
use crate::interface::port::rate_limit_api::RateLimitPort;
|
||||
|
||||
/// Settings keys persisted across restarts so the next process can resume the
|
||||
/// same TTL window. Stable wire format with the DB.
|
||||
const KEY_ORIGINAL: &str = "soar_rate_limit_original";
|
||||
const KEY_EXPIRES: &str = "soar_rate_limit_expires";
|
||||
struct ActiveAdjustment {
|
||||
original: [u64; 4],
|
||||
expires: Instant,
|
||||
}
|
||||
|
||||
enum RateLimitCmd {
|
||||
Adjust {
|
||||
@ -38,25 +29,18 @@ enum RateLimitCmd {
|
||||
},
|
||||
}
|
||||
|
||||
/// Lock-free handle to the rate-limit owner task. Cloning is cheap (just an
|
||||
/// `mpsc::Sender`).
|
||||
#[derive(Clone)]
|
||||
pub struct RateLimitOwnerHandle {
|
||||
tx: mpsc::Sender<RateLimitCmd>,
|
||||
}
|
||||
|
||||
impl RateLimitOwnerHandle {
|
||||
/// Spawn the owner task on the current tokio runtime. The owner holds
|
||||
/// the only mutating references to the DB rate-limit settings and the
|
||||
/// eBPF rate-limit map for the duration of an Adjust / Restore batch.
|
||||
///
|
||||
/// Each command body runs inside `spawn_blocking` because both halves
|
||||
/// (rusqlite via r2d2 and eBPF map writes) are synchronous I/O — running
|
||||
/// them directly inside the async owner loop would block the tokio
|
||||
/// worker thread for the entire adjust/restore batch.
|
||||
pub fn spawn(db: Arc<dyn AppRepo>, rate_limit: Arc<dyn RateLimitPort>, channel_capacity: usize) -> Self {
|
||||
pub fn spawn(rate_limit: Arc<dyn RateLimitPort>, config: Arc<ArcSwap<AppConfig>>, channel_capacity: usize) -> Self {
|
||||
let (tx, mut rx) = mpsc::channel::<RateLimitCmd>(channel_capacity.max(1));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut state: Option<ActiveAdjustment> = None;
|
||||
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
match cmd {
|
||||
RateLimitCmd::Adjust {
|
||||
@ -66,25 +50,72 @@ impl RateLimitOwnerHandle {
|
||||
attack_type,
|
||||
reply,
|
||||
} => {
|
||||
let db_c = db.clone();
|
||||
let rl_c = rate_limit.clone();
|
||||
let cfg = config.load_full();
|
||||
let current_state = state.is_some();
|
||||
let join = tokio::task::spawn_blocking(move || {
|
||||
adjust(&db_c, rl_c.as_ref(), factor, ttl_secs, &source_ip, &attack_type)
|
||||
if current_state {
|
||||
Ok(None)
|
||||
} else {
|
||||
first_adjust(rl_c.as_ref(), &cfg, factor).map(Some)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let expires = Instant::now() + Duration::from_secs(ttl_secs);
|
||||
let result = match join {
|
||||
Ok(r) => r,
|
||||
Ok(Ok(Some(original))) => {
|
||||
log!(SoarLog::RateLimitAdjusted(
|
||||
format!("{factor}"),
|
||||
ttl_secs,
|
||||
source_ip.clone(),
|
||||
attack_type,
|
||||
format_change(&original),
|
||||
));
|
||||
state = Some(ActiveAdjustment {
|
||||
original: original.new_snapshot,
|
||||
expires,
|
||||
});
|
||||
Ok(format!(
|
||||
"Rate limits reduced by factor {factor} for {ttl_secs}s (triggered by {source_ip})"
|
||||
))
|
||||
}
|
||||
Ok(Ok(None)) => {
|
||||
if let Some(ref mut s) = state {
|
||||
s.expires = expires;
|
||||
}
|
||||
log!(SoarLog::RateLimitAdjusted(
|
||||
format!("{factor}"),
|
||||
ttl_secs,
|
||||
source_ip.clone(),
|
||||
attack_type,
|
||||
"TTL extended (rates already reduced)".to_string(),
|
||||
));
|
||||
Ok(format!(
|
||||
"Rate limit TTL extended by {ttl_secs}s (triggered by {source_ip})"
|
||||
))
|
||||
}
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(e) => Err(SoarError::RateLimitOwnerJoinFailed(e.to_string()).into()),
|
||||
};
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
RateLimitCmd::RestoreIfExpired { reply } => {
|
||||
let db_c = db.clone();
|
||||
let rl_c = rate_limit.clone();
|
||||
let join = tokio::task::spawn_blocking(move || restore_if_expired(&db_c, rl_c.as_ref())).await;
|
||||
let result = match join {
|
||||
Ok(r) => r,
|
||||
Err(e) => Err(SoarError::RateLimitOwnerJoinFailed(e.to_string()).into()),
|
||||
let result = match &state {
|
||||
Some(adj) if Instant::now() >= adj.expires => {
|
||||
let original = adj.original;
|
||||
let rl_c = rate_limit.clone();
|
||||
let join = tokio::task::spawn_blocking(move || restore(rl_c.as_ref(), &original)).await;
|
||||
match join {
|
||||
Ok(Ok(())) => {
|
||||
state = None;
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => Err(e),
|
||||
Err(e) => Err(SoarError::RateLimitOwnerJoinFailed(e.to_string()).into()),
|
||||
}
|
||||
}
|
||||
_ => Ok(()),
|
||||
};
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
@ -125,118 +156,65 @@ impl RateLimitOwnerHandle {
|
||||
}
|
||||
}
|
||||
|
||||
fn adjust(
|
||||
db: &Arc<dyn AppRepo>,
|
||||
rate_limit: &dyn RateLimitPort,
|
||||
factor: f64,
|
||||
ttl_secs: u64,
|
||||
source_ip: &str,
|
||||
attack_type: &str,
|
||||
) -> Result<String, Error> {
|
||||
let current_packet = rate_limit.get_packet_rate().unwrap_or(10000);
|
||||
let current_syn = rate_limit.get_syn_rate().unwrap_or(1000);
|
||||
let current_udp = rate_limit.get_udp_rate().unwrap_or(5000);
|
||||
let current_dns = rate_limit.get_dns_rate().unwrap_or(2000);
|
||||
|
||||
if db.get_setting(KEY_ORIGINAL)?.filter(|s| !s.is_empty()).is_none() {
|
||||
let original = serde_json::json!({
|
||||
"packet_rate": current_packet,
|
||||
"syn_rate": current_syn,
|
||||
"udp_rate": current_udp,
|
||||
"dns_rate": current_dns,
|
||||
});
|
||||
db.set_setting(KEY_ORIGINAL, &original.to_string())?;
|
||||
}
|
||||
|
||||
let expires_at = Utc::now() + ChronoDuration::seconds(ttl_secs as i64);
|
||||
db.set_setting(KEY_EXPIRES, &expires_at.format("%Y-%m-%d %H:%M:%S").to_string())?;
|
||||
|
||||
let new_packet = (current_packet as f64 * factor) as u64;
|
||||
let new_syn = (current_syn as f64 * factor) as u64;
|
||||
let new_udp = (current_udp as f64 * factor) as u64;
|
||||
let new_dns = (current_dns as f64 * factor) as u64;
|
||||
|
||||
rate_limit.set_packet_rate(new_packet.max(1))?;
|
||||
rate_limit.set_syn_rate(new_syn.max(1))?;
|
||||
rate_limit.set_udp_rate(new_udp.max(1))?;
|
||||
rate_limit.set_dns_rate(new_dns.max(1))?;
|
||||
|
||||
log!(SoarLog::RateLimitAdjusted(
|
||||
format!("{}", factor),
|
||||
ttl_secs,
|
||||
source_ip.to_string(),
|
||||
attack_type.to_string(),
|
||||
format!(
|
||||
"packet {}→{}, syn {}→{}, udp {}→{}, dns {}→{}",
|
||||
current_packet,
|
||||
new_packet.max(1),
|
||||
current_syn,
|
||||
new_syn.max(1),
|
||||
current_udp,
|
||||
new_udp.max(1),
|
||||
current_dns,
|
||||
new_dns.max(1),
|
||||
),
|
||||
));
|
||||
|
||||
Ok(format!(
|
||||
"Rate limits reduced by factor {} for {}s (triggered by {})",
|
||||
factor, ttl_secs, source_ip
|
||||
))
|
||||
struct AdjustResult {
|
||||
orig: [u64; 4],
|
||||
new_snapshot: [u64; 4],
|
||||
}
|
||||
|
||||
fn restore_if_expired(db: &Arc<dyn AppRepo>, rate_limit: &dyn RateLimitPort) -> Result<(), Error> {
|
||||
let expires_str = match db.get_setting(KEY_EXPIRES)?.filter(|s| !s.is_empty()) {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
fn first_adjust(rate_limit: &dyn RateLimitPort, config: &AppConfig, factor: f64) -> Result<AdjustResult, Error> {
|
||||
let ebpf = &config.ebpf;
|
||||
let orig = [
|
||||
rate_limit.get_packet_rate().unwrap_or(ebpf.default_packet_rate),
|
||||
rate_limit.get_syn_rate().unwrap_or(ebpf.default_syn_rate),
|
||||
rate_limit.get_udp_rate().unwrap_or(ebpf.default_udp_rate),
|
||||
rate_limit.get_dns_rate().unwrap_or(ebpf.default_dns_rate),
|
||||
];
|
||||
|
||||
let expires = NaiveDateTime::parse_from_str(&expires_str, "%Y-%m-%d %H:%M:%S")
|
||||
.map(|dt| dt.and_utc())
|
||||
.unwrap_or_else(|_| Utc::now());
|
||||
let new: [u64; 4] = std::array::from_fn(|i| ((orig[i] as f64 * factor) as u64).max(1));
|
||||
|
||||
if Utc::now() < expires {
|
||||
return Ok(());
|
||||
rate_limit.set_packet_rate(new[0])?;
|
||||
rate_limit.set_syn_rate(new[1])?;
|
||||
rate_limit.set_udp_rate(new[2])?;
|
||||
rate_limit.set_dns_rate(new[3])?;
|
||||
|
||||
Ok(AdjustResult {
|
||||
orig,
|
||||
new_snapshot: orig,
|
||||
})
|
||||
}
|
||||
|
||||
fn restore(rate_limit: &dyn RateLimitPort, original: &[u64; 4]) -> Result<(), Error> {
|
||||
let mut errors = Vec::new();
|
||||
if let Err(e) = rate_limit.set_packet_rate(original[0]) {
|
||||
errors.push(format!("packet_rate: {e}"));
|
||||
}
|
||||
|
||||
let original_str = match db.get_setting(KEY_ORIGINAL)?.filter(|s| !s.is_empty()) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
db.set_setting(KEY_EXPIRES, "")?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(original) = serde_json::from_str::<serde_json::Value>(&original_str) {
|
||||
let mut restore_errors = Vec::new();
|
||||
if let Some(v) = original.get("packet_rate").and_then(|v| v.as_u64())
|
||||
&& let Err(e) = rate_limit.set_packet_rate(v)
|
||||
{
|
||||
restore_errors.push(format!("packet_rate: {}", e));
|
||||
}
|
||||
if let Some(v) = original.get("syn_rate").and_then(|v| v.as_u64())
|
||||
&& let Err(e) = rate_limit.set_syn_rate(v)
|
||||
{
|
||||
restore_errors.push(format!("syn_rate: {}", e));
|
||||
}
|
||||
if let Some(v) = original.get("udp_rate").and_then(|v| v.as_u64())
|
||||
&& let Err(e) = rate_limit.set_udp_rate(v)
|
||||
{
|
||||
restore_errors.push(format!("udp_rate: {}", e));
|
||||
}
|
||||
if let Some(v) = original.get("dns_rate").and_then(|v| v.as_u64())
|
||||
&& let Err(e) = rate_limit.set_dns_rate(v)
|
||||
{
|
||||
restore_errors.push(format!("dns_rate: {}", e));
|
||||
}
|
||||
if restore_errors.is_empty() {
|
||||
log!(SoarLog::RateLimitRestored);
|
||||
} else {
|
||||
log!(SoarLog::RateLimitRestoreFailed(restore_errors.join(", ")));
|
||||
}
|
||||
if let Err(e) = rate_limit.set_syn_rate(original[1]) {
|
||||
errors.push(format!("syn_rate: {e}"));
|
||||
}
|
||||
if let Err(e) = rate_limit.set_udp_rate(original[2]) {
|
||||
errors.push(format!("udp_rate: {e}"));
|
||||
}
|
||||
if let Err(e) = rate_limit.set_dns_rate(original[3]) {
|
||||
errors.push(format!("dns_rate: {e}"));
|
||||
}
|
||||
if errors.is_empty() {
|
||||
log!(SoarLog::RateLimitRestored);
|
||||
} else {
|
||||
log!(SoarLog::RateLimitRestoreFailed(errors.join(", ")));
|
||||
}
|
||||
|
||||
db.set_setting(KEY_ORIGINAL, "")?;
|
||||
db.set_setting(KEY_EXPIRES, "")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_change(result: &AdjustResult) -> String {
|
||||
format!(
|
||||
"packet {}→{}, syn {}→{}, udp {}→{}, dns {}→{}",
|
||||
result.orig[0],
|
||||
result.new_snapshot[0],
|
||||
result.orig[1],
|
||||
result.new_snapshot[1],
|
||||
result.orig[2],
|
||||
result.new_snapshot[2],
|
||||
result.orig[3],
|
||||
result.new_snapshot[3],
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,27 +1,8 @@
|
||||
use super::helpers::{override_string_nonempty, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "misc")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AclConfig {
|
||||
#[setting(key = "geoip_db_name", default = "net-guardia/static/geo/dbip-city-lite.mmdb")]
|
||||
pub geoip_db_name: String,
|
||||
}
|
||||
|
||||
impl AclConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
geoip_db_name: "net-guardia/static/geo/dbip-city-lite.mmdb".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut config = Self::defaults();
|
||||
override_string_nonempty(&mut config.geoip_db_name, repo, "geoip_db_name")?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "geoip_db_name", "net-guardia/static/geo/dbip-city-lite.mmdb")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
use super::helpers::{override_parsed, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthConfig {
|
||||
pub jwt_expiry_hours: u64,
|
||||
}
|
||||
|
||||
impl AuthConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self { jwt_expiry_hours: 24 }
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(&mut cfg.jwt_expiry_hours, repo, "jwt_expiry_hours")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "jwt_expiry_hours", "24")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -21,3 +21,15 @@ pub const STAGING_SUBDIR: &str = ".staging";
|
||||
/// Fallback port used when `http_port` is unset / unparseable. Matches
|
||||
/// the `defaults()` of `HttpServerConfig` and the setup-wizard default.
|
||||
pub const HTTP_FALLBACK_PORT: u16 = 8080;
|
||||
|
||||
// ── Audit ──────────────────────────────────────────────────────────
|
||||
pub const FUSION_AUDIT_ACTOR: &str = "FusionEngine";
|
||||
pub const FUSION_AUDIT_ACTION: &str = "fused_threat_emitted";
|
||||
pub const AUDIT_ACTOR_SECURITY_ADMIN_PREFIX: &str = "SecurityAdmin";
|
||||
|
||||
// ── Flow Trace ─────────────────────────────────────────────────────
|
||||
pub const FLOW_TRACE_FILE_MARKER: &str = "flow-trace-";
|
||||
pub const FLOW_TRACE_FILE_EXT: &str = ".csv";
|
||||
|
||||
// ── Event Channels ────────────────────────────────────────────────
|
||||
pub const EVENT_CHANNEL_CAPACITY: usize = 256;
|
||||
|
||||
@ -1,64 +1,27 @@
|
||||
use super::helpers::{override_parsed, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
/// Shared shape for scan / lateral / botnet correlation detectors.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CorrelationDetectorParams {
|
||||
pub window_secs: u64,
|
||||
pub threshold: usize,
|
||||
}
|
||||
|
||||
#[config_settings(section = "correlation")]
|
||||
#[setting(key = "correlation_scan_window_secs", default = "120", path = "scan.window_secs")]
|
||||
#[setting(key = "correlation_scan_threshold", default = "20", path = "scan.threshold")]
|
||||
#[setting(
|
||||
key = "correlation_lateral_window_secs",
|
||||
default = "300",
|
||||
path = "lateral.window_secs"
|
||||
)]
|
||||
#[setting(key = "correlation_lateral_threshold", default = "5", path = "lateral.threshold")]
|
||||
#[setting(key = "correlation_botnet_window_secs", default = "300", path = "botnet.window_secs")]
|
||||
#[setting(key = "correlation_botnet_threshold", default = "10", path = "botnet.threshold")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CorrelationConfig {
|
||||
pub scan: CorrelationDetectorParams,
|
||||
pub lateral: CorrelationDetectorParams,
|
||||
pub botnet: CorrelationDetectorParams,
|
||||
/// Maximum tracked source/destination IPs per detector to bound memory.
|
||||
/// Shared across all three detectors since they share the same memory
|
||||
/// concern under DDoS.
|
||||
#[setting(key = "correlation_max_tracked_entries", default = "10000")]
|
||||
pub max_tracked_entries: usize,
|
||||
}
|
||||
|
||||
impl CorrelationConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
scan: CorrelationDetectorParams {
|
||||
window_secs: 120,
|
||||
threshold: 20,
|
||||
},
|
||||
lateral: CorrelationDetectorParams {
|
||||
window_secs: 300,
|
||||
threshold: 5,
|
||||
},
|
||||
botnet: CorrelationDetectorParams {
|
||||
window_secs: 300,
|
||||
threshold: 10,
|
||||
},
|
||||
max_tracked_entries: 10_000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(&mut cfg.scan.window_secs, repo, "correlation_scan_window_secs")?;
|
||||
override_parsed(&mut cfg.scan.threshold, repo, "correlation_scan_threshold")?;
|
||||
override_parsed(&mut cfg.lateral.window_secs, repo, "correlation_lateral_window_secs")?;
|
||||
override_parsed(&mut cfg.lateral.threshold, repo, "correlation_lateral_threshold")?;
|
||||
override_parsed(&mut cfg.botnet.window_secs, repo, "correlation_botnet_window_secs")?;
|
||||
override_parsed(&mut cfg.botnet.threshold, repo, "correlation_botnet_threshold")?;
|
||||
override_parsed(&mut cfg.max_tracked_entries, repo, "correlation_max_tracked_entries")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "correlation_scan_window_secs", "120")?;
|
||||
seed_key(repo, "correlation_scan_threshold", "20")?;
|
||||
seed_key(repo, "correlation_lateral_window_secs", "300")?;
|
||||
seed_key(repo, "correlation_lateral_threshold", "5")?;
|
||||
seed_key(repo, "correlation_botnet_window_secs", "300")?;
|
||||
seed_key(repo, "correlation_botnet_threshold", "10")?;
|
||||
seed_key(repo, "correlation_max_tracked_entries", "10000")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,119 +1,40 @@
|
||||
use super::helpers::{override_parsed, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DetectionConfig {
|
||||
pub fusion: FusionConfig,
|
||||
pub beaconing: BeaconingConfig,
|
||||
pub cleanup_interval_secs: u64,
|
||||
}
|
||||
|
||||
impl DetectionConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
fusion: FusionConfig::defaults(),
|
||||
beaconing: BeaconingConfig::defaults(),
|
||||
cleanup_interval_secs: 60,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
cfg.fusion = FusionConfig::from_settings(repo)?;
|
||||
cfg.beaconing = BeaconingConfig::from_settings(repo)?;
|
||||
override_parsed(&mut cfg.cleanup_interval_secs, repo, "detection_cleanup_interval_secs")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
FusionConfig::seed_defaults(repo)?;
|
||||
BeaconingConfig::seed_defaults(repo)?;
|
||||
seed_key(repo, "detection_cleanup_interval_secs", "60")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "fusion")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FusionConfig {
|
||||
#[setting(key = "fusion_dedup_window_secs", default = "30")]
|
||||
pub dedup_window_secs: u64,
|
||||
#[setting(key = "fusion_repeat_offender_window_secs", default = "7200")]
|
||||
pub repeat_offender_window_secs: u64,
|
||||
#[setting(key = "fusion_max_dedup_entries", default = "50000")]
|
||||
pub max_dedup_entries: usize,
|
||||
}
|
||||
|
||||
impl FusionConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
dedup_window_secs: 30,
|
||||
repeat_offender_window_secs: 2 * 60 * 60,
|
||||
max_dedup_entries: 50_000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(&mut cfg.dedup_window_secs, repo, "fusion_dedup_window_secs")?;
|
||||
override_parsed(
|
||||
&mut cfg.repeat_offender_window_secs,
|
||||
repo,
|
||||
"fusion_repeat_offender_window_secs",
|
||||
)?;
|
||||
override_parsed(&mut cfg.max_dedup_entries, repo, "fusion_max_dedup_entries")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "fusion_dedup_window_secs", "30")?;
|
||||
seed_key(repo, "fusion_repeat_offender_window_secs", "7200")?;
|
||||
seed_key(repo, "fusion_max_dedup_entries", "50000")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[config_settings(section = "beaconing")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BeaconingConfig {
|
||||
#[setting(key = "beaconing_analysis_interval_secs", default = "30")]
|
||||
pub analysis_interval_secs: u64,
|
||||
#[setting(key = "beaconing_min_observations", default = "5")]
|
||||
pub min_observations: usize,
|
||||
#[setting(key = "beaconing_cv_threshold", default = "0.3")]
|
||||
pub cv_threshold: f64,
|
||||
#[setting(key = "beaconing_max_cache_entries", default = "50000")]
|
||||
pub max_cache_entries: usize,
|
||||
#[setting(key = "beaconing_expiry_secs", default = "600")]
|
||||
pub expiry_secs: u64,
|
||||
#[setting(key = "beaconing_alert_cooldown_secs", default = "300")]
|
||||
pub alert_cooldown_secs: u64,
|
||||
}
|
||||
|
||||
impl BeaconingConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
analysis_interval_secs: 30,
|
||||
min_observations: 5,
|
||||
cv_threshold: 0.3,
|
||||
max_cache_entries: 50_000,
|
||||
expiry_secs: 600,
|
||||
alert_cooldown_secs: 300,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(
|
||||
&mut cfg.analysis_interval_secs,
|
||||
repo,
|
||||
"beaconing_analysis_interval_secs",
|
||||
)?;
|
||||
override_parsed(&mut cfg.min_observations, repo, "beaconing_min_observations")?;
|
||||
override_parsed(&mut cfg.cv_threshold, repo, "beaconing_cv_threshold")?;
|
||||
override_parsed(&mut cfg.max_cache_entries, repo, "beaconing_max_cache_entries")?;
|
||||
override_parsed(&mut cfg.expiry_secs, repo, "beaconing_expiry_secs")?;
|
||||
override_parsed(&mut cfg.alert_cooldown_secs, repo, "beaconing_alert_cooldown_secs")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "beaconing_analysis_interval_secs", "30")?;
|
||||
seed_key(repo, "beaconing_min_observations", "5")?;
|
||||
seed_key(repo, "beaconing_cv_threshold", "0.3")?;
|
||||
seed_key(repo, "beaconing_max_cache_entries", "50000")?;
|
||||
seed_key(repo, "beaconing_expiry_secs", "600")?;
|
||||
seed_key(repo, "beaconing_alert_cooldown_secs", "300")?;
|
||||
Ok(())
|
||||
}
|
||||
#[config_settings(section = "detection")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DetectionConfig {
|
||||
#[setting(flatten)]
|
||||
pub fusion: FusionConfig,
|
||||
#[setting(flatten)]
|
||||
pub beaconing: BeaconingConfig,
|
||||
#[setting(key = "detection_cleanup_interval_secs", default = "60")]
|
||||
pub cleanup_interval_secs: u64,
|
||||
}
|
||||
|
||||
@ -1,27 +1,8 @@
|
||||
use super::helpers::{override_parsed, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "dns")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DnsFilterConfig {
|
||||
#[setting(key = "dns_max_domains_per_request", default = "1000")]
|
||||
pub max_domains_per_request: usize,
|
||||
}
|
||||
|
||||
impl DnsFilterConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
max_domains_per_request: 1000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(&mut cfg.max_domains_per_request, repo, "dns_max_domains_per_request")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "dns_max_domains_per_request", "1000")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,75 +1,45 @@
|
||||
use super::helpers::{override_parsed, override_string, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EbpfConfig {
|
||||
// ── network ────────────────────────────────────────────────────
|
||||
#[setting(section = "network", key = "ingress_interface", default = "eth0")]
|
||||
pub ingress_ifname: String,
|
||||
#[setting(section = "network", key = "egress_interface", default = "eth1")]
|
||||
pub egress_ifname: String,
|
||||
pub combined_queue_count: u32,
|
||||
pub channel_size: usize,
|
||||
pub fill_queue_size: u32,
|
||||
pub comp_queue_size: u32,
|
||||
pub tx_queue_size: u32,
|
||||
pub rx_queue_size: u32,
|
||||
pub frame_size: u32,
|
||||
pub frame_count: u32,
|
||||
#[setting(section = "network", key = "refresh_interval", default = "5")]
|
||||
pub refresh_interval: u64,
|
||||
|
||||
// ── xdp ────────────────────────────────────────────────────────
|
||||
#[setting(section = "xdp", key = "combined_queue_count", default = "1")]
|
||||
pub combined_queue_count: u32,
|
||||
#[setting(section = "xdp", key = "channel_size", default = "4096")]
|
||||
pub channel_size: usize,
|
||||
#[setting(section = "xdp", key = "fill_queue_size", default = "4096")]
|
||||
pub fill_queue_size: u32,
|
||||
#[setting(section = "xdp", key = "comp_queue_size", default = "4096")]
|
||||
pub comp_queue_size: u32,
|
||||
#[setting(section = "xdp", key = "tx_queue_size", default = "4096")]
|
||||
pub tx_queue_size: u32,
|
||||
#[setting(section = "xdp", key = "rx_queue_size", default = "4096")]
|
||||
pub rx_queue_size: u32,
|
||||
#[setting(section = "xdp", key = "frame_size", default = "4096")]
|
||||
pub frame_size: u32,
|
||||
#[setting(section = "xdp", key = "frame_count", default = "4096")]
|
||||
pub frame_count: u32,
|
||||
#[setting(section = "xdp", key = "packet_buffer_size", default = "2048")]
|
||||
pub packet_buffer_size: usize,
|
||||
#[setting(section = "xdp", key = "buffer_pool_capacity", default = "1024")]
|
||||
pub buffer_pool_capacity: usize,
|
||||
}
|
||||
|
||||
impl EbpfConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
ingress_ifname: "eth0".to_string(),
|
||||
egress_ifname: "eth1".to_string(),
|
||||
combined_queue_count: 1,
|
||||
channel_size: 4096,
|
||||
fill_queue_size: 4096,
|
||||
comp_queue_size: 4096,
|
||||
tx_queue_size: 4096,
|
||||
rx_queue_size: 4096,
|
||||
frame_size: 4096,
|
||||
frame_count: 4096,
|
||||
refresh_interval: 5,
|
||||
packet_buffer_size: 2048,
|
||||
buffer_pool_capacity: 1024,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_string(&mut cfg.ingress_ifname, repo, "ingress_interface")?;
|
||||
override_string(&mut cfg.egress_ifname, repo, "egress_interface")?;
|
||||
override_parsed(&mut cfg.combined_queue_count, repo, "combined_queue_count")?;
|
||||
override_parsed(&mut cfg.channel_size, repo, "channel_size")?;
|
||||
override_parsed(&mut cfg.fill_queue_size, repo, "fill_queue_size")?;
|
||||
override_parsed(&mut cfg.comp_queue_size, repo, "comp_queue_size")?;
|
||||
override_parsed(&mut cfg.tx_queue_size, repo, "tx_queue_size")?;
|
||||
override_parsed(&mut cfg.rx_queue_size, repo, "rx_queue_size")?;
|
||||
override_parsed(&mut cfg.frame_size, repo, "frame_size")?;
|
||||
override_parsed(&mut cfg.frame_count, repo, "frame_count")?;
|
||||
override_parsed(&mut cfg.refresh_interval, repo, "refresh_interval")?;
|
||||
override_parsed(&mut cfg.packet_buffer_size, repo, "packet_buffer_size")?;
|
||||
override_parsed(&mut cfg.buffer_pool_capacity, repo, "buffer_pool_capacity")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "ingress_interface", "eth0")?;
|
||||
seed_key(repo, "egress_interface", "eth1")?;
|
||||
seed_key(repo, "combined_queue_count", "1")?;
|
||||
seed_key(repo, "channel_size", "4096")?;
|
||||
seed_key(repo, "fill_queue_size", "4096")?;
|
||||
seed_key(repo, "comp_queue_size", "4096")?;
|
||||
seed_key(repo, "tx_queue_size", "4096")?;
|
||||
seed_key(repo, "rx_queue_size", "4096")?;
|
||||
seed_key(repo, "frame_size", "4096")?;
|
||||
seed_key(repo, "frame_count", "4096")?;
|
||||
seed_key(repo, "refresh_interval", "5")?;
|
||||
seed_key(repo, "packet_buffer_size", "2048")?;
|
||||
seed_key(repo, "buffer_pool_capacity", "1024")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── internal (not exposed in API) ──────────────────────────────
|
||||
#[setting(section = "xdp", key = "default_packet_rate", default = "10000", api = false)]
|
||||
pub default_packet_rate: u64,
|
||||
#[setting(section = "xdp", key = "default_syn_rate", default = "100", api = false)]
|
||||
pub default_syn_rate: u64,
|
||||
#[setting(section = "xdp", key = "default_udp_rate", default = "5000", api = false)]
|
||||
pub default_udp_rate: u64,
|
||||
#[setting(section = "xdp", key = "default_dns_rate", default = "200", api = false)]
|
||||
pub default_dns_rate: u64,
|
||||
}
|
||||
|
||||
@ -23,13 +23,6 @@ pub(in crate::domain) fn override_bool(target: &mut bool, repo: &dyn SettingRepo
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(in crate::domain) fn override_string(target: &mut String, repo: &dyn SettingRepo, key: &str) -> Result<(), Error> {
|
||||
if let Some(v) = repo.get_setting(key)? {
|
||||
*target = v;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(in crate::domain) fn override_string_nonempty(
|
||||
target: &mut String,
|
||||
repo: &dyn SettingRepo,
|
||||
|
||||
@ -1,35 +1,14 @@
|
||||
use super::helpers::{override_bool, override_csv, override_parsed, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "http")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HttpServerConfig {
|
||||
#[setting(key = "http_port", default = "8080")]
|
||||
pub port: u16,
|
||||
#[setting(key = "cors_allowed_origins", default = "", api = false)]
|
||||
pub cors_allowed_origins: Vec<String>,
|
||||
#[setting(key = "force_https", default = "false")]
|
||||
pub force_https: bool,
|
||||
}
|
||||
|
||||
impl HttpServerConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
port: 8080,
|
||||
cors_allowed_origins: Vec::new(),
|
||||
force_https: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(&mut cfg.port, repo, "http_port")?;
|
||||
override_csv(&mut cfg.cors_allowed_origins, repo, "cors_allowed_origins")?;
|
||||
override_bool(&mut cfg.force_https, repo, "force_https")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "http_port", "8080")?;
|
||||
seed_key(repo, "cors_allowed_origins", "")?;
|
||||
seed_key(repo, "force_https", "false")?;
|
||||
Ok(())
|
||||
}
|
||||
#[setting(key = "jwt_expiry_hours", default = "24")]
|
||||
pub jwt_expiry_hours: u64,
|
||||
}
|
||||
|
||||
@ -1,219 +1,89 @@
|
||||
use super::helpers::{override_bool, override_parsed, override_string_nonempty, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MlConfig {
|
||||
// ── models ─────────────────────────────────────────────────────
|
||||
#[setting(section = "models", key = "deep_autoencoder_name", default = "deep_autoencoder.onnx")]
|
||||
pub deep_autoencoder_name: String,
|
||||
#[setting(section = "models", key = "classifier_name", default = "classifier.onnx")]
|
||||
pub classifier_name: String,
|
||||
#[setting(section = "models", key = "models_config_name", default = "inference_config.json")]
|
||||
pub models_config_name: String,
|
||||
|
||||
// ── inference ──────────────────────────────────────────────────
|
||||
#[setting(section = "inference", key = "max_concurrent_flows", default = "10000")]
|
||||
pub max_concurrent_flows: usize,
|
||||
#[setting(section = "inference", key = "min_packets_for_inference", default = "5")]
|
||||
pub min_packets_for_inference: usize,
|
||||
pub min_packets_floor: usize,
|
||||
#[setting(section = "inference", key = "inference_interval_secs", default = "5")]
|
||||
pub inference_interval_secs: u64,
|
||||
#[setting(section = "inference", key = "aggregator_window_secs", default = "30")]
|
||||
pub aggregator_window_secs: u64,
|
||||
#[setting(section = "inference", key = "inference_batch_size", default = "200")]
|
||||
pub inference_batch_size: usize,
|
||||
pub confirmation_window_fraction: u64,
|
||||
#[setting(section = "inference", key = "traffic_logging_mode", default = "false")]
|
||||
pub traffic_logging_mode: bool,
|
||||
#[setting(section = "inference", key = "traffic_log_csv_path", default = "traffic_log.csv")]
|
||||
pub traffic_log_csv_path: String,
|
||||
pub flow_trace_max_file_bytes: u64,
|
||||
pub flow_trace_max_file_age_secs: u64,
|
||||
pub flow_trace_total_budget_bytes: u64,
|
||||
pub traffic_logger_channel_capacity: usize,
|
||||
pub model_upload_max_onnx_bytes: usize,
|
||||
pub model_upload_max_manifest_bytes: usize,
|
||||
pub model_upload_max_scaler_bytes: usize,
|
||||
|
||||
// ── ml ─────────────────────────────────────────────────────────
|
||||
#[setting(section = "ml", key = "ml_min_packets_floor", default = "5")]
|
||||
pub min_packets_floor: usize,
|
||||
#[setting(section = "ml", key = "ml_confirmation_window_fraction", default = "2")]
|
||||
pub confirmation_window_fraction: u64,
|
||||
#[setting(section = "ml", key = "ml_drift_window_secs", default = "3600")]
|
||||
pub drift_window_secs: u64,
|
||||
#[setting(section = "ml", key = "ml_drift_max_snapshots", default = "10000")]
|
||||
pub drift_max_snapshots: usize,
|
||||
#[setting(section = "ml", key = "ml_drift_channel_capacity", default = "1024")]
|
||||
pub drift_channel_capacity: usize,
|
||||
#[setting(section = "ml", key = "ml_alert_channel_capacity", default = "1024")]
|
||||
pub alert_channel_capacity: usize,
|
||||
#[setting(section = "ml", key = "ml_circuit_breaker_threshold", default = "5")]
|
||||
pub circuit_breaker_threshold: u32,
|
||||
#[setting(section = "ml", key = "ml_circuit_breaker_window_secs", default = "60")]
|
||||
pub circuit_breaker_window_secs: u64,
|
||||
#[setting(section = "ml", key = "ml_circuit_breaker_cooldown_secs", default = "120")]
|
||||
pub circuit_breaker_cooldown_secs: u64,
|
||||
#[setting(section = "ml", key = "ml_onnx_load_timeout_secs", default = "5")]
|
||||
pub onnx_load_timeout_secs: u64,
|
||||
#[setting(section = "ml", key = "ml_model_watcher_debounce_secs", default = "5")]
|
||||
pub model_watcher_debounce_secs: u64,
|
||||
#[setting(section = "ml", key = "ml_flow_max_packets_per_direction", default = "1000")]
|
||||
pub flow_max_packets_per_direction: usize,
|
||||
#[setting(section = "ml", key = "ml_flow_max_periods", default = "1000")]
|
||||
pub flow_max_periods: usize,
|
||||
#[setting(section = "ml", key = "ml_flow_idle_threshold_us", default = "1000000")]
|
||||
pub flow_idle_threshold_us: u64,
|
||||
#[setting(section = "ml", key = "ml_flow_bulk_min_packets", default = "4")]
|
||||
pub flow_bulk_min_packets: u64,
|
||||
#[setting(section = "ml", key = "ml_flow_bulk_min_bytes", default = "1000")]
|
||||
pub flow_bulk_min_bytes: u64,
|
||||
#[setting(section = "ml", key = "ml_flow_idle_timeout_us", default = "120000000")]
|
||||
pub flow_idle_timeout_us: u64,
|
||||
#[setting(section = "ml", key = "ml_flow_terminated_timeout_us", default = "5000000")]
|
||||
pub flow_terminated_timeout_us: u64,
|
||||
}
|
||||
|
||||
impl MlConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
deep_autoencoder_name: "deep_autoencoder.onnx".to_string(),
|
||||
classifier_name: "classifier.onnx".to_string(),
|
||||
models_config_name: "inference_config.json".to_string(),
|
||||
max_concurrent_flows: 10_000,
|
||||
min_packets_for_inference: 5,
|
||||
min_packets_floor: 5,
|
||||
inference_interval_secs: 5,
|
||||
aggregator_window_secs: 30,
|
||||
inference_batch_size: 200,
|
||||
confirmation_window_fraction: 2,
|
||||
traffic_logging_mode: false,
|
||||
traffic_log_csv_path: "traffic_log.csv".to_string(),
|
||||
flow_trace_max_file_bytes: 500 * 1024 * 1024,
|
||||
flow_trace_max_file_age_secs: 3600,
|
||||
flow_trace_total_budget_bytes: 10 * 1024 * 1024 * 1024,
|
||||
traffic_logger_channel_capacity: 65_536,
|
||||
model_upload_max_onnx_bytes: 100 * 1024 * 1024,
|
||||
model_upload_max_manifest_bytes: 64 * 1024,
|
||||
model_upload_max_scaler_bytes: 64 * 1024,
|
||||
drift_window_secs: 3600,
|
||||
drift_max_snapshots: 10_000,
|
||||
drift_channel_capacity: 1024,
|
||||
alert_channel_capacity: 1024,
|
||||
circuit_breaker_threshold: 5,
|
||||
circuit_breaker_window_secs: 60,
|
||||
circuit_breaker_cooldown_secs: 120,
|
||||
onnx_load_timeout_secs: 5,
|
||||
model_watcher_debounce_secs: 5,
|
||||
flow_max_packets_per_direction: 1000,
|
||||
flow_max_periods: 1000,
|
||||
flow_idle_threshold_us: 1_000_000,
|
||||
flow_bulk_min_packets: 4,
|
||||
flow_bulk_min_bytes: 1000,
|
||||
flow_idle_timeout_us: 120_000_000,
|
||||
flow_terminated_timeout_us: 5_000_000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_string_nonempty(&mut cfg.deep_autoencoder_name, repo, "deep_autoencoder_name")?;
|
||||
override_string_nonempty(&mut cfg.classifier_name, repo, "classifier_name")?;
|
||||
override_string_nonempty(&mut cfg.models_config_name, repo, "models_config_name")?;
|
||||
override_parsed(&mut cfg.max_concurrent_flows, repo, "max_concurrent_flows")?;
|
||||
override_parsed(&mut cfg.min_packets_for_inference, repo, "min_packets_for_inference")?;
|
||||
override_parsed(&mut cfg.min_packets_floor, repo, "ml_min_packets_floor")?;
|
||||
override_parsed(&mut cfg.inference_interval_secs, repo, "inference_interval_secs")?;
|
||||
override_parsed(&mut cfg.aggregator_window_secs, repo, "aggregator_window_secs")?;
|
||||
override_parsed(&mut cfg.inference_batch_size, repo, "inference_batch_size")?;
|
||||
override_parsed(
|
||||
&mut cfg.confirmation_window_fraction,
|
||||
repo,
|
||||
"ml_confirmation_window_fraction",
|
||||
)?;
|
||||
override_bool(&mut cfg.traffic_logging_mode, repo, "traffic_logging_mode")?;
|
||||
override_string_nonempty(&mut cfg.traffic_log_csv_path, repo, "traffic_log_csv_path")?;
|
||||
override_parsed(&mut cfg.flow_trace_max_file_bytes, repo, "flow_trace_max_file_bytes")?;
|
||||
override_parsed(
|
||||
&mut cfg.flow_trace_max_file_age_secs,
|
||||
repo,
|
||||
"flow_trace_max_file_age_secs",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.flow_trace_total_budget_bytes,
|
||||
repo,
|
||||
"flow_trace_total_budget_bytes",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.traffic_logger_channel_capacity,
|
||||
repo,
|
||||
"traffic_logger_channel_capacity",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.model_upload_max_onnx_bytes,
|
||||
repo,
|
||||
"model_upload_max_onnx_bytes",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.model_upload_max_manifest_bytes,
|
||||
repo,
|
||||
"model_upload_max_manifest_bytes",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.model_upload_max_scaler_bytes,
|
||||
repo,
|
||||
"model_upload_max_scaler_bytes",
|
||||
)?;
|
||||
override_parsed(&mut cfg.drift_window_secs, repo, "ml_drift_window_secs")?;
|
||||
override_parsed(&mut cfg.drift_max_snapshots, repo, "ml_drift_max_snapshots")?;
|
||||
override_parsed(&mut cfg.drift_channel_capacity, repo, "ml_drift_channel_capacity")?;
|
||||
override_parsed(&mut cfg.alert_channel_capacity, repo, "ml_alert_channel_capacity")?;
|
||||
override_parsed(&mut cfg.circuit_breaker_threshold, repo, "ml_circuit_breaker_threshold")?;
|
||||
override_parsed(
|
||||
&mut cfg.circuit_breaker_window_secs,
|
||||
repo,
|
||||
"ml_circuit_breaker_window_secs",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.circuit_breaker_cooldown_secs,
|
||||
repo,
|
||||
"ml_circuit_breaker_cooldown_secs",
|
||||
)?;
|
||||
override_parsed(&mut cfg.onnx_load_timeout_secs, repo, "ml_onnx_load_timeout_secs")?;
|
||||
override_parsed(
|
||||
&mut cfg.model_watcher_debounce_secs,
|
||||
repo,
|
||||
"ml_model_watcher_debounce_secs",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.flow_max_packets_per_direction,
|
||||
repo,
|
||||
"ml_flow_max_packets_per_direction",
|
||||
)?;
|
||||
override_parsed(&mut cfg.flow_max_periods, repo, "ml_flow_max_periods")?;
|
||||
override_parsed(&mut cfg.flow_idle_threshold_us, repo, "ml_flow_idle_threshold_us")?;
|
||||
override_parsed(&mut cfg.flow_bulk_min_packets, repo, "ml_flow_bulk_min_packets")?;
|
||||
override_parsed(&mut cfg.flow_bulk_min_bytes, repo, "ml_flow_bulk_min_bytes")?;
|
||||
override_parsed(&mut cfg.flow_idle_timeout_us, repo, "ml_flow_idle_timeout_us")?;
|
||||
override_parsed(
|
||||
&mut cfg.flow_terminated_timeout_us,
|
||||
repo,
|
||||
"ml_flow_terminated_timeout_us",
|
||||
)?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "deep_autoencoder_name", "deep_autoencoder.onnx")?;
|
||||
seed_key(repo, "classifier_name", "classifier.onnx")?;
|
||||
seed_key(repo, "models_config_name", "inference_config.json")?;
|
||||
seed_key(repo, "max_concurrent_flows", "10000")?;
|
||||
seed_key(repo, "min_packets_for_inference", "5")?;
|
||||
seed_key(repo, "ml_min_packets_floor", "5")?;
|
||||
seed_key(repo, "inference_interval_secs", "5")?;
|
||||
seed_key(repo, "aggregator_window_secs", "30")?;
|
||||
seed_key(repo, "inference_batch_size", "200")?;
|
||||
seed_key(repo, "ml_confirmation_window_fraction", "2")?;
|
||||
seed_key(repo, "traffic_logging_mode", "false")?;
|
||||
seed_key(repo, "traffic_log_csv_path", "traffic_log.csv")?;
|
||||
seed_key(repo, "flow_trace_max_file_bytes", &(500_u64 * 1024 * 1024).to_string())?;
|
||||
seed_key(repo, "flow_trace_max_file_age_secs", "3600")?;
|
||||
seed_key(
|
||||
repo,
|
||||
"flow_trace_total_budget_bytes",
|
||||
&(10_u64 * 1024 * 1024 * 1024).to_string(),
|
||||
)?;
|
||||
seed_key(repo, "traffic_logger_channel_capacity", "65536")?;
|
||||
seed_key(
|
||||
repo,
|
||||
"model_upload_max_onnx_bytes",
|
||||
&(100_usize * 1024 * 1024).to_string(),
|
||||
)?;
|
||||
seed_key(repo, "model_upload_max_manifest_bytes", &(64_usize * 1024).to_string())?;
|
||||
seed_key(repo, "model_upload_max_scaler_bytes", &(64_usize * 1024).to_string())?;
|
||||
seed_key(repo, "ml_drift_window_secs", "3600")?;
|
||||
seed_key(repo, "ml_drift_max_snapshots", "10000")?;
|
||||
seed_key(repo, "ml_drift_channel_capacity", "1024")?;
|
||||
seed_key(repo, "ml_alert_channel_capacity", "1024")?;
|
||||
seed_key(repo, "ml_circuit_breaker_threshold", "5")?;
|
||||
seed_key(repo, "ml_circuit_breaker_window_secs", "60")?;
|
||||
seed_key(repo, "ml_circuit_breaker_cooldown_secs", "120")?;
|
||||
seed_key(repo, "ml_onnx_load_timeout_secs", "5")?;
|
||||
seed_key(repo, "ml_model_watcher_debounce_secs", "5")?;
|
||||
seed_key(repo, "ml_flow_max_packets_per_direction", "1000")?;
|
||||
seed_key(repo, "ml_flow_max_periods", "1000")?;
|
||||
seed_key(repo, "ml_flow_idle_threshold_us", "1000000")?;
|
||||
seed_key(repo, "ml_flow_bulk_min_packets", "4")?;
|
||||
seed_key(repo, "ml_flow_bulk_min_bytes", "1000")?;
|
||||
seed_key(repo, "ml_flow_idle_timeout_us", "120000000")?;
|
||||
seed_key(repo, "ml_flow_terminated_timeout_us", "5000000")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── flow_trace ─────────────────────────────────────────────────
|
||||
#[setting(section = "flow_trace", key = "flow_trace_max_file_bytes", default = "524288000")]
|
||||
pub flow_trace_max_file_bytes: u64,
|
||||
#[setting(section = "flow_trace", key = "flow_trace_max_file_age_secs", default = "3600")]
|
||||
pub flow_trace_max_file_age_secs: u64,
|
||||
#[setting(
|
||||
section = "flow_trace",
|
||||
key = "flow_trace_total_budget_bytes",
|
||||
default = "10737418240"
|
||||
)]
|
||||
pub flow_trace_total_budget_bytes: u64,
|
||||
#[setting(section = "flow_trace", key = "traffic_logger_channel_capacity", default = "65536")]
|
||||
pub traffic_logger_channel_capacity: usize,
|
||||
|
||||
// ── model_upload ───────────────────────────────────────────────
|
||||
#[setting(section = "model_upload", key = "model_upload_max_onnx_bytes", default = "104857600")]
|
||||
pub model_upload_max_onnx_bytes: usize,
|
||||
#[setting(section = "model_upload", key = "model_upload_max_manifest_bytes", default = "65536")]
|
||||
pub model_upload_max_manifest_bytes: usize,
|
||||
#[setting(section = "model_upload", key = "model_upload_max_scaler_bytes", default = "65536")]
|
||||
pub model_upload_max_scaler_bytes: usize,
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
pub mod acl;
|
||||
pub mod auth;
|
||||
pub mod constants;
|
||||
pub mod correlation;
|
||||
pub mod detection;
|
||||
@ -11,12 +10,12 @@ pub mod ml;
|
||||
pub mod notification;
|
||||
pub mod observability;
|
||||
pub mod pipeline;
|
||||
pub mod section;
|
||||
pub mod soar;
|
||||
pub mod suricata;
|
||||
pub mod system;
|
||||
|
||||
use crate::domain::common::config::acl::AclConfig;
|
||||
use crate::domain::common::config::auth::AuthConfig;
|
||||
use crate::domain::common::config::correlation::CorrelationConfig;
|
||||
use crate::domain::common::config::detection::DetectionConfig;
|
||||
use crate::domain::common::config::dns_filter::DnsFilterConfig;
|
||||
@ -36,7 +35,6 @@ use crate::interface::port::setting::SettingRepo;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub acl: AclConfig,
|
||||
pub auth: AuthConfig,
|
||||
pub correlation: CorrelationConfig,
|
||||
pub detection: DetectionConfig,
|
||||
pub dns_filter: DnsFilterConfig,
|
||||
@ -55,7 +53,6 @@ impl AppConfig {
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let cfg = Self {
|
||||
acl: AclConfig::from_settings(repo)?,
|
||||
auth: AuthConfig::from_settings(repo)?,
|
||||
correlation: CorrelationConfig::from_settings(repo)?,
|
||||
detection: DetectionConfig::from_settings(repo)?,
|
||||
dns_filter: DnsFilterConfig::from_settings(repo)?,
|
||||
@ -75,7 +72,6 @@ impl AppConfig {
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
AclConfig::seed_defaults(repo)?;
|
||||
AuthConfig::seed_defaults(repo)?;
|
||||
CorrelationConfig::seed_defaults(repo)?;
|
||||
DetectionConfig::seed_defaults(repo)?;
|
||||
DnsFilterConfig::seed_defaults(repo)?;
|
||||
@ -172,7 +168,7 @@ mod tests {
|
||||
assert_eq!(cfg.ebpf.egress_ifname, "eth1");
|
||||
assert_eq!(cfg.ebpf.combined_queue_count, 1);
|
||||
assert_eq!(cfg.ebpf.frame_size, 4096);
|
||||
assert_eq!(cfg.auth.jwt_expiry_hours, 24);
|
||||
assert_eq!(cfg.http_server.jwt_expiry_hours, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -1,95 +1,36 @@
|
||||
use super::helpers::{override_parsed, override_string, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "telegram")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TelegramConfig {
|
||||
#[setting(key = "telegram_rate_limit_max_messages", default = "20")]
|
||||
pub rate_limit_max_messages: u32,
|
||||
#[setting(key = "telegram_rate_limit_window_secs", default = "60")]
|
||||
pub rate_limit_window_secs: u32,
|
||||
#[setting(key = "telegram_max_retries", default = "2")]
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
impl TelegramConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
rate_limit_max_messages: 20,
|
||||
rate_limit_window_secs: 60,
|
||||
max_retries: 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(
|
||||
&mut cfg.rate_limit_max_messages,
|
||||
repo,
|
||||
"telegram_rate_limit_max_messages",
|
||||
)?;
|
||||
override_parsed(&mut cfg.rate_limit_window_secs, repo, "telegram_rate_limit_window_secs")?;
|
||||
override_parsed(&mut cfg.max_retries, repo, "telegram_max_retries")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "telegram_rate_limit_max_messages", "20")?;
|
||||
seed_key(repo, "telegram_rate_limit_window_secs", "60")?;
|
||||
seed_key(repo, "telegram_max_retries", "2")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[config_settings(section = "smtp")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpConfig {
|
||||
#[setting(key = "smtp_host", default = "")]
|
||||
pub host: String,
|
||||
#[setting(key = "smtp_port", default = "587")]
|
||||
pub port: u16,
|
||||
#[setting(key = "smtp_username", default = "")]
|
||||
pub username: String,
|
||||
#[setting(key = "smtp_sender", default = "")]
|
||||
pub sender: String,
|
||||
#[setting(key = "smtp_recipient", default = "")]
|
||||
pub recipient: String,
|
||||
}
|
||||
|
||||
impl SmtpConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
host: String::new(),
|
||||
port: 587,
|
||||
username: String::new(),
|
||||
sender: String::new(),
|
||||
recipient: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_string(&mut cfg.host, repo, "smtp_host")?;
|
||||
override_parsed(&mut cfg.port, repo, "smtp_port")?;
|
||||
override_string(&mut cfg.username, repo, "smtp_username")?;
|
||||
override_string(&mut cfg.sender, repo, "smtp_sender")?;
|
||||
override_string(&mut cfg.recipient, repo, "smtp_recipient")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(_repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[config_settings]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotificationConfig {
|
||||
#[setting(flatten)]
|
||||
pub telegram: TelegramConfig,
|
||||
#[setting(flatten)]
|
||||
pub smtp: SmtpConfig,
|
||||
}
|
||||
|
||||
impl NotificationConfig {
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
telegram: TelegramConfig::from_settings(repo)?,
|
||||
smtp: SmtpConfig::from_settings(repo)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
TelegramConfig::seed_defaults(repo)?;
|
||||
SmtpConfig::seed_defaults(repo)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,71 +1,26 @@
|
||||
use super::helpers::{override_parsed, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "observability")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObservabilityConfig {
|
||||
#[setting(key = "log_level", default = "info", default_debug = "debug")]
|
||||
pub log_level: String,
|
||||
#[setting(key = "log_buffer_capacity", default = "5000")]
|
||||
pub log_buffer_capacity: usize,
|
||||
#[setting(key = "log_buffer_max_message_bytes", default = "8192")]
|
||||
pub log_buffer_max_message_bytes: usize,
|
||||
#[setting(key = "log_max_download_size", default = "52428800")]
|
||||
pub log_max_download_size: u64,
|
||||
#[setting(key = "log_live_default_limit", default = "500")]
|
||||
pub log_live_default_limit: usize,
|
||||
#[setting(key = "log_live_max_limit", default = "2000")]
|
||||
pub log_live_max_limit: usize,
|
||||
#[setting(key = "fusion_explain_scan_limit", default = "5000")]
|
||||
pub fusion_explain_scan_limit: i64,
|
||||
#[setting(key = "fusion_explain_response_cap", default = "200")]
|
||||
pub fusion_explain_response_cap: usize,
|
||||
#[setting(key = "default_event_channel_capacity", default = "256")]
|
||||
pub default_event_channel_capacity: usize,
|
||||
#[setting(key = "drop_channel_capacity", default = "100")]
|
||||
pub drop_channel_capacity: usize,
|
||||
}
|
||||
|
||||
impl ObservabilityConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
log_buffer_capacity: 5_000,
|
||||
log_buffer_max_message_bytes: 8_192,
|
||||
log_max_download_size: 50 * 1024 * 1024,
|
||||
log_live_default_limit: 500,
|
||||
log_live_max_limit: 2_000,
|
||||
fusion_explain_scan_limit: 5_000,
|
||||
fusion_explain_response_cap: 200,
|
||||
default_event_channel_capacity: 256,
|
||||
drop_channel_capacity: 100,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(&mut cfg.log_buffer_capacity, repo, "log_buffer_capacity")?;
|
||||
override_parsed(
|
||||
&mut cfg.log_buffer_max_message_bytes,
|
||||
repo,
|
||||
"log_buffer_max_message_bytes",
|
||||
)?;
|
||||
override_parsed(&mut cfg.log_max_download_size, repo, "log_max_download_size")?;
|
||||
override_parsed(&mut cfg.log_live_default_limit, repo, "log_live_default_limit")?;
|
||||
override_parsed(&mut cfg.log_live_max_limit, repo, "log_live_max_limit")?;
|
||||
override_parsed(&mut cfg.fusion_explain_scan_limit, repo, "fusion_explain_scan_limit")?;
|
||||
override_parsed(
|
||||
&mut cfg.fusion_explain_response_cap,
|
||||
repo,
|
||||
"fusion_explain_response_cap",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.default_event_channel_capacity,
|
||||
repo,
|
||||
"default_event_channel_capacity",
|
||||
)?;
|
||||
override_parsed(&mut cfg.drop_channel_capacity, repo, "drop_channel_capacity")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "log_buffer_capacity", "5000")?;
|
||||
seed_key(repo, "log_buffer_max_message_bytes", "8192")?;
|
||||
seed_key(repo, "log_max_download_size", &(50_u64 * 1024 * 1024).to_string())?;
|
||||
seed_key(repo, "log_live_default_limit", "500")?;
|
||||
seed_key(repo, "log_live_max_limit", "2000")?;
|
||||
seed_key(repo, "fusion_explain_scan_limit", "5000")?;
|
||||
seed_key(repo, "fusion_explain_response_cap", "200")?;
|
||||
seed_key(repo, "default_event_channel_capacity", "256")?;
|
||||
seed_key(repo, "drop_channel_capacity", "100")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,35 +1,10 @@
|
||||
use super::helpers::{override_csv, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "pipeline")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PipelineConfig {
|
||||
#[setting(key = "pipeline_ingress", default = "access_control,rate_limit,service", api = false)]
|
||||
pub ingress: Vec<String>,
|
||||
#[setting(key = "pipeline_egress", default = "", api = false)]
|
||||
pub egress: Vec<String>,
|
||||
}
|
||||
|
||||
impl PipelineConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
ingress: vec![
|
||||
"access_control".to_string(),
|
||||
"rate_limit".to_string(),
|
||||
"service".to_string(),
|
||||
],
|
||||
egress: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_csv(&mut cfg.ingress, repo, "pipeline_ingress")?;
|
||||
override_csv(&mut cfg.egress, repo, "pipeline_egress")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "pipeline_ingress", "access_control,rate_limit,service")?;
|
||||
seed_key(repo, "pipeline_egress", "")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
106
net-guardia/src/domain/common/config/section.rs
Normal file
106
net-guardia/src/domain/common/config/section.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use crate::domain::common::config::acl::AclConfig;
|
||||
use crate::domain::common::config::correlation::CorrelationConfig;
|
||||
use crate::domain::common::config::detection::{BeaconingConfig, DetectionConfig, FusionConfig};
|
||||
use crate::domain::common::config::dns_filter::DnsFilterConfig;
|
||||
use crate::domain::common::config::ebpf::EbpfConfig;
|
||||
use crate::domain::common::config::http_server::HttpServerConfig;
|
||||
use crate::domain::common::config::ml::MlConfig;
|
||||
use crate::domain::common::config::notification::{SmtpConfig, TelegramConfig};
|
||||
use crate::domain::common::config::observability::ObservabilityConfig;
|
||||
use crate::domain::common::config::soar::SoarConfig;
|
||||
use crate::domain::common::config::suricata::SuricataConfig;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ConfigSection {
|
||||
Network,
|
||||
Http,
|
||||
Inference,
|
||||
Xdp,
|
||||
Models,
|
||||
Misc,
|
||||
Soar,
|
||||
Ml,
|
||||
FlowTrace,
|
||||
ModelUpload,
|
||||
Telegram,
|
||||
Dns,
|
||||
Smtp,
|
||||
Suricata,
|
||||
Detection,
|
||||
Fusion,
|
||||
Beaconing,
|
||||
Correlation,
|
||||
Observability,
|
||||
}
|
||||
|
||||
impl ConfigSection {
|
||||
pub const ALL: &[Self] = &[
|
||||
Self::Network,
|
||||
Self::Http,
|
||||
Self::Inference,
|
||||
Self::Xdp,
|
||||
Self::Models,
|
||||
Self::Misc,
|
||||
Self::Soar,
|
||||
Self::Ml,
|
||||
Self::FlowTrace,
|
||||
Self::ModelUpload,
|
||||
Self::Telegram,
|
||||
Self::Dns,
|
||||
Self::Smtp,
|
||||
Self::Suricata,
|
||||
Self::Detection,
|
||||
Self::Fusion,
|
||||
Self::Beaconing,
|
||||
Self::Correlation,
|
||||
Self::Observability,
|
||||
];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Network => "network",
|
||||
Self::Http => "http",
|
||||
Self::Inference => "inference",
|
||||
Self::Xdp => "xdp",
|
||||
Self::Models => "models",
|
||||
Self::Misc => "misc",
|
||||
Self::Soar => "soar",
|
||||
Self::Ml => "ml",
|
||||
Self::FlowTrace => "flow_trace",
|
||||
Self::ModelUpload => "model_upload",
|
||||
Self::Telegram => "telegram",
|
||||
Self::Dns => "dns",
|
||||
Self::Smtp => "smtp",
|
||||
Self::Suricata => "suricata",
|
||||
Self::Detection => "detection",
|
||||
Self::Fusion => "fusion",
|
||||
Self::Beaconing => "beaconing",
|
||||
Self::Correlation => "correlation",
|
||||
Self::Observability => "observability",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keys(self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::Network => EbpfConfig::NETWORK_KEYS,
|
||||
Self::Http => HttpServerConfig::API_KEYS,
|
||||
Self::Inference => MlConfig::INFERENCE_KEYS,
|
||||
Self::Xdp => EbpfConfig::XDP_KEYS,
|
||||
Self::Models => MlConfig::MODELS_KEYS,
|
||||
Self::Misc => AclConfig::API_KEYS,
|
||||
Self::Soar => SoarConfig::API_KEYS,
|
||||
Self::Ml => MlConfig::ML_KEYS,
|
||||
Self::FlowTrace => MlConfig::FLOW_TRACE_KEYS,
|
||||
Self::ModelUpload => MlConfig::MODEL_UPLOAD_KEYS,
|
||||
Self::Telegram => TelegramConfig::API_KEYS,
|
||||
Self::Dns => DnsFilterConfig::API_KEYS,
|
||||
Self::Smtp => SmtpConfig::API_KEYS,
|
||||
Self::Suricata => SuricataConfig::API_KEYS,
|
||||
Self::Detection => DetectionConfig::API_KEYS,
|
||||
Self::Fusion => FusionConfig::API_KEYS,
|
||||
Self::Beaconing => BeaconingConfig::API_KEYS,
|
||||
Self::Correlation => CorrelationConfig::API_KEYS,
|
||||
Self::Observability => ObservabilityConfig::API_KEYS,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,115 +1,34 @@
|
||||
use super::helpers::{override_parsed, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "soar")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SoarConfig {
|
||||
#[setting(key = "soar_max_auto_block_cap", default = "100")]
|
||||
pub max_auto_block_cap: u32,
|
||||
#[setting(key = "soar_max_ttl_secs", default = "86400")]
|
||||
pub max_ttl_secs: u64,
|
||||
#[setting(key = "soar_handle_concurrency", default = "16")]
|
||||
pub handle_concurrency: usize,
|
||||
#[setting(key = "soar_max_pending_unblock_retries", default = "5")]
|
||||
pub max_pending_unblock_retries: i64,
|
||||
#[setting(key = "soar_default_block_ttl_secs", default = "1800")]
|
||||
pub default_block_ttl_secs: u64,
|
||||
#[setting(key = "soar_default_rate_limit_factor", default = "0.5")]
|
||||
pub default_rate_limit_factor: f64,
|
||||
#[setting(key = "soar_default_rate_limit_ttl_secs", default = "600")]
|
||||
pub default_rate_limit_ttl_secs: u64,
|
||||
#[setting(key = "soar_default_webhook_timeout_secs", default = "10")]
|
||||
pub default_webhook_timeout_secs: u64,
|
||||
#[setting(key = "soar_default_frequency_window_secs", default = "60")]
|
||||
pub default_frequency_window_secs: u64,
|
||||
#[setting(key = "soar_default_single_source_high_min_confidence", default = "0.95")]
|
||||
pub default_single_source_high_min_confidence: f32,
|
||||
#[setting(key = "soar_default_cooldown_expiry_secs", default = "3600")]
|
||||
pub default_cooldown_expiry_secs: u64,
|
||||
#[setting(key = "soar_rate_limit_cmd_channel_capacity", default = "64")]
|
||||
pub rate_limit_cmd_channel_capacity: usize,
|
||||
#[setting(key = "soar_frequency_max_tracked_keys", default = "50000")]
|
||||
pub frequency_max_tracked_keys: usize,
|
||||
#[setting(key = "soar_fallback_cooldown_secs", default = "300")]
|
||||
pub fallback_cooldown_secs: i64,
|
||||
}
|
||||
|
||||
impl SoarConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
max_auto_block_cap: 100,
|
||||
max_ttl_secs: 86_400,
|
||||
handle_concurrency: 16,
|
||||
max_pending_unblock_retries: 5,
|
||||
default_block_ttl_secs: 1800,
|
||||
default_rate_limit_factor: 0.5,
|
||||
default_rate_limit_ttl_secs: 600,
|
||||
default_webhook_timeout_secs: 10,
|
||||
default_frequency_window_secs: 60,
|
||||
default_single_source_high_min_confidence: 0.95,
|
||||
default_cooldown_expiry_secs: 3600,
|
||||
rate_limit_cmd_channel_capacity: 64,
|
||||
frequency_max_tracked_keys: 50_000,
|
||||
fallback_cooldown_secs: 300,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_parsed(&mut cfg.max_auto_block_cap, repo, "soar_max_auto_block_cap")?;
|
||||
override_parsed(&mut cfg.max_ttl_secs, repo, "soar_max_ttl_secs")?;
|
||||
override_parsed(&mut cfg.handle_concurrency, repo, "soar_handle_concurrency")?;
|
||||
override_parsed(
|
||||
&mut cfg.max_pending_unblock_retries,
|
||||
repo,
|
||||
"soar_max_pending_unblock_retries",
|
||||
)?;
|
||||
override_parsed(&mut cfg.default_block_ttl_secs, repo, "soar_default_block_ttl_secs")?;
|
||||
override_parsed(
|
||||
&mut cfg.default_rate_limit_factor,
|
||||
repo,
|
||||
"soar_default_rate_limit_factor",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.default_rate_limit_ttl_secs,
|
||||
repo,
|
||||
"soar_default_rate_limit_ttl_secs",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.default_webhook_timeout_secs,
|
||||
repo,
|
||||
"soar_default_webhook_timeout_secs",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.default_frequency_window_secs,
|
||||
repo,
|
||||
"soar_default_frequency_window_secs",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.default_single_source_high_min_confidence,
|
||||
repo,
|
||||
"soar_default_single_source_high_min_confidence",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.default_cooldown_expiry_secs,
|
||||
repo,
|
||||
"soar_default_cooldown_expiry_secs",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.rate_limit_cmd_channel_capacity,
|
||||
repo,
|
||||
"soar_rate_limit_cmd_channel_capacity",
|
||||
)?;
|
||||
override_parsed(
|
||||
&mut cfg.frequency_max_tracked_keys,
|
||||
repo,
|
||||
"soar_frequency_max_tracked_keys",
|
||||
)?;
|
||||
override_parsed(&mut cfg.fallback_cooldown_secs, repo, "soar_fallback_cooldown_secs")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "soar_max_auto_block_cap", "100")?;
|
||||
seed_key(repo, "soar_max_ttl_secs", "86400")?;
|
||||
seed_key(repo, "soar_handle_concurrency", "16")?;
|
||||
seed_key(repo, "soar_max_pending_unblock_retries", "5")?;
|
||||
seed_key(repo, "soar_default_block_ttl_secs", "1800")?;
|
||||
seed_key(repo, "soar_default_rate_limit_factor", "0.5")?;
|
||||
seed_key(repo, "soar_default_rate_limit_ttl_secs", "600")?;
|
||||
seed_key(repo, "soar_default_webhook_timeout_secs", "10")?;
|
||||
seed_key(repo, "soar_default_frequency_window_secs", "60")?;
|
||||
seed_key(repo, "soar_default_single_source_high_min_confidence", "0.95")?;
|
||||
seed_key(repo, "soar_default_cooldown_expiry_secs", "3600")?;
|
||||
seed_key(repo, "soar_rate_limit_cmd_channel_capacity", "64")?;
|
||||
seed_key(repo, "soar_frequency_max_tracked_keys", "50000")?;
|
||||
seed_key(repo, "soar_fallback_cooldown_secs", "300")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,75 +1,30 @@
|
||||
use super::helpers::{override_bool, override_parsed, override_string_nonempty, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "suricata")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SuricataConfig {
|
||||
#[setting(key = "suricata_enabled", default = "false")]
|
||||
pub enabled: bool,
|
||||
#[setting(key = "suricata_binary_path", default = "/usr/bin/suricata")]
|
||||
pub binary_path: String,
|
||||
#[setting(key = "suricata_config_path", default = "/etc/netguardia/suricata.yaml")]
|
||||
pub config_path: String,
|
||||
#[setting(key = "suricata_eve_log_path", default = "/var/log/netguardia/eve.json")]
|
||||
pub eve_log_path: String,
|
||||
#[setting(key = "suricata_auto_restart_on_crash", default = "true")]
|
||||
pub auto_restart_on_crash: bool,
|
||||
#[setting(key = "suricata_restart_backoff_secs", default = "10")]
|
||||
pub restart_backoff_secs: u64,
|
||||
#[setting(key = "suricata_poll_interval_ms", default = "200")]
|
||||
pub poll_interval_ms: u64,
|
||||
#[setting(key = "suricata_file_wait_interval_secs", default = "1")]
|
||||
pub file_wait_interval_secs: u64,
|
||||
#[setting(key = "suricata_confidence_high", default = "0.95")]
|
||||
pub confidence_high: f32,
|
||||
#[setting(key = "suricata_confidence_medium", default = "0.80")]
|
||||
pub confidence_medium: f32,
|
||||
#[setting(key = "suricata_confidence_low", default = "0.65")]
|
||||
pub confidence_low: f32,
|
||||
#[setting(key = "suricata_confidence_info", default = "0.50")]
|
||||
pub confidence_info: f32,
|
||||
}
|
||||
|
||||
impl SuricataConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
binary_path: "/usr/bin/suricata".to_string(),
|
||||
config_path: "/etc/netguardia/suricata.yaml".to_string(),
|
||||
eve_log_path: "/var/log/netguardia/eve.json".to_string(),
|
||||
auto_restart_on_crash: true,
|
||||
restart_backoff_secs: 10,
|
||||
poll_interval_ms: 200,
|
||||
file_wait_interval_secs: 1,
|
||||
confidence_high: 0.95,
|
||||
confidence_medium: 0.80,
|
||||
confidence_low: 0.65,
|
||||
confidence_info: 0.50,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_bool(&mut cfg.enabled, repo, "suricata_enabled")?;
|
||||
override_string_nonempty(&mut cfg.binary_path, repo, "suricata_binary_path")?;
|
||||
override_string_nonempty(&mut cfg.config_path, repo, "suricata_config_path")?;
|
||||
override_string_nonempty(&mut cfg.eve_log_path, repo, "suricata_eve_log_path")?;
|
||||
override_bool(&mut cfg.auto_restart_on_crash, repo, "suricata_auto_restart_on_crash")?;
|
||||
override_parsed(&mut cfg.restart_backoff_secs, repo, "suricata_restart_backoff_secs")?;
|
||||
override_parsed(&mut cfg.poll_interval_ms, repo, "suricata_poll_interval_ms")?;
|
||||
override_parsed(
|
||||
&mut cfg.file_wait_interval_secs,
|
||||
repo,
|
||||
"suricata_file_wait_interval_secs",
|
||||
)?;
|
||||
override_parsed(&mut cfg.confidence_high, repo, "suricata_confidence_high")?;
|
||||
override_parsed(&mut cfg.confidence_medium, repo, "suricata_confidence_medium")?;
|
||||
override_parsed(&mut cfg.confidence_low, repo, "suricata_confidence_low")?;
|
||||
override_parsed(&mut cfg.confidence_info, repo, "suricata_confidence_info")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "suricata_enabled", "false")?;
|
||||
seed_key(repo, "suricata_binary_path", "/usr/bin/suricata")?;
|
||||
seed_key(repo, "suricata_config_path", "/etc/netguardia/suricata.yaml")?;
|
||||
seed_key(repo, "suricata_eve_log_path", "/var/log/netguardia/eve.json")?;
|
||||
seed_key(repo, "suricata_auto_restart_on_crash", "true")?;
|
||||
seed_key(repo, "suricata_restart_backoff_secs", "10")?;
|
||||
seed_key(repo, "suricata_poll_interval_ms", "200")?;
|
||||
seed_key(repo, "suricata_file_wait_interval_secs", "1")?;
|
||||
seed_key(repo, "suricata_confidence_high", "0.95")?;
|
||||
seed_key(repo, "suricata_confidence_medium", "0.80")?;
|
||||
seed_key(repo, "suricata_confidence_low", "0.65")?;
|
||||
seed_key(repo, "suricata_confidence_info", "0.50")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,12 @@
|
||||
use super::helpers::{override_string_nonempty, seed_key};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use macros::config_settings;
|
||||
|
||||
#[config_settings(section = "system")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SystemConfig {
|
||||
#[setting(key = "database_path", default = "net-guardia.db", api = false)]
|
||||
pub database_path: String,
|
||||
#[setting(key = "report_dir", default = "/var/lib/netguardia/reports", api = false)]
|
||||
pub report_dir: String,
|
||||
#[setting(key = "log_dir", default = "logs", api = false)]
|
||||
pub log_dir: String,
|
||||
}
|
||||
|
||||
impl SystemConfig {
|
||||
pub fn defaults() -> Self {
|
||||
Self {
|
||||
database_path: "net-guardia.db".to_string(),
|
||||
report_dir: "/var/lib/netguardia/reports".to_string(),
|
||||
log_dir: "logs".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_settings(repo: &dyn SettingRepo) -> Result<Self, Error> {
|
||||
let mut cfg = Self::defaults();
|
||||
override_string_nonempty(&mut cfg.database_path, repo, "database_path")?;
|
||||
override_string_nonempty(&mut cfg.report_dir, repo, "report_dir")?;
|
||||
override_string_nonempty(&mut cfg.log_dir, repo, "log_dir")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
pub fn seed_defaults(repo: &dyn SettingRepo) -> Result<(), Error> {
|
||||
seed_key(repo, "report_dir", "/var/lib/netguardia/reports")?;
|
||||
seed_key(repo, "log_dir", "logs")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,9 +35,6 @@ traceable! {
|
||||
#[error("Failed to set user groups")]
|
||||
SetUserGroupsFailed => tracing::Level::WARN,
|
||||
|
||||
#[error("Failed to bridge ML alert to SOAR")]
|
||||
MlSoarBridgeFailed => tracing::Level::WARN,
|
||||
|
||||
#[error("Failed to store XDP mode")]
|
||||
XdpModeStoreFailed => tracing::Level::WARN,
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ impl FromStr for DetectionSource {
|
||||
// -- Detection Event (internal pipeline) --------------------------------------
|
||||
|
||||
/// Raw detection from any source. Sent via mpsc channel to DetectionOrchestrator.
|
||||
/// Not published through CommunicationManager — this is a private internal pipeline.
|
||||
/// Not published through broadcast — this is a private internal pipeline.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DetectionEvent {
|
||||
pub source: DetectionSource,
|
||||
|
||||
@ -20,8 +20,5 @@ loggable! {
|
||||
|
||||
#[error("AuditLogger: event channel closed")]
|
||||
AuditChannelClosed => tracing::Level::INFO,
|
||||
|
||||
#[error("AuditLogger: failed to subscribe to events")]
|
||||
AuditSubscribeFailed => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,8 +11,5 @@ loggable! {
|
||||
|
||||
#[error("Setup HTTP server error: {error}")]
|
||||
SetupServerError { error: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Fusion WebSocket failed to subscribe to ThreatDetectedEvent: {err}")]
|
||||
FusionSubscribeFailed { err: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@ use crate::domain::detection::ml_detection::AlertMessage;
|
||||
struct TimedSourceSet {
|
||||
sources: HashSet<String>,
|
||||
window_start: Instant,
|
||||
/// Most recent alert to this destination (used for protocol/confidence in DetectionEvent).
|
||||
last_alert: AlertMessage,
|
||||
last_alerted: Option<Instant>,
|
||||
}
|
||||
|
||||
/// Detects coordinated attacks: multiple source IPs targeting the same destination IP:port.
|
||||
@ -47,6 +47,7 @@ impl BotnetDetector {
|
||||
sources: HashSet::new(),
|
||||
window_start: now,
|
||||
last_alert: alert.clone(),
|
||||
last_alerted: None,
|
||||
});
|
||||
|
||||
let set = entry.value_mut();
|
||||
@ -61,6 +62,13 @@ impl BotnetDetector {
|
||||
set.last_alert = alert.clone();
|
||||
|
||||
if set.sources.len() >= self.threshold {
|
||||
if let Some(last) = set.last_alerted {
|
||||
if now.duration_since(last) < self.window {
|
||||
set.sources.clear();
|
||||
set.window_start = now;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(set.sources.len())
|
||||
} else {
|
||||
None
|
||||
@ -90,10 +98,10 @@ impl BotnetDetector {
|
||||
c2_score: 0.0,
|
||||
};
|
||||
|
||||
// Reset after alerting to avoid repeated alerts within same window
|
||||
if let Some(mut entry) = self.state.get_mut(&key) {
|
||||
entry.sources.clear();
|
||||
entry.window_start = now;
|
||||
entry.last_alerted = Some(now);
|
||||
}
|
||||
|
||||
return Some(event);
|
||||
|
||||
@ -152,7 +152,7 @@ impl FlowData {
|
||||
|
||||
if packet.is_forward {
|
||||
if self.fwd_packets.len() < limits.max_packets_per_direction {
|
||||
self.fwd_packets.push(packet_data.clone());
|
||||
self.fwd_packets.push(packet_data);
|
||||
}
|
||||
self.fwd_total_bytes += packet.packet_length as u64;
|
||||
self.fwd_header_bytes += packet.header_length as u64;
|
||||
@ -162,7 +162,7 @@ impl FlowData {
|
||||
Self::update_bulk_state(&mut self.fwd_bulk_state, &packet_data, limits);
|
||||
} else {
|
||||
if self.bwd_packets.len() < limits.max_packets_per_direction {
|
||||
self.bwd_packets.push(packet_data.clone());
|
||||
self.bwd_packets.push(packet_data);
|
||||
}
|
||||
self.bwd_total_bytes += packet.packet_length as u64;
|
||||
self.bwd_header_bytes += packet.header_length as u64;
|
||||
@ -309,12 +309,21 @@ impl FlowTracker {
|
||||
|
||||
/// 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) -> Vec<FlowData> {
|
||||
pub fn get_uninferred_flows(&self, limit: usize) -> Vec<FlowData> {
|
||||
let mut result = Vec::new();
|
||||
for (_, entry) in self.active.iter() {
|
||||
if result.len() >= limit {
|
||||
break;
|
||||
}
|
||||
let mut flow = entry.lock();
|
||||
if flow.last_time_us > flow.last_inferred_us {
|
||||
let snapshot = flow.clone();
|
||||
let snapshot = FlowData {
|
||||
fwd_packets: std::mem::take(&mut flow.fwd_packets),
|
||||
bwd_packets: std::mem::take(&mut flow.bwd_packets),
|
||||
active_periods: std::mem::take(&mut flow.active_periods),
|
||||
idle_periods: std::mem::take(&mut flow.idle_periods),
|
||||
..flow.clone()
|
||||
};
|
||||
flow.last_inferred_us = flow.last_time_us;
|
||||
result.push(snapshot);
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ use crate::domain::detection::ml_detection::AlertMessage;
|
||||
struct TimedDestSet {
|
||||
dests: HashSet<String>,
|
||||
window_start: Instant,
|
||||
last_alerted: Option<Instant>,
|
||||
}
|
||||
|
||||
/// Detects lateral movement: an internal IP reaching many other internal IPs.
|
||||
@ -48,6 +49,7 @@ impl LateralMovementDetector {
|
||||
let mut entry = self.state.entry(key.clone()).or_insert_with(|| TimedDestSet {
|
||||
dests: HashSet::new(),
|
||||
window_start: now,
|
||||
last_alerted: None,
|
||||
});
|
||||
|
||||
let set = entry.value_mut();
|
||||
@ -60,6 +62,13 @@ impl LateralMovementDetector {
|
||||
set.dests.insert(alert.dst_ip.clone());
|
||||
|
||||
if set.dests.len() >= self.threshold {
|
||||
if let Some(last) = set.last_alerted {
|
||||
if now.duration_since(last) < self.window {
|
||||
set.dests.clear();
|
||||
set.window_start = now;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(set.dests.len())
|
||||
} else {
|
||||
None
|
||||
@ -87,10 +96,10 @@ impl LateralMovementDetector {
|
||||
c2_score: 0.0,
|
||||
};
|
||||
|
||||
// Reset after alerting
|
||||
if let Some(mut entry) = self.state.get_mut(&key) {
|
||||
entry.dests.clear();
|
||||
entry.window_start = now;
|
||||
entry.last_alerted = Some(now);
|
||||
}
|
||||
|
||||
return Some(event);
|
||||
|
||||
@ -127,11 +127,8 @@ loggable! {
|
||||
#[error("Fusion: window evicted under LRU pressure ({key_src} {key_type})")]
|
||||
FusionWindowEvicted { key_src: String, key_type: String } => tracing::Level::WARN,
|
||||
|
||||
#[error("Fusion: failed to publish ThreatDetectedEvent: {err}")]
|
||||
FusionPublishFailed { err: String } => tracing::Level::ERROR,
|
||||
|
||||
#[error("Fusion: failed to publish AuditEvent: {err}")]
|
||||
FusionAuditPublishFailed { err: String } => tracing::Level::ERROR,
|
||||
#[error("Detection event dropped (channel full): {detector} {attack_type} from {source_ip}")]
|
||||
DetectionChannelDrop { detector: String, attack_type: String, source_ip: String } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ impl FlowKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PacketData {
|
||||
pub timestamp_us: u64,
|
||||
pub length: u32,
|
||||
|
||||
@ -13,6 +13,7 @@ struct TimedPortSet {
|
||||
ports: HashSet<u16>,
|
||||
window_start: Instant,
|
||||
last_dst_ip: String,
|
||||
last_alerted: Option<Instant>,
|
||||
}
|
||||
|
||||
/// Detects port scanning: a single source IP probing many destination ports.
|
||||
@ -45,6 +46,7 @@ impl ScanDetector {
|
||||
ports: HashSet::new(),
|
||||
window_start: now,
|
||||
last_dst_ip: alert.dst_ip.clone(),
|
||||
last_alerted: None,
|
||||
});
|
||||
|
||||
let set = entry.value_mut();
|
||||
@ -59,6 +61,13 @@ impl ScanDetector {
|
||||
set.last_dst_ip = alert.dst_ip.clone();
|
||||
|
||||
if set.ports.len() >= self.threshold {
|
||||
if let Some(last) = set.last_alerted {
|
||||
if now.duration_since(last) < self.window {
|
||||
set.ports.clear();
|
||||
set.window_start = now;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((set.ports.len(), set.last_dst_ip.clone()))
|
||||
} else {
|
||||
None
|
||||
@ -82,10 +91,10 @@ impl ScanDetector {
|
||||
c2_score: 0.0,
|
||||
};
|
||||
|
||||
// Reset after alerting
|
||||
if let Some(mut entry) = self.state.get_mut(&key) {
|
||||
entry.ports.clear();
|
||||
entry.window_start = now;
|
||||
entry.last_alerted = Some(now);
|
||||
}
|
||||
|
||||
return Some(event);
|
||||
|
||||
@ -229,7 +229,7 @@ impl PlaybookMatcher {
|
||||
let key = (playbook_id, source_ip.to_string());
|
||||
if let Some(last_exec) = self.cooldowns.get(&key) {
|
||||
let elapsed = last_exec.elapsed();
|
||||
if elapsed.as_secs() < cooldown_secs as u64 {
|
||||
if elapsed.as_secs() < cooldown_secs.max(0) as u64 {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -247,7 +247,7 @@ impl PlaybookMatcher {
|
||||
.playbooks
|
||||
.load()
|
||||
.iter()
|
||||
.map(|p| p.cooldown_secs as u64)
|
||||
.map(|p| p.cooldown_secs.max(0) as u64)
|
||||
.max()
|
||||
.unwrap_or(default_cooldown_expiry);
|
||||
let expiry = Duration::from_secs(max_cooldown_secs.saturating_mul(2).max(default_cooldown_expiry));
|
||||
|
||||
@ -5,6 +5,7 @@ use std::time::{Duration, SystemTime};
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam::queue::SegQueue;
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::core::inference::alert::MLAlert;
|
||||
@ -18,6 +19,7 @@ use crate::domain::common::config::constants::{MANIFEST_FILENAME, MODELS_DIR};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::error::system::SystemError;
|
||||
use crate::domain::common::event::AuditEvent;
|
||||
use crate::domain::common::log::system::SystemLog;
|
||||
use crate::domain::common::system::health::EbpfHealth;
|
||||
use crate::domain::detection::flow_features::FlowFeatures;
|
||||
@ -29,7 +31,6 @@ use crate::domain::detection::ml_detection::EngineConfig;
|
||||
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
|
||||
use crate::domain::detection::model_adapter::ModelSourceState;
|
||||
use crate::domain::detection::model_source::ModelInfo;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::infrastructure::health::SystemHealth;
|
||||
use crate::infrastructure::statistics::FlowStatistics;
|
||||
|
||||
@ -53,7 +54,7 @@ impl AppServices {
|
||||
ml_manifest: Option<ModelManifest>,
|
||||
drift_detector: DriftDetectorHandle,
|
||||
ebpf_health: Arc<ArcSwap<EbpfHealth>>,
|
||||
comm: Arc<CommunicationManager>,
|
||||
audit_tx: broadcast::Sender<AuditEvent>,
|
||||
) -> Result<Self, Error> {
|
||||
let health = SystemHealth::new(app_config.clone(), ebpf_health)?;
|
||||
|
||||
@ -126,7 +127,7 @@ impl AppServices {
|
||||
header,
|
||||
policy,
|
||||
config.ml.traffic_logger_channel_capacity,
|
||||
Some(comm.clone()),
|
||||
Some(audit_tx.clone()),
|
||||
)
|
||||
.map_err(|e| MiscError::TrafficLogCreateError(csv_path.clone(), e.to_string()))?;
|
||||
log!(SystemLog::TrafficLoggingEnabled(csv_path));
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
|
||||
use crate::domain::common::event::{AuditEvent, DriftDetectedEvent};
|
||||
use crate::domain::common::log::audit::AuditLog;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::interface::port::audit::AuditRepo;
|
||||
|
||||
/// Subscribes to `AuditEvent` and persists each entry to the `audit_log` table.
|
||||
@ -19,12 +19,16 @@ impl AuditLogger {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// Subscribe to AuditEvent and DriftDetectedEvent on the communication manager
|
||||
/// and start background tasks that persist events to DB + structured logs.
|
||||
pub fn start(self: Arc<Self>, comm: &CommunicationManager) {
|
||||
// Subscribe to AuditEvent
|
||||
if let Ok(mut rx) = comm.subscribe_event::<AuditEvent>() {
|
||||
/// Start background tasks that persist audit and drift events to DB + structured logs.
|
||||
pub fn start(
|
||||
self: Arc<Self>,
|
||||
audit_rx: broadcast::Receiver<AuditEvent>,
|
||||
drift_rx: broadcast::Receiver<DriftDetectedEvent>,
|
||||
) {
|
||||
// Drain AuditEvent receiver
|
||||
{
|
||||
let this = self.clone();
|
||||
let mut rx = audit_rx;
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
@ -41,13 +45,12 @@ impl AuditLogger {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log!(AuditLog::AuditSubscribeFailed);
|
||||
}
|
||||
|
||||
// Subscribe to DriftDetectedEvent — log as audit trail entry
|
||||
if let Ok(mut rx) = comm.subscribe_event::<DriftDetectedEvent>() {
|
||||
// Drain DriftDetectedEvent receiver — log as audit trail entry
|
||||
{
|
||||
let this = self;
|
||||
let mut rx = drift_rx;
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
@ -64,8 +67,6 @@ impl AuditLogger {
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log!(AuditLog::AuditSubscribeFailed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,373 +0,0 @@
|
||||
//! Cross-BC in-process event bus (technical service, not a BC).
|
||||
//!
|
||||
//! Per `docs/strategy/DOMAIN_MAP.md` §2, Communication Bus is a Technical
|
||||
//! Service — it has no ubiquitous language, no domain expert, no aggregate.
|
||||
//! It stays in `infrastructure/` and never takes a BC folder name. The trait
|
||||
//! surface (`Event`, `Command`, `Query`, `EventBroadcaster`, `CommandHandler`)
|
||||
//! lives at `interface/communication/` and remains untouched.
|
||||
|
||||
use std::any::{Any, TypeId};
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::interface::communication::command::*;
|
||||
use crate::interface::communication::event::Event;
|
||||
use crate::interface::communication::event::EventBroadcaster;
|
||||
use crate::interface::communication::query::*;
|
||||
|
||||
/// Inline TypedEventBroadcaster (adapted from MirrorSphere's model).
|
||||
pub struct TypedEventBroadcaster<E: Event> {
|
||||
pub sender: broadcast::Sender<E>,
|
||||
}
|
||||
|
||||
impl<E: Event + 'static> EventBroadcaster for TypedEventBroadcaster<E> {
|
||||
fn subscribe_typed(&self) -> Box<dyn Any + Send> {
|
||||
Box::new(self.sender.subscribe())
|
||||
}
|
||||
|
||||
fn broadcast_event(&self, event: Box<dyn Any + Send>) -> Result<(), Error> {
|
||||
let typed_event = *event.downcast::<E>().map_err(|_| MiscError::TypeMismatch)?;
|
||||
let _ = self.sender.send(typed_event);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Central communication hub using the command/query/event pattern.
|
||||
/// Adapted from MirrorSphere's CommunicationManager for NetGuardia.
|
||||
pub struct CommunicationManager {
|
||||
command_handlers: DashMap<TypeId, CommandHandlerFn>,
|
||||
query_handlers: DashMap<TypeId, QueryHandlerFn>,
|
||||
event_broadcasters: DashMap<TypeId, Box<dyn EventBroadcaster>>,
|
||||
channel_capacity: usize,
|
||||
}
|
||||
|
||||
impl CommunicationManager {
|
||||
pub fn new(channel_capacity: usize) -> Self {
|
||||
Self {
|
||||
command_handlers: DashMap::new(),
|
||||
query_handlers: DashMap::new(),
|
||||
event_broadcasters: DashMap::new(),
|
||||
channel_capacity: channel_capacity.max(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_service<S: Send + Sync + 'static>(self: Arc<Self>, service: Arc<S>) -> ServiceRegistrar<S> {
|
||||
ServiceRegistrar::new(service, self)
|
||||
}
|
||||
|
||||
pub fn register_command_handler<C: Command + 'static>(&self, handler: Arc<dyn CommandHandler<C> + Send + Sync>) {
|
||||
let type_id = TypeId::of::<C>();
|
||||
let boxed_handler: CommandHandlerFn = Box::new(move |command: Box<dyn Any + Send>| {
|
||||
let handler = handler.clone();
|
||||
Box::pin(async move {
|
||||
let command = *command.downcast::<C>().map_err(|_| MiscError::TypeMismatch)?;
|
||||
handler.handle_command(command).await
|
||||
}) as CommandFuture
|
||||
});
|
||||
|
||||
self.command_handlers.insert(type_id, boxed_handler);
|
||||
}
|
||||
|
||||
pub async fn send_command<C: Command + 'static>(&self, command: C) -> Result<(), Error> {
|
||||
let type_id = TypeId::of::<C>();
|
||||
if let Some(handler) = self.command_handlers.get(&type_id) {
|
||||
handler(Box::new(command)).await
|
||||
} else {
|
||||
Err(MiscError::HandlerNotFound)?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_query_handler<Q: Query + 'static>(&self, handler: Arc<dyn QueryHandler<Q> + Send + Sync>) {
|
||||
let type_id = TypeId::of::<Q>();
|
||||
let boxed_handler: QueryHandlerFn = Box::new(move |query: Box<dyn Any + Send>| {
|
||||
let handler = handler.clone();
|
||||
Box::pin(async move {
|
||||
let query = *query.downcast::<Q>().map_err(|_| MiscError::TypeMismatch)?;
|
||||
let response = handler.handle_query(query).await?;
|
||||
Ok(Box::new(response) as Box<dyn Any + Send>)
|
||||
}) as QueryFuture
|
||||
});
|
||||
|
||||
self.query_handlers.insert(type_id, boxed_handler);
|
||||
}
|
||||
|
||||
pub async fn send_query<Q: Query + 'static>(&self, query: Q) -> Result<Q::Response, Error> {
|
||||
let type_id = TypeId::of::<Q>();
|
||||
if let Some(handler) = self.query_handlers.get(&type_id) {
|
||||
let response = handler(Box::new(query)).await?;
|
||||
Ok(*response
|
||||
.downcast::<Q::Response>()
|
||||
.map_err(|_| MiscError::TypeMismatch)?)
|
||||
} else {
|
||||
Err(MiscError::HandlerNotFound)?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_event_type<E: Event + 'static>(&self) {
|
||||
let type_id = TypeId::of::<E>();
|
||||
let (tx, _) = broadcast::channel::<E>(self.channel_capacity);
|
||||
let broadcaster = TypedEventBroadcaster { sender: tx };
|
||||
self.event_broadcasters.insert(type_id, Box::new(broadcaster));
|
||||
}
|
||||
|
||||
pub fn subscribe_event<E: Event + 'static>(&self) -> Result<broadcast::Receiver<E>, Error> {
|
||||
let type_id = TypeId::of::<E>();
|
||||
let broadcaster = self
|
||||
.event_broadcasters
|
||||
.get(&type_id)
|
||||
.ok_or(MiscError::TypeNotRegistered)?;
|
||||
let receiver_box = broadcaster.subscribe_typed();
|
||||
let receiver = *receiver_box
|
||||
.downcast::<broadcast::Receiver<E>>()
|
||||
.map_err(|_| MiscError::TypeMismatch)?;
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
pub async fn publish_event<E: Event + 'static>(&self, event: E) -> Result<(), Error> {
|
||||
let type_id = TypeId::of::<E>();
|
||||
let broadcaster = self
|
||||
.event_broadcasters
|
||||
.get(&type_id)
|
||||
.ok_or(MiscError::TypeNotRegistered)?;
|
||||
broadcaster.broadcast_event(Box::new(event))
|
||||
}
|
||||
|
||||
/// Synchronous counterpart for callers that live outside the tokio
|
||||
/// runtime — in particular, the Flow Trace writer thread, which
|
||||
/// runs on a dedicated `std::thread` and can't `.await`. The
|
||||
/// internal broadcast channel is already non-blocking, so the
|
||||
/// `async fn` sibling never actually yields; this variant exposes
|
||||
/// the same work without the ceremony.
|
||||
pub fn publish_event_sync<E: Event + 'static>(&self, event: E) -> Result<(), Error> {
|
||||
let type_id = TypeId::of::<E>();
|
||||
let broadcaster = self
|
||||
.event_broadcasters
|
||||
.get(&type_id)
|
||||
.ok_or(MiscError::TypeNotRegistered)?;
|
||||
broadcaster.broadcast_event(Box::new(event))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fluent builder for registering a service's command/query/event handlers.
|
||||
pub struct ServiceRegistrar<S> {
|
||||
service: Arc<S>,
|
||||
comm: Arc<CommunicationManager>,
|
||||
}
|
||||
|
||||
impl<S: Send + Sync + 'static> ServiceRegistrar<S> {
|
||||
fn new(service: Arc<S>, comm: Arc<CommunicationManager>) -> Self {
|
||||
Self { service, comm }
|
||||
}
|
||||
|
||||
pub fn command<C: Command + 'static>(self) -> Self
|
||||
where
|
||||
S: CommandHandler<C>,
|
||||
{
|
||||
let handler: Arc<dyn CommandHandler<C> + Send + Sync> = self.service.clone();
|
||||
self.comm.register_command_handler::<C>(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn query<Q: Query + 'static>(self) -> Self
|
||||
where
|
||||
S: QueryHandler<Q>,
|
||||
{
|
||||
let handler: Arc<dyn QueryHandler<Q> + Send + Sync> = self.service.clone();
|
||||
self.comm.register_query_handler::<Q>(handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Arc<CommunicationManager> {
|
||||
self.comm
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Mutex;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::*;
|
||||
use crate::interface::communication::command::Command;
|
||||
use crate::interface::communication::event::Event;
|
||||
use crate::interface::communication::message::Message;
|
||||
use crate::interface::communication::query::Query;
|
||||
|
||||
// ── Test Command ─────────────────────────────────────────────────
|
||||
|
||||
struct TestCommand {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl Message for TestCommand {
|
||||
type Response = ();
|
||||
}
|
||||
impl Command for TestCommand {}
|
||||
|
||||
struct TestCommandHandler {
|
||||
received: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CommandHandler<TestCommand> for TestCommandHandler {
|
||||
async fn handle_command(&self, command: TestCommand) -> Result<(), Error> {
|
||||
self.received.lock().unwrap().push(command.value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test Query ───────────────────────────────────────────────────
|
||||
|
||||
struct TestQuery {
|
||||
input: i32,
|
||||
}
|
||||
|
||||
impl Message for TestQuery {
|
||||
type Response = i32;
|
||||
}
|
||||
impl Query for TestQuery {}
|
||||
|
||||
struct TestQueryHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl QueryHandler<TestQuery> for TestQueryHandler {
|
||||
async fn handle_query(&self, query: TestQuery) -> Result<i32, Error> {
|
||||
Ok(query.input * 2)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test Event ───────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestEvent {
|
||||
message: String,
|
||||
}
|
||||
impl Event for TestEvent {}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_command_dispatch() {
|
||||
let received = Arc::new(Mutex::new(Vec::new()));
|
||||
let handler = Arc::new(TestCommandHandler {
|
||||
received: received.clone(),
|
||||
});
|
||||
|
||||
let comm = Arc::new(CommunicationManager::new(256));
|
||||
comm.register_command_handler::<TestCommand>(handler);
|
||||
|
||||
comm.send_command(TestCommand { value: "hello".into() }).await.unwrap();
|
||||
|
||||
let msgs = received.lock().unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
assert_eq!(msgs[0], "hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_command_not_found() {
|
||||
let comm = CommunicationManager::new(256);
|
||||
let result = comm.send_command(TestCommand { value: "nope".into() }).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_dispatch() {
|
||||
let handler = Arc::new(TestQueryHandler);
|
||||
let comm = Arc::new(CommunicationManager::new(256));
|
||||
comm.register_query_handler::<TestQuery>(handler);
|
||||
|
||||
let result = comm.send_query(TestQuery { input: 21 }).await.unwrap();
|
||||
assert_eq!(result, 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_query_not_found() {
|
||||
let comm = CommunicationManager::new(256);
|
||||
let result = comm.send_query(TestQuery { input: 1 }).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_event_pub_sub() {
|
||||
let comm = CommunicationManager::new(256);
|
||||
comm.register_event_type::<TestEvent>();
|
||||
|
||||
let mut receiver = comm.subscribe_event::<TestEvent>().unwrap();
|
||||
|
||||
comm.publish_event(TestEvent { message: "ping".into() }).await.unwrap();
|
||||
|
||||
let event = receiver.recv().await.unwrap();
|
||||
assert_eq!(event.message, "ping");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_event_not_registered() {
|
||||
let comm = CommunicationManager::new(256);
|
||||
let result = comm.subscribe_event::<TestEvent>();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_event_sync_delivers_to_subscriber() {
|
||||
let comm = CommunicationManager::new(256);
|
||||
comm.register_event_type::<TestEvent>();
|
||||
let mut rx = comm.subscribe_event::<TestEvent>().unwrap();
|
||||
|
||||
comm.publish_event_sync(TestEvent { message: "sync".into() }).unwrap();
|
||||
|
||||
let event = rx.recv().await.unwrap();
|
||||
assert_eq!(event.message, "sync");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publish_event_sync_errors_when_type_unregistered() {
|
||||
let comm = CommunicationManager::new(256);
|
||||
let result = comm.publish_event_sync(TestEvent {
|
||||
message: "dropped".into(),
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_event_multiple_subscribers() {
|
||||
let comm = CommunicationManager::new(256);
|
||||
comm.register_event_type::<TestEvent>();
|
||||
|
||||
let mut rx1 = comm.subscribe_event::<TestEvent>().unwrap();
|
||||
let mut rx2 = comm.subscribe_event::<TestEvent>().unwrap();
|
||||
|
||||
comm.publish_event(TestEvent {
|
||||
message: "broadcast".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(rx1.recv().await.unwrap().message, "broadcast");
|
||||
assert_eq!(rx2.recv().await.unwrap().message, "broadcast");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_service_registrar() {
|
||||
let received = Arc::new(Mutex::new(Vec::new()));
|
||||
let handler = Arc::new(TestCommandHandler {
|
||||
received: received.clone(),
|
||||
});
|
||||
|
||||
let comm = Arc::new(CommunicationManager::new(256));
|
||||
let _comm = comm.clone().with_service(handler).command::<TestCommand>().build();
|
||||
|
||||
comm.send_command(TestCommand {
|
||||
value: "via_registrar".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msgs = received.lock().unwrap();
|
||||
assert_eq!(msgs[0], "via_registrar");
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::event::AuditEvent;
|
||||
use crate::domain::common::log::system::SystemLog;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::interface::communication::command::CommandHandler;
|
||||
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
|
||||
use crate::interface::communication::query::QueryHandler;
|
||||
use crate::interface::communication::query_types::GetEnforceModeQuery;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
|
||||
/// Map enforce-mode string to u8: monitor=0, ml_only=1, enforce=2.
|
||||
@ -26,46 +21,36 @@ pub fn enforce_mode_to_u8(mode: &str) -> u8 {
|
||||
/// Handles enforce-mode commands and queries by delegating to the repository.
|
||||
pub struct EnforceModeHandler {
|
||||
db: Arc<dyn AppRepo>,
|
||||
comm: Arc<CommunicationManager>,
|
||||
audit_tx: broadcast::Sender<AuditEvent>,
|
||||
/// Shared AtomicU8 cache: Monitor=0, MlOnly=1, Enforce=2.
|
||||
enforce_cache: Arc<AtomicU8>,
|
||||
}
|
||||
|
||||
impl EnforceModeHandler {
|
||||
pub fn new(db: Arc<dyn AppRepo>, comm: Arc<CommunicationManager>, enforce_cache: Arc<AtomicU8>) -> Self {
|
||||
pub fn new(db: Arc<dyn AppRepo>, audit_tx: broadcast::Sender<AuditEvent>, enforce_cache: Arc<AtomicU8>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
comm,
|
||||
audit_tx,
|
||||
enforce_cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CommandHandler<ChangeEnforceModeCommand> for EnforceModeHandler {
|
||||
async fn handle_command(&self, command: ChangeEnforceModeCommand) -> Result<(), Error> {
|
||||
self.db.set_setting("enforce_mode", &command.mode)?;
|
||||
self.enforce_cache
|
||||
.store(enforce_mode_to_u8(&command.mode), Ordering::SeqCst);
|
||||
log!(SystemLog::EnforceModeChanged(command.mode.clone()));
|
||||
pub fn change_mode(&self, mode: String) -> Result<(), Error> {
|
||||
self.db.set_setting("enforce_mode", &mode)?;
|
||||
self.enforce_cache.store(enforce_mode_to_u8(&mode), Ordering::SeqCst);
|
||||
log!(SystemLog::EnforceModeChanged(mode.clone()));
|
||||
|
||||
// Publish audit event for the mode change
|
||||
let _ = self
|
||||
.comm
|
||||
.publish_event(AuditEvent {
|
||||
actor: "admin".to_string(),
|
||||
action: "enforce_mode_changed".to_string(),
|
||||
detail: serde_json::json!({ "new_mode": command.mode }).to_string(),
|
||||
})
|
||||
.await;
|
||||
let _ = self.audit_tx.send(AuditEvent {
|
||||
actor: "admin".to_string(),
|
||||
action: "enforce_mode_changed".to_string(),
|
||||
detail: serde_json::json!({ "new_mode": mode }).to_string(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl QueryHandler<GetEnforceModeQuery> for EnforceModeHandler {
|
||||
async fn handle_query(&self, _query: GetEnforceModeQuery) -> Result<String, Error> {
|
||||
pub fn get_mode(&self) -> Result<String, Error> {
|
||||
match self.db.get_setting("enforce_mode")? {
|
||||
Some(mode) => Ok(mode),
|
||||
None => Ok("monitor".to_string()),
|
||||
@ -77,73 +62,53 @@ impl QueryHandler<GetEnforceModeQuery> for EnforceModeHandler {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::adapter::persistence::Database;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
|
||||
use crate::interface::communication::query_types::GetEnforceModeQuery;
|
||||
use crate::domain::common::config::constants::EVENT_CHANNEL_CAPACITY;
|
||||
|
||||
fn test_handler() -> (Arc<EnforceModeHandler>, Arc<CommunicationManager>) {
|
||||
fn test_handler() -> EnforceModeHandler {
|
||||
let db = Arc::new(Database::new(":memory:").unwrap()) as Arc<dyn AppRepo>;
|
||||
let cache = Arc::new(AtomicU8::new(0));
|
||||
let comm = Arc::new(CommunicationManager::new(256));
|
||||
comm.register_event_type::<AuditEvent>();
|
||||
let handler = Arc::new(EnforceModeHandler::new(db, comm.clone(), cache));
|
||||
let _ = comm
|
||||
.clone()
|
||||
.with_service(handler.clone())
|
||||
.command::<ChangeEnforceModeCommand>()
|
||||
.query::<GetEnforceModeQuery>()
|
||||
.build();
|
||||
(handler, comm)
|
||||
let (audit_tx, _rx) = broadcast::channel::<AuditEvent>(EVENT_CHANNEL_CAPACITY);
|
||||
EnforceModeHandler::new(db, audit_tx, cache)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_mode_is_monitor() {
|
||||
let (_, comm) = test_handler();
|
||||
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
|
||||
#[test]
|
||||
fn test_default_mode_is_monitor() {
|
||||
let handler = test_handler();
|
||||
let mode = handler.get_mode().unwrap();
|
||||
assert_eq!(mode, "monitor");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_to_enforce() {
|
||||
let (_, comm) = test_handler();
|
||||
comm.send_command(ChangeEnforceModeCommand { mode: "enforce".into() })
|
||||
.await
|
||||
.unwrap();
|
||||
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
|
||||
#[test]
|
||||
fn test_change_to_enforce() {
|
||||
let handler = test_handler();
|
||||
handler.change_mode("enforce".into()).unwrap();
|
||||
let mode = handler.get_mode().unwrap();
|
||||
assert_eq!(mode, "enforce");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_back_to_monitor() {
|
||||
let (_, comm) = test_handler();
|
||||
comm.send_command(ChangeEnforceModeCommand { mode: "enforce".into() })
|
||||
.await
|
||||
.unwrap();
|
||||
comm.send_command(ChangeEnforceModeCommand { mode: "monitor".into() })
|
||||
.await
|
||||
.unwrap();
|
||||
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
|
||||
#[test]
|
||||
fn test_change_back_to_monitor() {
|
||||
let handler = test_handler();
|
||||
handler.change_mode("enforce".into()).unwrap();
|
||||
handler.change_mode("monitor".into()).unwrap();
|
||||
let mode = handler.get_mode().unwrap();
|
||||
assert_eq!(mode, "monitor");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_change_to_ml_only() {
|
||||
let (_, comm) = test_handler();
|
||||
comm.send_command(ChangeEnforceModeCommand { mode: "ml_only".into() })
|
||||
.await
|
||||
.unwrap();
|
||||
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
|
||||
#[test]
|
||||
fn test_change_to_ml_only() {
|
||||
let handler = test_handler();
|
||||
handler.change_mode("ml_only".into()).unwrap();
|
||||
let mode = handler.get_mode().unwrap();
|
||||
assert_eq!(mode, "ml_only");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cycle_all_modes() {
|
||||
let (_, comm) = test_handler();
|
||||
#[test]
|
||||
fn test_cycle_all_modes() {
|
||||
let handler = test_handler();
|
||||
for mode_str in ["enforce", "ml_only", "monitor"] {
|
||||
comm.send_command(ChangeEnforceModeCommand { mode: mode_str.into() })
|
||||
.await
|
||||
.unwrap();
|
||||
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
|
||||
handler.change_mode(mode_str.into()).unwrap();
|
||||
let mode = handler.get_mode().unwrap();
|
||||
assert_eq!(mode, mode_str);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ use actix_web::web::route;
|
||||
use actix_web::{App, HttpResponse, HttpServer, web};
|
||||
use arc_swap::ArcSwap;
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::adapter::ebpf::EbpfServices;
|
||||
use crate::adapter::http::model_upload::PromoteGate;
|
||||
@ -19,6 +20,7 @@ use crate::adapter::http::{
|
||||
use crate::adapter::persistence::Database;
|
||||
use crate::adapter::websocket::routes as ws;
|
||||
use crate::core::common::config_service::ConfigService;
|
||||
use crate::core::common::log_buffer::LogBuffer;
|
||||
use crate::core::common::notification_service::NotificationService;
|
||||
use crate::core::data_plane::acl_service::AclService;
|
||||
use crate::core::data_plane::dns_filter_service::DnsFilterService;
|
||||
@ -34,11 +36,13 @@ use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::config::constants::HTTP_FALLBACK_PORT;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::http::HttpError;
|
||||
use crate::domain::common::event::{AuditEvent, ThreatDetectedEvent};
|
||||
use crate::domain::common::log::http::HttpLog;
|
||||
use crate::domain::common::system::readiness::ReadinessState;
|
||||
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
|
||||
use crate::infrastructure::app_services::AppServices;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::infrastructure::enforce_mode_handler::EnforceModeHandler;
|
||||
use crate::infrastructure::logger::Logger;
|
||||
use crate::infrastructure::secret_store::SecretStore;
|
||||
use crate::infrastructure::suricata_manager::SuricataManager;
|
||||
use crate::infrastructure::system::ShutdownHandle;
|
||||
@ -58,7 +62,9 @@ pub struct HttpServerParams {
|
||||
pub db: Arc<Database>,
|
||||
pub secret_store: Arc<SecretStore>,
|
||||
pub jwt_service: Arc<JwtService>,
|
||||
pub comm: Arc<CommunicationManager>,
|
||||
pub threat_tx: broadcast::Sender<ThreatDetectedEvent>,
|
||||
pub audit_tx: broadcast::Sender<AuditEvent>,
|
||||
pub enforce_handler: Arc<EnforceModeHandler>,
|
||||
pub setup_complete: SetupCompleteFlag,
|
||||
pub ready: ReadyFlag,
|
||||
pub readiness_state: Arc<ReadinessState>,
|
||||
@ -70,6 +76,8 @@ pub struct HttpServerParams {
|
||||
pub rate_limit_service: Arc<RateLimitService>,
|
||||
pub force_https: ForceHttpsFlag,
|
||||
pub shutdown_handle: Arc<ShutdownHandle>,
|
||||
pub logger: Arc<Logger>,
|
||||
pub log_buffer: Arc<LogBuffer>,
|
||||
pub suricata_manager: Arc<SuricataManager>,
|
||||
pub soar_engine: Arc<SoarEngine>,
|
||||
}
|
||||
@ -224,7 +232,9 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
|
||||
let db = params.db;
|
||||
let secret_store = params.secret_store;
|
||||
let jwt_service = params.jwt_service;
|
||||
let comm = params.comm;
|
||||
let threat_tx = params.threat_tx;
|
||||
let audit_tx = params.audit_tx;
|
||||
let enforce_handler = params.enforce_handler;
|
||||
let setup_complete = params.setup_complete;
|
||||
let ready = params.ready;
|
||||
let readiness_state = params.readiness_state;
|
||||
@ -236,6 +246,8 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
|
||||
let rate_limit_service = params.rate_limit_service;
|
||||
let force_https = params.force_https;
|
||||
let shutdown_handle = params.shutdown_handle;
|
||||
let logger = params.logger;
|
||||
let log_buffer = params.log_buffer;
|
||||
let suricata_manager = params.suricata_manager;
|
||||
let soar_engine = params.soar_engine;
|
||||
let port = app_config.load().http_server.port;
|
||||
@ -272,7 +284,9 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
|
||||
.app_data(web::Data::from(db.clone()))
|
||||
.app_data(web::Data::from(secret_store.clone()))
|
||||
.app_data(web::Data::from(jwt_service.clone()))
|
||||
.app_data(web::Data::from(comm.clone()))
|
||||
.app_data(web::Data::new(threat_tx.clone()))
|
||||
.app_data(web::Data::new(audit_tx.clone()))
|
||||
.app_data(web::Data::from(enforce_handler.clone()))
|
||||
.app_data(web::Data::new(setup_complete.clone()))
|
||||
.app_data(web::Data::new(ready.clone()))
|
||||
.app_data(web::Data::from(readiness_state.clone()))
|
||||
@ -282,6 +296,8 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
|
||||
.app_data(web::Data::from(notification_service.clone()))
|
||||
.app_data(web::Data::from(playbook_service.clone()))
|
||||
.app_data(web::Data::from(rate_limit_service.clone()))
|
||||
.app_data(web::Data::from(logger.clone()))
|
||||
.app_data(web::Data::from(log_buffer.clone()))
|
||||
.app_data(web::Data::from(suricata_manager.clone()))
|
||||
.app_data(web::Data::from(soar_engine.clone()))
|
||||
.app_data(web::Data::from(promote_lock.clone()));
|
||||
|
||||
177
net-guardia/src/infrastructure/logger.rs
Normal file
177
net-guardia/src/infrastructure/logger.rs
Normal file
@ -0,0 +1,177 @@
|
||||
use std::fs;
|
||||
use std::{env, io};
|
||||
|
||||
use tracing::Level;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::filter::Directive;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
use tracing_subscriber::fmt::layer as fmt_layer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::{Layer, filter, reload};
|
||||
|
||||
use crate::core::common::log_buffer::{LogBuffer, LogBufferLayer};
|
||||
use crate::domain::common::config::observability::ObservabilityConfig;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::io::IOError;
|
||||
use crate::interface::utils::logging::FilterControl;
|
||||
|
||||
pub struct Logger {
|
||||
filter_handle: Box<dyn FilterControl>,
|
||||
preserved_directives: Vec<String>,
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
pub fn initialize(config: &ObservabilityConfig) -> Result<(Self, LogBuffer), Error> {
|
||||
let log_directory = "logs";
|
||||
fs::create_dir_all(log_directory).map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
|
||||
|
||||
let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "NetGuardia");
|
||||
|
||||
let stdout_layer = fmt_layer()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_thread_ids(true)
|
||||
.with_target(false)
|
||||
.with_ansi(true);
|
||||
|
||||
let file_layer = fmt_layer()
|
||||
.with_file(false)
|
||||
.with_line_number(false)
|
||||
.with_thread_ids(false)
|
||||
.with_target(true)
|
||||
.with_ansi(false)
|
||||
.with_writer(file_appender);
|
||||
|
||||
let level: Level = config
|
||||
.log_level
|
||||
.parse()
|
||||
.ok()
|
||||
.or_else(|| env::var("RUST_LOG").ok().and_then(|s| s.parse().ok()))
|
||||
.unwrap_or(Level::INFO);
|
||||
|
||||
let mut preserved: Vec<String> = env::var("RUST_LOG")
|
||||
.ok()
|
||||
.map(|raw| {
|
||||
raw.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|d| !d.is_empty() && d.contains('='))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if !preserved.iter().any(|d| d == "maxminddb=warn") {
|
||||
preserved.push("maxminddb=warn".to_string());
|
||||
}
|
||||
|
||||
let mut filter = EnvFilter::new(level.to_string());
|
||||
filter = Self::apply_directives(filter, &preserved);
|
||||
let (filter_layer, reload_handle) = reload::Layer::new(filter);
|
||||
|
||||
let (log_buffer_layer, log_buffer) =
|
||||
LogBufferLayer::new(config.log_buffer_capacity, config.log_buffer_max_message_bytes);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(stdout_layer)
|
||||
.with(file_layer)
|
||||
.with(log_buffer_layer)
|
||||
.init();
|
||||
|
||||
let logger = Self {
|
||||
filter_handle: Box::new(reload_handle),
|
||||
preserved_directives: preserved,
|
||||
};
|
||||
Ok((logger, log_buffer))
|
||||
}
|
||||
|
||||
pub fn initialize_cli() -> Result<(), Error> {
|
||||
let stdout_layer = fmt_layer()
|
||||
.without_time()
|
||||
.with_level(false)
|
||||
.with_target(false)
|
||||
.with_file(false)
|
||||
.with_line_number(false)
|
||||
.with_thread_ids(false)
|
||||
.with_ansi(false)
|
||||
.with_writer(io::stdout)
|
||||
.with_filter(LevelFilter::INFO);
|
||||
|
||||
let stderr_layer = fmt_layer()
|
||||
.without_time()
|
||||
.with_level(false)
|
||||
.with_target(false)
|
||||
.with_writer(io::stderr)
|
||||
.with_filter(filter::filter_fn(|m| m.level() <= &Level::WARN));
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(stdout_layer)
|
||||
.with(stderr_layer)
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_directives(mut filter: EnvFilter, directives: &[String]) -> EnvFilter {
|
||||
for d in directives {
|
||||
if let Ok(parsed) = d.parse::<Directive>() {
|
||||
filter = filter.add_directive(parsed);
|
||||
}
|
||||
}
|
||||
filter
|
||||
}
|
||||
|
||||
pub fn current_level(&self) -> String {
|
||||
extract_main_level(&self.filter_handle.current_filter())
|
||||
}
|
||||
|
||||
pub fn set_level(&self, level: &str) -> Result<String, String> {
|
||||
let parsed_level: Level = level.parse().map_err(|_| {
|
||||
format!(
|
||||
"Invalid log level '{}'. Valid levels: trace, debug, info, warn, error",
|
||||
level
|
||||
)
|
||||
})?;
|
||||
|
||||
let filter = EnvFilter::new(parsed_level.to_string());
|
||||
let filter = Self::apply_directives(filter, &self.preserved_directives);
|
||||
self.filter_handle.reload_filter(filter)?;
|
||||
|
||||
Ok(parsed_level.to_string().to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
impl<L> FilterControl for reload::Handle<EnvFilter, L> {
|
||||
fn reload_filter(&self, filter: EnvFilter) -> Result<(), String> {
|
||||
self.reload(filter).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn current_filter(&self) -> String {
|
||||
self.with_current(|f| f.to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_main_level(raw: &str) -> String {
|
||||
raw.split(',')
|
||||
.map(str::trim)
|
||||
.find(|d| !d.is_empty() && !d.contains('='))
|
||||
.unwrap_or(raw)
|
||||
.to_lowercase()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn strips_per_target_directives() {
|
||||
assert_eq!(extract_main_level("maxminddb=warn,debug"), "debug");
|
||||
assert_eq!(extract_main_level("info,maxminddb=warn"), "info");
|
||||
assert_eq!(extract_main_level("DEBUG"), "debug");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_when_no_bare_level() {
|
||||
assert_eq!(extract_main_level("maxminddb=warn"), "maxminddb=warn");
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::{Layer, filter, reload};
|
||||
|
||||
use crate::core::common::observability::log_buffer::LogBufferLayer;
|
||||
use crate::domain::common::config::observability::ObservabilityConfig;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::io::IOError;
|
||||
use crate::interface::utils::logging::FilterControl;
|
||||
@ -30,7 +31,7 @@ static PRESERVED_DIRECTIVES: OnceLock<Vec<String>> = OnceLock::new();
|
||||
pub struct Logging;
|
||||
|
||||
impl Logging {
|
||||
pub fn initialize(log_buffer_capacity: usize, log_buffer_max_message_bytes: usize) -> Result<(), Error> {
|
||||
pub fn initialize(config: &ObservabilityConfig) -> Result<(), Error> {
|
||||
let log_directory = "logs";
|
||||
fs::create_dir_all(log_directory).map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
|
||||
|
||||
@ -51,18 +52,13 @@ impl Logging {
|
||||
.with_ansi(false)
|
||||
.with_writer(file_appender);
|
||||
|
||||
let level = env::var("RUST_LOG")
|
||||
// RUST_LOG overrides the config value when present
|
||||
let level: Level = env::var("RUST_LOG")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<Level>().ok())
|
||||
.unwrap_or(if cfg!(debug_assertions) {
|
||||
Level::DEBUG
|
||||
} else {
|
||||
Level::INFO
|
||||
});
|
||||
.and_then(|s| s.parse().ok())
|
||||
.or_else(|| config.log_level.parse().ok())
|
||||
.unwrap_or(Level::INFO);
|
||||
|
||||
// Collect per-target directives from RUST_LOG plus our hardcoded
|
||||
// `maxminddb=warn` so `set_level` can reapply them on each rebuild
|
||||
// instead of losing them to `EnvFilter::new(level)`.
|
||||
let mut preserved: Vec<String> = env::var("RUST_LOG")
|
||||
.ok()
|
||||
.map(|raw| {
|
||||
@ -77,7 +73,7 @@ impl Logging {
|
||||
}
|
||||
let _ = PRESERVED_DIRECTIVES.set(preserved);
|
||||
|
||||
let mut filter = EnvFilter::from_default_env().add_directive(level.into());
|
||||
let mut filter = EnvFilter::new(level.to_string());
|
||||
if let Some(directives) = PRESERVED_DIRECTIVES.get() {
|
||||
for d in directives {
|
||||
if let Ok(parsed) = d.parse::<Directive>() {
|
||||
@ -92,10 +88,9 @@ impl Logging {
|
||||
.with(filter_layer)
|
||||
.with(stdout_layer)
|
||||
.with(file_layer)
|
||||
.with(LogBufferLayer::new(log_buffer_capacity, log_buffer_max_message_bytes))
|
||||
.with(LogBufferLayer::new(config.log_buffer_capacity, config.log_buffer_max_message_bytes))
|
||||
.init();
|
||||
|
||||
// Store type-erased handle for runtime log level changes
|
||||
let _ = FILTER_HANDLE.set(Box::new(reload_handle));
|
||||
|
||||
Ok(())
|
||||
@ -1,12 +1,12 @@
|
||||
pub mod app_services;
|
||||
pub mod audit_logger;
|
||||
pub mod cli;
|
||||
pub mod communication_manager;
|
||||
pub mod ebpf_preflight;
|
||||
pub mod enforce_mode_handler;
|
||||
pub mod geoip;
|
||||
pub mod health;
|
||||
pub mod http_server;
|
||||
pub mod logger;
|
||||
pub mod secret_store;
|
||||
pub mod service_factory;
|
||||
pub mod statistics;
|
||||
|
||||
@ -12,6 +12,7 @@ use aya::programs::{Xdp, XdpFlags};
|
||||
use aya_log::EbpfLogger;
|
||||
use common::define::pipeline::*;
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::adapter::access_control::AccessControlAdapter;
|
||||
use crate::adapter::ebpf::EbpfServices;
|
||||
@ -29,6 +30,7 @@ use crate::core::response::engine::SoarEngine;
|
||||
use crate::core::response::playbook_service::PlaybookService;
|
||||
use crate::core::response::scheduler::TtlScheduler;
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::config::constants::EVENT_CHANNEL_CAPACITY;
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::misc::MiscError;
|
||||
use crate::domain::common::event::{AuditEvent, DriftDetectedEvent, ThreatDetectedEvent};
|
||||
@ -44,14 +46,11 @@ use crate::domain::detection::log::MLLog;
|
||||
use crate::domain::detection::manifest::ModelManifest;
|
||||
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
|
||||
use crate::infrastructure::app_services::AppServices;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::infrastructure::ebpf_preflight;
|
||||
use crate::infrastructure::enforce_mode_handler::EnforceModeHandler;
|
||||
use crate::infrastructure::geoip::GeoIpService;
|
||||
use crate::infrastructure::secret_store::SecretStore;
|
||||
use crate::infrastructure::suricata_manager::SuricataManager;
|
||||
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
|
||||
use crate::interface::communication::query_types::GetEnforceModeQuery;
|
||||
use crate::interface::port::access_control::AccessControlPort;
|
||||
use crate::interface::port::access_control_admin::AccessControlAdminPort;
|
||||
use crate::interface::port::app_repo::AppRepo;
|
||||
@ -70,10 +69,13 @@ pub struct AppState {
|
||||
pub inference_config: Arc<MLInferenceConfig>,
|
||||
pub ebpf_services: Arc<EbpfServices>,
|
||||
pub app_services: Arc<AppServices>,
|
||||
pub db: Arc<Database>,
|
||||
pub database: Arc<Database>,
|
||||
pub secret_store: Arc<SecretStore>,
|
||||
pub jwt_service: Arc<JwtService>,
|
||||
pub comm: Arc<CommunicationManager>,
|
||||
pub threat_tx: broadcast::Sender<ThreatDetectedEvent>,
|
||||
pub audit_tx: broadcast::Sender<AuditEvent>,
|
||||
pub drift_tx: broadcast::Sender<DriftDetectedEvent>,
|
||||
pub enforce_handler: Arc<EnforceModeHandler>,
|
||||
pub soar_engine: Arc<SoarEngine>,
|
||||
pub ttl_scheduler: TtlScheduler,
|
||||
pub report_scheduler: ReportScheduler,
|
||||
@ -175,7 +177,7 @@ impl ServiceFactory {
|
||||
|
||||
let jwt_service = Arc::new(JwtService::new(
|
||||
&secret_store_port,
|
||||
app_config.load().auth.jwt_expiry_hours,
|
||||
app_config.load().http_server.jwt_expiry_hours,
|
||||
)?);
|
||||
|
||||
// Initialize ML drift detector from inference config baselines
|
||||
@ -199,17 +201,13 @@ impl ServiceFactory {
|
||||
enforce_mode_to_u8(&mode_str)
|
||||
}));
|
||||
|
||||
// CommunicationManager and its event channels must exist before
|
||||
// AppServices spins up the TrafficLogger: the writer thread can
|
||||
// publish `flow_trace_stopped` audit events the moment it tries
|
||||
// to open its first rotated file, and an unregistered channel
|
||||
// would silently drop that evidence.
|
||||
let comm = Arc::new(CommunicationManager::new(
|
||||
app_config.load().observability.default_event_channel_capacity,
|
||||
));
|
||||
comm.register_event_type::<ThreatDetectedEvent>();
|
||||
comm.register_event_type::<DriftDetectedEvent>();
|
||||
comm.register_event_type::<AuditEvent>();
|
||||
// Named broadcast channels replace the old TypeId-based CommunicationManager.
|
||||
// They must exist before AppServices spins up the TrafficLogger: the writer
|
||||
// thread can publish `flow_trace_stopped` audit events the moment it tries
|
||||
// to open its first rotated file.
|
||||
let (threat_tx, _) = broadcast::channel::<ThreatDetectedEvent>(EVENT_CHANNEL_CAPACITY);
|
||||
let (audit_tx, _) = broadcast::channel::<AuditEvent>(EVENT_CHANNEL_CAPACITY);
|
||||
let (drift_tx, _) = broadcast::channel::<DriftDetectedEvent>(EVENT_CHANNEL_CAPACITY);
|
||||
|
||||
let app_services = Arc::new(AppServices::new(
|
||||
app_config.clone(),
|
||||
@ -217,20 +215,14 @@ impl ServiceFactory {
|
||||
ml_manifest.clone(),
|
||||
drift_detector.clone(),
|
||||
ebpf_health.clone(),
|
||||
comm.clone(),
|
||||
audit_tx.clone(),
|
||||
)?);
|
||||
|
||||
let enforce_handler = Arc::new(EnforceModeHandler::new(
|
||||
db.clone() as Arc<dyn AppRepo>,
|
||||
comm.clone(),
|
||||
audit_tx.clone(),
|
||||
enforce_level_cache.clone(),
|
||||
));
|
||||
let _ = comm
|
||||
.clone()
|
||||
.with_service(enforce_handler)
|
||||
.command::<ChangeEnforceModeCommand>()
|
||||
.query::<GetEnforceModeQuery>()
|
||||
.build();
|
||||
|
||||
// Seed default SOAR playbooks if empty
|
||||
(db.as_ref() as &dyn SoarRepo).seed_default_playbooks()?;
|
||||
@ -337,10 +329,13 @@ impl ServiceFactory {
|
||||
inference_config,
|
||||
ebpf_services,
|
||||
app_services,
|
||||
db,
|
||||
database: db,
|
||||
secret_store,
|
||||
jwt_service,
|
||||
comm,
|
||||
threat_tx,
|
||||
audit_tx,
|
||||
drift_tx,
|
||||
enforce_handler,
|
||||
soar_engine,
|
||||
ttl_scheduler,
|
||||
report_scheduler,
|
||||
|
||||
@ -28,7 +28,7 @@ use tokio::time::sleep;
|
||||
|
||||
use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::event::{DetectionEvent, DetectionSource};
|
||||
use crate::domain::detection::log::SuricataLog;
|
||||
use crate::domain::detection::log::{DetectionLog, SuricataLog};
|
||||
|
||||
/// IANA protocol numbers for Suricata's `proto` strings.
|
||||
const IANA_PROTO_ICMP: u8 = 1;
|
||||
@ -139,7 +139,13 @@ impl SuricataMonitor {
|
||||
};
|
||||
// mpsc is bounded; if the orchestrator is backed up, drop rather than
|
||||
// block the tail (eve.json will fill the disk if we block).
|
||||
let _ = self.detection_tx.try_send(event);
|
||||
if let Err(tokio::sync::mpsc::error::TrySendError::Full(d)) = self.detection_tx.try_send(event) {
|
||||
log!(DetectionLog::DetectionChannelDrop(
|
||||
format!("{:?}", d.source),
|
||||
d.attack_type,
|
||||
d.source_ip,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a Suricata alert JSON object to a DetectionEvent. Returns None if
|
||||
@ -184,9 +190,12 @@ impl SuricataMonitor {
|
||||
signature,
|
||||
));
|
||||
|
||||
let category = alert.get("category").and_then(|x| x.as_str()).unwrap_or("unknown");
|
||||
let canonical = crate::domain::detection::attack_type::translate(DetectionSource::Suricata, category);
|
||||
|
||||
Some(DetectionEvent {
|
||||
source: DetectionSource::Suricata,
|
||||
attack_type: "suricata_alert".to_string(),
|
||||
attack_type: canonical.as_str().to_string(),
|
||||
confidence,
|
||||
source_ip: src_ip,
|
||||
dest_ip,
|
||||
|
||||
@ -9,7 +9,7 @@ use aya::maps::{MapData, ProgramArray};
|
||||
use macros::log;
|
||||
use sd_notify::NotifyState;
|
||||
use tokio::signal::ctrl_c;
|
||||
use tokio::sync::broadcast::{Receiver, error::RecvError};
|
||||
use tokio::sync::broadcast::{self, Receiver, error::RecvError};
|
||||
use tokio::sync::mpsc::{self, Sender};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::{interval, sleep};
|
||||
@ -18,6 +18,7 @@ use crate::adapter::ebpf::EbpfServices;
|
||||
use crate::adapter::http::model_upload;
|
||||
use crate::adapter::persistence::Database;
|
||||
use crate::core::common::config_service::ConfigService;
|
||||
use crate::core::common::log_buffer::LogBuffer;
|
||||
use crate::core::common::notification_service::NotificationService;
|
||||
use crate::core::correlation::engine::CorrelationEngine;
|
||||
use crate::core::data_plane::acl_service::AclService;
|
||||
@ -37,7 +38,8 @@ use crate::domain::common::config::AppConfig;
|
||||
use crate::domain::common::config::constants::{MODELS_DIR, STAGING_SUBDIR};
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::domain::common::error::system::SystemError;
|
||||
use crate::domain::common::event::{DetectionEvent, DetectionSource, DriftDetectedEvent};
|
||||
use crate::domain::common::event::AuditEvent;
|
||||
use crate::domain::common::event::{DetectionEvent, DetectionSource, DriftDetectedEvent, ThreatDetectedEvent};
|
||||
use crate::domain::common::log::system::SystemLog;
|
||||
use crate::domain::common::system::health::EbpfHealth;
|
||||
use crate::domain::common::system::readiness::ReadinessState;
|
||||
@ -47,8 +49,9 @@ use crate::domain::detection::ml_detection::AlertMessage;
|
||||
use crate::domain::detection::ml_inference_config::MLInferenceConfig;
|
||||
use crate::infrastructure::app_services::AppServices;
|
||||
use crate::infrastructure::audit_logger::AuditLogger;
|
||||
use crate::infrastructure::communication_manager::CommunicationManager;
|
||||
use crate::infrastructure::enforce_mode_handler::EnforceModeHandler;
|
||||
use crate::infrastructure::http_server::{self, HttpServerParams};
|
||||
use crate::infrastructure::logger::Logger;
|
||||
use crate::infrastructure::secret_store::SecretStore;
|
||||
use crate::infrastructure::service_factory::ServiceFactory;
|
||||
use crate::infrastructure::suricata_manager::SuricataManager;
|
||||
@ -59,17 +62,12 @@ use crate::interface::port::packet_sink::PacketSinkFactory;
|
||||
use crate::interface::port::setting::SettingRepo;
|
||||
use crate::interface::port::stats::StatsRepo;
|
||||
|
||||
/// API-triggered shutdown mode.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShutdownMode {
|
||||
Shutdown,
|
||||
Restart,
|
||||
}
|
||||
|
||||
/// Handle for triggering shutdown from HTTP endpoints. Backed by a
|
||||
/// capacity-1 mpsc so the first trigger atomically wins via `try_send`,
|
||||
/// and subsequent calls receive `TrySendError::Full` — no lock, no
|
||||
/// `Option::take`, no `Mutex`.
|
||||
pub struct ShutdownHandle {
|
||||
tx: Sender<ShutdownMode>,
|
||||
}
|
||||
@ -79,25 +77,23 @@ impl ShutdownHandle {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
/// Trigger shutdown. Returns false if already triggered or the
|
||||
/// receiver has been dropped.
|
||||
pub fn trigger(&self, mode: ShutdownMode) -> bool {
|
||||
self.tx.try_send(mode).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Orchestrates system lifecycle: startup ordering and shutdown.
|
||||
/// Construction is delegated to `ServiceFactory::build()`.
|
||||
/// Setup mode is handled by main.rs — System only runs when setup is complete.
|
||||
pub struct System {
|
||||
pub app_config: Arc<ArcSwap<AppConfig>>,
|
||||
pub inference_config: Arc<MLInferenceConfig>,
|
||||
pub ebpf_services: Arc<EbpfServices>,
|
||||
pub app_services: Arc<AppServices>,
|
||||
pub db: Arc<Database>,
|
||||
pub database: Arc<Database>,
|
||||
pub secret_store: Arc<SecretStore>,
|
||||
pub jwt_service: Arc<JwtService>,
|
||||
pub comm: Arc<CommunicationManager>,
|
||||
pub threat_tx: broadcast::Sender<ThreatDetectedEvent>,
|
||||
pub audit_tx: broadcast::Sender<AuditEvent>,
|
||||
pub drift_tx: broadcast::Sender<DriftDetectedEvent>,
|
||||
pub enforce_handler: Arc<EnforceModeHandler>,
|
||||
pub soar_engine: Arc<SoarEngine>,
|
||||
pub ttl_scheduler: Option<TtlScheduler>,
|
||||
pub report_scheduler: Option<ReportScheduler>,
|
||||
@ -119,18 +115,20 @@ pub struct System {
|
||||
}
|
||||
|
||||
impl System {
|
||||
/// Build System from DB. Only called after setup is confirmed complete.
|
||||
pub async fn new(db: Arc<Database>) -> Result<Self, Error> {
|
||||
let state = ServiceFactory::build(db).await?;
|
||||
pub async fn new(database: Arc<Database>) -> Result<Self, Error> {
|
||||
let state = ServiceFactory::build(database).await?;
|
||||
Ok(System {
|
||||
app_config: state.app_config,
|
||||
inference_config: state.inference_config,
|
||||
ebpf_services: state.ebpf_services,
|
||||
app_services: state.app_services,
|
||||
db: state.db,
|
||||
database: state.database,
|
||||
secret_store: state.secret_store,
|
||||
jwt_service: state.jwt_service,
|
||||
comm: state.comm,
|
||||
threat_tx: state.threat_tx,
|
||||
audit_tx: state.audit_tx,
|
||||
drift_tx: state.drift_tx,
|
||||
enforce_handler: state.enforce_handler,
|
||||
soar_engine: state.soar_engine,
|
||||
ttl_scheduler: Some(state.ttl_scheduler),
|
||||
report_scheduler: Some(state.report_scheduler),
|
||||
@ -152,9 +150,7 @@ impl System {
|
||||
})
|
||||
}
|
||||
|
||||
/// Start all services and HTTP server. Setup is already complete at this point.
|
||||
/// Returns the shutdown mode requested (Shutdown or Restart).
|
||||
pub async fn run(&mut self) -> Result<ShutdownMode, Error> {
|
||||
pub async fn run(&mut self, logger: Arc<Logger>, log_buffer: Arc<LogBuffer>) -> Result<ShutdownMode, Error> {
|
||||
log!(SystemLog::Initializing);
|
||||
|
||||
// Sweep model-upload staging directories left over from failed
|
||||
@ -226,7 +222,7 @@ impl System {
|
||||
|
||||
// Start SOAR engine
|
||||
self.soar_engine.recover_active_blocks().await?;
|
||||
self.soar_engine.clone().start(self.comm.clone())?;
|
||||
self.soar_engine.clone().start(self.threat_tx.subscribe());
|
||||
|
||||
// Start TTL scheduler
|
||||
if let Some(ttl) = self.ttl_scheduler.take() {
|
||||
@ -239,22 +235,22 @@ impl System {
|
||||
}
|
||||
|
||||
// Start audit logger (subscribe to AuditEvent + DriftDetectedEvent, persist to DB)
|
||||
let audit_logger = Arc::new(AuditLogger::new(self.db.clone() as Arc<dyn AuditRepo>));
|
||||
audit_logger.start(&self.comm);
|
||||
let audit_logger = Arc::new(AuditLogger::new(self.database.clone() as Arc<dyn AuditRepo>));
|
||||
audit_logger.start(self.audit_tx.subscribe(), self.drift_tx.subscribe());
|
||||
|
||||
// Start stats aggregator (writes weekly_* settings for Report engine)
|
||||
let stats_aggregator = StatsAggregator::new(
|
||||
self.db.clone() as Arc<dyn StatsRepo>,
|
||||
self.db.clone() as Arc<dyn SettingRepo + Send + Sync>,
|
||||
self.database.clone() as Arc<dyn StatsRepo>,
|
||||
self.database.clone() as Arc<dyn SettingRepo + Send + Sync>,
|
||||
);
|
||||
stats_aggregator.start();
|
||||
|
||||
// Start drift detection background task
|
||||
{
|
||||
let drift_detector = self.drift_detector.clone();
|
||||
let comm_drift = self.comm.clone();
|
||||
let drift_tx = self.drift_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
Self::run_drift_monitor(drift_detector, comm_drift).await;
|
||||
Self::run_drift_monitor(drift_detector, drift_tx).await;
|
||||
});
|
||||
}
|
||||
|
||||
@ -263,7 +259,8 @@ impl System {
|
||||
let orchestrator = DetectionOrchestrator::new(
|
||||
&self.app_config,
|
||||
detection_rx,
|
||||
self.comm.clone(),
|
||||
self.threat_tx.clone(),
|
||||
self.audit_tx.clone(),
|
||||
self.geoip.clone(),
|
||||
self.app_services.fusion_metrics.clone(),
|
||||
);
|
||||
@ -296,7 +293,7 @@ impl System {
|
||||
|
||||
// Initialize force_https flag from DB setting
|
||||
let force_https = Arc::new(AtomicBool::new(
|
||||
self.db
|
||||
self.database
|
||||
.get_setting("force_https")
|
||||
.ok()
|
||||
.flatten()
|
||||
@ -330,10 +327,12 @@ impl System {
|
||||
inference_config: self.inference_config.clone(),
|
||||
ebpf_services: self.ebpf_services.clone(),
|
||||
app_services: self.app_services.clone(),
|
||||
db: self.db.clone(),
|
||||
db: self.database.clone(),
|
||||
secret_store: self.secret_store.clone(),
|
||||
jwt_service: self.jwt_service.clone(),
|
||||
comm: self.comm.clone(),
|
||||
threat_tx: self.threat_tx.clone(),
|
||||
audit_tx: self.audit_tx.clone(),
|
||||
enforce_handler: self.enforce_handler.clone(),
|
||||
setup_complete: setup_flag,
|
||||
ready: ready_flag,
|
||||
readiness_state,
|
||||
@ -345,6 +344,8 @@ impl System {
|
||||
rate_limit_service: self.rate_limit_service.clone(),
|
||||
force_https,
|
||||
shutdown_handle: shutdown_handle.clone(),
|
||||
logger,
|
||||
log_buffer,
|
||||
suricata_manager: self.suricata_manager.clone(),
|
||||
soar_engine: self.soar_engine.clone(),
|
||||
};
|
||||
@ -405,7 +406,6 @@ impl System {
|
||||
let ebpf_services = self.ebpf_services.clone();
|
||||
let app_services = self.app_services.clone();
|
||||
log!(SystemLog::Terminating);
|
||||
|
||||
if let Some(tx) = self.suricata_shutdown.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
@ -415,25 +415,8 @@ impl System {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize ML model attack type names to SOAR playbook event names.
|
||||
fn normalize_attack_type(raw: &str) -> String {
|
||||
match raw {
|
||||
"Brute Force" => "brute_force".to_string(),
|
||||
"C2 Communication" => "c2_communication".to_string(),
|
||||
"DoS/DDoS" => "threat_detected".to_string(),
|
||||
"Exploitation" | "Malware" | "Web Attack" => "threat_detected".to_string(),
|
||||
"Bot" | "DNS Tunneling" => "threat_detected".to_string(),
|
||||
"Reconnaissance" => "port_scan".to_string(),
|
||||
"Normal" => "normal".to_string(),
|
||||
other => {
|
||||
log!(DetectionLog::UnknownMlAttackType(other.to_string()));
|
||||
"threat_detected".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Periodically check the drift detector and publish DriftDetectedEvent when drift is found.
|
||||
async fn run_drift_monitor(drift_detector: DriftDetectorHandle, comm: Arc<CommunicationManager>) {
|
||||
async fn run_drift_monitor(drift_detector: DriftDetectorHandle, drift_tx: broadcast::Sender<DriftDetectedEvent>) {
|
||||
let mut interval = interval(Duration::from_secs(60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
@ -447,7 +430,7 @@ impl System {
|
||||
drifted_features: report.drifted_features,
|
||||
max_deviation: report.max_deviation,
|
||||
};
|
||||
if let Err(e) = comm.publish_event(event).await {
|
||||
if let Err(e) = drift_tx.send(event) {
|
||||
log!(SystemError::DriftEventPublishFailed(e));
|
||||
}
|
||||
}
|
||||
@ -498,6 +481,23 @@ impl System {
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize ML model attack type names to SOAR playbook event names.
|
||||
fn normalize_attack_type(raw: &str) -> String {
|
||||
match raw {
|
||||
"Brute Force" => "brute_force".to_string(),
|
||||
"C2 Communication" => "c2_communication".to_string(),
|
||||
"DoS/DDoS" => "threat_detected".to_string(),
|
||||
"Exploitation" | "Malware" | "Web Attack" => "threat_detected".to_string(),
|
||||
"Bot" | "DNS Tunneling" => "threat_detected".to_string(),
|
||||
"Reconnaissance" => "port_scan".to_string(),
|
||||
"Normal" => "normal".to_string(),
|
||||
other => {
|
||||
log!(DetectionLog::UnknownMlAttackType(other.to_string()));
|
||||
"threat_detected".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to attach the XDP programs to the configured interfaces.
|
||||
/// If either attach fails, record the reason in `ebpf_health` and
|
||||
/// continue — the rest of the system keeps running.
|
||||
@ -517,10 +517,10 @@ impl System {
|
||||
|
||||
match (ingress_result, egress_result) {
|
||||
(Ok(ingress_mode), Ok(egress_mode)) => {
|
||||
if let Err(e) = self.db.set_setting("xdp_ingress_mode", &ingress_mode) {
|
||||
if let Err(e) = self.database.set_setting("xdp_ingress_mode", &ingress_mode) {
|
||||
log!(SystemError::XdpModeStoreFailed(e));
|
||||
}
|
||||
if let Err(e) = self.db.set_setting("xdp_egress_mode", &egress_mode) {
|
||||
if let Err(e) = self.database.set_setting("xdp_egress_mode", &egress_mode) {
|
||||
log!(SystemError::XdpModeStoreFailed(e));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::communication::message::Message;
|
||||
|
||||
pub type CommandFuture = Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'static>>;
|
||||
pub type CommandHandlerFn = Box<dyn Fn(Box<dyn Any + Send>) -> CommandFuture + Send + Sync>;
|
||||
|
||||
pub trait Command: Message<Response = ()> {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait CommandHandler<C: Command> {
|
||||
async fn handle_command(&self, command: C) -> Result<(), Error>;
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
use crate::interface::communication::command::Command;
|
||||
use crate::interface::communication::message::Message;
|
||||
|
||||
// ── System Commands ──────────────────────────────────────────────────
|
||||
|
||||
pub struct ChangeEnforceModeCommand {
|
||||
pub mode: String,
|
||||
}
|
||||
|
||||
impl Message for ChangeEnforceModeCommand {
|
||||
type Response = ();
|
||||
}
|
||||
impl Command for ChangeEnforceModeCommand {}
|
||||
@ -1,10 +1 @@
|
||||
use std::any::Any;
|
||||
|
||||
use crate::domain::common::error::Error;
|
||||
|
||||
pub trait Event: Send + Clone + 'static {}
|
||||
|
||||
pub trait EventBroadcaster: Send + Sync {
|
||||
fn subscribe_typed(&self) -> Box<dyn Any + Send>;
|
||||
fn broadcast_event(&self, event: Box<dyn Any + Send>) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
pub trait Message: Send + 'static {
|
||||
type Response: Send + 'static;
|
||||
}
|
||||
@ -1,7 +1,2 @@
|
||||
pub mod command;
|
||||
pub mod command_types;
|
||||
pub mod event;
|
||||
pub mod event_types;
|
||||
pub mod message;
|
||||
pub mod query;
|
||||
pub mod query_types;
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::domain::common::error::Error;
|
||||
use crate::interface::communication::message::Message;
|
||||
|
||||
pub type QueryFuture = Pin<Box<dyn Future<Output = Result<Box<dyn Any + Send>, Error>> + Send + 'static>>;
|
||||
pub type QueryHandlerFn = Box<dyn Fn(Box<dyn Any + Send>) -> QueryFuture + Send + Sync>;
|
||||
|
||||
pub trait Query: Message {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait QueryHandler<Q: Query> {
|
||||
async fn handle_query(&self, query: Q) -> Result<Q::Response, Error>;
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
use crate::interface::communication::message::Message;
|
||||
use crate::interface::communication::query::Query;
|
||||
|
||||
// ── System Queries ───────────────────────────────────────────────────
|
||||
|
||||
pub struct GetEnforceModeQuery;
|
||||
|
||||
impl Message for GetEnforceModeQuery {
|
||||
type Response = String;
|
||||
}
|
||||
impl Query for GetEnforceModeQuery {}
|
||||
@ -14,9 +14,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use domain::common::error::Error;
|
||||
use domain::common::error::system::SystemError;
|
||||
use domain::common::log::system::SystemLog;
|
||||
use macros::log;
|
||||
use sd_notify::NotifyState;
|
||||
use tokio::time::sleep;
|
||||
@ -24,88 +21,79 @@ use tokio::{signal, time};
|
||||
|
||||
use crate::adapter::persistence::Database;
|
||||
use crate::core::identity::jwt::JwtService;
|
||||
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::password;
|
||||
use crate::infrastructure::cli::{Cli, handle_subcommand};
|
||||
use crate::infrastructure::http_server;
|
||||
use crate::infrastructure::logger::Logger;
|
||||
use crate::infrastructure::secret_store::SecretStore;
|
||||
use crate::infrastructure::system::{ShutdownMode, System};
|
||||
use crate::interface::port::secret_store::SecretStorePort;
|
||||
use crate::utils::logging::Logging;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let cli = Cli::parse();
|
||||
if cli.command.is_some() {
|
||||
Logging::initialize_cli()?;
|
||||
return handle_subcommand(&cli);
|
||||
fn seed_default_admin(database: &Arc<Database>) -> Result<(), Error> {
|
||||
if database.user_count().unwrap_or(0) != 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Open DB before logging so the in-memory log ring buffer can size itself
|
||||
// from the observability config. Any `log!` emitted during DB bring-up
|
||||
// (e.g. `DbEncryptionDisabled`) is silently dropped by tracing because no
|
||||
// subscriber is yet installed — acceptable for a single startup warning.
|
||||
let db = Arc::new(Database::new(&cli.db_path)?);
|
||||
|
||||
let log_buffer_capacity: usize = db
|
||||
.get_setting("log_buffer_capacity")?
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5_000);
|
||||
let log_buffer_max_message_bytes: usize = db
|
||||
.get_setting("log_buffer_max_message_bytes")?
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(8_192);
|
||||
Logging::initialize(log_buffer_capacity, log_buffer_max_message_bytes)?;
|
||||
if db.user_count().unwrap_or(0) == 0 {
|
||||
let hash = password::hash_password("admin")?;
|
||||
let admin_user_id = db.insert_user("admin", &hash, "admin", false)?;
|
||||
if let Ok(groups) = db.list_user_groups()
|
||||
&& let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == "Administrator")
|
||||
&& let Err(e) = db.set_user_groups(admin_user_id, &[group_id])
|
||||
{
|
||||
log!(SystemError::SetUserGroupsFailed(e));
|
||||
}
|
||||
log!(SystemLog::DefaultAdminCreated);
|
||||
let hash = password::hash_password("admin")?;
|
||||
let admin_user_id = database.insert_user("admin", &hash, "admin", false)?;
|
||||
if let Ok(groups) = database.list_user_groups()
|
||||
&& let Some((group_id, _, _, _, _)) = groups.into_iter().find(|(_, name, _, _, _)| name == "Administrator")
|
||||
&& let Err(err) = database.set_user_groups(admin_user_id, &[group_id])
|
||||
{
|
||||
log!(SystemError::SetUserGroupsFailed(err));
|
||||
}
|
||||
log!(SystemLog::DefaultAdminCreated);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let setup_complete = db.get_setting("setup_complete")?.map(|v| v == "true").unwrap_or(false);
|
||||
if !setup_complete {
|
||||
log!(SystemLog::SetupMode);
|
||||
fn is_setup_complete(database: &Arc<Database>) -> Result<bool, Error> {
|
||||
Ok(database
|
||||
.get_setting("setup_complete")?
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(false))
|
||||
}
|
||||
|
||||
let secret_store = Arc::new(SecretStore::new(db.clone()));
|
||||
let secrets: Arc<dyn SecretStorePort> = secret_store.clone();
|
||||
let jwt_service = Arc::new(JwtService::new(&secrets, 24)?);
|
||||
let setup_flag = Arc::new(AtomicBool::new(false));
|
||||
async fn run_setup_wizard(database: &Arc<Database>) -> Result<(), Error> {
|
||||
log!(SystemLog::SetupMode);
|
||||
|
||||
let handle = http_server::start_setup_server(db.clone(), secret_store, jwt_service, setup_flag.clone(), 8080)?;
|
||||
let secret_store = Arc::new(SecretStore::new(database.clone()));
|
||||
let secrets: Arc<dyn SecretStorePort> = secret_store.clone();
|
||||
let jwt_service = Arc::new(JwtService::new(&secrets, 24)?);
|
||||
let setup_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let flag = setup_flag.clone();
|
||||
let setup_done = async move {
|
||||
loop {
|
||||
time::sleep(Duration::from_millis(500)).await;
|
||||
if flag.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
let handle =
|
||||
http_server::start_setup_server(database.clone(), secret_store, jwt_service, setup_flag.clone(), 8080)?;
|
||||
|
||||
tokio::select! {
|
||||
_ = setup_done => {
|
||||
log!(SystemLog::SetupCompleted);
|
||||
}
|
||||
_ = signal::ctrl_c() => {
|
||||
log!(SystemLog::ShutdownDuringSetup);
|
||||
handle.stop(true).await;
|
||||
return Ok(());
|
||||
let flag = setup_flag.clone();
|
||||
let setup_done = async move {
|
||||
loop {
|
||||
time::sleep(Duration::from_millis(500)).await;
|
||||
if flag.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handle.stop(true).await;
|
||||
log!(SystemLog::SetupServerStopped);
|
||||
tokio::select! {
|
||||
_ = setup_done => {
|
||||
log!(SystemLog::SetupCompleted);
|
||||
}
|
||||
_ = signal::ctrl_c() => {
|
||||
log!(SystemLog::ShutdownDuringSetup);
|
||||
handle.stop(true).await;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let mut system = System::new(db).await?;
|
||||
let mode = system.run().await?;
|
||||
system.terminate().await?;
|
||||
handle.stop(true).await;
|
||||
log!(SystemLog::SetupServerStopped);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_shutdown(mode: ShutdownMode, system: System) -> Result<(), Error> {
|
||||
match mode {
|
||||
ShutdownMode::Restart => {
|
||||
log!(SystemLog::Restart);
|
||||
@ -124,3 +112,29 @@ async fn main() -> Result<(), Error> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let cli = Cli::parse();
|
||||
if cli.command.is_some() {
|
||||
Logger::initialize_cli()?;
|
||||
return handle_subcommand(&cli);
|
||||
}
|
||||
|
||||
let database = Arc::new(Database::new(&cli.db_path)?);
|
||||
let obs_config = ObservabilityConfig::from_settings(database.as_ref())?;
|
||||
let (logger, log_buffer) = Logger::initialize(&obs_config)?;
|
||||
let logger = Arc::new(logger);
|
||||
let log_buffer = Arc::new(log_buffer);
|
||||
|
||||
seed_default_admin(&database)?;
|
||||
|
||||
if !is_setup_complete(&database)? {
|
||||
run_setup_wizard(&database).await?;
|
||||
}
|
||||
|
||||
let mut system = System::new(database).await?;
|
||||
let mode = system.run(logger, log_buffer).await?;
|
||||
system.terminate().await?;
|
||||
handle_shutdown(mode, system).await
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
pub mod boot_time;
|
||||
pub mod logging;
|
||||
pub mod packet_parser;
|
||||
pub mod static_files;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user