资讯动态

CNNVD漏洞资源文件解析入库与风险评分实战指南

发布时间:2026/10/15 4:54:46 来源:尧图企业网站定制
简介这是一份以中国国家信息安全漏洞库CNNVD历史数据为核心的漏洞研究资源包面向安全研究员、渗透测试学习者及系统运维人员可用于漏洞特征分析、脆弱性评估与安全防护策略研究。压缩包共25个文件整体68.85MB包含23个gz压缩包、1个xml文件和1个md说明文档。gz包按年份存储1999年及之前至2019年的CNNVD漏洞XML数据md文件提供整体目录说明便于快速定位特定年份的漏洞记录。资料按年份精细切分可直接导入数据库或编写脚本做批量解析、趋势统计和关联分析也可用于查阅典型漏洞的详细描述、影响版本与危害等级帮助读者理解漏洞演变规律并提升漏洞管理能力。该资源已有1057人学习下载适合需要长期跟踪国内漏洞情报、构建本地漏洞知识库的进阶安全从业者。1. CNNVD漏洞资源文件安全数据链路里最缺的那一环做漏洞管理平台的人都有一个共同体验手里握着CVE和NVD的公开数据但真到了国内资产的漏洞研判、等保核查、风险收敛时总感觉差一口气——差的就是中文语境下的权威描述、厂商影响范围的本土化标注以及和实际业务系统能对上号的漏洞编号。CNNVD漏洞资源文件补的正是这一环。它把国家漏洞库积累的漏洞条目以结构化文件的形式分发给有需要的团队既适合安全研发做数据底座也适合运维和等保测评人员做资产关联分析。这篇笔记会从文件形态讲起一路到解析入库、与CVE对齐、增量更新最后落在一个能直接用的风险评分技巧上。踩过的坑我都会标出来照着做能少走不少弯路。2. 认识CNNVD漏洞资源文件数据形态与一条完整下载链路2.1 CNNVD的数据组织方式从页面查询到文件分发CNNVD中国国家漏洞库平时大家用得最多的是它的网页查询入口但网页查询只适合人工看一两条做平台集成的人需要的是一整份结构化数据。于是就有了CNNVD漏洞资源文件这回事。常见做法是按批次打包分发压缩包内部是XML或JSON格式的漏洞条目每个条目包含CNNVD编号、对应的CVE编号、漏洞名称、危害等级、漏洞类型、受影响实体、发布时间、更新时间、漏洞描述和解决方案。和NVD的数据对比就能看出差异。NVD偏重英文描述和CVSS评分CNNVD文件的优势在于中文描述和本地化信息比如国内厂商产品的受影响版本范围、中文描述里的攻击路径描述这些字段在NVD里基本找不到。对于做等保或国内资产测绘的团队来说CNNVD的字段更接近实际业务语言不需要再做一层中文翻译映射。文件的分发频次常见是两类全量快照和增量更新。全量快照适合第一次建库文件较大但一次性把历史数据补齐增量更新则按周或按月发布新增条目适合已经跑起来的系统做定时同步。我这边的习惯是首次搭建用全量快照建底表之后每天拉增量包做merge这样既控制带宽又能保证新漏洞在24小时内出现在自己的库里。2.2 用脚本拉取并校验一份CNNVD资源文件CNNVD的资源文件通常不会放在公开匿名FTP上常见渠道是官网的数据共享栏目或通过专用接口申请下载。拿到下载地址后我的习惯是先写一段脚本做拉取、解压和格式预检避免把半截文件或HTML错误页当成数据入库。#!/bin/bash # 拉取CNNVD增量资源文件并做基础校验 # 用法: ./fetch_cnnvd.sh 文件URL URL$1 OUT_DIR/data/cnnvd_raw TODAY$(date %Y%m%d) mkdir -p $OUT_DIR # 下载并允许服务器端校验注意有些环境需要加 -k 跳过证书校验 wget --tries3 --timeout60 -O $OUT_DIR/cnnvd_$TODAY.zip $URL # 检查文件类型确认不是HTML错误页 file $OUT_DIR/cnnvd_$TODAY.zip # 解压到独立目录保留原始文件名 mkdir -p $OUT_DIR/$TODAY unzip -o $OUT_DIR/cnnvd_$TODAY.zip -d $OUT_DIR/$TODAY # 打印目录结构和文件行数确认解压出来的主数据文件 ls -lh $OUT_DIR/$TODAY wc -l $OUT_DIR/$TODAY/*.xml $OUT_DIR/$TODAY/*.json 2/dev/null这段脚本里的参数有几个值得注意。--tries3控制重试次数CNNVD服务器在数据发布日经常有并发压力试一次失败就放弃是常态三次重试能覆盖大部分临时抖动。--timeout60是单次连接超时但要注意这不是总超时大文件下载时靠这个参数保连接可靠性。file命令做类型探测非常关键我遇到过服务器返回登录页面的情况如果不检查类型后边的解析阶段会爆出一堆莫名其妙的错误。解压完成后wc -l只能给出粗略的行数XML文件如果是一行一条记录那行数就是条数如果是格式化换行的XML行数就不能说明问题。真正的条数校验要在解析阶段做我会在下一章给出具体方案。还需要留意文件名里的日期信息有些解压出来的文件名是固定的比如cnnvd.xml有些会带发布日期后缀建议在入库时把下载日期也记录下来方便后边做时间线追踪。3. 解析入库把CNNVD资源文件变成一张可查询的漏洞表3.1 字段先对齐CNNVD里到底有哪些值值得落库拿到XML或JSON后第一步不是写解析代码而是把字段清单列出来确定哪些入库、哪些丢弃、哪些做归一化。下面这个表格是我做过的项目里最常用的字段映射照着这个结构建表基本能满足漏洞管理平台80%的需求。目标字段来源字段处理说明cnnvd_id漏洞编号主键之一保持原始字符串格式cve_id关联CVE编号可空用于和NVD数据关联vuln_name漏洞名称保留中文原名搜索场景用severity危害等级归一化为高/中/低/超危四档vuln_type漏洞类型对应CWE分类可做统计affected受影响实体自由文本保留原文并建全文索引description漏洞描述全文存储检索用solution解决方案全文存储修复工单引用publish_time发布时间统一转成ISO 8601格式update_time更新时间增量更新时用这个字段判断新旧raw_hash原始条目哈希计算MD5用于去重和变更检测这里要强调severity的归一化。CNNVD的历史条目里危害等级写法并不统一早年间有“低危”“中危”“高危”的中文写法后来引入了“超危”概念还有部分条目直接写CVSS分数。如果原样入库查询和统计都会出问题。我一般会在解析层做一次字典映射把“危急/严重/超危”统一归到“超危”“高/高危”归到“高危”其余类推。raw_hash这个字段是我后来补上的作用很大。CNNVD的条目会有修订同一个编号的漏洞描述或等级发生变化时哈希值会变。有了这个字段增量更新时能快速识别出“这是修改过的旧条目”而不是“新条目”避免重复插数据。3.2 用Python完成一轮CNNVD文件解析与SQLite入库我一般用Python来做解析原因很简单xml.etree和json都是标准库不需要额外装依赖加上SQLite做本地验证库也很轻。下面这段代码是一个完整的解析入库示例基于XML格式的假设结构写的如果你拿到的是JSON改一下load的方式就行。import hashlib import sqlite3 import xml.etree.ElementTree as ET from datetime import datetime # 建立SQLite连接开启事务批量写入 conn sqlite3.connect(/data/cnnvd.db) conn.execute(PRAGMA journal_modeWAL) cur conn.cursor() # 建表cnnvd_id为主键raw_hash用于后续变更检测 cur.execute( CREATE TABLE IF NOT EXISTS cnnvd_vuln ( cnnvd_id TEXT PRIMARY KEY, cve_id TEXT, vuln_name TEXT, severity TEXT, vuln_type TEXT, affected TEXT, description TEXT, solution TEXT, publish_time TEXT, update_time TEXT, raw_hash TEXT ) ) # 归一化危害等级 def norm_severity(raw: str) - str: raw raw.strip().lower() mapping { 超危: 超危, 危急: 超危, 严重: 超危, critical: 超危, 高危: 高危, 高: 高危, high: 高危, 中危: 中危, 中: 中危, medium: 中危, 低危: 低危, 低: 低危, low: 低危, } return mapping.get(raw, 未知) # 解析XML文件逐条写入 def parse_xml_to_db(xml_path: str): tree ET.parse(xml_path) root tree.getroot() insert_sql INSERT OR REPLACE INTO cnnvd_vuln (cnnvd_id, cve_id, vuln_name, severity, vuln_type, affected, description, solution, publish_time, update_time, raw_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) for item in root.findall(vuln): cnnvd_id item.findtext(cnnvd_id, ).strip() cve_id item.findtext(cve_id, ).strip() name item.findtext(name, ).strip() sev_raw item.findtext(severity, ).strip() vtype item.findtext(vuln_type, ).strip() affected item.findtext(affected, ).strip() desc item.findtext(description, ).strip() solution item.findtext(solution, ).strip() pub item.findtext(publish_time, ).strip() upd item.findtext(update_time, ).strip() # 计算原始哈希用于识别内容变更 raw_text cnnvd_id cve_id name affected desc upd raw_hash hashlib.md5(raw_text.encode(utf-8)).hexdigest() cur.execute(insert_sql, ( cnnvd_id, cve_id or None, name, norm_severity(sev_raw), vtype, affected, desc, solution, pub, upd, raw_hash )) conn.commit() print(f解析完成: {xml_path}) if __name__ __main__: parse_xml_to_db(/data/cnnvd_raw/20250120/cnnvd.xml) conn.close()这段代码有几个设计点需要说明。INSERT OR REPLACE配合cnnvd_id主键天然实现了按编号幂等写入——同一个漏洞重复执行不会产生双份数据这是增量导入的基础保障。raw_hash的计算里我故意没有包含severity和solution只关注影响范围和描述变化因为等级变更比较常见但不太影响关联分析而描述变更通常意味着内容大改。norm_severity函数里做了一个宽泛的映射把中英文混写的历史数据都收拢到四档。这里有一个要注意的地方mapping.get(raw, 未知)会把所有映射不到的值归为“未知”如果后续统计发现“未知”占比过高说明原始文件里的等级字段格式比预想的更复杂需要回头补充映射词典。XML的解析用了标准库的ElementTree对CNNVD这种几百MB的中等规模文件性能够用。如果你拿到的是上GB的全量文件建议换成lxml的iterparse流式解析避免一次性把整棵树加载进内存导致进程被OOM杀掉。SQLite侧开启了WAL模式这是为了后续增量更新时读写不互相阻塞实测在同时跑查询和导入时性能提升明显。4. 与CVE/NVD对齐关联、去重与时间线校准4.1 关联策略用CVE编号做主键还是用CNNVD编号做主键CNNVD文件里的漏洞条目和CVE编号并不是严格的一一对应关系。同一个CVE可能对应两个CNNVD条目一个是原始发布另一个可能是后续补充的变体记录也有CNNVD条目没有对应CVE的情况这类通常是中国特有产品的漏洞NVD根本不会收录。这就带来一个建表时的选择问题到底以哪个编号做底表主键。我建议以CNNVD编号为底表主键CVE编号做成普通索引。理由有三点。第一CNNVD资源文件是以CNNVD编号为条目单位的解析时不会出现主键冲突第二和国内资产做关联时国内产品漏洞主要靠CNNVD编号溯源CVE反而是辅助字段第三和NVD数据关联时用CVE编号做join即可不需要反客为主。关联NVD数据时常见的操作是用CVE编号把两个表拉起来然后对比CVSS分数与CNNVD危害等级是否一致。但要注意两者的发布时间和更新时间经常会对不上NVD可能晚几天才更新CVSS v3.1评分而CNNVD的等级是基于中文描述评出来的标准不同所以数值有差异时不要大惊小怪。我见过有团队把这个差异当数据质量问题去排查最后发现纯粹是两套评分体系造成的白费了不少功夫。4.2 增量更新的常规做法全量快照加增量补丁CNNVD资源文件如果做每日增量常见的做法是拉取新增时间段的XML文件然后使用上一章的INSERT OR REPLACE逻辑合并进主表。这里有个坑增量文件里的条目可能包含对旧条目的修订而不是纯新增。所以增量更新不能简单地“只插入没有的”而要做upsert否则修订内容就丢了。我常用的是一个带日期的merge策略#!/bin/bash # 增量合并脚本每天定时执行 TODAY$(date %Y%m%d) RAW_DIR/data/cnnvd_raw/$TODAY # 先解压当天增量文件 unzip -o $RAW_DIR/*.zip -d $RAW_DIR # 执行Python合并脚本 python3 /opt/scripts/merge_cnnvd.py $RAW_DIR/*.xmlmerge脚本的核心逻辑是这样先用update_time字段判断条目时间是否晚于本地记录如果晚于则重新计算哈希并执行INSERT OR REPLACE如果晚于但哈希相同说明CNNVD那边只是例行刷新没有实质变更可以直接跳过。import sys from datetime import datetime def should_update(local_row, new_row): 本地记录与增量条目的比较规则 if local_row is None: return True local_time datetime.fromisoformat(local_row[update_time]) new_time datetime.fromisoformat(new_row[update_time]) if new_time local_time: return True if new_time local_time and local_row[raw_hash] ! new_row[raw_hash]: return True return False这个函数的决策逻辑值得展开说。第一行条件处理的是本地压根没有这条记录的情况直接插入。第三行处理的是CNNVD时间戳没变但内容变了的情况这种比较罕见但一旦发生说明对方修正了历史数据但没更新时间字段哈希检测能兜住这个漏网之鱼。实际跑下来哈希兜底生效的概率大概是千分之一但安全数据这条链路里千分之一的漏报也可能变成某个突发漏洞没人预警。时间线校准是另一个容易忽略的环节。CNNVD文件里的publish_time是漏洞公开时间update_time是条目修订时间。做统计报表时应该用publish_time来拉时间轴用update_time做排序取最新。如果把两者混用增量数据和时间线统计就会对不上报表上出现“昨天新增100个漏洞但实际只新增10个修订了90个”的怪现象。5. CNNVD资源文件使用避坑指南编码、编号与数据质量5.1 解压后中文乱码现象、原因、解决现象XML文件打开后中文全部变成乱码英文正常。这种情况大多发生在Windows环境下编辑过的文件或者下载链路中转过程中编码被改写过。原因通常是文件实际编码是GBK或GB18030但XML声明里写着UTF-8。解决方法是解析前先做编码探测和修正不要硬用UTF-8去读。import chardet def load_xml_with_encoding(path: str): 自动探测XML文件编码并修正 with open(path, rb) as f: raw f.read(4096) result chardet.detect(raw) real_encoding result[encoding] or utf-8 if real_encoding.lower() ! utf-8: # 用探测到的编码重新读取并转成UTF-8覆盖写入 content raw.decode(real_encoding, errorsreplace) with open(path, w, encodingutf-8) as f: f.write(content) return ET.parse(path)这里有个血泪经验chardet.detect只用前4KB做探测如果文件开头是大量英文标签中文字符出现在后面探测结果可能误判为ASCII导致后续解析还是乱码。更稳妥的做法是直接跳过前10行把包含中文的片段喂给探测函数。我是在被坑了一次后才改成这样做的。如果你不想引依赖也可以直接解码尝试发现UnicodeDecodeError再回退GBK但这样每次异常都要抛一次性能上慢一些。5.2 CNNVD编号与CVE编号一对多去重翻车现象以cve_id为主键建表导入时报主键冲突或者去重后漏洞条目数量比预期少很多。原因同一个CVE ID被CNNVD拆成多个条目比如一个CVE同时影响多个产品线CNNVD会分产品线记录。解决底表以CNNVD编号为主键CVE编号作为普通索引相关联表时用LEFT JOIN而不是INNER JOIN防止CVE为空的行被过滤掉。这个坑在第一次做NVD和CNNVD关联时最容易爆。我当时的翻车现场是用CVE编号做关联去重结果某国产设备的漏洞全被合并成一条后续做资产匹配时那些特定产品版本的记录全部丢失。后来改成“CNNVD编号为骨CVE编号为肉”的结构数据才真正对得起来。5.3 受影响实体是自由文本正则解析是个坑现象从affected字段里提取产品名和版本号正则写好后跑出来的结果忽多忽少今天能提取明天不行。原因这个字段是人工填写的自由文本写法包括“XX系统V1.0”“XX系统 1.0.0.1”“XX系统1.2.3”等多种格式没有统一规范。解决不要试图用一条正则吃遍所有格式而是建一个品牌关键词表先匹配产品名再在其后截取版本片段做解析。这一步我已经踩到麻木了。最终方案是维护了一个产品别名表比如把“某OA系统”和“该OA系统”“OA办公系统”都映射到同一个产品ID版本提取只对产品命中后的后缀片段做正则。这样才能把准确率稳定在95%以上剩下的5%粗糙但可接受因为安全分析看的是趋势不是逐条精度。5.4 增量包不是每天都有重复导入后数据翻倍现象每天定时拉增量某天发现库里漏洞总数突然翻倍但日志显示导入成功没有报错。原因增量包不是每个工作日都发布节假日或数据整理期经常不发而下载脚本把上一次的旧文件当作新文件重新下载主键虽然没冲突但关联表里如果用了自增ID就会产生一堆重复记录。解决下载前先比对文件名里的日期和已有的最新update_time如果增量包日期早于库内最新记录直接跳过导入。我在代码里加了一个前置判断拉完文件后不急着解压先看文件名里的日期标签如果小于等于库内最大update_time就把文件移到/data/cnnvd_raw/duplicates/目录归档不入库。这个逻辑看起来简单但每天定时任务能少跑一轮无效解析也避免了后续查数的困惑。5.5 危害等级标准混用分数与等级对不上现象有一条漏洞severity标的是“高危”但NVD那边CVSS v3.1分数只有5.4中等两边对照时数据对不上。原因CNNVD历史数据早期以“高中低”为主后来引入CVSS v3.0和v3.1评分但存量数据没有完全重评部分条目用的是旧标准。解决使用等级字段时以CNNVD自身的等级为准不要强行对齐CVSS分数如果要做关联分析加一个cvss_source字段注明该分数来自哪套标准避免后续统计时标准混乱。这个兼容性问题是这类数据源最麻烦的部分。我现在的做法是入库时顺便存一个score_version字段取值为v2“v3.0”“v3.1”或者none。查询时如果需要按CVSS分数筛选就必须先指定版本否则结果没有意义。这也是我后面做风险评分时要刻意绕开的坑。6. 进阶用法把CNNVD资源文件变成资产风险评分依据拿到解析好的漏洞数据后最常见的进阶需求是把资产表和漏洞表关联起来给每一台服务器或业务系统算一个风险分。这个分数不求精确但要有说服力——能告诉运维哪台机器最该先打补丁。我的做法是做一个三表关联资产表保存主机IP、系统类型、操作系统版本漏洞表保存CNNVD条目和受影响实体关联表通过产品关键词把两者串起来。评分规则是超危记10分、高危7分、中危4分、低危1分再按资产暴露面做加权。SQL可以这样写SELECT a.ip, a.hostname, SUM(CASE v.severity WHEN 超危 THEN 10 WHEN 高危 THEN 7 WHEN 中危 THEN 4 WHEN 低危 THEN 1 ELSE 0 END) AS risk_score, COUNT(v.cnnvd_id) AS vuln_count FROM assets a LEFT JOIN asset_vuln_map m ON a.id m.asset_id LEFT JOIN cnnvd_vuln v ON m.cnnvd_id v.cnnvd_id WHERE a.is_active 1 GROUP BY a.ip, a.hostname ORDER BY risk_score DESC;这个查询的效果是直接把风险最高的前几台机器拉出来。要注意LEFT JOIN的意义——资产表里有些机器可能没有关联到任何漏洞分数为0但它们在列表里仍然可见方便运维确认是不是漏扫没覆盖到。如果用INNER JOIN这些机器会直接消失造成“看不见的盲区”。一个验证技巧跑完这个查询后随机抽3台高风险的机器人工登录操作系统核对一下是否真的存在关联的产品版本如果命中率低于70%说明影响版本解析的准确率还有问题优先回头调产品别名表。这个验证本身是给数据链路做体检不只是为了信服别人更是让自己心里有底。我现在的习惯是每周跑一次这个评分生成一张趋势表对比周与周之间的分数浮动。如果某台机器风险分突然涨了一大截多半是新上线的服务引入了带漏洞的组件这时候去排查比等到被攻击再处理成本低得多。做安全数据这行最值钱的能力其实就是把静态数据变成动态的决策信号。希望这篇笔记里踩过的坑和总结的经验能帮到你少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