feat: multi-task inference pipeline, remove backward compat

T6: Replace AE hard gate with multi-task model (3 output heads):
  - anomaly_head (sigmoid) > threshold → is_attack
  - class_head (softmax 7 classes) argmax → attack_type
  - c2_head (sigmoid) override when confidence > class_head C2 prob
  - "Normal" class → force benign, skip SOAR

T7: Update SOAR routing for 7-class taxonomy:
  - C2 Communication → c2_communication
  - Cryptomining → cryptomining
  - DoS/DDoS (merged) → threat_detected
  - Normal → skip (no SOAR trigger)

T8: Update models and config:
  - New classifier.onnx (950KB multi-task, 34 features, 3 outputs)
  - New deep_autoencoder.onnx (444KB, 33 features)
  - inference_config.json: 33→33 AE features, 7 classes, thresholds

Remove all backward compatibility code:
  - Delete 8 pub use re-exports from model/mod.rs, fix ~30 imports
  - Delete config_loader::InferenceConfig type alias
  - Delete model/config re-export of system::config::*
  - Delete legacy single-output classifier inference path
  - Delete serde defaults for new MLInferenceConfig fields
  - Delete old attack label aliases (DDoS, DoS → only DoS/DDoS)
  - Delete SOAR condition_threshold legacy fallback
  - Delete feature_extractor old CSV column name aliases
  - Remove condition_threshold from runtime Playbook struct

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-04-03 16:44:58 +08:00
parent 4216906dd3
commit 4bfb01c1a2
45 changed files with 482 additions and 420 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,12 @@
{
"created_at": "2026-03-05T14:04:08.736531",
"created_at": "2026-04-03T15:37:09.215359",
"framework": "PyTorch",
"model_type": "MultiTaskModel",
"model": {
"deep_autoencoder": {
"file": "deep_autoencoder.onnx",
"input_dim": 27,
"encoding_dim": 16,
"input_dim": 33,
"encoding_dim": 24,
"ae_feature_names": [
"flow_duration",
"fwd_packets",
@ -33,15 +34,26 @@
"fwd_pkt_len_std",
"bwd_pkt_len_std",
"fwd_seg_size_min",
"fwd_act_data_pkts"
"fwd_act_data_pkts",
"fwd_iat_std",
"bwd_iat_std",
"flow_iat_std",
"fwd_bwd_bytes_ratio",
"pkt_len_variance",
"fwd_iat_skewness"
],
"ae_threshold": 0.17166012525558472
"ae_threshold": 0.23287349939346313
},
"classifier": {
"file": "classifier.onnx",
"type": "ResNet MLP",
"n_features": 28,
"n_classes": 5,
"type": "MultiTaskModel",
"n_features": 34,
"n_classes": 7,
"outputs": [
"anomaly",
"class_probs",
"c2_score"
],
"classifier_feature_names": [
"flow_duration",
"fwd_packets",
@ -70,6 +82,12 @@
"bwd_pkt_len_std",
"fwd_seg_size_min",
"fwd_act_data_pkts",
"fwd_iat_std",
"bwd_iat_std",
"flow_iat_std",
"fwd_bwd_bytes_ratio",
"pkt_len_variance",
"fwd_iat_skewness",
"ae_anomaly_score"
]
}
@ -77,68 +95,68 @@
"preprocessing": {
"ae_clip_params": {
"flow_duration": {
"lower": 1.0,
"upper": 118485085.19000001
"lower": 0.0,
"upper": 70550165.55999511
},
"fwd_packets": {
"lower": 0.0,
"upper": 54.0
"lower": 1.0,
"upper": 120.0
},
"bwd_packets": {
"lower": 0.0,
"upper": 82.0
"upper": 112.0
},
"fwd_bytes": {
"lower": 0.0,
"upper": 6960.0
"upper": 21320.685742187383
},
"bwd_bytes": {
"lower": 0.0,
"upper": 89558.6950000003
"upper": 78556.0
},
"flow_bytes_per_sec": {
"lower": 0.0,
"upper": 12500000.0
"upper": 1457941.3831249783
},
"flow_pkts_per_sec": {
"lower": 0.0226829350855,
"upper": 2000000.0
"lower": 0.0,
"upper": 30612.244140625
},
"fwd_win_bytes": {
"lower": -1.0,
"upper": 65535.0
"lower": 0.0,
"upper": 65280.0
},
"bwd_win_bytes": {
"lower": -1.0,
"upper": 64000.0
"lower": 0.0,
"upper": 65535.0
},
"fwd_pkt_len_mean": {
"lower": 0.0,
"upper": 744.7272727272729
"upper": 1500.0
},
"bwd_pkt_len_mean": {
"lower": 0.0,
"upper": 1393.9601153594992
"upper": 690.0833129882812
},
"fwd_iat_mean": {
"lower": 0.0,
"upper": 86366722.035
"upper": 5468977.71249968
},
"bwd_iat_mean": {
"lower": 0.0,
"upper": 21700000.0
"upper": 5162028.484999921
},
"flow_iat_mean": {
"lower": 1.0,
"upper": 86353400.64500001
"lower": 0.0,
"upper": 166524640.0
},
"pkt_len_mean": {
"lower": 0.0,
"upper": 945.0294113974604
"upper": 1312.2810998535142
},
"dst_port": {
"lower": 0.0,
"upper": 64499.0
"upper": 63375.0
},
"protocol": {
"lower": 0.0,
@ -146,103 +164,139 @@
},
"psh_flag_cnt": {
"lower": 0.0,
"upper": 240.0
"upper": 1.0
},
"ack_flag_cnt": {
"lower": 0.0,
"upper": 655.0
"upper": 1.0
},
"syn_flag_cnt": {
"lower": 0.0,
"upper": 4.0
"upper": 1.0
},
"fin_flag_cnt": {
"lower": 0.0,
"upper": 2.0
"upper": 1.0
},
"rst_flag_cnt": {
"lower": 0.0,
"upper": 1.0
"upper": 0.0
},
"pkt_len_std": {
"lower": 0.0,
"upper": 943.9221842600019
"upper": 1051.5718536376853
},
"fwd_pkt_len_std": {
"lower": 0.0,
"upper": 721.4199648708689
"upper": 227.21978759765625
},
"bwd_pkt_len_std": {
"lower": 0.0,
"upper": 911.0272448420104
"upper": 777.4441128540009
},
"fwd_seg_size_min": {
"lower": 0.0,
"upper": 40.0
"upper": 1902198.5299999905
},
"fwd_act_data_pkts": {
"lower": 0.0,
"upper": 58.0
"upper": 3.0
},
"fwd_iat_std": {
"lower": 0.0,
"upper": 4873774.342499985
},
"bwd_iat_std": {
"lower": 0.0,
"upper": 4677281.179999962
},
"flow_iat_std": {
"lower": 0.0,
"upper": 4679977.562499992
},
"fwd_bwd_bytes_ratio": {
"lower": 0.0,
"upper": 1.0
},
"pkt_len_variance": {
"lower": 0.0,
"upper": 483294.3096875
},
"fwd_iat_skewness": {
"lower": 0.0,
"upper": 0.0
}
},
"ae_scaler": {
"mean": [
9783470.902880548,
3.6532605393470496,
3.2219949045990472,
311.35281195367173,
1407.508354811673,
298997.28705051675,
32378.36735569276,
6158.3256759444475,
6125.023325143747,
58.1683929498305,
149.72168204849987,
2991407.86700246,
543337.1970583026,
2727511.340181723,
105.03829359866097,
13463.473246959382,
8.920585011866685,
5.457814320208867,
12.214631683524651,
0.4885976379312313,
0.246598391240437,
0.08478201266637703,
151.71809799252043,
76.89649795414182,
143.21014925828936,
21.447338538177014,
3.2607463735155715
553573.8261433974,
5.537481338484507,
4.545714712966507,
732.9122877032092,
1594.293060562387,
14524.457216310835,
408.40078292070046,
5041.6718942499765,
8858.56079223287,
293.4158400234335,
68.50546049610982,
48053.28191342106,
39629.174786987496,
2756276.585577172,
24.55453629422013,
13882.541797179327,
9.155290259001973,
0.02040285405796568,
0.04693573572753199,
0.009265432030886539,
0.005048852822986429,
0.0,
23.07985361268022,
2.8912962051498923,
10.050823938165351,
21807.221235950656,
0.027325504830147902,
47055.31297750816,
37633.95415201727,
49435.96380716072,
0.5572453787198782,
5243.747976892553,
0.0
],
"std": [
27895851.239715174,
6.4263677035863696,
8.326275195918974,
739.2548064174216,
7898.711708881367,
1187804.3239195002,
190280.1548511611,
15556.664474188405,
17012.235322987635,
98.89458775902833,
242.79522892109904,
13146157.964503227,
2242915.166631539,
12878527.145353919,
151.95316919115754,
21264.915159327567,
4.943209145195101,
23.02750953458606,
57.0633472601567,
1.2662749497077417,
0.4553262784517569,
0.278557037237646,
211.1373807979287,
143.47790203327,
228.24531573376805,
9.323458649060111,
7.228913366949205
5769069.210865143,
12.731563443887879,
12.145072707978656,
2227.8956432424648,
7544.580451556596,
131574.08066204368,
3062.4073062866864,
11584.347904359805,
19321.807845687003,
456.85162824410736,
84.09080802725346,
457034.0398467031,
414969.2560970854,
21002348.667211507,
134.6784018236429,
20905.109391832277,
5.217401538588734,
0.13728916522249587,
0.21092403535986087,
0.09575684583550836,
0.07080745853335217,
1.0,
123.51344190707131,
22.368753242426347,
79.86295403283017,
173264.80375874366,
0.25736117191332675,
435636.77402945235,
389941.84503591515,
432399.5330873597,
0.3392136503327526,
44261.86086917189,
1.0
],
"feature_names": [
"flow_duration",
@ -271,7 +325,13 @@
"fwd_pkt_len_std",
"bwd_pkt_len_std",
"fwd_seg_size_min",
"fwd_act_data_pkts"
"fwd_act_data_pkts",
"fwd_iat_std",
"bwd_iat_std",
"flow_iat_std",
"fwd_bwd_bytes_ratio",
"pkt_len_variance",
"fwd_iat_skewness"
]
},
"post_scaling_clip": {
@ -281,9 +341,11 @@
},
"attack_labels": {
"0": "Brute Force",
"1": "DDoS",
"2": "DoS",
"3": "Exploitation",
"4": "Reconnaissance"
"1": "C2 Communication",
"2": "Cryptomining",
"3": "DoS\/DDoS",
"4": "Exploitation",
"5": "Normal",
"6": "Reconnaissance"
}
}

View File

@ -26,179 +26,78 @@
"fwd_pkt_len_std",
"bwd_pkt_len_std",
"fwd_seg_size_min",
"fwd_act_data_pkts"
"fwd_act_data_pkts",
"fwd_iat_std",
"bwd_iat_std",
"flow_iat_std",
"fwd_bwd_bytes_ratio",
"pkt_len_variance",
"fwd_iat_skewness"
],
"ae_clip_params": {
"flow_duration": {
"lower": 1.0,
"upper": 118485085.19000001
},
"fwd_packets": {
"lower": 0.0,
"upper": 54.0
},
"bwd_packets": {
"lower": 0.0,
"upper": 82.0
},
"fwd_bytes": {
"lower": 0.0,
"upper": 6960.0
},
"bwd_bytes": {
"lower": 0.0,
"upper": 89558.6950000003
},
"flow_bytes_per_sec": {
"lower": 0.0,
"upper": 12500000.0
},
"flow_pkts_per_sec": {
"lower": 0.0226829350855,
"upper": 2000000.0
},
"fwd_win_bytes": {
"lower": -1.0,
"upper": 65535.0
},
"bwd_win_bytes": {
"lower": -1.0,
"upper": 64000.0
},
"fwd_pkt_len_mean": {
"lower": 0.0,
"upper": 744.7272727272729
},
"bwd_pkt_len_mean": {
"lower": 0.0,
"upper": 1393.9601153594992
},
"fwd_iat_mean": {
"lower": 0.0,
"upper": 86366722.035
},
"bwd_iat_mean": {
"lower": 0.0,
"upper": 21700000.0
},
"flow_iat_mean": {
"lower": 1.0,
"upper": 86353400.64500001
},
"pkt_len_mean": {
"lower": 0.0,
"upper": 945.0294113974604
},
"dst_port": {
"lower": 0.0,
"upper": 64499.0
},
"protocol": {
"lower": 0.0,
"upper": 17.0
},
"psh_flag_cnt": {
"lower": 0.0,
"upper": 240.0
},
"ack_flag_cnt": {
"lower": 0.0,
"upper": 655.0
},
"syn_flag_cnt": {
"lower": 0.0,
"upper": 4.0
},
"fin_flag_cnt": {
"lower": 0.0,
"upper": 2.0
},
"rst_flag_cnt": {
"lower": 0.0,
"upper": 1.0
},
"pkt_len_std": {
"lower": 0.0,
"upper": 943.9221842600019
},
"fwd_pkt_len_std": {
"lower": 0.0,
"upper": 721.4199648708689
},
"bwd_pkt_len_std": {
"lower": 0.0,
"upper": 911.0272448420104
},
"fwd_seg_size_min": {
"lower": 0.0,
"upper": 40.0
},
"fwd_act_data_pkts": {
"lower": 0.0,
"upper": 58.0
}
"flow_duration": { "lower": 0.0, "upper": 70550165.55999511 },
"fwd_packets": { "lower": 1.0, "upper": 120.0 },
"bwd_packets": { "lower": 0.0, "upper": 112.0 },
"fwd_bytes": { "lower": 0.0, "upper": 21320.685742187383 },
"bwd_bytes": { "lower": 0.0, "upper": 78556.0 },
"flow_bytes_per_sec": { "lower": 0.0, "upper": 1457941.3831249783 },
"flow_pkts_per_sec": { "lower": 0.0, "upper": 30612.244140625 },
"fwd_win_bytes": { "lower": 0.0, "upper": 65280.0 },
"bwd_win_bytes": { "lower": 0.0, "upper": 65535.0 },
"fwd_pkt_len_mean": { "lower": 0.0, "upper": 1500.0 },
"bwd_pkt_len_mean": { "lower": 0.0, "upper": 690.0833129882812 },
"fwd_iat_mean": { "lower": 0.0, "upper": 5468977.71249968 },
"bwd_iat_mean": { "lower": 0.0, "upper": 5162028.484999921 },
"flow_iat_mean": { "lower": 0.0, "upper": 166524640.0 },
"pkt_len_mean": { "lower": 0.0, "upper": 1312.2810998535142 },
"dst_port": { "lower": 0.0, "upper": 63375.0 },
"protocol": { "lower": 0.0, "upper": 17.0 },
"psh_flag_cnt": { "lower": 0.0, "upper": 1.0 },
"ack_flag_cnt": { "lower": 0.0, "upper": 1.0 },
"syn_flag_cnt": { "lower": 0.0, "upper": 1.0 },
"fin_flag_cnt": { "lower": 0.0, "upper": 1.0 },
"rst_flag_cnt": { "lower": 0.0, "upper": 0.0 },
"pkt_len_std": { "lower": 0.0, "upper": 1051.5718536376853 },
"fwd_pkt_len_std": { "lower": 0.0, "upper": 227.21978759765625 },
"bwd_pkt_len_std": { "lower": 0.0, "upper": 777.4441128540009 },
"fwd_seg_size_min": { "lower": 0.0, "upper": 1902198.5299999905 },
"fwd_act_data_pkts": { "lower": 0.0, "upper": 3.0 },
"fwd_iat_std": { "lower": 0.0, "upper": 4873774.342499985 },
"bwd_iat_std": { "lower": 0.0, "upper": 4677281.179999962 },
"flow_iat_std": { "lower": 0.0, "upper": 4679977.562499992 },
"fwd_bwd_bytes_ratio": { "lower": 0.0, "upper": 1.0 },
"pkt_len_variance": { "lower": 0.0, "upper": 483294.3096875 },
"fwd_iat_skewness": { "lower": 0.0, "upper": 0.0 }
},
"ae_scaler_mean": [
9783470.902880548,
3.6532605393470496,
3.2219949045990472,
311.35281195367173,
1407.508354811673,
298997.28705051675,
32378.36735569276,
6158.3256759444475,
6125.023325143747,
58.1683929498305,
149.72168204849987,
2991407.86700246,
543337.1970583026,
2727511.340181723,
105.03829359866097,
13463.473246959382,
8.920585011866685,
5.457814320208867,
12.214631683524651,
0.4885976379312313,
0.246598391240437,
0.08478201266637703,
151.71809799252043,
76.89649795414182,
143.21014925828936,
21.447338538177014,
3.2607463735155715
553573.8261433974, 5.537481338484507, 4.545714712966507,
732.9122877032092, 1594.293060562387, 14524.457216310835,
408.40078292070046, 5041.6718942499765, 8858.56079223287,
293.4158400234335, 68.50546049610982, 48053.28191342106,
39629.174786987496, 2756276.585577172, 24.55453629422013,
13882.541797179327, 9.155290259001973, 0.02040285405796568,
0.04693573572753199, 0.009265432030886539, 0.005048852822986429,
0.0, 23.07985361268022, 2.8912962051498923, 10.050823938165351,
21807.221235950656, 0.027325504830147902, 47055.31297750816,
37633.95415201727, 49435.96380716072, 0.5572453787198782,
5243.747976892553, 0.0
],
"ae_scaler_std": [
27895851.239715174,
6.4263677035863696,
8.326275195918974,
739.2548064174216,
7898.711708881367,
1187804.3239195002,
190280.1548511611,
15556.664474188405,
17012.235322987635,
98.89458775902833,
242.79522892109904,
13146157.964503227,
2242915.166631539,
12878527.145353919,
151.95316919115754,
21264.915159327567,
4.943209145195101,
23.02750953458606,
57.0633472601567,
1.2662749497077417,
0.4553262784517569,
0.278557037237646,
211.1373807979287,
143.47790203327,
228.24531573376805,
9.323458649060111,
7.228913366949205
5769069.210865143, 12.731563443887879, 12.145072707978656,
2227.8956432424648, 7544.580451556596, 131574.08066204368,
3062.4073062866864, 11584.347904359805, 19321.807845687003,
456.85162824410736, 84.09080802725346, 457034.0398467031,
414969.2560970854, 21002348.667211507, 134.6784018236429,
20905.109391832277, 5.217401538588734, 0.13728916522249587,
0.21092403535986087, 0.09575684583550836, 0.07080745853335217,
1.0, 123.51344190707131, 22.368753242426347, 79.86295403283017,
173264.80375874366, 0.25736117191332675, 435636.77402945235,
389941.84503591515, 432399.5330873597, 0.3392136503327526,
44261.86086917189, 1.0
],
"ae_post_clip_min": -5.0,
"ae_post_clip_max": 5.0,
"ae_threshold": 0.17166012525558472,
"ae_threshold": 0.23287349939346313,
"classifier_feature_names": [
"flow_duration",
"fwd_packets",
@ -227,13 +126,60 @@
"bwd_pkt_len_std",
"fwd_seg_size_min",
"fwd_act_data_pkts",
"fwd_iat_std",
"bwd_iat_std",
"flow_iat_std",
"fwd_bwd_bytes_ratio",
"pkt_len_variance",
"fwd_iat_skewness",
"ae_anomaly_score"
],
"attack_labels": {
"0": "Brute Force",
"1": "DDoS",
"2": "DoS",
"3": "Exploitation",
"4": "Reconnaissance"
"1": "C2 Communication",
"2": "Cryptomining",
"3": "DoS/DDoS",
"4": "Exploitation",
"5": "Normal",
"6": "Reconnaissance"
},
"anomaly_threshold": 0.5,
"c2_threshold": 0.5,
"model_type": "MultiTaskModel",
"output_names": ["anomaly", "class_probs", "c2_score"],
"ae_feature_weights": {
"flow_duration": 1.0,
"fwd_packets": 1.0,
"bwd_packets": 1.0,
"fwd_bytes": 1.0,
"bwd_bytes": 1.0,
"flow_bytes_per_sec": 1.0,
"flow_pkts_per_sec": 1.0,
"fwd_win_bytes": 4.0,
"bwd_win_bytes": 4.0,
"fwd_pkt_len_mean": 1.0,
"bwd_pkt_len_mean": 1.0,
"fwd_iat_mean": 1.0,
"bwd_iat_mean": 1.0,
"flow_iat_mean": 1.0,
"pkt_len_mean": 1.0,
"dst_port": 1.0,
"protocol": 1.0,
"psh_flag_cnt": 2.0,
"ack_flag_cnt": 1.0,
"syn_flag_cnt": 2.0,
"fin_flag_cnt": 2.0,
"rst_flag_cnt": 2.0,
"pkt_len_std": 1.0,
"fwd_pkt_len_std": 1.0,
"bwd_pkt_len_std": 1.0,
"fwd_seg_size_min": 1.0,
"fwd_act_data_pkts": 1.0,
"fwd_iat_std": 1.5,
"bwd_iat_std": 1.5,
"flow_iat_std": 0.1,
"fwd_bwd_bytes_ratio": 2.0,
"pkt_len_variance": 0.1,
"fwd_iat_skewness": 0.1
}
}
}

