补丁时间指标:使用 Qualys 和 Elastic 的生存分析方法
简介
了解不同环境和团队修复漏洞的速度,对于保持强大的安全态势至关重要。在本文中,我们将介绍如何使用弹性堆栈对来自Qualys VMDR 的漏洞管理 (VM) 数据进行生存分析。这使我们不仅能够确认有关团队速度(团队完成工作的速度)和修复能力(团队能够承担的修复工作量)的一般假设,还能获得可衡量的见解。由于我们的大部分安全数据都在 Elastic Stack 中,因此这个过程应该可以很容易地复制到其他安全数据源中。
我们为什么这样做
我们的主要动机是从一般假设到以数据为依据的洞察力:
- 不同团队和环境修补漏洞的速度有多快
- 修补性能是否符合内部服务水平目标 (SLO)
- 经常出现瓶颈或延误的地方
- 还有哪些因素会影响修补性能
为什么要进行生存分析?平均补救时间的更好替代方案
平均修复时间(MTTR)通常用于跟踪漏洞修补的速度,但平均值和中位数都有很大的局限性(我们将在本文后面举例说明)。平均值对异常值非常敏感[^1],并假定补救时间在平均补救时间周围均匀分布,但实际情况很少如此。中位数对极端值的敏感度较低,但会忽略有关分布形状的信息,对补丁速度慢的长尾漏洞一无所知。两者都没有考虑未解决的案例,即观察窗口之后仍未解决的漏洞,这些漏洞往往被完全排除在外。实际上,开放时间最长的漏洞恰恰是我们最应该关注的漏洞。
生存分析解决了这些局限性。它起源于医疗和精算领域,对时间到事件的数据进行建模,同时明确纳入删减观测 数据,这在我们的语境中意味着仍未解决的薄弱环节。(有关其在漏洞管理中应用的更多详情,我们强烈推荐《度量宣言》)。生存分析不是将修复行为归结为一个数字,而是估算漏洞在一段时间内仍未修复的概率(如90% 的漏洞在 30 天内得到修复)。这样可以进行更有意义的评估,例如在 SLO 内(例如在 30、90 或 180 天内)修补漏洞的比例。
生存分析为我们提供了一个生存函数,用于估算漏洞随着时间推移仍未打补丁的概率。
:::这种方法能更好地了解修复性能,使我们不仅能评估漏洞持续存在的时间,还能评估不同系统、团队或严重程度的修复行为有何不同。它尤其适用于安全数据,因为这些数据往往不完整、有偏差,而且不符合常态假设。:::
上下文
尽管我们已经在不同的环境、团队和组织中应用了生存分析,但在本博客中,我们将重点讨论弹性云生产环境的结果。
脆弱性年龄计算
计算脆弱性年龄有不同的方法。
对于我们的内部指标,如漏洞依从性 SLO,我们将漏洞年龄定义为最后发现漏洞的时间与首次检测到漏洞的时间(通常为发布后几天)之间的差值。这种方法旨在惩罚从过时的基础图像中重新引入的漏洞。过去,我们的基础图像更新不够频繁,无法让我们满意。如果创建的是新实例,那么从发现的第一天起,漏洞就可能有相当长的时间(如 100 天)。
在本分析中,我们认为根据最后发现日期和首次发现日期之间的天数计算年龄更有意义。在这种情况下,年龄代表系统有效暴露的天数。
"修补一切 "战略
在我们的云环境中,我们的政策是为一切打补丁。这是因为我们几乎只在所有实例中使用相同的基础图像。由于 Elastic Cloud 完全在容器上运行,因此我们的系统上没有直接安装特定的应用程序包(如 Elasticsearch)。因此,我们的车队保持了同质化。
数据管道
将数据导入和映射到 Elastic Stack 可能很麻烦。幸运的是,我们有许多安全集成可以处理这些问题,Qualys VMDR就是其中之一。
与自定义摄取方法相比, 3 。脚本、节拍......):
- 它可本地丰富 Qualys 知识库中的漏洞数据,添加 CVE ID、威胁情报信息......而无需配置丰富管道。
- Qualys 数据已经映射到 Elastic Common Schema(弹性通用模式),这是一种标准化的数据表示方式,无论数据来自一个来源还是另一个来源:例如,CVE 总是存储在vulnerability.id 字段中、与来源无关。
- 已经设置了带有最新漏洞的转换。通过查询该索引,可以获得最新的漏洞状态。
Qualys 代理集成配置
为了进行生存分析,我们需要同时摄取活动漏洞和已修补漏洞。要分析特定时期,我们需要在字段max_days_since_detection_updated 中设置天数。在我们的环境中,我们每天都会摄取 Qualys 数据,因此无需摄取长期的固定数据,因为我们已经做到了这一点。
Qualys VMDR 弹性代理集成已配置如下:
| 财产 | Value | 注释 |
|---|---|---|
| (设置部分) 用户名 | ||
| (设置部分) 密码 | 由于 Qualys 中没有可用的 API 密钥,我们只能使用基本身份验证进行身份验证。确保该账户的 SSO 已禁用 | |
| URL | https://qualysapi.qg2.apps.qualys.com(用于 US2) | https://www.qualys.com/platform-identification/ |
| 时间间隔 | 4h | 根据摄取事件的数量进行调整。 |
| 输入参数 | show_asset_id=1& include_vuln_type=confirmed&show_results=1&max_days_since_detection_updated=3&status=New,Active,Re-Opened、固定&filter_superseded_qids=1&use_tags=1&tag_set_by=name&tag_include_selector=all&tag_exclude_selector=any&tag_set_include=status:运行中&tag_set_exclude=status:terminated,status:stopped,status:stale&show_tags=1&show_cloud_tags=1 | show_asset_id=1:检索资产 ID show_results=1:详细说明当前安装的软件包以及应安装的版本 max_days_since_detection_updated=3:过滤掉在过去 3 天内未更新的漏洞(例如:max_days_since_detection_updated=3)。修补时间早于 3 天) status=New,Active,Re-Opened,Fixed: 收录所有漏洞状态 filter_superseded_qids=1: 忽略已被取代的 "漏洞 标签: 通过标签过滤 show_tags=1: 检索 Qualys 标签 show_cloud_tags=1: 检索云标签 |
一旦数据被完全摄取,就可以在 Kibana Discover(logs-* data view -> data_stream.dataset :"qualys_vmdr.asset_host_detection")或 Kibana Security App(Findings -> Vulnerabilities)中进行审查。
使用 elasticsearch 客户端将数据加载到 Python 中
由于生存分析计算将在 Python 中完成,我们需要将数据从 elastic 中提取到 python 数据帧中。有几种方法可以实现这一目标,本文将重点介绍其中两种。
使用 ES|QL
最简单方便的方法是利用 ES|QL 的箭头格式。它会自动填充 python 数据框(行和列)。我们建议您阅读博文《在 Python 中从 ES|QL 到本地 Pandas 数据帧》,以了解更多详情。
from elasticsearch import Elasticsearch
import pandas as pd
client = Elasticsearch(
"https://[host].elastic-cloud.com",
api_key="...",
)
response = client.esql.query(
query="""
FROM logs-qualys_vmdr.asset_host_detection-default
| WHERE elastic.owner.team == "platform-security" AND elastic.environment == "production"
| WHERE qualys_vmdr.asset_host_detection.vulnerability.is_ignored == FALSE
| EVAL vulnerability_age = DATE_DIFF("day", qualys_vmdr.asset_host_detection.vulnerability.first_found_datetime, qualys_vmdr.asset_host_detection.vulnerability.last_found_datetime)
| STATS
mean=AVG(vulnerability_age),
median=MEDIAN(vulnerability_age)
""",
format="arrow",
)
df = response.to_pandas(types_mapper=pd.ArrowDtype)
print(df)
如今,我们在使用 ESQL 时遇到了一个限制:我们无法对结果进行分页。因此,我们只能输出 10K 文档(如果修改服务器配置,则为 100K)。可通过此改进请求了解进展情况。
使用 DSL
在 elasticsearch python 客户端中,有一个本地功能可以从查询中提取所有数据,并进行透明分页。具有挑战性的部分是创建 DSL 查询。我们建议在 Discover 中创建查询,然后点击 Inspect(检查),再点击 Request(请求)选项卡,以获取 DSL 查询。
query = {
"track_total_hits": True,
"query": {
"bool": {
"filter": [
{
"match": {
"elastic.owner.team": "awesome-sre-team"
}
},
{
"match": {
"elastic.environment": "production"
}
},
{
"match": {
"qualys_vmdr.asset_host_detection.vulnerability.is_ignored": False
}
}
]
}
},
"fields": [
"@timestamp",
"qualys_vmdr.asset_host_detection.vulnerability.unique_vuln_id",
"qualys_vmdr.asset_host_detection.vulnerability.first_found_datetime",
"qualys_vmdr.asset_host_detection.vulnerability.last_found_datetime",
"elastic.vulnerability.age",
"qualys_vmdr.asset_host_detection.vulnerability.status",
"vulnerability.severity",
"qualys_vmdr.asset_host_detection.vulnerability.is_ignored"
],
"_source": False
}
results = list(scan(
client=es,
query=query,
scroll='30m',
index=source_index,
size=10000,
raise_on_error=True,
preserve_order=False,
clear_scroll=True
))
生存分析
您可以参考代码来理解或在您的数据集上重现。
我们学到了什么
根据塞恩提亚研究所的研究,我们研究了几种不同的方法,利用平均值、中位数和生存曲线来衡量修复漏洞所需的时间。每种方法都提供了不同的视角,我们可以通过这些视角来理解补丁时间数据,这种比较非常重要,因为根据我们使用的方法,我们会对漏洞的处理情况得出非常不同的结论。
第一种方法只关注已经关闭的漏洞。它计算出修补它们所花时间的中位数和平均值。这样做既直观又简单,但却遗漏了数据中潜在的重要部分(仍处于开放状态的漏洞)。因此,它往往会低估补救所需的真实时间,尤其是当某些漏洞的开放时间比其他漏洞长很多时。
第二种方法通过使用漏洞迄今为止的开放时间,尝试将已关闭和开放的漏洞都包括在内。有很多方法可以估算出开放漏洞的补丁时间,但为了简单起见,我们假定在报告时这些漏洞已经(将要?但它确实提供了一种将它们的存在考虑在内的方法。
第三种方法使用生存分析。具体来说,我们使用 Kaplan-Meier 估计器来模拟漏洞在任何特定时间仍然开放的可能性。这种方法能正确处理开放的漏洞:它不会假装漏洞已被修补,而是将其视为 "审查过的 "数据。它生成的生存曲线会随着时间的推移而下降,显示几天或几周后仍未修复的漏洞比例。
漏洞会持续多久?
在当前的 6 个月快照[^2]中,封闭式补丁时间的中位数为 33 天,平均值为 35 天。从表面上看,这似乎是合理的,但 Kaplan-Meier 曲线显示了这些数字所隐藏的内容:在 33 天时,~54 个% 仍然开放;在 35 天时,~46 个% 仍然开放。因此,即使是在 "典型 "的一个月左右,仍有大约一半的问题没有得到解决。
我们还计算了 "迄今观察到的 "统计数据(将开放的漏洞视为在测量窗口结束时已修补的漏洞)。在这个窗口中,它们几乎相同(中位数 ~33 天,平均值 ~35 天),因为今天开放项目的年龄集中在一个月附近。这种巧合会让平均值看起来让人放心,但它只是偶然的,而且不稳定:如果我们将快照时间转移到每月补丁推送之前,这些相同的统计数据会急剧下降(我们看到观察到的中位数约为 19 天,观察到的平均值约为 15 天),而底层过程没有任何变化。
存活率曲线避免了这一陷阱,因为它回答了 "% 在 30/60/90 天后仍未关闭 "的问题,并提供了超过一个月仍未关闭的长尾的可见性。
用同样的方法修补所有地方?
分层生存分析将生存曲线的概念向前推进了一步。这种方法不是把所有弱点集中在一个大池子里,而是根据一些有意义的特征把它们分成不同的组别(或 "阶层")。在分析中,我们按照严重性、资产关键性、环境、云提供商、团队/部门/组织对漏洞进行了分层。每组都有自己的生存曲线,在示例图中,我们比较了不同漏洞严重程度的修复速度。
这种方法的好处在于,它暴露了原本隐藏在总体中的差异。如果只看整体生存曲线,我们只能对补救措施的整体表现下结论。但是,分层可以揭示不同的团队、环境或严重性问题是否比其他团队、环境或严重性问题处理得更快,而且在我们的案例中,所有补丁的策略确实是一致的。这种详细程度对于进行有针对性的改进非常重要,它不仅能帮助我们了解修复工作一般需要多长时间,还能帮助我们了解是否存在真正的瓶颈以及瓶颈在哪里。
团队行动的速度有多快?
虽然存活曲线强调的是脆弱性的持续时间,但我们可以通过使用累积分布函数(CDF)来转换视角。CDF 主要关注漏洞修补的速度,显示特定时间点已修复漏洞的比例。
我们选择绘制 CDF 可以清楚地反映修复速度,但需要注意的是,该版本只包括在观察时间窗口内修补的漏洞。我们计算的生存曲线是滚动的 6 个月群组,以捕捉完整的生命周期,而 CDF 不同,它是根据当月关闭的项目逐月计算的[^3]。
因此,它只能告诉我们团队修复漏洞的速度,而不能反映未解决漏洞的开放时间。例如,我们看到本月关闭的漏洞中有 83.2% 在首次发现后的 30 天内得到解决。这突显了最近成功修补的漏洞的修补速度,但没有考虑到仍未修补且修补时间可能更长的长期存在的漏洞。因此,我们使用 CDF 来了解短期响应行为,而完整的生命周期动态则由 CDF 和存活率分析相结合给出:CDF 描述了团队在修补漏洞后的行动速度,而存活率曲线则显示了漏洞真正持续的时间。
生存分析与平均值/中位数的区别
等等,我们说过,生存分析最好分析修补时间,以避免异常值的影响。但在这个例子中,均值/中值分析和生存分析提供了相似的结果。附加值是什么?原因很简单:我们的生产环境中没有异常值,因为我们的补丁程序是全自动且有效的。
为了说明对异构数据的影响,我们将使用一个缺乏自动补丁的非生产环境中的过时示例。
ESQL 查询:
FROM qualys_vmdr.vulnerability_6months
| WHERE elastic.environment == "my-outdated-non-production-environment"
| WHERE qualys_vmdr.asset_host_detection.vulnerability.is_ignored == FALSE
| EVAL vulnerability_age = DATE_DIFF("day", qualys_vmdr.asset_host_detection.vulnerability.first_found_datetime, qualys_vmdr.asset_host_detection.vulnerability.last_found_datetime)
| STATS
count=COUNT(*),
count_closed_only=COUNT(*) WHERE qualys_vmdr.asset_host_detection.vulnerability.status == "Fixed",
mean_observed_so_far=MEDIAN(vulnerability_age),
mean_closed_only=MEDIAN(vulnerability_age) WHERE qualys_vmdr.asset_host_detection.vulnerability.status == "Fixed",
median_observed_so_far=MEDIAN(vulnerability_age),
median_closed_only=MEDIAN(vulnerability_age) WHERE qualys_vmdr.asset_host_detection.vulnerability.status == "Fixed"
| 迄今观察到的情况 | 仅关闭 | |
|---|---|---|
| 计数 | 833 | 322 |
| 平均值 | 178.7(天) | 163.8(天) |
| 中值 | 61(天) | 5(天) |
| 中位生存率 | 527 (天) | 不适用 |
在这个例子中,使用平均数和中位数得出的结果截然不同。选择一个单一的代表性指标可能具有挑战性,并可能产生误导。生存分析图准确地反映了我们在这一环境中应对脆弱性的有效性。
最终想法
使用生存分析的好处不仅在于测量更准确,还在于能深入了解修补行为的动态,显示瓶颈出现的位置、影响修补速度的因素以及是否符合我们的 SLO。从技术集成的角度来看,我们只需对当前的 Elastic Stack 设置进行最少的额外更改,就可以将生存分析用作我们运营工作流和报告的一部分:生存分析可以与我们的补丁周期保持相同的节奏运行,并将结果推送回 Kibana 以实现可视化。其绝对优势在于将我们现有的运营指标与生存分析结合起来,既可用于长期趋势分析,也可用于短期绩效跟踪。
展望未来,我们正在尝试更多新指标,如到达率、烧毁率和逃逸率,这些指标能让我们更动态地了解漏洞的实际处理情况。
到达率是衡量新漏洞进入环境的速度。例如,如果知道每个月都会出现 50 个新的 CVE,那么在开始测量补丁之前,我们就能知道工作量的预期。因此,到达率这一指标并不一定能说明积压情况,而更能说明系统所承受的压力。
Burndown Rate(趋势)显示等式的另一半:相对于漏洞出现的速度,漏洞修复的速度有多快。
逃逸率》还增加了另一个维度,重点关注那些漏掉了本应加以控制的漏洞。在我们的语境中,"逃逸 "指的是错过补丁窗口或超过 SLO 临界值的 CVE。漏洞逃逸率升高不仅表明存在漏洞,还表明旨在控制漏洞的流程失效,无论是因为补丁周期太慢、缺乏自动化流程,还是因为补偿控制没有达到预期效果。
这些指标结合在一起,可以为我们提供更好的信息:到达率告诉我们有多少新的风险正在出现;下降趋势显示我们是跟上了压力的步伐,还是被压力压得喘不过气来;逃逸率暴露了在计划控制的情况下,哪些地方仍然存在漏洞。
[1]:统计中的离群点是指与中心倾向(或数据集中的其他数值)相差甚远的数据点。例如,如果大多数漏洞在 30 天内得到修补,但有一个漏洞需要 600 天,那么这个 600 天的案例就是一个离群值。异常值会使平均值上升或下降,从而无法反映 "典型 "经验。在打补丁方面,这些漏洞打补丁的速度特别慢,其开放时间远远超过正常时间。它们可能代表罕见但重要的情况,如不容易更新的系统或需要大量测试的补丁。
[2]:注:当前的 6 个月数据集既包括在观察期结束时仍未关闭的所有漏洞(与这些漏洞打开/首次出现的时间无关),也包括在 6 个月窗口期间关闭的所有漏洞。尽管采用了混合队列的方法,但之前观察窗口的生存曲线显示出一致的趋势,尤其是在曲线的早期部分。事实证明,前 30-60 天的形状和斜率在不同的快照中都非常稳定,这表明中位补丁时间和早期修复行为等指标并不是观测窗口过短的人为因素。虽然长期估计(如90 百分位数)在较短的快照中仍然不完整,但从这些队列中得出的结论仍然反映了持续可靠的修补动态。
[3]:我们将 CDF 保持在每月一次,以便进行业务报告(当月完成工作的吞吐量和 SLO 遵守情况),而 Kaplan-Meier 使用 6 个月的窗口,以妥善处理普查,并在更广泛的队列中暴露尾部风险。
