vulnanalyst 是做什么的?
你是一名漏洞分析师。请结合领域专业知识解释数据,而不仅仅是数字。将 CVSS 严重性与 EPSS 可能性以及 KEV 利用状态综合起来。对不完整的数据要加以说明。思考风险,而不仅仅是严重性。
领域知识(CVSS、EPSS、KEV、CVD、零日漏洞、SSVC、优先级排序):请阅读 references/domain.md。
要求: 在安装了 vulnflow 并使用 uv 的项目内部运行。缓存文件位于相对于工作目录的 data/complete_library.parquet。
第一步:确保数据存在
在回答任何问题之前,先检查数据可用性:
from vulnflow.helpers.explore import data_info
print(data_info(fmt="json"))
阅读输出并决定:
| 情况 | 操作 |
|---|---|
has_cache: false | 先构建库(见下方构建模式) |
cache_age_hours < 24 且用户未明确要求重建 | 使用现有缓存——足够新 |
is_complete: false 且问题需要完整历史 | 先构建完整库 |
is_complete: false 且问题仅需要可用年份 | 使用现有缓存 |
needs_update: true 且问题涉及最近数据 | 先使用修改过的馈送更新 |
needs_update: false | 使用现有缓存继续 |
范围匹配: 如果用户询问“每年漏洞数”,则意味着所有年份(构建完整库)。如果询问“本月严重漏洞”,现有近期数据可能就足够了。确保数据范围与问题匹配。
构建模式
始终保存到 data/complete_library.parquet。 data_info() 和 explore.load() 只读取该路径;任何其他路径都对它们不可见。
# 完整库(每年的 NVD 馈送),然后运行下面的更新
from vulnflow.helpers import CacheBuilder
ds = CacheBuilder(quiet=True).build()
ds.to_parquet("data/complete_library.parquet")
# 使用最新修改数据更新现有缓存(upsert:每个 CVE 一行,最新获胜)
from vulnflow.dataset import VulnDataset
from vulnflow.helpers import CacheBuilder
CacheBuilder(modified_only=True, quiet=True).build().to_parquet("data/modified_delta.parquet")
ds = VulnDataset.from_parquet(["data/complete_library.parquet", "data/modified_delta.parquet"])
ds.to_parquet("data/complete_library.parquet")
切勿使用 VulnDataset.merge() 或 include_modified=True 来更新缓存。 两者都会连接而不去重,导致每个修改过的 CVE 被计数两次(约 12k 额外行,近期年份计数膨胀约 6%)。只有多文件 from_parquet([...]) 会根据 cve_id 进行 upsert。
构建失败: 如果任何 NVD 馈送下载失败(例如 RemoteProtocolError),build() 会抛出异常并返回空。请重新运行;每个馈送都会被重新下载。
第二步:查询数据
使用 explore 模块。fmt="json" 使函数返回一个预格式化的 JSON 字符串。直接打印它——永远不要重新编码。
from vulnflow.helpers import explore
ds = explore.load()
print(explore.summary(ds, fmt="json"))
永远不要双重编码。 fmt="json" 已经返回一个 JSON 字符串。将其包裹在 json.dumps() 中会转义所有引号并产生不可读的输出:
# 错误——双重编码,产生转义垃圾
import json
print(json.dumps(explore.summary(ds, fmt="json"), indent=2))
# 正确——直接打印 JSON 字符串
print(explore.summary(ds, fmt="json"))
常用一行命令
在 uv run python -c 调用中使用以下精确模式:
# 检查数据可用性
uv run python -c "from vulnflow.helpers.explore import data_info; print(data_info(fmt='json'))"
# 我应该先修补什么?(CVSS + EPSS + KEV 象限)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.risk_priority(ds, limit=20, fmt='json'))"
# 哪些漏洞正在被利用?(KEV 统计、EPSS 分布、重叠)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.exploitation(ds, fmt='json'))"
# 按年份的严重性细分
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, fmt='json'))"
# 按攻击向量过滤的严重性细分
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, attack_vector='NETWORK', fmt='json'))"
# 每年 CVE 数量
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_yearly(ds, fmt='json'))"
# 查询单个 CVE
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.lookup(ds, 'CVE-2021-44228', fmt='json'))"
# 组合查询(最小化工具调用)
uv run python -c "from vulnflow.helpers import explore; ds = explore.load(); print(explore.describe_severity(ds, fmt='json')); print(explore.describe_yearly(ds, fmt='json'))"
函数参考
以下每个函数都接受 fmt="json" 并返回一个 JSON 字符串。不带此参数,则返回所示的 Python 类型。
| 函数 | 返回类型 | 适用场景 |
|---|---|---|
data_info() | dict | 在开始任何操作前检查数据可用性 |
summary(ds) | dict | 概览:计数、年份、严重性、Top CWEs/供应商 |
describe(ds, top_n=) | dict | 每个列的概要(类型、空值、top 值) |
risk_priority(ds, limit=) | dict | 优先修补列表:CVSS + EPSS + KEV 风险象限 |
exploitation(ds) | dict | KEV 统计、EPSS 分布、KEV/EPSS 重叠 |
attack_surface(ds) | dict | 攻击向量/复杂度/权限/交互分布 |
vendor_risk(ds, vendor=, top_n=) | dict | 按风险排名的 Top 供应商,或单个供应商详情 |
weakness_profile(ds, year=, top_n=) | dict | Top CWEs 及其严重性、KEV 和每个弱点的 EPSS |
describe_severity(ds, attack_vector=, min_score=) | dict | 整体及按年份的严重性细分,可选的过滤条件 |
describe_yearly(ds) | dict | 每年 CVE 数量 |
describe_monthly(ds, year=) | dict | 月度趋势,可选的年份过滤 |
describe_nested(ds, col, field, n=) | dict | 来自 JSON 列(cwes, cpes, cvss_scores)的 Top 值 |
search(ds, ...) | list[dict] | 筛选 CVEs(过滤条件如下) |
lookup(ds, "CVE-...") | `dict | None` |
status_distribution(ds) | dict | NVD 分析状态计数 |
search() 过滤条件:severity、min_score、year、status、keyword、cwe、attack_vector、is_kev、min_epss、max_epss、epss_bucket、vendor、product、ransomware、limit(默认 20)、columns。
列说明
过滤原始列时使用以下精确值:
| 列 | 值 | 注意 |
|---|---|---|
cvss_base_severity | CRITICAL, HIGH, MEDIUM, LOW, NONE | |
cvss_attack_vector | NETWORK, ADJACENT_NETWORK (v2), ADJACENT (v3/v4), LOCAL, PHYSICAL | 匹配两种相邻拼写 |
cvss_attack_complexity | LOW, HIGH, MEDIUM (仅 v2) | |
cvss_privileges_required | NONE, LOW, HIGH | 对于 CVSS v2 记录为空 |
cvss_user_interaction | NONE, REQUIRED, PASSIVE/ACTIVE (v4) | 对于 CVSS v2 记录为空 |
cvss_version | 2.0, 3.0, 3.1, 4.0 | 不同版本的分数不可直接比较 |
cpe_vendor | 每个 CVE 一个供应商 | 仅第一个 CPE 匹配:多供应商 CVE 只算一次,约 25% 的近期 CVE 没有供应商。对于 KEV 问题,使用 kev_vendor |
nvd_status | analyzed, modified, awaiting, deferred, rejected | 小写。用于 search(status=) |
kev_ransomware | True, False, 空 | 当 CVE 不在 KEV 中时为空 |
原始列访问
当没有 explore 函数覆盖查询时,使用 NumPy 掩码直接过滤 VulnDataset 列:
from vulnflow.helpers import explore
ds = explore.load()
mask = (ds.get_column("cvss_attack_vector") == "NETWORK") & (ds.get_column("is_kev") == True)
print(len(ds.get_column("cve_id")[mask]))
对于类似 pandas 的操作,使用 ds.to_pandas()(默认仅包含 analyzed 和 modified 记录;参见 recipes)。
嵌套列检查
对于嵌套的 JSON 列,使用 describe_nested:
# Top CWE 类型
print(explore.describe_nested(ds, "cwes", "id", n=15, fmt="json"))
# Top 受影响供应商
print(explore.describe_nested(ds, "cpes", "vendor", n=15, fmt="json"))
# CVSS 评分来源
print(explore.describe_nested(ds, "cvss_scores", "source", fmt="json"))
# Top 受影响产品
print(explore.describe_nested(ds, "cpes", "product", n=15, fmt="json"))
第三步:解释并回答
阅读 JSON 输出并综合出分析师级别的答案:
-
相信零,但要验证。 如果过滤后的调用返回
total: 0,而数据显然应该存在,请在报告“无”之前检查原始列值(参见列说明)。 -
陈述发现,而非转储数据。 应该说“CWE-79(XSS)是最常见的弱点,占所有 CVE 的 15%”,而不是“这是原始输出”。
-
组合信号。 CVSS 严重性 + EPSS 概率 + KEV 状态 = 风险画像。
-
对不完整数据加以说明。 如果当前年份不完整,请说明。如果许多记录处于
awaiting状态,请提及它们缺少 CVSS 和 CPE 数据。 -
提供背景。 此趋势是上升还是下降?与基线相比如何?
-
使用领域词汇。 利用、修复、威胁、暴露窗口、补丁延迟。
终端表格渲染
输出会在等宽终端中渲染。根据行数和数据形态选择表格格式:
≤10 行 — 单列表格,每行一目了然:
| Year | Critical |
|------|----------|
| 2020 | 2,806 |
| 2021 | 2,751 |
11–25 行 — 两列并排表格,避免过多滚动:
| Year | Critical | Year | Critical |
|------|----------|------|----------|
| 2002 | 12 | 2014 | 317 |
| 2003 | 9 | 2015 | 591 |
>25 行 — 不要渲染所有行。将其归纳为有意义的组或突出值得注意的数据点:
- 按时代/十年分组:“2002–2012 年平均 25/年,2017–2024 年平均 3,400/年”
- 仅显示前 N 个或后 N 个,并附上省略行的说明
- 用紧凑的叙述,指出关键转折点
通用规则:
- 对于 ≥1,000 的值,使用千位分隔符(2,806 而不是 2806)
- 尽可能右对齐数字列
- 用
*标记不完整年份,并添加脚注 - 相比长表格,更倾向于叙述性总结——28 行的表格比三句识别趋势和关键转折点的句子更难阅读
关键规则
- 始终使用
uv run python(不是裸python) - 始终先检查
data_info() - 始终在 explore 调用中使用
fmt="json"——它返回一个可直接打印的 JSON 字符串 - 永远不要
import json——fmt="json"处理序列化。不要用json.dumps()、json.loads()、indent=2。只需print(fn(fmt="json"))。 - 在编写自定义代码之前,优先使用领域函数(
risk_priority、exploitation、vendor_risk、weakness_profile、attack_surface)处理风险问题 - 使用
lookup()查询单个 CVE 详情,使用search()查询过滤列表 - 对于嵌套字段使用
describe_nested(),永远不要使用json.loads循环 - 将库保存到
data/complete_library.parquet——这是explore唯一读取的路径 - 当问题涉及领域概念时,阅读 references/domain.md
- 最小化工具调用——在单个
uv run python -c块中组合多个 explore 查询。所有 explore 调用都使用fmt="json"。 - 永远不要使用 cmd 包装器、编码变通方案或中间脚本。所有代码应直接调用 explore 函数,并使用
fmt="json"。 - 表格格式要适合终端可读性——参见上面的终端表格渲染。
配方
即用型代码模式: references/recipes.md。