资讯动态

NVD API 2.0与CPE匹配:Python实现漏洞预警自动抓取与影响面分析

发布时间:2026/10/17 5:21:55 来源:尧图企业网站定制
搞漏洞监控的人都有个共同感受CVE编号满天飞但真正能落到自家系统上的预警靠的是结构化的数据。NVD美国国家漏洞库的API 2.0是我日常拉取漏洞数据的首选它不只给一个CVE编号还会附带CPE通用平台枚举信息、CVSS评分和受影响配置。最近帮团队搭了一套基于Python的定时任务每天自动把最新入库且带有CPE信息的漏洞抓下来再结合内部资产表做影响面判断。这篇把实现过程完整写一遍代码可以直接抄坑也提前帮你踩好了。适合这几类人看安全运维要做漏洞预警自动化、开发想快速接入NVD API 2.0、以及想搞清楚CPE匹配逻辑的工程师。大概二十分钟能读完读完能自己跑起来。1. 为什么要把NVD和CPE绑在一起看1.1 NVD API 2.0 到底给什么数据NVD的API 2.0和老的1.0相比响应结构完全重新设计过。老版本里每个漏洞节点直接就是一层cve对象新版本统一变成vulnerabilities数组包cve对象的结构看起来更规整实际解析的时候也干净很多。官方在2023年就把1.0停掉了现在还用老接口的话要么拿不到数据要么返回格式根本不兼容。一个典型的2.0响应长这样{ resultsPerPage: 50, startIndex: 0, totalResults: 1234, vulnerabilities: [ { cve: { id: CVE-2024-12345, published: 2024-01-01T00:00:00.000, lastModified: 2024-01-02T00:00:00.000, configurations: [ { nodes: [ { cpeMatch: [ { vulnerable: true, cpe23Uri: cpe:2.3:a:某厂商:某产品:1.0:*:*:*:*:*:*:* } ] } ] } ] } } ] }别看这个结构简单实际用起来有讲究。totalResults是总共命中的数量resultsPerPage是当前页大小startIndex是偏移量。这些字段就是分页循环的核心依据。除了这些基础字段cve对象里还有metricsCVSS评分、descriptions漏洞描述、weaknessesCWE编号等。做漏洞预警时CVSS评分和CPE是最有价值的两个字段一个决定优先级一个决定影响范围。1.2 CPE是漏洞能不能自动匹配的关键CPE全称Common Platform Enumeration翻译过来是通用平台枚举它本质上是一套描述软件、操作系统、硬件平台的标准化字符串。为什么会设计这种东西因为不同厂商对同一类产品的命名千奇百怪比如某家叫Web Server另一家叫HTTP Server人工看没问题程序化匹配就抓瞎了。CPE把每个产品统一成固定格式让机器能读、能比。CPE字符串长这样cpe:2.3:a:某厂商:某产品:1.0:*:*:*:*:*:*:*总共十三个段冒号分隔。第1段固定是cpe第2段是版本2.3第3段是组件类型a应用、o操作系统或者h硬件第4到第6段分别是对厂商、产品名、版本号剩下的段位表示更新版本、语言、软件版本等一般用*通配。理解这个格式之后你会发现所谓漏洞影响哪些资产在数据层面就是用CPE去和资产指纹做字符串匹配。在NVD的JSON里CPE信息藏在configurations节点的cpeMatch数组中。cpe23Uri就是完整的CPE字符串vulnerable: true表示该版本确实受影响false表示不受影响的版本。做预警脚本时需要注意一个漏洞可能对应多个CPE所以提取的时候不能只取第一个要遍历所有节点把所有vulnerable: true的CPE都收集起来。1.3 为什么只捞“带CPE的漏洞”而不是全量CVE刚开始做监控脚本的时候我直接全量拉CVE结果推送到群里一堆噪音大家根本看不过来。后来把过滤条件换成必须有CPE信息效果立刻好了很多。原因很简单一个漏洞如果连CPE都没有意味着它没有标准化的受影响版本描述。要么是漏洞影响范围还没确认要么是厂商根本不配合提供产品清单。这种漏洞对自动化运维来说几乎没有匹配价值只能靠人工阅读描述判断。而对于需要接资产系统、做自动化影响评估的场景CPE就是唯一能程序化使用的入口。所以这个脚本的定位非常明确不是收集所有CVE而是收集可被机器消化的CVE。有了CPE下游才能继续做资产匹配、生成修复工单、判断哪些服务器需要打补丁。2. 方案设计与技术选型2.1 整体流程和模块划分整个系统拆成五步定时触发、拉取数据、解析筛选、增量去重、输出结果。定时触发用系统的cron或者任务计划程序就行没必要上一套复杂的调度框架。拉取数据就是对NVD API发HTTP请求解析筛选是把JSON转成结构化记录增量去重靠保存上次运行的时间戳输出结果丢到文件、数据库或者直接推送给告警群。模块划分上我建议至少拆成三个Python文件别把逻辑全塞一个脚本里。nvd_client.py负责请求和分页parser.py负责解析和过滤main.py做编排和调度。这样后续想加HTTP缓存、加数据库存储、加新的通知渠道都只需要改对应模块不至于动一发而牵全身。这套方案的核心就是把网络请求、数据解析、业务逻辑三者解耦。刚开始写的时候可能觉得拆文件麻烦等跑上一周你就知道好处了。某个环节出错时单独跑一个模块就能定位问题不用在一坨代码里翻。2.2 为什么用 requests 标准库就够了有人可能会问为什么不直接上Scrapy或者用pandas来处理我的答案是杀鸡不用牛刀。NVD API就是纯REST接口没有复杂的登录流程没有动态渲染数据量也就是每天几十到几百条CVE。这种情况下requests库加Python标准库里的json、datetime已经完全够用。重框架带来的额外负担是维护成本。爬虫框架有中间件、下载器、调度器这些概念如果只是定期拉一个接口这些全是多余的。还有pandas虽然处理表格数据方便但NVD的JSON是嵌套结构用pandas反而要先做展平操作多一层转换就多一层出错风险。我的经验是当你的数据规模还没有达到单机处理不过来的程度时优先用最简单直接的方案。requests的Session配合重试机制加上time.sleep做限流控制已经足够稳定跑几个月。2.3 增量拉取的时间窗口设计增量拉取是预警脚本最核心的设计点。全量拉取虽然一次性能拿齐所有数据但NVD总数据量已经超过二十万条每次全量跑完要很久而且对API服务端压力很大很容易被限流。我的做法是只查最近24小时发布的数据。具体到API参数就是pubStartDate和pubEndDate。比如今天要跑就取当前UTC时间往前推24小时作为起始时间当前时间作为结束时间。有一个细节要注意NVD API要求日期格式必须带毫秒比如2024-01-01T00:00:00.000少了.000会直接返回400错误。时间窗口的大小可以根据团队需求调整。想要更实时可以每6小时跑一次每次窗口缩小到6小时想要减少请求次数可以每天跑一次窗口设48小时以弥补可能出现的异常。我目前是每8小时跑一次窗口设25小时这样即使某次任务挂了下个周期也能把遗漏的部分补回来。3. 核心代码实现从请求到解析3.1 环境准备基础环境要求很低Python 3.10版本以上就行我测试过3.8也能跑只是类型注解的写法需要稍微调整。依赖库只需要requests因为其他都是标准库。pip install requests如果你希望时间解析更稳健可以加一个python-dateutil库它能自动识别各种时间格式比手写datetime.strptime强一些。但纯标准库的datetime.fromisoformat其实也能应对大多数情况这个看个人习惯。另外强烈建议去NVD官网申请一个API Key完全免费申请流程就是注册账号然后填个表单。为什么要申请因为没带Key的时候API限流是每30秒5次请求一旦分页多或者窗口大很容易触发429或者403带上Key之后限流提高到每30秒50次请求还带更高的请求优先级响应速度明显快一截。3.2 封装API请求这是整个脚本的地基请求逻辑必须写得稳。我需要处理三件事参数拼接、请求头带上API Key、异常重试。import requests import time from datetime import datetime, timedelta, timezone API_KEY your_api_key_here BASE_URL https://services.nvd.nist.gov/rest/json/cves/2.0 def fetch_cves(start_date: datetime, end_date: datetime, start_index: int 0): params { pubStartDate: start_date.strftime(%Y-%m-%dT%H:%M:%S.000), pubEndDate: end_date.strftime(%Y-%m-%dT%H:%M:%S.000), resultsPerPage: 50, startIndex: start_index, } headers {apiKey: API_KEY} resp requests.get(BASE_URL, paramsparams, headersheaders, timeout30) if resp.status_code 403 or resp.status_code 429: print(f限流了状态码:{resp.status_code}等待重试) time.sleep(20) return fetch_cves(start_date, end_date, start_index) resp.raise_for_status() return resp.json()这段代码有几个值得说的点。params里的日期格式直接用strftime(%Y-%m-%dT%H:%M:%S.000)生成保证毫秒位是000。timeout30必须设置否则NVD偶尔响应慢的时候请求会一直挂着脚本就卡死了。限流时用递归重试加sleep(20)基本能扛过去。递归重试有个隐患如果连续被限流会无限递归下去。生产环境建议加一个最大重试次数比如最多重试3次超过就抛异常。这个在下面的分页代码里我会给一个更稳健的写法。3.3 分页和限流的正确处理NVD API单次最多返回200条默认50条实际我习惯用50条一页。当时间窗口内漏洞数量超过一页时就需要根据startIndex逐页拉取。我踩过的一个坑是用while index totalResults做循环当某一次请求返回的漏洞数量为0时会陷入死循环。正确做法是每次检查返回列表的长度如果为0就立刻终止。def fetch_all_cves(start_date: datetime, end_date: datetime): all_vulns [] start_index 0 max_retries 3 retries 0 while True: try: data fetch_cves(start_date, end_date, start_index) vulns data.get(vulnerabilities, []) all_vulns.extend(vulns) total data.get(totalResults, 0) print(f已拉取 {len(all_vulns)} / {total} 条) if not vulns or len(all_vulns) total: break start_index len(vulns) retries 0 time.sleep(1) except Exception as e: retries 1 if retries max_retries: raise print(f第 {retries} 次请求失败:{e}稍后重试) time.sleep(10) return all_vulns注意循环里我用了len(vulns)而不是固定的resultsPerPage来递增startIndex。因为某些情况下API返回的数量可能小于请求的数量用固定步长会漏掉数据。每次请求之间加sleep(1)配合API Key基本不会触发限流。这个分页函数写完之后整个数据拉取环节就闭环了。3.4 从JSON里提取CPE信息拿到原始数据之后核心工作就是解析嵌套JSON。这一步最容易出错因为不是每条CVE都有configurations字段。有些漏洞只有描述信息还没有补齐受影响配置有些configurations节点存在但内部的nodes为空数组。我的提取函数必须处理这些边界情况。def extract_cpe_list(cve_item: dict): cpe_list [] configurations cve_item.get(configurations, []) for config in configurations: nodes config.get(nodes, []) for node in nodes: for match in node.get(cpeMatch, []): if match.get(vulnerable): cpe_list.append(match.get(cpe23Uri)) return list(set(cpe_list))这个函数返回的是去重后的CPE列表。我特意只收集vulnerable: true的条目因为cpeMatch数组里可能同时包含受影响和不受影响的版本如果都收集进来后续做资产匹配会误报。还有一个值得注意的点有些CPE字符串里的版本号是*通配符表示影响所有版本。这种字符串在匹配时本身就带模糊性入库时最好单独标记避免后续做精确匹配时逻辑混乱。3.5 组装输出结果解析完成后把所有漏洞整理成一个结构化列表每条包含CVE编号、发布时间、CVSS评分、CPE列表和漏洞描述摘要。这样下游系统可以直接消费。def build_report(all_vulns: list): report [] for item in all_vulns: cve item.get(cve, {}) cve_id cve.get(id, ) published cve.get(published, ) # 提取CVSS v3评分没有就取v2 metrics cve.get(metrics, {}) cvss_score None if cvssMetricV31 in metrics: cvss_score metrics[cvssMetricV31][0][cvssData][baseScore] elif cvssMetricV2 in metrics: cvss_score metrics[cvssMetricV2][0][cvssData][baseScore] # 提取CPE cpe_list extract_cpe_list(cve) if not cpe_list: continue report.append({ cve_id: cve_id, published: published, cvss_score: cvss_score, cpe_list: cpe_list, }) report.sort(keylambda x: x[published], reverseTrue) return reportcontinue这行是过滤条件的核心如果没有CPE信息就直接跳过。这样最终输出的每条记录都带可用的CPE列表下游可以拿去做资产匹配。排序我用的是published字段保证最新发布的排在前面。实际运行下来的效果一天新增几十条漏洞过滤掉没有CPE的之后剩下大概百分之六七十推送到群里完全在可读范围内。4. 实操过程中的典型问题与排障经验4.1 一上来就被限流第一次跑脚本的时候我满怀期待地启动结果三秒钟之内连续收到403。后来一看文档无API Key的限流是每30秒5次请求相当于六秒一次。如果不算好节奏分页拉几页就直接被锁。解决方式就是申请API Key然后加sleep(1)延迟。我实测下来有Key而且每请求间隔1秒以上基本不会触发限流。如果确实因为某些操作触发了429后端返回的响应头里会带一个Retry-After字段等这个时间过去再重试就行。提示如果你跑的是多线程并发拉取限流问题会更严重。NVD的限流是按Key维度计算的不是按IP。可以用一个线程安全的计数器来控制请求速率。4.2 请求成功但返回空结果有一段时间我发现脚本跑完结果文件里一条数据都没有但手动去NVD网站上查明明有当天发布的漏洞。排查了半天才发现问题出在日期参数上。我原来写的是start_date.strftime(%Y-%m-%dT%H:%M:%S)不带.000毫秒位。NVD API严格校验时间格式不带毫秒会返回400但有一些情况它不报错而是返回空结果。后来把所有日期都统一为start_date.strftime(%Y-%m-%dT%H:%M:%S.000)困扰了两天的问题瞬间消失。4.3 时区问题导致重复数据这是另一个容易踩坑的地方。刚开始我直接用本地时间往API里传结果输出结果里每天都有重复的CVE。原因很简单NVD的时间基准是UTC我的机器在东八区本地时间比UTC快8小时。当我把本地时间转成字符串传给API时起始时间实际上是UTC时间的16点等于每次窗口都包含了一部分前一天的数据。解决方式是在脚本里统一用datetime.now(timezone.utc)获取当前UTC时间所有时间计算都在UTC时区完成最后输出报告时再转成本地时间展示。now datetime.now(timezone.utc) start now - timedelta(hours24)这样增量窗口永远是严格按UTC计算的24小时不会因为机器时区不同而出现偏移。4.4 修订漏洞的增量更新问题有个情况容易忽略NVD不只发布新漏洞还会不断修订老漏洞补充CPE信息或者调整评分。如果你只用pubStartDate获取增量数据那已经发布过的漏洞即使补了新CPE你也拿不到。解决方式有两种。一是定期用lastModStartDate参数拉取最近被修改的漏洞这个是NVD API 2.0专门提供的修订查询能力。我把脚本设计成两种模式--mode new查最新发布用于日常预警--mode modified查最新修订用于同步更新已有漏洞的CPE二是在本地存一份全量漏洞库每次拿新增的修订记录和已有数据做合并。后续维护成本会高一些但数据准确性更好。我目前是每周跑一次modified模式做补偿更新。4.5 常见问题速查表现象可能原因解决方式403/429触发API限流申请API Key请求间隔加sleep读取Retry-After头400时间格式不对日期必须带.000毫秒位格式为ISO 8601空结果时间窗口设置错误确认时区用UTC起始时间早于结束时间分页循环死循环返回列表为空时未判断检查if not vulns: break数据重复时区偏移导致窗口重叠统一使用datetime.now(timezone.utc)高级漏洞缺失CPE漏洞本身还没有配置节点跳过等待下次modified模式补录请求超时NVD响应慢设置timeout30配合重试机制4.6 几点实操心得这套脚本我前后迭代过三个版本第一版只用了两天就写出来了但真正稳定跑下来花了将近两周。最大的教训就是API调用类的脚本网络异常处理比业务逻辑本身更重要。我现在在代码里固定了几个习惯所有网络请求都带超时所有外部调用都有重试所有日期计算都基于UTC所有解析函数都容忍缺失字段。这些习惯不起眼但正是它们决定了脚本是能跑一个月还是一天就挂。另外输出结果最好加一份简单的统计信息比如本次新增漏洞X条其中带CPE的Y条过滤掉Z条。每次跑完扫一眼统计值如果异常变少大概率是API参数或者网络出了问题而不是真的没有新漏洞。这种第一时间的信号比等下游反馈快得多。最后分享一个扩展思路这个脚本拿到的CPE列表可以对接你们自己的资产指纹库。我后面的做法是把CPE列表和服务器上采集的软件版本做一次前缀匹配命中的机器自动生成待修复工单。整个过程不需要人工参与漏洞从NVD发布到工单生成最快可以控制在半小时以内。这篇文章讲到的部分就是整条链路里最源头的那一环。

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

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

免费获取报价 →
↑