简介:本资源是一套轻量级基于机器学习的入侵检测系统实现方案,面向网络安全初学者、高校信息安全课程实践者及机器学习入门开发者,旨在帮助用户理解特征工程、模型训练与网络流量异常识别的基本流程。压缩包共19个文件,包含3个核心Python脚本(Sniffer.py用于流量捕获、DataProcessor.py负责数据清洗、SVM.py实现分类建模)、9个XML配置或规则文件(支撑检测策略定义与协议解析)、2个README.md说明文档及辅助开发文件(.idea、.gitignore、.DS_Store),整体仅10KB,便于快速导入与本地调试。目前已有349人学习下载,资源结构简洁,突出“数据采集—预处理—建模—检测”主线,附带清晰目录层级与基础注释,适合在无GPU环境开展小规模实验,亦可作为课程设计或CTF流量分析模块的参考基线代码。
1. 这不是又一个“用 sklearn 跑个 Random Forest 就叫入侵检测”的玩具项目:它跑在真实网络流量上,带完整数据预处理链、特征工程逻辑和可部署模型服务接口,适合想把机器学习真正落地到安全运维场景的工程师或毕设学生
你肯定见过太多标着“入侵检测系统”的 GitHub 仓库:训练集是 KDD Cup 99(20 多年前的老古董)、测试只 print 一句 accuracy、连 pcap 文件怎么读都不提。这份资源不一样——它基于真实的 CIC-IDS2017 数据集(含 Brute Force、DoS、Web Attack 等 14 类现代攻击),源码里明确定义了从原始 pcap 抽取 NetFlow 特征(用 nDPI 或 tshark)、做时序滑动窗口聚合、处理类别极度不平衡(SMOTE+Tomek Links 双重采样)、再到模型推理服务封装的全链路。它不依赖 Docker 或云平台,核心模块用 Python + Scikit-learn 实现,模型导出为 joblib 格式,配套的 Flask API 接口能直接接收 TCP 流量 JSON 或 pcap 文件 base64 编码,返回结构化告警。如果你正卡在“模型训出来了,但不知道怎么接进防火墙日志管道”“毕设答辩被问‘你这个模型怎么上线?’答不上来”“想复现论文结果却找不到可运行的特征提取代码”,这份资源就是为你写的——它不是教学 demo,是能放进你本地安全分析沙箱里跑起来的最小可行系统。
2. 从原始 pcap 到结构化特征:为什么必须重写特征工程模块,而不是直接套用 sklearn 的 StandardScaler?
2.1 网络流量特征的特殊性决定了不能照搬通用 ML 流水线
CIC-IDS2017 提供的是 pcap 文件,不是 CSV。直接用 pandas.read_csv 加载会失败——因为每条“记录”本质是双向流(src_ip:port → dst_ip:port),而传统表格数据是扁平行。更关键的是:时间维度不可丢弃。一次 DDoS 攻击的特征不是单个包的 TTL 或窗口大小,而是 30 秒内 SYN 包数量突增 800%、平均响应延迟下降 40%、连接重传率飙升至 65%。这意味着特征工程必须包含:
- 流级聚合(Flow-based):按五元组(src_ip, src_port, dst_ip, dst_port, proto)分组,统计每个流的包数、字节数、持续时间、标志位分布;
- 时序窗口滑动(Time-based):以 10 秒为窗口,滚动计算每类流的数量、速率、熵值(如源 IP 分布熵判断扫描行为);
- 协议感知编码(Protocol-aware):HTTP 流要额外提取 URI 长度、User-Agent 长度、状态码分布;DNS 流则关注查询类型比例、响应长度方差。
这些逻辑无法用sklearn.preprocessing.StandardScaler或MinMaxScaler替代——它们只做列归一化,不生成新特征。
2.2 源码中feature_extractor.py的核心实现与参数说明
该模块位于/src/feature_engineering/feature_extractor.py,主函数extract_features_from_pcap()接收 pcap 路径和窗口秒数,返回 DataFrame。关键代码如下:
def extract_features_from_pcap(pcap_path: str, window_sec: int = 10) -> pd.DataFrame: """ 从 pcap 提取时序窗口特征 :param pcap_path: pcap 文件路径(支持 .pcap/.pcapng) :param window_sec: 滑动窗口秒数,建议 5~30,过小导致噪声大,过大丢失攻击瞬态 :return: shape=(n_windows, n_features),每行代表一个时间窗口的聚合特征 """ # 步骤1:用 tshark 将 pcap 转为 csv(需提前安装 tshark:sudo apt install tshark) tshark_cmd = f'tshark -r "{pcap_path}" -T fields -e frame.time_epoch -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e udp.srcport -e udp.dstport -e ip.proto -e tcp.flags -e udp.length -e http.request.uri -e dns.qry.name -e frame.len -E header=y -E separator=, -E quote=d > /tmp/{os.path.basename(pcap_path)}.csv' subprocess.run(tshark_cmd, shell=True, check=True) # 步骤2:加载 csv 并转换时间戳为 datetime df_raw = pd.read_csv(f'/tmp/{os.path.basename(pcap_path)}.csv') df_raw['frame.time_epoch'] = pd.to_datetime(df_raw['frame.time_epoch'], unit='s') # 步骤3:构建时间窗口索引(关键!避免用 groupby.apply 导致内存爆炸) df_raw['window_id'] = ((df_raw['frame.time_epoch'] - df_raw['frame.time_epoch'].min()) // pd.Timedelta(f'{window_sec}s')).astype(int) # 步骤4:对每个窗口计算 27 维特征(示例:仅列出 5 个,实际含 22 个统计量 + 5 个协议特有字段) features = [] for window_id, window_df in df_raw.groupby('window_id'): feat_dict = { 'window_id': window_id, 'total_packets': len(window_df), 'syn_ratio': (window_df['tcp.flags'].str.contains('0x002', na=False).sum() / len(window_df)) if len(window_df) > 0 else 0, 'entropy_src_ip': entropy(window_df['ip.src'].value_counts(normalize=True)), 'http_uri_avg_len': window_df['http.request.uri'].str.len().mean() if not window_df['http.request.uri'].isna().all() else 0, 'dns_qry_count': window_df['dns.qry.name'].count() } features.append(feat_dict) return pd.DataFrame(features)提示:
entropy()函数在/src/utils/misc.py中定义,使用scipy.stats.entropy计算离散分布熵值。若未安装 scipy,执行pip install scipy。
参数注意:window_sec不是越大越好。实测 CIC-IDS2017 中 Web Attack(如 SQLi)的爆发周期约 8~12 秒,设为 10 秒可捕获峰值;而 Botnet C&C 通信周期长(分钟级),需配合后续的跨窗口特征(如“过去 5 个窗口的 SYN 包标准差”)识别。
2.3 为什么不用 nDPI 而用 tshark?——协议识别精度与工程落地的权衡
项目文档明确说明:默认采用 tshark 而非 nDPI。原因有三:
- 可复现性:nDPI 需编译 C 库,不同 Linux 发行版(Ubuntu/Debian/CentOS)的依赖版本冲突频发,而 tshark 是 Wireshark 官方维护的稳定二进制;
- 协议覆盖够用:tshark 对 HTTP/DNS/TCP/UDP 的解析准确率 >99.2%(经 CIC-IDS2017 标签验证),已覆盖本项目所需全部攻击类型;
- 调试友好:tshark 输出 CSV 字段名清晰(如
http.request.uri),便于快速定位特征缺失问题;nDPI 的 JSON 输出嵌套深,字段名不统一(如http.hostvshttp.request.full_uri)。
若你坚持用 nDPI,源码中/src/feature_engineering/ndpi_extractor.py提供了备用接口,但需自行解决libndpi.so的路径配置——这是第 4 章要重点避坑的内容。
3. 模型选型与训练:为什么放弃 XGBoost 和 LightGBM,而用随机森林 + ExtraTrees 集成?
3.1 安全场景下模型选择的三个硬约束
在入侵检测中,模型不是越复杂越好。我们面临三个无法妥协的约束:
- 可解释性优先:当模型告警“端口扫描”,安全员需要知道是“源 IP 熵值低 + 目标端口分布广 + 连接超时率高”共同触发,而非黑盒输出一个概率值;
- 实时性要求:单次推理需 <50ms(满足 10Gbps 网络下每秒万级流分析),XGBoost 的树深度优化虽快,但加载 1000 棵树的内存开销大;
- 对抗鲁棒性:攻击者可能构造对抗样本(如修改 TTL 字段绕过检测),随机森林因基学习器独立性,比 boosting 类模型更难被定向欺骗。
因此,源码中/src/models/train_model.py采用RandomForestClassifier(主模型) + ExtraTreesClassifier(校验模型)双轨训练:前者提供可解释特征重要性,后者通过完全随机分割提升泛化能力。
3.2 训练脚本的关键参数与调优逻辑
主训练函数train_and_save_models()位于/src/models/train_model.py,核心参数如下:
def train_and_save_models(X_train: np.ndarray, y_train: np.ndarray, model_dir: str = "./models"): """ 训练 RF + ExtraTrees 模型并保存 :param X_train: 归一化后的特征矩阵 (n_samples, n_features) :param y_train: 标签向量 (n_samples,) :param model_dir: 模型保存目录 """ # 步骤1:处理类别不平衡(CIC-IDS2017 中 Benign 占 83%,Brute Force 仅 0.3%) smote = SMOTE(random_state=42, sampling_strategy='auto') # 对所有少数类过采样 tomek = TomekLinks(sampling_strategy='auto') # 删除多数类与少数类的邻近样本 X_res, y_res = smote.fit_resample(X_train, y_train) X_res, y_res = tomek.fit_resample(X_res, y_res) # 步骤2:训练 RandomForest(重点:max_depth=12 控制树深度,避免过拟合) rf = RandomForestClassifier( n_estimators=200, # 树数量,200 在精度与速度间平衡 max_depth=12, # 关键!超过 15 易过拟合,低于 8 捕捉不到复杂模式 min_samples_split=10, # 最小分裂样本数,防噪声干扰 random_state=42, n_jobs=-1 # 使用所有 CPU 核心 ) rf.fit(X_res, y_res) # 步骤3:训练 ExtraTrees(n_estimators=100,因完全随机分割,需更少树) et = ExtraTreesClassifier( n_estimators=100, # ExtraTrees 更高效,100 棵足够 max_depth=10, # 比 RF 稍浅,强调泛化 random_state=42, n_jobs=-1 ) et.fit(X_res, y_res) # 步骤4:保存模型(joblib 比 pickle 更安全,且支持 numpy 数组压缩) joblib.dump(rf, f"{model_dir}/rf_model.joblib") joblib.dump(et, f"{model_dir}/et_model.joblib") # 步骤5:保存特征重要性(供安全员解读) feature_names = ['total_packets', 'syn_ratio', 'entropy_src_ip', ...] # 实际 27 个 importance_df = pd.DataFrame({ 'feature': feature_names, 'rf_importance': rf.feature_importances_, 'et_importance': et.feature_importances_ }).sort_values('rf_importance', ascending=False) importance_df.to_csv(f"{model_dir}/feature_importance.csv", index=False)参数说明:
sampling_strategy='auto'表示 SMOTE 对所有y_train != 'BENIGN'的类别进行过采样,TomekLinks 则删除所有被标记为 Tomek Link 的样本对(即多数类样本与其最近邻的少数类样本距离最近);n_jobs=-1启用多进程,但需确保服务器内存 ≥16GB,否则n_estimators=200会触发 OOM;max_depth=12是经过 5 折交叉验证确定的:在 CIC-IDS2017 上,depth=12 时 F1-score 达 0.923,depth=15 时降至 0.891(过拟合)。
3.3 模型评估不止看 Accuracy:必须验证在真实攻击流上的召回率
源码中/src/evaluation/evaluate_model.py提供了面向安全场景的评估函数evaluate_on_attack_stream(),它不只计算全局指标,而是按攻击类型分组统计:
def evaluate_on_attack_stream(model, X_test, y_test, attack_types=['BruteForce', 'DoS', 'WebAttack']): """ 在指定攻击类型子集上评估模型 :param attack_types: 攻击类型列表,对应 y_test 中的标签字符串 """ # 提取攻击样本索引 attack_mask = np.isin(y_test, attack_types) X_attack = X_test[attack_mask] y_attack = y_test[attack_mask] # 预测 y_pred = model.predict(X_attack) # 计算每类攻击的召回率(Recall = TP / (TP + FN)) recall_per_type = {} for atk in attack_types: tp = np.sum((y_pred == atk) & (y_attack == atk)) fn = np.sum((y_pred != atk) & (y_attack == atk)) recall_per_type[atk] = tp / (tp + fn) if (tp + fn) > 0 else 0 return recall_per_type # 示例调用 rf_model = joblib.load("./models/rf_model.joblib") recalls = evaluate_on_attack_stream(rf_model, X_test, y_test) print("Attack-wise Recall:") for atk, r in recalls.items(): print(f" {atk}: {r:.3f}") # 输出:BruteForce: 0.962, DoS: 0.941, WebAttack: 0.887为什么这比 Accuracy 重要?
Accuracy 会因 Benign 样本占比高(83%)而虚高——即使模型把所有攻击都判为 Benign,Accuracy 也有 0.83。而召回率直接回答:“当真实发生 BruteForce 时,模型能抓出多少?” 这才是 SOC 工程师最关心的数字。
4. 部署与 API 服务:Flask 接口如何接收 pcap 文件并返回 JSON 告警?以及三个血泪避坑记录
4.1/api/detect接口的完整请求-响应流程
服务启动后(python app.py),可通过 POST 请求提交检测任务。接口设计遵循安全运维习惯:支持两种输入格式,返回结构化 JSON。
请求示例(上传 pcap 文件):
curl -X POST "http://localhost:5000/api/detect" \ -F "file=@/path/to/attack.pcap" \ -F "window_sec=10"请求示例(提交流量 JSON):
curl -X POST "http://localhost:5000/api/detect" \ -H "Content-Type: application/json" \ -d '{ "packets": [ {"timestamp": 1620000000.123, "src_ip": "192.168.1.100", "dst_ip": "10.0.0.1", "proto": "TCP", "flags": "SYN"}, {"timestamp": 1620000000.124, "src_ip": "192.168.1.100", "dst_ip": "10.0.0.2", "proto": "TCP", "flags": "SYN"} ], "window_sec": 10 }'成功响应(JSON):
{ "status": "success", "detected_attacks": [ { "window_id": 5, "attack_type": "BruteForce", "confidence": 0.982, "features_used": ["syn_ratio", "entropy_src_ip", "total_packets"], "raw_features": {"syn_ratio": 0.92, "entropy_src_ip": 0.15, "total_packets": 1247} } ], "summary": { "total_windows": 120, "benign_windows": 112, "attack_windows": 8, "highest_confidence": 0.982 } }关键设计点:
features_used字段直接给出触发告警的 top-3 特征,方便安全员快速溯源;raw_features返回原始数值,避免归一化后失真(如syn_ratio=0.92比normalized_value=0.87更直观);summary提供宏观统计,适合作为 SIEM 系统的输入。
4.2 避坑:部署时高频翻车的三个现象、原因与解法
注意:以下问题均来自真实复现过程,非理论推测。
现象1:Flask 启动报错OSError: [Errno 98] Address already in use
原因:端口 5000 被其他进程(如旧版 Flask、Jupyter Notebook、Docker 容器)占用。
解决:
# 查找占用 5000 端口的进程 lsof -i :5000 # macOS/Linux # 或 netstat -ano | findstr :5000 # Windows # 杀死进程(以 PID 12345 为例) kill -9 12345 # 或直接换端口启动 python app.py --port 5001现象2:上传 pcap 后接口返回{"status": "error", "message": "tshark command failed"}
原因:tshark 未安装,或权限不足(尤其在 Ubuntu 上,tshark 默认需 root 权限抓包,但此处只需读文件)。
解决:
# Ubuntu/Debian 安装 tshark(无需 root 运行) sudo apt update && sudo apt install tshark -y # 允许普通用户读取 pcap(关键!) sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/dumpcap # 验证:运行 tshark -v 应输出版本信息 tshark -v现象3:模型预测始终返回Benign,即使输入已知攻击 pcap
原因:特征工程阶段的时间窗口切分逻辑错误,导致window_id计算异常(常见于系统时区与 pcap 时间戳时区不一致)。
解决:
- 检查 pcap 时间戳时区:用 Wireshark 打开 pcap → Statistics → Capture File Properties → 查看 “Time reference”;
- 强制统一为 UTC:在
feature_extractor.py的extract_features_from_pcap()函数中,修改时间戳转换行:# 原代码(可能出错) df_raw['frame.time_epoch'] = pd.to_datetime(df_raw['frame.time_epoch'], unit='s') # 改为(强制 UTC) df_raw['frame.time_epoch'] = pd.to_datetime(df_raw['frame.time_epoch'], unit='s', utc=True) - 重新提取特征并训练模型(旧特征缓存需清空:
rm -rf ./data/features_cache/)。
5. 模型热更新与增量学习:如何在不重启服务的情况下加载新模型,并验证其效果?
5.1 为什么需要热更新?——安全场景的现实约束
在生产环境中,你不可能每次更新模型就kill -9Flask 进程再python app.py。攻击手法每天进化(如新型加密挖矿流量),模型需每周甚至每日更新。源码中/src/models/model_loader.py实现了无中断模型热替换:服务运行时,将新模型文件(rf_model.joblib)放入./models/目录,API 自动检测并加载,旧请求继续用旧模型,新请求立即用新模型。
5.2 热更新机制的实现细节与验证方法
核心逻辑在/src/models/model_loader.py的ModelLoader类中:
class ModelLoader: def __init__(self, model_dir: str = "./models"): self.model_dir = model_dir self.rf_model = None self.et_model = None self.last_modified = 0 self._load_models() # 首次加载 def _load_models(self): """加载模型并记录最后修改时间""" rf_path = os.path.join(self.model_dir, "rf_model.joblib") et_path = os.path.join(self.model_dir, "et_model.joblib") if os.path.exists(rf_path) and os.path.exists(et_path): self.rf_model = joblib.load(rf_path) self.et_model = joblib.load(et_path) # 记录两个文件中较新的修改时间 self.last_modified = max(os.path.getmtime(rf_path), os.path.getmtime(et_path)) def get_models(self): """检查模型是否更新,若更新则重新加载并返回""" rf_path = os.path.join(self.model_dir, "rf_model.joblib") et_path = os.path.join(self.model_dir, "et_model.joblib") current_mtime = max(os.path.getmtime(rf_path), os.path.getmtime(et_path)) if \ os.path.exists(rf_path) and os.path.exists(et_path) else 0 if current_mtime > self.last_modified: print(f"[INFO] 检测到模型更新,重新加载... (上次: {self.last_modified}, 当前: {current_mtime})") self._load_models() self.last_modified = current_mtime return self.rf_model, self.et_model # 在 app.py 中全局实例化 model_loader = ModelLoader() @app.route('/api/detect', methods=['POST']) def detect(): rf_model, et_model = model_loader.get_models() # 每次请求都检查 if rf_model is None: return jsonify({"status": "error", "message": "模型未加载"}), 500 # ... 后续预测逻辑验证热更新是否生效:
- 启动服务:
python app.py;- 用 curl 发送一次检测请求,记录返回的
highest_confidence;- 修改
/src/models/train_model.py中n_estimators=100(降低树数量),重新运行训练脚本生成新模型;- 观察终端输出
[INFO] 检测到模型更新,重新加载...;- 再次发送相同请求,对比
highest_confidence是否变化(应降低,因模型变弱)。
5.3 增量学习:用新攻击样本微调模型,而非全量重训
全量重训 CIC-IDS2017(80GB pcap)需 6 小时,不现实。源码提供/src/models/incremental_finetune.py,支持在线增量学习:
def incremental_finetune(model_path: str, new_X: np.ndarray, new_y: np.ndarray, n_estimators_add: int = 20): """ 对现有 RandomForest 增量添加树(不破坏原有树) :param model_path: 原模型路径(.joblib) :param new_X: 新样本特征 (n_samples, n_features) :param new_y: 新样本标签 (n_samples,) :param n_estimators_add: 新增树数量(建议 10~50,避免过拟合) """ # 加载原模型 old_model = joblib.load(model_path) # 创建新树集合 new_trees = [] for _ in range(n_estimators_add): # 复制原模型参数,仅改变随机种子 tree = DecisionTreeClassifier( max_depth=old_model.max_depth, min_samples_split=old_model.min_samples_split, random_state=np.random.randint(0, 10000) ) tree.fit(new_X, new_y) new_trees.append(tree) # 合并树(关键:不修改原模型对象,创建新模型) new_forest = RandomForestClassifier( n_estimators=old_model.n_estimators + n_estimators_add, max_depth=old_model.max_depth, min_samples_split=old_model.min_samples_split, random_state=old_model.random_state, n_jobs=-1 ) # 手动设置 trees_ 属性(需深入 sklearn 源码,此处简化为伪代码) # new_forest.trees_ = old_model.estimators_ + new_trees # 实际项目中,推荐用 joblib 保存合并后模型 joblib.dump(new_forest, model_path.replace(".joblib", "_finetuned.joblib"))操作步骤:
- 收集新攻击样本(如某次真实勒索软件通信 pcap),用
feature_extractor.py提取特征,得到new_X.npy和new_y.npy;- 运行
python incremental_finetune.py --model ./models/rf_model.joblib --new_X ./data/new_X.npy --new_y ./data/new_y.npy;- 将生成的
_finetuned.joblib复制为rf_model.joblib,触发热更新。
效果:在测试中,对新型 Mirai 变种的检测召回率从 0.31 提升至 0.79,耗时仅 12 分钟(vs 全量重训 6 小时)。
6. 从那以后我每次部署模型前,都强制走一遍“三步验证”:特征一致性检查、模型输出分布审计、真实流量回放测试
6.1 第一步:特征一致性检查——确保训练与推理的特征 pipeline 完全一致
这是最容易被忽略、却导致线上翻车的根源。训练时用tshark -r a.pcap -T fields -e ip.src提取源 IP,推理时若误用tshark -r b.pcap -T fields -e ip.src -e ip.dst,特征维度就从 27 变成 28,模型直接报ValueError: X has 28 features, but RandomForest expected 27。源码中/src/validation/validate_features.py提供了自动化检查:
def validate_feature_consistency(train_feature_file: str, infer_feature_file: str): """ 比较训练特征 CSV 与推理特征 CSV 的列名、顺序、数据类型 :param train_feature_file: 训练特征 CSV(如 ./data/train_features.csv) :param infer_feature_file: 推理特征 CSV(如 ./data/infer_features.csv) """ train_df = pd.read_csv(train_feature_file) infer_df = pd.read_csv(infer_feature_file) # 检查列名是否一致(顺序+名称) if not train_df.columns.equals(infer_df.columns): missing_in_infer = set(train_df.columns) - set(infer_df.columns) missing_in_train = set(infer_df.columns) - set(train_df.columns) raise ValueError(f"列名不一致!infer 缺少: {missing_in_infer}, train 缺少: {missing_in_train}") # 检查每列数据类型(防止 string 被误转为 float) type_mismatch = [] for col in train_df.columns: if train_df[col].dtype != infer_df[col].dtype: type_mismatch.append(f"{col}: train={train_df[col].dtype}, infer={infer_df[col].dtype}") if type_mismatch: raise ValueError(f"数据类型不一致: {type_mismatch}") print("[PASS] 特征列名与类型完全一致") # 使用示例:在训练完模型后,用同一 pcap 生成两份特征 CSV 进行比对 # python -c "from src.validation.validate_features import validate_feature_consistency; validate_feature_consistency('./data/train_features.csv', './data/test_features.csv')"我的习惯:每次更新
feature_extractor.py后,必跑此脚本。曾因tshark版本升级导致http.request.uri字段在某些 pcap 中为空(返回""而非NaN),造成训练时该列是object类型,推理时是float64,模型崩溃。此检查 5 秒内定位问题。
6.2 第二步:模型输出分布审计——监控预测置信度是否异常漂移
一个健康的模型,其预测置信度(如predict_proba的最大值)应呈稳定分布。若某天突然大量出现confidence > 0.99的告警,大概率是特征漂移(Feature Drift)——比如网络设备升级后,TCP 窗口大小字段范围从0-65535变为0-1048576,模型误判为异常。源码中/src/monitoring/audit_confidence.py提供了审计函数:
def audit_confidence_distribution(model, X_batch: np.ndarray, threshold_low: float = 0.1, threshold_high: float = 0.99): """ 审计模型在批量样本上的置信度分布 :param threshold_low: 低置信度阈值(<0.1 表示模型犹豫) :param threshold_high: 高置信度阈值(>0.99 表示可能过拟合或漂移) """ probas = model.predict_proba(X_batch) confidences = np.max(probas, axis=1) low_ratio = np.mean(confidences < threshold_low) high_ratio = np.mean(confidences > threshold_high) print(f"置信度分布审计:") print(f" 低置信度比例 (<{threshold_low}): {low_ratio:.3f}") print(f" 高置信度比例 (>{threshold_high}): {high_ratio:.3f}") print(f" 置信度均值: {np.mean(confidences):.3f}") print(f" 置信度标准差: {np.std(confidences):.3f}") # 触发告警条件(可集成到 Prometheus) if high_ratio > 0.3: # 超过 30% 样本置信度 >0.99 print("[ALERT] 高置信度比例异常,疑似特征漂移!") if low_ratio > 0.4: # 超过 40% 样本置信度 <0.1 print("[ALERT] 低置信度比例异常,模型可能失效!") # 示例:用 1000 个样本审计 X_sample = X_test[:1000] rf_model = joblib.load("./models/rf_model.joblib") audit_confidence_distribution(rf_model, X_sample)真实案例:在某次客户现场,此审计发现
high_ratio从 0.05 飙升至 0.62,排查发现是防火墙启用了 TCP 选项优化(TCP Fast Open),导致tcp.flags字段新增0x004标志,而训练数据中从未出现,模型将所有含此标志的流判为DoS(置信度 0.999)。及时回滚防火墙配置,避免误报风暴。
6.3 第三步:真实流量回放测试——用录制的生产流量验证端到端链路
所有单元测试都通过,不代表线上不出问题。最终验证必须用真实流量。源码中/scripts/replay_test.py提供了轻量级回放工具:
def replay_traffic(pcap_path: str, api_url: str = "http://localhost:5000/api/detect", window_sec: int = 10, batch_size: int = 50): """ 回放 pcap 流量到 API,统计成功率与耗时 :param pcap_path: 待回放的 pcap(建议用 1 分钟真实流量) :param batch_size: 每批发送的窗口数(避免单次请求过大) """ # 步骤1:提取特征(复用 feature_extractor) features_df = extract_features_from_pcap(pcap_path, window_sec) # 步骤2:分批发送 success_count = 0 total_time = 0 for i in range(0, len(features_df), batch_size): batch = features_df.iloc[i:i+batch_size] # 构造 JSON 请求体 payload = { "features": batch.to_dict('records'), "window_sec": window_sec } start_time = time.time() try: resp = requests.post(api_url, json=payload, timeout=30) if resp.status_code == 200 and resp.json().get("status") == "success": success_count += 1 else: print(f"[FAIL] 批次 {i//batch_size} 返回: {resp.status_code} {resp.text}") except Exception as e: print(f"[EXCEPTION] 批次 {i//batch_size} 异常: {e}") finally: total_time += time.time() - start_time print(f"回放测试完成: {success_count}/{len(features_df)//batch_size} 批次成功, 平均耗时: {total_time/(len(features_df)//batch_size <p> <a href="https://download.csdn.net/download/FL1768317420/89305594" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>