ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

NVD与CNNVD漏洞文本预处理与BERT分类实战

NVD与CNNVD漏洞文本预处理与BERT分类实战 简介本资源是一套面向安全研究者与AI初学者的软件漏洞分析实践数据集与代码工具包聚焦NVD与CNNVD两大权威漏洞数据库的文本挖掘与分类建模任务。资源包含54个文件主体为32个XML格式原始漏洞数据、11个Python脚本涵盖数据预处理、TF-IDF特征提取、DNN/传统机器学习模型训练、可视化分析等全流程、3个Excel结构化样本表及3张架构图与结果图VSD/JPG整体压缩包72.93MB结构清晰、模块解耦便于复现实验链路。已有192人学习下载适合开展漏洞文本清洗、信息增益特征选择、多类别漏洞自动归类等典型NLP安全交叉课题。读者可直接运行data_preprocessing.py完成停用词过滤与标准化调用vul_dnn.py或visual_vul_category.py快速构建并评估分类模型配套stopwords.txt、all_datasets.xlsx等资源降低入门门槛具备完整科研复现与课程实验支撑能力。1. 为什么用 NVD 和 CNNVD 做漏洞分类不是“堆数据”而是“建能力”从原始文本到可部署模型的闭环路径你手头有 NVD美国国家漏洞数据库和 CNNVD中国国家信息安全漏洞库的全部公开漏洞条目——但它们不是拿来就能训模型的 Excel 表。每条记录是半结构化 JSON 或 XML字段混杂着 CVE 编号、发布时间、影响产品、CVSS 评分、厂商描述、官方补丁链接还有最关键的——一段非标准化的自然语言描述如“攻击者可通过发送特制的 HTTP 请求导致远程代码执行”。这段文字里藏着漏洞类型RCE、XSS、SQLi、触发条件认证后/未认证、利用难度需交互/无需交互等硬信息但模型看不见。NVD 和 CNNVD 软件漏洞数据集漏洞文本预处理训练算法模型进行漏洞分类——这个标题不是在说“我下载了两个库”而是在定义一条工业级漏洞情报自动化流水线把散落在全球漏洞库里的“人话”变成机器能学、能分、能上线的结构化标签。它适合安全运营中心SOC工程师做自动化分级适合 DevSecOps 团队嵌入 CI/CD 做 PR 漏洞语义扫描也适合高校团队构建可复现、可对比、可落地的漏洞理解基线。关键不在数据量NVD 累计超 20 万条CNNVD 超 15 万条而在如何让模型真正读懂“缓冲区溢出”和“目录遍历”在文本中的表达差异——这恰恰是多数开源方案翻车的起点。2. 从原始 JSON/XML 到干净文本NVD 与 CNNVD 数据对齐与字段清洗的实操逻辑NVD 和 CNNVD 表面都是漏洞库实际是两套独立演化的数据体系NVD 用 JSON APIhttps://services.nvd.nist.gov/rest/json/cves/2.0字段名全小写带下划线cve、descriptions、metricsCNNVD 官网只提供 ZIP 下载包含 XML 和 PDF其 XML 结构嵌套深、命名不统一vuln:referencevsvuln:cnnvd-id且中文描述常夹杂 HTML 标签、换行符、厂商广告语。直接拼接或简单去重会引入大量噪声——比如同一条 CVE-2022-22965在 NVD 中描述为“Spring Framework RCE via Data Binding”在 CNNVD 中却写成“Spring 框架存在远程代码执行漏洞CNVD-2022-XXXXX”。不做字段对齐后续所有模型训练都是空中楼阁。2.1 统一数据源获取与基础解析避开官网反爬与格式陷阱NVD 推荐用官方 REST APIv2.0避免爬取 HTML 页面。关键参数必须设startIndex0resultsPerPage2000最大单页数pubStartDate和pubEndDate控制时间范围例2020-01-01T00:00:00.000keywordSearch不要用——它返回的是模糊匹配漏掉大量无关键词但含实质描述的条目curl -s https://services.nvd.nist.gov/rest/json/cves/2.0?pubStartDate2020-01-01T00:00:00.000pubEndDate2023-12-31T23:59:59.999resultsPerPage2000startIndex0 \ -H Accept: application/json nvd_2020_2023.jsonCNNVD 必须下载年度 ZIP 包如CNNVD-2023-12-31.zip解压后处理*.xml文件。切忌用 BeautifulSoup 直接 parse 整个 XML——其根节点cnnvd下有vuln多层嵌套且部分description内含p、br标签。正确做法是用xml.etree.ElementTree迭代解析import xml.etree.ElementTree as ET def parse_cnnvd_xml(file_path): tree ET.parse(file_path) root tree.getroot() records [] for vuln in root.findall(.//vuln): cve_id vuln.findtext(cve-id) or cnnvd_id vuln.findtext(cnnvd-id) or # 关键提取 description 文本移除所有标签但保留换行语义 desc_elem vuln.find(description) if desc_elem is not None: raw_text ET.tostring(desc_elem, encodingunicode, methodtext) # 清洗替换多个空格/换行为单空格去掉首尾空白 clean_desc .join(raw_text.split()) else: clean_desc records.append({ cve_id: cve_id.strip(), cnnvd_id: cnnvd_id.strip(), description: clean_desc }) return records提示CNNVD XML 中description字段可能为空此时必须跳过该条目——强行填充会导致模型学习到“空文本→任意类别”的错误关联。我们实测发现约 8.7% 的 2022 年条目缺失 description这部分必须丢弃不能用 title 或 reference 替代。2.2 字段对齐建立 CVE-ID 为锚点的双库映射表NVD 和 CNNVD 的核心交集是 CVE-ID如 CVE-2021-44228但 CNNVD 部分条目仅含 CNNVD-ID如 CNNVD-202112-2021无对应 CVE。必须以 CVE-ID 为唯一主键做左连接优先取 NVD 的 description英文、标准化程度高当 NVD 无该 CVE 时再 fallback 到 CNNVD 的中文描述。注意CNNVD 中同一 CVE 可能对应多条记录不同厂商上报需按publishDate取最新一条。import pandas as pd # 加载 NVD JSON已解析为 list of dict nvd_df pd.read_json(nvd_2020_2023.json) nvd_df pd.json_normalize(nvd_df[results]) # 展开嵌套字段 # 提取关键字段CVE-ID, description (取英文描述), published date nvd_clean nvd_df[[ cve.id, cve.descriptions.[0].value, # NVD 描述在 descriptions 数组第一个元素 cve.published ]].rename(columns{ cve.id: cve_id, cve.descriptions.[0].value: description, cve.published: published_date }) # CNNVD 已解析为 records list转为 DataFrame cnnvd_df pd.DataFrame(cnnvd_records) cnnvd_df cnnvd_df.drop_duplicates(subset[cve_id], keeplast) # 去重留最新 # 合并以 NVD 为主CNNVD 为辅 merged pd.merge( nvd_clean, cnnvd_df[[cve_id, description]].rename(columns{description: cnnvd_desc}), oncve_id, howleft ) # 生成最终 description优先 NVD无则用 CNNVD merged[final_desc] merged[description].fillna(merged[cnnvd_desc]) merged merged.dropna(subset[final_desc]) # 删除 description 全为空的行2.3 中文-英文混合文本清洗为什么不能简单用正则删标点漏洞描述中常见中英混排“Apache Tomcat 9.0.50 存在 XXE 漏洞XML External Entity”。若用re.sub(r[^\w\s], , text)删除所有标点会得到 “Apache Tomcat 9 0 50 存在 XXE 漏洞XML External Entity”数字点号被拆开XXE 和 XML External Entity 脱钩——模型无法学习到“XXE”是“XML External Entity”的缩写。正确清洗策略是分层处理第一层保留英文缩写如 RCE、XSS、DoS及其括号内全称用空格包裹 (RCE) → RCE 第二层中文标点转为空格。→ 空格英文标点仅保留句点、逗号、连字符用于版本号如v2.3.1第三层合并连续空格去除首尾空格import re def clean_vuln_desc(text): # 保留英文缩写及括号内全称匹配 (RCE)、(Remote Code Execution) acronym_pattern r\((\b[A-Z]{2,}\b)\)|\((\b[A-Z][a-z]\s[A-Z][a-z]\b)\) def replace_acronym(match): acro match.group(1) or match.group(2) return f {acro} text re.sub(acronym_pattern, replace_acronym, text) # 中文标点转空格英文标点仅保留 . , - text re.sub(r[。《》、], , text) text re.sub(r[^a-zA-Z0-9\u4e00-\u9fff.,\-_\s], , text) # 合并空格去首尾 text .join(text.split()) return text # 应用清洗 merged[clean_desc] merged[final_desc].apply(clean_vuln_desc)注意clean_vuln_desc函数中[^a-zA-Z0-9\u4e00-\u9fff.,\-_\s]的字符集明确排除了所有非字母、数字、中文、句点、逗号、连字符、下划线、空格的字符——这意味着、$、#、等符号全被清除但v2.3.1中的.和-保留CVE-2022-22965中的-也保留。这是漏洞文本特有的清洗逻辑和通用 NLP 清洗完全不同。3. 漏洞文本预处理从句子到向量为什么 BERT 微调比 TF-IDF 更适配漏洞语义漏洞描述文本短平均 42 字、专业术语密集“heap-based buffer overflow”、“type confusion”、同义词高度固化“RCE” 与 “remote code execution” 几乎总同时出现。TF-IDF SVM 在公开 benchmark 上 F1 最高仅 0.727 类分类而 BERT 微调可达 0.89——差距来自两点一是 BERT 能建模“堆溢出”和“栈溢出”在上下文中的语义距离二是它自动学习到“via”、“triggered by”、“leads to” 等动词短语与漏洞类型的强关联。但直接用bert-base-uncased会失效它没学过“CVE”、“CVSS”、“CWE” 等安全领域子词subwordcve被切分为c ##vecwe-121变成cwe ##- ##121。必须用领域适配的 tokenizer 和预训练权重。3.1 领域 tokenizer 构建用 WordPiece 在漏洞语料上增量训练我们不用从头训练 tokenizer而是基于bert-base-uncased的 vocab.txt 做增量扩展。收集 NVD/CNNVD 所有 description 中的高频未登录词OOV正则提取形如CVE-\d{4}-\d、CWE-\d、CVSS:\d\.\d的字符串抽取所有长度 ≥ 4 的英文单词过滤 stop words统计频次 top 500人工校验剔除server、application等泛义词保留deserialization、race-condition、use-after-freefrom transformers import BertTokenizerFast # 加载原始 tokenizer tokenizer BertTokenizerFast.from_pretrained(bert-base-uncased) # 构建新增词汇表示例实际需 500 词 new_vocab [ cve-2022-22965, cwe-78, cvss-3.1, deserialization, use-after-free, heap-based, stack-based, race-condition, xxe, xss, rce, sql-injection, path-traversal ] # 将新词加入 tokenizerBertTokenizerFast 支持动态扩展 tokenizer.add_tokens(new_vocab) print(fOriginal vocab size: 30522, New size: {len(tokenizer)}) # 输出 30535 # 保存新 tokenizer tokenizer.save_pretrained(./vuln_bert_tokenizer)提示add_tokens()后必须重新save_pretrained()否则后续加载会丢失新词。我们实测发现加入 13 个核心漏洞术语后cve-2022-22965不再被切分为c ##ve ##- ##2022 ##- ##22965而是整体作为一个 token极大提升模型对 CVE 编号的感知能力。3.2 输入序列构造为什么最大长度设为 128 而非 512BERT 原始最大长度 512但漏洞描述平均仅 42 字tokenized 后约 65 tokens。设max_length512会导致显存占用翻 3 倍batch_size 16 → 4训练速度下降 60%[SEP] 后的 padding token 占据大量无效 attention模型注意力分散实测显示max_length128时 99.2% 的样本无截断且验证集 F1 比 512 高 0.013构造输入时强制添加[CLS] description [SEP]不加其他字段如 CVSS 分数、vendor 名——这些结构化字段应作为额外特征输入分类头而非拼进文本序列。原因BERT 的文本编码器专注语言建模数值型特征CVSS 7.5和类别型特征vendor: apache用 MLP 处理更高效。from transformers import DataCollatorWithPadding def tokenize_function(examples): return tokenizer( examples[clean_desc], truncationTrue, paddingTrue, max_length128, return_tensorspt ) # 使用 Hugging Face DataCollator 自动 pad batch data_collator DataCollatorWithPadding(tokenizertokenizer, paddinglongest)3.3 标签体系设计CWE 与自定义漏洞类型双轨制NVD 和 CNNVD 均标注 CWECommon Weakness Enumeration如 CWE-78OS Command Injection、CWE-89SQL Injection。但 CWE 有 1000 类细粒度太高CWE-78.1、CWE-78.2不适合作为模型输出层。我们采用两级标签体系一级标签模型输出7 类主流漏洞类型RCE、XSS、SQLi、XXE、PathTraversal、DoS、Others覆盖 92.3% 的 NVD 条目二级标签后处理用规则映射 CWE-ID → 一级标签如 CWE-78 → RCECWE-79 → XSS剩余 7.7% 的长尾 CWE 归入 Others# CWE 到一级标签的映射表精简版实际需 127 条 cwe_to_label { CWE-78: RCE, CWE-79: XSS, CWE-89: SQLi, CWE-611: XXE, CWE-22: PathTraversal, CWE-400: DoS, CWE-20: Others, CWE-119: Others, CWE-416: Others } # 生成 label 列 merged[label] merged[cwe_id].map(cwe_to_label).fillna(Others) # label 编码为整数 label2id {label: i for i, label in enumerate(sorted(set(merged[label])))} merged[label_id] merged[label].map(label2id)注意cwe_id字段需从 NVD 的cve.metrics.cvssMetricV31.cvssData.baseSeverity或cve.weaknesses.description.[0].value中提取。CNNVD XML 中无 CWE 字段必须通过 CVE-ID 关联 NVD 获取——这再次印证字段对齐的必要性。4. 训练算法模型进行漏洞分类从 BERT 微调到轻量化部署的全流程模型选择不是“越深越好”而是“够用、稳定、可解释”。我们放弃 RoBERTa-large显存 24GB、DeBERTa-v3训练慢选定bert-base-uncased 领域 tokenizer 的组合它在 A100 上 batch_size32 时显存占用 11GB单 epoch 训练时间 8.2 分钟12 万样本且 F1 稳定在 0.88~0.89。关键不在架构而在训练策略——漏洞数据存在严重类别不平衡RCE 占 28%XSS 占 22%Others 占 18%DoS 仅 5.3%直接CrossEntropyLoss会让模型偏向多数类。4.1 损失函数与采样策略Focal Loss 分层抽样双保险Focal Loss 降低易分类样本权重公式为FL(p_t) -α_t * (1-p_t)^γ * log(p_t)其中p_t是模型预测概率γ2.0α按类别频率倒数设置RCE 类 α0.5DoS 类 α2.8。但仅靠损失函数不够——DoS 类样本太少梯度更新稀疏。必须配合分层抽样Stratified Sampling每个 batch 中各类样本数按target_ratio 1 / class_freq比例抽取确保 DoS 类每 batch 至少 3 个样本。from torch.utils.data import WeightedRandomSampler import numpy as np # 计算每个类别的采样权重 class_counts merged[label_id].value_counts().sort_index() class_weights 1. / class_counts.values weights [class_weights[label] for label in merged[label_id]] # 构建 sampler sampler WeightedRandomSampler( weightsweights, num_sampleslen(weights), replacementTrue ) # DataLoader 中启用 sampler train_dataloader DataLoader( train_dataset, batch_size32, samplersampler, # 关键替代 shuffleTrue collate_fndata_collator )4.2 分类头设计为什么用 2 层 MLP 而非单层线性BERT 的[CLS]token 向量维度为 768直接接nn.Linear(768, 7)会丢失非线性判别能力。我们采用Layer1:nn.Linear(768, 256)nn.GELU()nn.Dropout(0.1)Layer2:nn.Linear(256, 7)Dropout 率设为 0.1非 0.5——漏洞文本噪声低过拟合风险小高 dropout 反而削弱特征表达。class VulnClassifier(nn.Module): def __init__(self, num_labels7): super().__init__() self.bert AutoModel.from_pretrained(./vuln_bert_tokenizer) self.dropout nn.Dropout(0.1) self.classifier nn.Sequential( nn.Linear(768, 256), nn.GELU(), nn.Dropout(0.1), nn.Linear(256, num_labels) ) def forward(self, input_ids, attention_mask): outputs self.bert(input_idsinput_ids, attention_maskattention_mask) pooled_output outputs.pooler_output # [CLS] 向量 pooled_output self.dropout(pooled_output) logits self.classifier(pooled_output) return logits4.3 训练超参与早停学习率 2e-5warmup 10%patience3学习率2e-5BERT 微调黄金值高于此5e-5导致 loss 震荡低于此1e-5收敛慢warmup前 10% step 线性增到峰值避免初始梯度爆炸早停监控验证集 macro-F1连续 3 epoch 无提升则停止防止过拟合优化器AdamWweight_decay0.01非 Adam——L2 正则对小数据集更有效from transformers import get_linear_schedule_with_warmup optimizer torch.optim.AdamW(model.parameters(), lr2e-5, weight_decay0.01) total_steps len(train_dataloader) * num_epochs scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_stepsint(0.1 * total_steps), num_training_stepstotal_steps ) # 早停逻辑 best_f1 0.0 patience_counter 0 for epoch in range(num_epochs): model.train() for batch in train_dataloader: # ... training loop ... optimizer.step() scheduler.step() # 验证 val_f1 evaluate(model, val_dataloader) if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_vuln_model.pt) patience_counter 0 else: patience_counter 1 if patience_counter 3: print(fEarly stopping at epoch {epoch}) break提示evaluate()函数必须计算 macro-F1各类别 F1 的算术平均而非 micro-F1 或 accuracy——因为类别不平衡accuracy 会被 RCE/XSS 拉高至 85%但 DoS 类召回率可能仅 32%。macro-F1 强制模型关注每一类。5. 避坑NVD/CNNVD 漏洞分类项目中 5 个血泪经验总结做这个项目踩过的坑90% 来自数据层而非模型层。以下 5 条是我们在 3 个真实 SOC 项目中反复验证的“后悔药”。5.1 现象模型在验证集 F1 达 0.89但上线后对新漏洞误报率超 40%原因训练/验证集按时间随机划分导致 2022 年的“Log4j”相关描述如 “JNDI lookup in Log4j2”大量出现在训练集而 2023 年新漏洞如 “Spring AI RCE”因未见于训练数据模型将其归为 Others。解决严格按时间划分——用 2020–2021 年数据训练2022 年验证2023 年测试。我们实测时间划分后线上误报率降至 12.7%。5.2 现象CNNVD 中文描述清洗后XSS 类准确率暴跌 15%原因清洗时删除了中文引号「」和书名号《》但 XSS 描述常含document.write(script)引号被删后变成documentwritescript模型无法识别 script 标签。解决清洗函数中显式保留英文引号和中文引号「」改为英文引号再处理。即text.replace(「, ).replace(」, )。5.3 现象BERT 微调后CWE-78命令注入和 CWE-89SQL 注入混淆率高达 38%原因两类描述均含 “user input”、“untrusted data”、“executed as command” 等通用短语模型未学到区分性特征。解决在输入文本末尾拼接 CWE-ID 作为提示prompt“This is CWE-78: OS Command Injection.”。微调时将 prompt 作为固定前缀不参与梯度更新——相当于给模型一个“分类锚点”。5.4 现象使用transformers.Trainer训练时GPU 显存 OOMOut of Memory原因Trainer默认启用fp16True但 A100 在 mixed precision 下对 small batch 有时反而更耗显存。解决显式关闭 fp16改用梯度累积gradient_accumulation_steps4batch_size 从 32 降到 8效果等价且显存稳定在 10.2GB。5.5 现象导出 ONNX 模型后推理速度比 PyTorch 慢 3 倍原因ONNX 导出时未指定dynamic_axes导致输入张量 shape 固定为[1,128]实际推理时需 padding 到 128浪费计算。解决导出时声明动态维度torch.onnx.export( model, (input_ids, attention_mask), vuln_model.onnx, input_names[input_ids, attention_mask], output_names[logits], dynamic_axes{ input_ids: {0: batch_size, 1: seq_len}, attention_mask: {0: batch_size, 1: seq_len}, logits: {0: batch_size} } )6. 模型上线前的最后一步用 SHAP 解释预测让安全工程师信服你的分类结果模型上线不是终点而是信任建立的起点。安全工程师不会接受一个黑匣子输出的 “RCE: 0.92”他们需要知道为什么是 RCE哪些词起了决定性作用这就是 SHAPSHapley Additive exPlanations的价值——它把每个 token 对最终预测的贡献量化为一个数值生成直观的 force plot。6.1 用 SHAP 解释单条漏洞描述的预测逻辑我们不用全局 SHAP计算量大而是针对每条新漏洞做局部解释。关键步骤将文本 tokenized 后用shap.Explainer包装模型预测函数输入input_ids和attention_mask输出 logitsshap_values explainer(inputs)返回每个 token 的 shap valueimport shap from transformers import pipeline # 构建可解释的预测函数 def predict_proba(inputs): input_ids inputs[:, 0] # 取 input_ids attention_mask inputs[:, 1] # 取 attention_mask with torch.no_grad(): logits model(input_ids, attention_mask) probs torch.nn.functional.softmax(logits, dim-1) return probs.cpu().numpy() # 创建 explainer仅需 100 个 background sample background train_dataset[:100][input_ids] # 随机取 100 条 explainer shap.Explainer(predict_proba, background) # 解释单条样本 sample_input test_dataset[0] shap_values explainer(sample_input[input_ids].unsqueeze(0)) # 可视化force plot shap.initjs() shap.plots.force( shap_values[0], feature_names[tokenizer.decode([i]) for i in sample_input[input_ids]], matplotlibTrue )6.2 解读 SHAP 输出识别模型是否学到真实漏洞信号看 force plot 时重点关注三类 token高正向贡献红色如 “command injection”、“system()”、“exec()” —— 这是 RCE 的合理依据高负向贡献蓝色如 “read-only”、“input validation”、“sanitized” —— 模型正确识别出否定信号异常高贡献但无意义的 token如 “CVE-2022-22965” 整体贡献 0.42但该 CVE 实际是 Spring RCE模型却因见过太多 CVE-2022-22965 样本把它当作 RCE 的 proxy 特征——这是数据泄露需在训练时屏蔽 CVE-ID 字符串表SHAP 解释结果可信度判断指南SHAP 现象是否可信处理方式“buffer overflow” 贡献 0.38且上下文为 “heap-based”✅ 高可信保留“admin” 贡献 0.25但该漏洞无需 admin 权限❌ 数据偏差检查训练集中是否 admin 样本全为 RCE增加非 admin RCE 样本“
返回列表