diff --git a/models/classifier.onnx b/models/classifier.onnx index 43b003c..0ba3a0e 100644 Binary files a/models/classifier.onnx and b/models/classifier.onnx differ diff --git a/models/deep_autoencoder.onnx b/models/deep_autoencoder.onnx index b5ffc06..ffe2d63 100644 Binary files a/models/deep_autoencoder.onnx and b/models/deep_autoencoder.onnx differ diff --git a/models/full_config.json b/models/full_config.json index b590d5d..2cd99fb 100644 --- a/models/full_config.json +++ b/models/full_config.json @@ -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" } } \ No newline at end of file diff --git a/models/inference_config.json b/models/inference_config.json index 1d42207..3e0324c 100644 --- a/models/inference_config.json +++ b/models/inference_config.json @@ -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 } -} \ No newline at end of file +} diff --git a/net-guardia/src/adapter/access_control_adapter.rs b/net-guardia/src/adapter/access_control_adapter.rs index 4614a67..6499258 100644 --- a/net-guardia/src/adapter/access_control_adapter.rs +++ b/net-guardia/src/adapter/access_control_adapter.rs @@ -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 { diff --git a/net-guardia/src/adapter/http/acl.rs b/net-guardia/src/adapter/http/acl.rs index f667bbf..bc1cdb3 100644 --- a/net-guardia/src/adapter/http/acl.rs +++ b/net-guardia/src/adapter/http/acl.rs @@ -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 { diff --git a/net-guardia/src/adapter/http/auth.rs b/net-guardia/src/adapter/http/auth.rs index e1a72b8..96db684 100644 --- a/net-guardia/src/adapter/http/auth.rs +++ b/net-guardia/src/adapter/http/auth.rs @@ -116,7 +116,6 @@ async fn login(body: web::Json, db: web::Data, 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) -> 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 { diff --git a/net-guardia/src/adapter/persistence/repository.rs b/net-guardia/src/adapter/persistence/repository.rs index c06d702..de80598 100644 --- a/net-guardia/src/adapter/persistence/repository.rs +++ b/net-guardia/src/adapter/persistence/repository.rs @@ -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, Error> { + pub fn validate_api_key(&self, api_key: &str) -> Result, 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, Error> { + fn validate_api_key(&self, api_key: &str) -> Result, Error> { self.validate_api_key(api_key) } fn insert_api_key(&self, key_hash: &str, name: &str, permission_level: &str) -> Result { diff --git a/net-guardia/src/adapter/websocket/alert_websocket.rs b/net-guardia/src/adapter/websocket/alert_websocket.rs index 3c328ff..f3fba72 100644 --- a/net-guardia/src/adapter/websocket/alert_websocket.rs +++ b/net-guardia/src/adapter/websocket/alert_websocket.rs @@ -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) -> Result { let (response, session, msg_stream) = handle(&req, body)?; diff --git a/net-guardia/src/adapter/websocket/drop_websocket.rs b/net-guardia/src/adapter/websocket/drop_websocket.rs index e212408..66b9294 100644 --- a/net-guardia/src/adapter/websocket/drop_websocket.rs +++ b/net-guardia/src/adapter/websocket/drop_websocket.rs @@ -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; diff --git a/net-guardia/src/adapter/websocket/flow_websocket.rs b/net-guardia/src/adapter/websocket/flow_websocket.rs index a81e984..83961ac 100644 --- a/net-guardia/src/adapter/websocket/flow_websocket.rs +++ b/net-guardia/src/adapter/websocket/flow_websocket.rs @@ -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 { diff --git a/net-guardia/src/adapter/websocket/health_websocket.rs b/net-guardia/src/adapter/websocket/health_websocket.rs index c8a6c3a..91937a1 100644 --- a/net-guardia/src/adapter/websocket/health_websocket.rs +++ b/net-guardia/src/adapter/websocket/health_websocket.rs @@ -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( diff --git a/net-guardia/src/core/acl_service.rs b/net-guardia/src/core/acl_service.rs index 74e4d82..ced498d 100644 --- a/net-guardia/src/core/acl_service.rs +++ b/net-guardia/src/core/acl_service.rs @@ -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. diff --git a/net-guardia/src/core/auth/extractor.rs b/net-guardia/src/core/auth/extractor.rs index e2f7ce0..52394bd 100644 --- a/net-guardia/src/core/auth/extractor.rs +++ b/net-guardia/src/core/auth/extractor.rs @@ -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. /// diff --git a/net-guardia/src/core/auth/jwt.rs b/net-guardia/src/core/auth/jwt.rs index 7b62106..8ae90cd 100644 --- a/net-guardia/src/core/auth/jwt.rs +++ b/net-guardia/src/core/auth/jwt.rs @@ -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; diff --git a/net-guardia/src/core/ebpf/access_control.rs b/net-guardia/src/core/ebpf/access_control.rs index f9f9844..b6b01c4 100644 --- a/net-guardia/src/core/ebpf/access_control.rs +++ b/net-guardia/src/core/ebpf/access_control.rs @@ -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>, diff --git a/net-guardia/src/core/ebpf/drop_monitor.rs b/net-guardia/src/core/ebpf/drop_monitor.rs index f6644ef..7da97db 100644 --- a/net-guardia/src/core/ebpf/drop_monitor.rs +++ b/net-guardia/src/core/ebpf/drop_monitor.rs @@ -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, diff --git a/net-guardia/src/core/ebpf/protocol_filter.rs b/net-guardia/src/core/ebpf/protocol_filter.rs index 565cd7d..4fa292d 100644 --- a/net-guardia/src/core/ebpf/protocol_filter.rs +++ b/net-guardia/src/core/ebpf/protocol_filter.rs @@ -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>, diff --git a/net-guardia/src/core/ebpf/xsk_manager.rs b/net-guardia/src/core/ebpf/xsk_manager.rs index a62ba96..741a9ae 100644 --- a/net-guardia/src/core/ebpf/xsk_manager.rs +++ b/net-guardia/src/core/ebpf/xsk_manager.rs @@ -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; diff --git a/net-guardia/src/core/email/scheduler.rs b/net-guardia/src/core/email/scheduler.rs index 9754d0a..b912317 100644 --- a/net-guardia/src/core/email/scheduler.rs +++ b/net-guardia/src/core/email/scheduler.rs @@ -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>, diff --git a/net-guardia/src/core/ml/aggregator.rs b/net-guardia/src/core/ml/aggregator.rs index d4527a9..4e23608 100644 --- a/net-guardia/src/core/ml/aggregator.rs +++ b/net-guardia/src/core/ml/aggregator.rs @@ -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>, @@ -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] diff --git a/net-guardia/src/core/ml/alert.rs b/net-guardia/src/core/ml/alert.rs index 3e025c4..77e0f98 100644 --- a/net-guardia/src/core/ml/alert.rs +++ b/net-guardia/src/core/ml/alert.rs @@ -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, diff --git a/net-guardia/src/core/ml/config_loader.rs b/net-guardia/src/core/ml/config_loader.rs index 040ca3a..bda40bb 100644 --- a/net-guardia/src/core/ml/config_loader.rs +++ b/net-guardia/src/core/ml/config_loader.rs @@ -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 { @@ -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) } } diff --git a/net-guardia/src/core/ml/engine.rs b/net-guardia/src/core/ml/engine.rs index 6c971b8..d856a9b 100644 --- a/net-guardia/src/core/ml/engine.rs +++ b/net-guardia/src/core/ml/engine.rs @@ -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, - config: Arc, + config: Arc, ml_alert: Arc, drift_detector: Arc>, engine_config: EngineConfig, diff --git a/net-guardia/src/core/ml/feature_extractor.rs b/net-guardia/src/core/ml/feature_extractor.rs index 5063a4d..32140e7 100644 --- a/net-guardia/src/core/ml/feature_extractor.rs +++ b/net-guardia/src/core/ml/feature_extractor.rs @@ -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, diff --git a/net-guardia/src/core/ml/flow_tracker.rs b/net-guardia/src/core/ml/flow_tracker.rs index df3b009..52cfc4a 100644 --- a/net-guardia/src/core/ml/flow_tracker.rs +++ b/net-guardia/src/core/ml/flow_tracker.rs @@ -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 { diff --git a/net-guardia/src/core/ml/inference.rs b/net-guardia/src/core/ml/inference.rs index 3c90702..2a0de83 100644 --- a/net-guardia/src/core/ml/inference.rs +++ b/net-guardia/src/core/ml/inference.rs @@ -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, - pub config: Arc, + pub config: Arc, } impl Inference { - pub fn new(models: Arc, config: Arc) -> Self { + pub fn new(models: Arc, config: Arc) -> 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::() { + 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::() { + Ok(v) => v.iter().copied().collect::>(), + 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::() { + 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::() { + return class_probs.get(idx).copied().unwrap_or(0.0); + } + } + 0.0 + } + fn preprocess_ae_features(&self, flow: &FlowData) -> Vec { 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 { 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) -> TractResult<(String, f32)> { - let result = self.models.classifier.run(tvec![input.into_tensor().into()])?; - - let output = result[0].to_array_view::()?; - - 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)) - } } diff --git a/net-guardia/src/core/ml/model_loader.rs b/net-guardia/src/core/ml/model_loader.rs index 9bc84d1..78a9e75 100644 --- a/net-guardia/src/core/ml/model_loader.rs +++ b/net-guardia/src/core/ml/model_loader.rs @@ -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, inference_config: &Arc) -> Result { + pub fn load_models(app_config: &Arc, inference_config: &Arc) -> Result { Ok(Self { deep_autoencoder: Self::loader( &app_config.inference.deep_autoencoder_name, diff --git a/net-guardia/src/core/soar/engine.rs b/net-guardia/src/core/soar/engine.rs index e584957..6e7e54d 100644 --- a/net-guardia/src/core/soar/engine.rs +++ b/net-guardia/src/core/soar/engine.rs @@ -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![], diff --git a/net-guardia/src/core/system.rs b/net-guardia/src/core/system.rs index 020c2d3..1101808 100644 --- a/net-guardia/src/core/system.rs +++ b/net-guardia/src/core/system.rs @@ -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, - pub inference_config: Arc, + pub inference_config: Arc, pub ebpf_services: Arc, pub app_services: Arc, pub db: Arc, @@ -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"); diff --git a/net-guardia/src/infrastructure/app_config.rs b/net-guardia/src/infrastructure/app_config.rs index 6a6c5c3..7b7cd8c 100644 --- a/net-guardia/src/infrastructure/app_config.rs +++ b/net-guardia/src/infrastructure/app_config.rs @@ -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(), diff --git a/net-guardia/src/infrastructure/app_services.rs b/net-guardia/src/infrastructure/app_services.rs index ff989a3..0509367 100644 --- a/net-guardia/src/infrastructure/app_services.rs +++ b/net-guardia/src/infrastructure/app_services.rs @@ -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, - inference_config: Arc, + inference_config: Arc, drift_detector: Arc>, ) -> Result { let health = SystemHealth::new(app_config.clone())?; diff --git a/net-guardia/src/infrastructure/health.rs b/net-guardia/src/infrastructure/health.rs index 83b2547..caec10d 100644 --- a/net-guardia/src/infrastructure/health.rs +++ b/net-guardia/src/infrastructure/health.rs @@ -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, }; diff --git a/net-guardia/src/infrastructure/http_server.rs b/net-guardia/src/infrastructure/http_server.rs index aa9b90e..9b80064 100644 --- a/net-guardia/src/infrastructure/http_server.rs +++ b/net-guardia/src/infrastructure/http_server.rs @@ -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, - pub inference_config: Arc, + pub inference_config: Arc, pub ebpf_services: Arc, pub app_services: Arc, pub db: Arc, diff --git a/net-guardia/src/infrastructure/service_factory.rs b/net-guardia/src/infrastructure/service_factory.rs index 9384c94..bcd31ca 100644 --- a/net-guardia/src/infrastructure/service_factory.rs +++ b/net-guardia/src/infrastructure/service_factory.rs @@ -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, - pub inference_config: Arc, + pub inference_config: Arc, pub ebpf_services: Arc, pub app_services: Arc, pub db: Arc, @@ -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; diff --git a/net-guardia/src/infrastructure/statistics.rs b/net-guardia/src/infrastructure/statistics.rs index b0b69fe..c96248e 100644 --- a/net-guardia/src/infrastructure/statistics.rs +++ b/net-guardia/src/infrastructure/statistics.rs @@ -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/. diff --git a/net-guardia/src/interface/port/api_key.rs b/net-guardia/src/interface/port/api_key.rs index 697d4df..94e4af2 100644 --- a/net-guardia/src/interface/port/api_key.rs +++ b/net-guardia/src/interface/port/api_key.rs @@ -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) diff --git a/net-guardia/src/model/config/mod.rs b/net-guardia/src/model/config/mod.rs index 81c710f..0b6d2c4 100644 --- a/net-guardia/src/model/config/mod.rs +++ b/net-guardia/src/model/config/mod.rs @@ -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::*; diff --git a/net-guardia/src/model/detection/flow_features.rs b/net-guardia/src/model/detection/flow_features.rs index 7b6d39c..4ad67d0 100644 --- a/net-guardia/src/model/detection/flow_features.rs +++ b/net-guardia/src/model/detection/flow_features.rs @@ -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 { diff --git a/net-guardia/src/model/detection/ml_detection.rs b/net-guardia/src/model/detection/ml_detection.rs index 00c3f50..c876be3 100644 --- a/net-guardia/src/model/detection/ml_detection.rs +++ b/net-guardia/src/model/detection/ml_detection.rs @@ -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, Graph>>; diff --git a/net-guardia/src/model/mod.rs b/net-guardia/src/model/mod.rs index 0f4dc1c..dfdd007 100644 --- a/net-guardia/src/model/mod.rs +++ b/net-guardia/src/model/mod.rs @@ -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; diff --git a/net-guardia/src/model/monitoring/flow_stats.rs b/net-guardia/src/model/monitoring/flow_stats.rs index 938606f..67ec434 100644 --- a/net-guardia/src/model/monitoring/flow_stats.rs +++ b/net-guardia/src/model/monitoring/flow_stats.rs @@ -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 { diff --git a/net-guardia/src/model/soar/playbook.rs b/net-guardia/src/model/soar/playbook.rs index 7d2d811..1fc2868 100644 --- a/net-guardia/src/model/soar/playbook.rs +++ b/net-guardia/src/model/soar/playbook.rs @@ -7,10 +7,8 @@ pub struct Playbook { pub name: String, pub enabled: bool, pub trigger_event: String, - pub condition_threshold: Option, pub cooldown_secs: i64, pub actions: Vec, - /// Multi-condition rules (AND logic). Empty = legacy threshold-only mode. pub conditions: Vec, } diff --git a/net-guardia/src/model/system/config.rs b/net-guardia/src/model/system/config.rs index 23f4ff9..70df18b 100644 --- a/net-guardia/src/model/system/config.rs +++ b/net-guardia/src/model/system/config.rs @@ -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, pub attack_labels: HashMap, + pub anomaly_threshold: f32, + pub c2_threshold: f32, + pub model_type: String, + pub output_names: Vec, + pub ae_feature_weights: HashMap, } impl MLInferenceConfig { diff --git a/net-guardia/src/utils/packet_parser.rs b/net-guardia/src/utils/packet_parser.rs index 0055e92..5344a78 100644 --- a/net-guardia/src/utils/packet_parser.rs +++ b/net-guardia/src/utils/packet_parser.rs @@ -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 {