View File

@ -5,9 +5,9 @@ use async_trait::async_trait;
use crate::core::ebpf::access_control::AccessControl;
use crate::interface::port::access_control::AccessControlPort;
use crate::model::direction::FlowDirection;
use crate::model::access_control::list_type::ListType;
use crate::model::error::Error;
use crate::model::list_type::ListType;
use crate::model::monitoring::direction::FlowDirection;
/// Adapter that implements AccessControlPort by delegating to the eBPF AccessControl.
pub struct EbpfAccessControlAdapter {

View File

@ -4,8 +4,8 @@ use actix_web::{HttpResponse, Responder, Scope, web};
use serde::Deserialize;
use crate::core::acl_service::AclService;
use crate::model::direction::FlowDirection;
use crate::model::list_type::ListType;
use crate::model::access_control::list_type::ListType;
use crate::model::monitoring::direction::FlowDirection;
#[derive(Deserialize)]
struct CountryCodesRequest {

View File

@ -116,7 +116,6 @@ async fn login(body: web::Json<LoginRequest>, db: web::Data<Repo>, jwt: web::Dat
// Permissions come exclusively from groups — no role-based fallback
let permissions = db.get_user_permissions(id).unwrap_or_default();
// Derive role from groups for backwards compat in JWT
let groups = db.get_user_groups(id).unwrap_or_default();
let role = if groups.iter().any(|(_id, name, _desc, _perms)| name == "Administrator") {
"admin".to_string()
@ -257,7 +256,6 @@ async fn list_users(_auth: AuthClaims, db: web::Data<Repo>) -> impl Responder {
.iter()
.map(|(gid, name)| serde_json::json!({"id": gid, "name": name}))
.collect();
// Derive role from groups for backwards compat
let role = if user_groups.iter().any(|(_id, name)| name == "Administrator") {
"admin"
} else {

View File

@ -1048,7 +1048,7 @@ impl Database {
/// Validate an API key and return Claims if valid.
/// Computes SHA-256 hash of the key and looks it up in api_keys table.
pub fn validate_api_key(&self, api_key: &str) -> Result<Option<crate::model::auth::Claims>, Error> {
pub fn validate_api_key(&self, api_key: &str) -> Result<Option<crate::model::identity::auth::Claims>, Error> {
use std::fmt::Write;
// SHA-256 hash the key
@ -1115,7 +1115,7 @@ impl Database {
],
};
Ok(Some(crate::model::auth::Claims {
Ok(Some(crate::model::identity::auth::Claims {
sub: -id, // negative ID to distinguish from user IDs
username: format!("api:{}", name),
role: level,
@ -2026,7 +2026,7 @@ impl crate::interface::port::audit::AuditPort for Database {
}
impl crate::interface::port::api_key::ApiKeyPort for Database {
fn validate_api_key(&self, api_key: &str) -> Result<Option<crate::model::auth::Claims>, Error> {
fn validate_api_key(&self, api_key: &str) -> Result<Option<crate::model::identity::auth::Claims>, Error> {
self.validate_api_key(api_key)
}
fn insert_api_key(&self, key_hash: &str, name: &str, permission_level: &str) -> Result<i64, Error> {

View File

@ -8,7 +8,7 @@ use crate::core::ml::alert::MLAlert;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;
use crate::model::ml_detection::AlertMessage;
use crate::model::detection::ml_detection::AlertMessage;
pub async fn websocket_alert(req: HttpRequest, body: web::Payload, ai: web::Data<MLAlert>) -> Result<HttpResponse> {
let (response, session, msg_stream) = handle(&req, body)?;

View File

@ -5,7 +5,7 @@ use macros::log;
use tokio::sync::broadcast;
use crate::core::ebpf::drop_monitor::DropMonitor;
use crate::model::drop_event::DropEventMessage;
use crate::model::monitoring::drop_event::DropEventMessage;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;

View File

@ -6,7 +6,7 @@ use futures_util::StreamExt;
use tokio::time::interval;
use crate::infrastructure::statistics::FlowStatistics;
use crate::model::flow_stats::FlowSubscription;
use crate::model::monitoring::flow_stats::FlowSubscription;
/// Default subscription: all flows, no filter, 5 second interval
fn default_subscription() -> FlowSubscription {

View File

@ -7,7 +7,7 @@ use tokio::sync::broadcast;
use crate::infrastructure::health::SystemHealth;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::health::SystemHealthMetrics;
use crate::model::system::health::SystemHealthMetrics;
use crate::model::log::http::HttpLog;
pub async fn websocket_system_health(

View File

@ -4,12 +4,12 @@ use std::sync::Arc;
use crate::core::ebpf::access_control::AccessControl;
use crate::core::ebpf::geo_block::GeoBlock;
use crate::interface::port::repository::RepositoryPort;
use crate::model::direction::FlowDirection;
use crate::model::monitoring::direction::FlowDirection;
use macros::log;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::list_type::ListType;
use crate::model::access_control::list_type::ListType;
/// Domain service that coordinates ACL changes between DB persistence and eBPF data plane.
/// Atomic write: eBPF first, then DB. If DB fails, rollback eBPF.

View File

@ -3,7 +3,7 @@ use std::future::{Ready, ready};
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpMessage, HttpRequest};
use crate::model::auth::Claims;
use crate::model::identity::auth::Claims;
/// Actix-web extractor that pulls `Claims` from request extensions.
///

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::ErrorKind};
use crate::interface::port::secret_store::SecretStorePort;
use crate::model::auth::Claims;
use crate::model::identity::auth::Claims;
use crate::model::error::Error;
use crate::model::error::auth::AuthError;

View File

@ -7,11 +7,11 @@ use common::model::ip_address::{IPv4, IPv6, Port};
use common::model::port_rule::PortRule;
use tokio::sync::RwLock;
use crate::model::direction::FlowDirection;
use crate::model::access_control::ip_address::NativeConvert;
use crate::model::access_control::list_type::ListType;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::ip_address::NativeConvert;
use crate::model::list_type::ListType;
use crate::model::monitoring::direction::FlowDirection;
pub struct AccessControl {
ipv4_src_whitelist: RwLock<MapWrapper<IPv4>>,

View File

@ -10,7 +10,7 @@ use common::model::drop_event::DropEvent as RawDropEvent;
use parking_lot::Mutex;
use crate::model::config::constants::DROP_CHANNEL_CAPACITY;
use crate::model::drop_event::{DropCounters, DropEventMessage};
use crate::model::monitoring::drop_event::{DropCounters, DropEventMessage};
pub struct DropMonitor {
broadcast_tx: broadcast::Sender<DropEventMessage>,

View File

@ -10,7 +10,7 @@ use tokio::sync::RwLock;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::ip_address::NativeConvert;
use crate::model::access_control::ip_address::NativeConvert;
pub struct ProtocolFilter {
ipv4_http_service: RwLock<HttpServiceWrapper<AddrPortV4>>,

View File

@ -20,8 +20,8 @@ use crate::core::ebpf::dns_filter::DnsFilter;
use crate::core::ml::engine::Engine;
use crate::core::ml::flow_tracker::FlowTracker;
use crate::infrastructure::app_config::AppConfig;
use crate::model::config::NetworkConfig;
use crate::model::direction::Direction;
use crate::model::monitoring::direction::Direction;
use crate::model::system::config::NetworkConfig;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::system::SystemError;

View File

@ -25,8 +25,7 @@ impl SmtpClient {
///
/// Returns `None` if any required setting (`smtp_host`, `smtp_port`,
/// `smtp_username`, `smtp_password`) is missing.
/// If a `SecretStorePort` is provided, reads the password from the secret store
/// (falling back to the settings table for backward compat before migration).
/// If a `SecretStorePort` is provided, reads the password from the secret store.
pub fn from_database(
db: &dyn RepositoryPort,
secrets: Option<&dyn SecretStorePort>,

View File

@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::model::ml_detection::FlowKey;
use crate::model::detection::ml_detection::FlowKey;
pub struct AttackAggregator {
detections: HashMap<FlowKey, Vec<(Instant, f32)>>,
@ -31,7 +31,7 @@ impl AttackAggregator {
// DDoS/DoS: high frequency, need more confirmations to avoid alert storms
// C2/Cryptomining: low frequency, alert on first detection
let effective_min = match attack_type {
Some("DDoS") | Some("DoS") => self.min_detections.saturating_mul(2).max(1),
Some("DoS/DDoS") => self.min_detections.saturating_mul(2).max(1),
Some("C2 Communication") | Some("Cryptomining") => 1,
_ => self.min_detections,
};
@ -80,14 +80,14 @@ mod tests {
}
#[test]
fn ddos_requires_double_min_detections() {
fn dos_ddos_requires_double_min_detections() {
let mut agg = AttackAggregator::new(60, 3);
let key = test_key();
// DDoS needs 6 detections (3 * 2)
// DoS/DDoS needs 6 detections (3 * 2)
for _ in 0..5 {
assert!(!agg.should_alert(&key, 5.0, 1.0, Some("DDoS")));
assert!(!agg.should_alert(&key, 5.0, 1.0, Some("DoS/DDoS")));
}
assert!(agg.should_alert(&key, 5.0, 1.0, Some("DDoS")));
assert!(agg.should_alert(&key, 5.0, 1.0, Some("DoS/DDoS")));
}
#[test]

View File

@ -3,7 +3,7 @@ use tokio::sync::broadcast;
use crate::model::config::constants::ML_ALERT_CHANNEL_CAPACITY;
use crate::model::log::ml::MLLog;
use crate::model::ml_detection::{AlertMessage, DetectionResult};
use crate::model::detection::ml_detection::{AlertMessage, DetectionResult};
pub struct MLAlert {
broadcast_tx: broadcast::Sender<AlertMessage>,

View File

@ -2,11 +2,7 @@ use std::fs;
use std::path::PathBuf;
use crate::model::error::ml::MLError;
use crate::model::config::MLInferenceConfig;
/// Backward-compatible alias so existing `use config_loader::InferenceConfig` paths still compile.
pub type InferenceConfig = MLInferenceConfig;
use crate::model::system::config::MLInferenceConfig;
impl MLInferenceConfig {
pub fn load_file(file: &str) -> Result<Self, MLError> {
@ -23,6 +19,11 @@ impl MLInferenceConfig {
if config.ae_scaler_std.len() != config.ae_feature_names.len() {
return Err(MLError::ConfigParseFailed("scaler std length mismatch"));
}
if config.output_names.len() != 3 {
return Err(MLError::ConfigParseFailed(
"MultiTaskModel requires exactly 3 output_names (anomaly, class_probs, c2_score)",
));
}
Ok(config)
}
}

View File

@ -7,7 +7,7 @@ use tokio::sync::oneshot;
use tokio::time::interval;
use super::aggregator::AttackAggregator;
use super::config_loader::InferenceConfig;
use crate::model::system::config::MLInferenceConfig;
use super::drift_detector::DriftDetector;
use super::flow_tracker::{FlowData, FlowTracker};
use super::inference::Inference;
@ -17,7 +17,7 @@ use crate::model::detection::flow_features::FlowFeatures;
use super::alert::MLAlert;
use crate::model::log::ml::MLLog;
use crate::model::ml_detection::{EngineConfig, InferenceStats};
use crate::model::detection::ml_detection::{EngineConfig, InferenceStats};
/// Per-queue tracker. With symmetric hash in eBPF, both directions of a flow
/// land on the same queue, so per-queue trackers correctly see bidirectional flows.
@ -38,7 +38,7 @@ pub struct Engine {
impl Engine {
pub fn new(
models: Arc<MLModels>,
config: Arc<InferenceConfig>,
config: Arc<MLInferenceConfig>,
ml_alert: Arc<MLAlert>,
drift_detector: Arc<Mutex<DriftDetector>>,
engine_config: EngineConfig,

View File

@ -1,7 +1,7 @@
use common::define::tcp_flags::*;
use super::flow_tracker::FlowData;
use crate::model::ml_detection::PacketData;
use crate::model::detection::ml_detection::PacketData;
use crate::model::detection::flow_features::FlowFeatures;
@ -356,10 +356,10 @@ impl PrecomputedStats {
"Subflow Fwd Bytes" => self.fwd_total_bytes,
"Subflow Bwd Packets" => self.bwd_count,
"Subflow Bwd Bytes" => self.bwd_total_bytes,
"Init_Win_bytes_forward" | "Init Fwd Win Byts" | "fwd_win_bytes" => self.init_win_bytes_fwd,
"Init_Win_bytes_backward" | "Init Bwd Win Byts" | "bwd_win_bytes" => self.init_win_bytes_bwd,
"act_data_pkt_fwd" | "Fwd Act Data Pkts" | "fwd_act_data_pkts" => self.act_data_pkt_fwd,
"min_seg_size_forward" | "Fwd Seg Size Min" | "fwd_seg_size_min" => self.min_seg_size_forward,
"fwd_win_bytes" => self.init_win_bytes_fwd,
"bwd_win_bytes" => self.init_win_bytes_bwd,
"fwd_act_data_pkts" => self.act_data_pkt_fwd,
"fwd_seg_size_min" => self.min_seg_size_forward,
"Active Mean" => self.active_mean,
"Active Std" => self.active_std,
"Active Max" => self.active_max,

View File

@ -7,9 +7,9 @@ use crate::model::config::constants::{
FLOW_BULK_MIN_BYTES, FLOW_BULK_MIN_PACKETS, FLOW_IDLE_THRESHOLD_US, FLOW_IDLE_TIMEOUT_US,
FLOW_MAX_PACKETS_PER_DIRECTION, FLOW_MAX_PERIODS, FLOW_TERMINATED_TIMEOUT_US,
};
use crate::model::direction::Direction;
use crate::model::ml_detection::{BulkState, FlowKey, PacketData};
use crate::model::user_packet::UserPacket;
use crate::model::monitoring::direction::Direction;
use crate::model::detection::ml_detection::{BulkState, FlowKey, PacketData};
use crate::model::monitoring::user_packet::UserPacket;
#[derive(Debug, Clone)]
pub struct FlowData {

View File

@ -3,20 +3,20 @@ use std::sync::Arc;
use macros::log;
use tract_onnx::prelude::*;
use super::config_loader::InferenceConfig;
use super::flow_tracker::FlowData;
use super::model_loader::MLModels;
use crate::model::detection::flow_features::FlowFeatures;
use crate::model::detection::ml_detection::DetectionResult;
use crate::model::log::ml::MLLog;
use crate::model::ml_detection::DetectionResult;
use crate::model::system::config::MLInferenceConfig;
pub struct Inference {
pub models: Arc<MLModels>,
pub config: Arc<InferenceConfig>,
pub config: Arc<MLInferenceConfig>,
}
impl Inference {
pub fn new(models: Arc<MLModels>, config: Arc<InferenceConfig>) -> Self {
pub fn new(models: Arc<MLModels>, config: Arc<MLInferenceConfig>) -> Self {
Self { models, config }
}
@ -53,16 +53,6 @@ impl Inference {
let cls_input = self.build_classifier_input(&ae_features, ae_score);
let (attack_type, confidence) = match self.run_classifier(cls_input) {
Ok(result) => result,
Err(e) => {
log!(MLLog::InferenceFailed("LightGBM".to_string(), e.to_string()));
return None;
}
};
let is_attack = ae_score >= self.config.ae_threshold;
let flow_key = format!(
"{}:{} -> {}:{} (proto {}) [{}]",
flow.flow_key.src_ip_string(),
@ -73,6 +63,86 @@ impl Inference {
flow.direction
);
let result = match self.models.classifier.run(tvec![cls_input.into_tensor().into()]) {
Ok(r) => r,
Err(e) => {
log!(MLLog::InferenceFailed("MultiTaskModel".to_string(), e.to_string()));
return None;
}
};
// Output 0: anomaly (shape [1,1], sigmoid)
let anomaly_score = match result[0].to_array_view::<f32>() {
Ok(v) => *v.iter().next().unwrap_or(&0.0),
Err(e) => {
log!(MLLog::InferenceFailed("anomaly_head".to_string(), e.to_string()));
return None;
}
};
// Output 1: class_probs (shape [1, n_classes], softmax)
let class_probs = match result[1].to_array_view::<f32>() {
Ok(v) => v.iter().copied().collect::<Vec<f32>>(),
Err(e) => {
log!(MLLog::InferenceFailed("class_head".to_string(), e.to_string()));
return None;
}
};
// Output 2: c2_score (shape [1,1], sigmoid)
let c2_score = match result[2].to_array_view::<f32>() {
Ok(v) => *v.iter().next().unwrap_or(&0.0),
Err(e) => {
log!(MLLog::InferenceFailed("c2_head".to_string(), e.to_string()));
return None;
}
};
let is_attack = anomaly_score > self.config.anomaly_threshold;
// Determine attack type from class_head argmax
let (predicted_class, class_confidence) = class_probs
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, &p)| (i, p))
.unwrap_or((0, 0.0));
let mut attack_type = self
.config
.attack_labels
.get(&predicted_class.to_string())
.cloned()
.unwrap_or_else(|| "UNKNOWN".to_string());
let mut confidence = class_confidence;
// C2 head override: if C2 head fires and its confidence exceeds the class head's
// C2 probability, prefer the dedicated C2 head judgment.
if c2_score > self.config.c2_threshold {
let c2_class_prob = self.find_class_prob_for("C2 Communication", &class_probs);
if c2_score > c2_class_prob {
attack_type = "C2 Communication".to_string();
confidence = c2_score;
}
}
// "Normal" class prediction means benign
if attack_type == "Normal" {
return Some(DetectionResult {
flow_key,
flow_key_raw: flow.flow_key.clone(),
direction: flow.direction,
is_attack: false,
attack_type: None,
confidence: class_confidence,
ae_score,
threshold: self.config.anomaly_threshold,
packet_count: flow.packet_count() as u64,
flow_duration_us: flow.duration_us(),
});
}
Some(DetectionResult {
flow_key,
flow_key_raw: flow.flow_key.clone(),
@ -81,12 +151,22 @@ impl Inference {
attack_type: if is_attack { Some(attack_type) } else { None },
confidence,
ae_score,
threshold: self.config.ae_threshold,
threshold: self.config.anomaly_threshold,
packet_count: flow.packet_count() as u64,
flow_duration_us: flow.duration_us(),
})
}
/// Find the softmax probability for a given label name in class_probs.
fn find_class_prob_for(&self, label: &str, class_probs: &[f32]) -> f32 {
for (key, name) in &self.config.attack_labels {
if name == label && let Ok(idx) = key.parse::<usize>() {
return class_probs.get(idx).copied().unwrap_or(0.0);
}
}
0.0
}
fn preprocess_ae_features(&self, flow: &FlowData) -> Vec<f32> {
let mut features = FlowFeatures::extract(flow, &self.config.ae_feature_names);
features.winsorize(&self.config.ae_clip_params, &self.config.ae_feature_names);
@ -99,7 +179,7 @@ impl Inference {
tract_ndarray::Array2::from_shape_fn((1, v.len()), |(_, j)| v[j])
}
/// Classifier 輸入 = 已預處理的 ae_features ++ [ae_anomaly_score]
/// Classifier input = preprocessed ae_features ++ [ae_anomaly_score]
fn build_classifier_input(&self, ae_features: &[f32], ae_score: f32) -> tract_ndarray::Array2<f32> {
let n = ae_features.len() + 1;
tract_ndarray::Array2::from_shape_fn((1, n), |(_, j)| {
@ -124,29 +204,4 @@ impl Inference {
Ok(mse)
}
fn run_classifier(&self, input: tract_ndarray::Array2<f32>) -> TractResult<(String, f32)> {
let result = self.models.classifier.run(tvec![input.into_tensor().into()])?;
let output = result[0].to_array_view::<f32>()?;
let mut max_prob: f32 = 0.0;
let mut predicted_class: usize = 0;
for (i, &prob) in output.iter().enumerate() {
if prob > max_prob {
max_prob = prob;
predicted_class = i;
}
}
let attack_type = self
.config
.attack_labels
.get(&predicted_class.to_string())
.cloned()
.unwrap_or_else(|| "UNKNOWN".to_string());
Ok((attack_type, max_prob))
}
}

View File

@ -3,16 +3,16 @@ use tract_onnx::prelude::*;
use crate::infrastructure::app_config::AppConfig;
use crate::model::error::ml::MLError;
use crate::model::ml_detection::RunnableModel;
use crate::model::detection::ml_detection::RunnableModel;
use super::config_loader::InferenceConfig;
use crate::model::system::config::MLInferenceConfig;
pub struct MLModels {
pub deep_autoencoder: RunnableModel,
pub classifier: RunnableModel,
}
impl MLModels {
pub fn load_models(app_config: &Arc<AppConfig>, inference_config: &Arc<InferenceConfig>) -> Result<Self, MLError> {
pub fn load_models(app_config: &Arc<AppConfig>, inference_config: &Arc<MLInferenceConfig>) -> Result<Self, MLError> {
Ok(Self {
deep_autoencoder: Self::loader(
&app_config.inference.deep_autoencoder_name,

View File

@ -107,12 +107,12 @@ impl SoarEngine {
// Check if this row belongs to the same playbook as the last one
let needs_new = playbooks.last().is_none_or(|last| last.id != pb_id);
if needs_new {
let _ = threshold; // stored in DB but no longer used at runtime
playbooks.push(Playbook {
id: pb_id,
name,
enabled,
trigger_event,
condition_threshold: threshold,
cooldown_secs: cooldown,
actions: Vec::new(),
conditions: Vec::new(),
@ -265,13 +265,9 @@ impl SoarEngine {
}
/// Evaluate all conditions on a playbook (AND logic).
/// If conditions vec is empty, falls back to legacy `condition_threshold` check.
/// If no conditions are configured, the playbook matches unconditionally.
fn evaluate_conditions(&self, pb: &Playbook, event: &ThreatDetectedEvent) -> bool {
if pb.conditions.is_empty() {
// Legacy: use inline threshold if present
if let Some(threshold) = pb.condition_threshold {
return (event.confidence as f64) >= threshold;
}
return true;
}
@ -1485,7 +1481,6 @@ mod tests {
id: 999,
name: "test-playbook".to_string(),
trigger_event: "threat_detected".to_string(),
condition_threshold: None,
cooldown_secs: 60,
enabled: true,
actions: vec![],

View File

@ -11,7 +11,7 @@ use crate::core::config_service::ConfigService;
use crate::core::dns_filter_service::DnsFilterService;
use crate::core::ebpf::EbpfServices;
use crate::core::email::scheduler::ReportScheduler;
use crate::core::ml::config_loader::InferenceConfig;
use crate::model::system::config::MLInferenceConfig;
use crate::core::ml::drift_detector::DriftDetector;
use crate::core::notification_service::NotificationService;
use crate::core::playbook_service::PlaybookService;
@ -32,7 +32,7 @@ use crate::model::event::{DetectionEvent, DetectionSource, DriftDetectedEvent};
use crate::model::log::detection::DetectionLog;
use crate::model::log::ml::MLLog;
use crate::model::log::system::SystemLog;
use crate::model::ml_detection::AlertMessage;
use crate::model::detection::ml_detection::AlertMessage;
use crate::model::system::readiness::ReadinessState;
/// API-triggered shutdown mode.
@ -70,7 +70,7 @@ impl ShutdownHandle {
/// Setup mode is handled by main.rs — System only runs when setup is complete.
pub struct System {
pub app_config: Arc<AppConfig>,
pub inference_config: Arc<InferenceConfig>,
pub inference_config: Arc<MLInferenceConfig>,
pub ebpf_services: Arc<EbpfServices>,
pub app_services: Arc<AppServices>,
pub db: Arc<Database>,
@ -337,9 +337,12 @@ impl System {
fn normalize_attack_type(raw: &str) -> String {
match raw {
"Brute Force" => "brute_force".to_string(),
"DDoS" | "DoS" => "threat_detected".to_string(),
"C2 Communication" => "c2_communication".to_string(),
"Cryptomining" => "cryptomining".to_string(),
"DoS/DDoS" => "threat_detected".to_string(),
"Exploitation" => "threat_detected".to_string(),
"Reconnaissance" => "port_scan".to_string(),
"Normal" => "normal".to_string(),
other => {
log!(DetectionLog::UnknownMlAttackType(other.to_string()));
"threat_detected".to_string()
@ -383,11 +386,17 @@ impl System {
loop {
match rx.recv().await {
Ok(alert) => {
let raw_type = alert.attack_type.as_deref().unwrap_or("unknown");
let normalized = Self::normalize_attack_type(raw_type);
// "Normal" class means benign — no SOAR trigger needed
if normalized == "normal" {
continue;
}
let event = DetectionEvent {
source: DetectionSource::ML,
attack_type: Self::normalize_attack_type(
&alert.attack_type.unwrap_or_else(|| "unknown".into()),
),
attack_type: normalized,
confidence: alert.confidence,
source_ip: alert.src_ip,
dest_ip: alert.dst_ip,
@ -439,13 +448,18 @@ mod tests {
}
#[test]
fn normalize_ddos() {
assert_eq!(System::normalize_attack_type("DDoS"), "threat_detected");
fn normalize_c2_communication() {
assert_eq!(System::normalize_attack_type("C2 Communication"), "c2_communication");
}
#[test]
fn normalize_dos() {
assert_eq!(System::normalize_attack_type("DoS"), "threat_detected");
fn normalize_cryptomining() {
assert_eq!(System::normalize_attack_type("Cryptomining"), "cryptomining");
}
#[test]
fn normalize_dos_ddos() {
assert_eq!(System::normalize_attack_type("DoS/DDoS"), "threat_detected");
}
#[test]
@ -458,6 +472,11 @@ mod tests {
assert_eq!(System::normalize_attack_type("Reconnaissance"), "port_scan");
}
#[test]
fn normalize_normal() {
assert_eq!(System::normalize_attack_type("Normal"), "normal");
}
#[test]
fn normalize_unknown_falls_back_to_threat_detected() {
assert_eq!(System::normalize_attack_type("SomethingNew"), "threat_detected");

View File

@ -1,12 +1,12 @@
use crate::adapter::persistence::Database;
use crate::model::config::{HttpConfig, InferenceConfig as InfConfig, MiscConfig, NetworkConfig, PipelineConfig};
use crate::model::system::config::{HttpConfig, InferenceConfig, MiscConfig, NetworkConfig, PipelineConfig};
use crate::model::error::Error;
use crate::model::error::system::SystemError;
pub struct AppConfig {
pub http: HttpConfig,
pub network: NetworkConfig,
pub inference: InfConfig,
pub inference: InferenceConfig,
pub misc: MiscConfig,
pub pipeline: PipelineConfig,
}
@ -109,7 +109,7 @@ impl AppConfig {
packet_buffer_size: 2048,
buffer_pool_capacity: 1024,
},
inference: InfConfig {
inference: InferenceConfig {
deep_autoencoder_name: "deep_autoencoder.onnx".into(),
classifier_name: "classifier.onnx".into(),
models_config_name: "inference_config.json".into(),

View File

@ -6,7 +6,7 @@ use macros::log;
use tokio::sync::oneshot;
use crate::core::ml::alert::MLAlert;
use crate::core::ml::config_loader::InferenceConfig;
use crate::model::system::config::MLInferenceConfig;
use crate::core::ml::drift_detector::DriftDetector;
use crate::core::ml::engine::Engine;
use crate::core::ml::model_loader::MLModels;
@ -19,7 +19,7 @@ use crate::model::error::Error;
use crate::model::error::misc::MiscError;
use crate::model::error::system::SystemError;
use crate::model::log::system::SystemLog;
use crate::model::ml_detection::EngineConfig;
use crate::model::detection::ml_detection::EngineConfig;
/// Application-level service orchestrator.
/// Holds all runtime services (health monitoring, ML inference, flow statistics)
@ -36,7 +36,7 @@ pub struct AppServices {
impl AppServices {
pub fn new(
app_config: Arc<AppConfig>,
inference_config: Arc<InferenceConfig>,
inference_config: Arc<MLInferenceConfig>,
drift_detector: Arc<parking_lot::Mutex<DriftDetector>>,
) -> Result<Self, Error> {
let health = SystemHealth::new(app_config.clone())?;

View File

@ -8,7 +8,7 @@ use tokio::time::interval;
use crate::infrastructure::app_config::AppConfig;
use crate::model::error::Error;
use crate::model::health::{
use crate::model::system::health::{
ConfiguredNetworkStats, CpuCoreInfo, CpuDetails, LoadAverage, MemoryUsage, NetworkStats, SystemHealthMetrics,
SystemHealthStatus, SystemInfo,
};

View File

@ -17,7 +17,7 @@ use crate::core::auth::setup_guard::{SetupCompleteFlag, SetupGuard};
use crate::core::config_service::ConfigService;
use crate::core::dns_filter_service::DnsFilterService;
use crate::core::ebpf::EbpfServices;
use crate::core::ml::config_loader::InferenceConfig;
use crate::model::system::config::MLInferenceConfig;
use crate::core::notification_service::NotificationService;
use crate::core::playbook_service::PlaybookService;
use crate::core::rate_limit_service::RateLimitService;
@ -42,7 +42,7 @@ use crate::model::system::readiness::ReadinessState;
/// Parameters for starting the HTTP server, avoiding `#[cfg]` on function params.
pub struct HttpServerParams {
pub app_config: Arc<AppConfig>,
pub inference_config: Arc<InferenceConfig>,
pub inference_config: Arc<MLInferenceConfig>,
pub ebpf_services: Arc<EbpfServices>,
pub app_services: Arc<AppServices>,
pub db: Arc<Database>,

View File

@ -19,7 +19,7 @@ use crate::core::config_service::ConfigService;
use crate::core::dns_filter_service::DnsFilterService;
use crate::core::ebpf::EbpfServices;
use crate::core::email::scheduler::ReportScheduler;
use crate::core::ml::config_loader::InferenceConfig;
use crate::model::system::config::MLInferenceConfig;
use crate::core::ml::drift_detector::DriftDetector;
use crate::core::notification_service::NotificationService;
use crate::core::playbook_service::PlaybookService;
@ -40,11 +40,11 @@ use crate::interface::port::repository::RepositoryPort;
use crate::interface::port::secret_store::SecretStorePort;
use crate::interface::port::soar::SoarPort;
use crate::model::detection::drift::FeatureBaselines;
use crate::model::direction::FlowDirection;
use crate::model::monitoring::direction::FlowDirection;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::misc::MiscError;
use crate::model::list_type::ListType;
use crate::model::access_control::list_type::ListType;
use crate::model::log::ebpf::EbpfLog;
use crate::model::log::system::SystemLog;
use macros::log;
@ -52,7 +52,7 @@ use macros::log;
/// Holds all Arc-wrapped services that make up the running application.
pub struct AppState {
pub app_config: Arc<AppConfig>,
pub inference_config: Arc<InferenceConfig>,
pub inference_config: Arc<MLInferenceConfig>,
pub ebpf_services: Arc<EbpfServices>,
pub app_services: Arc<AppServices>,
pub db: Arc<Database>,
@ -101,7 +101,7 @@ impl ServiceFactory {
let ingress_program_array = Self::configure_ingress_pipeline(&mut ingress_ebpf, &app_config.pipeline.ingress)?;
let inference_config = Arc::new(InferenceConfig::load_file(&app_config.inference.models_config_name)?);
let inference_config = Arc::new(MLInferenceConfig::load_file(&app_config.inference.models_config_name)?);
// Write queue count to eBPF maps for symmetric hash redirect
let num_queues = app_config.network.combined_queue_count;

View File

@ -3,8 +3,8 @@ use std::time;
use crate::core::ml::engine::Engine;
use crate::core::ml::flow_tracker::FlowData;
use crate::model::direction::Direction;
use crate::model::flow_stats::{FlowPushPayload, FlowStatsEntry, FlowSubscription, FlowSummary, StatsSummary};
use crate::model::monitoring::direction::Direction;
use crate::model::monitoring::flow_stats::{FlowPushPayload, FlowStatsEntry, FlowSubscription, FlowSummary, StatsSummary};
/// Conversion from core::ml::FlowData to model::FlowStatsEntry.
/// Placed here (core layer) to maintain dependency rule: model/ must not import core/.

View File

@ -1,4 +1,4 @@
use crate::model::auth::Claims;
use crate::model::identity::auth::Claims;
use crate::model::error::Error;
/// Type alias for API key list items: (id, name, permission_level, created_at, last_used_at)

View File

@ -1,5 +1 @@
pub mod constants;
// Backward-compatible re-exports: `crate::model::config::*` continues to resolve
// the domain config types that previously lived at `crate::model::system::config::*`.
pub use super::system::config::*;

View File

@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::model::ml_detection::ClipParams;
use crate::model::detection::ml_detection::ClipParams;
#[derive(Debug, Clone)]
pub struct FlowFeatures {

View File

@ -3,8 +3,8 @@ use std::net::{Ipv4Addr, Ipv6Addr};
use serde::{Deserialize, Serialize};
use tract_onnx::prelude::{Graph, SimplePlan, TypedFact, TypedOp};
use crate::model::direction::Direction;
use crate::model::user_packet::UserPacket;
use crate::model::monitoring::direction::Direction;
use crate::model::monitoring::user_packet::UserPacket;
pub type RunnableModel = SimplePlan<TypedFact, Box<dyn TypedOp>, Graph<TypedFact, Box<dyn TypedOp>>>;

View File

@ -1,4 +1,3 @@
// Bounded Context subdirectories
pub mod access_control;
pub mod config;
pub mod detection;
@ -10,14 +9,3 @@ pub mod monitoring;
pub mod report;
pub mod soar;
pub mod system;
// Backward-compatible re-exports (existing imports continue to work)
pub use access_control::ip_address;
pub use access_control::list_type;
pub use detection::ml_detection;
pub use identity::auth;
pub use monitoring::direction;
pub use monitoring::drop_event;
pub use monitoring::flow_stats;
pub use monitoring::user_packet;
pub use system::health;

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::model::direction::Direction;
use crate::model::monitoring::direction::Direction;
#[derive(Debug, Clone, Serialize)]
pub struct FlowStatsEntry {

View File

@ -7,10 +7,8 @@ pub struct Playbook {
pub name: String,
pub enabled: bool,
pub trigger_event: String,
pub condition_threshold: Option<f64>,
pub cooldown_secs: i64,
pub actions: Vec<PlaybookAction>,
/// Multi-condition rules (AND logic). Empty = legacy threshold-only mode.
pub conditions: Vec<PlaybookCondition>,
}

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::model::ml_detection::ClipParams;
use crate::model::detection::ml_detection::ClipParams;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct HttpConfig {
@ -86,6 +86,11 @@ pub struct MLInferenceConfig {
pub ae_threshold: f32,
pub classifier_feature_names: Vec<String>,
pub attack_labels: HashMap<String, String>,
pub anomaly_threshold: f32,
pub c2_threshold: f32,
pub model_type: String,
pub output_names: Vec<String>,
pub ae_feature_weights: HashMap<String, f64>,
}
impl MLInferenceConfig {

View File

@ -1,6 +1,6 @@
use std::time;
use crate::model::user_packet::UserPacket;
use crate::model::monitoring::user_packet::UserPacket;
pub fn parse_packet(packet_data: &[u8]) -> Option<(UserPacket, usize)> {
if packet_data.len() < 14 {