一个面向云与身份事件响应的取证重建引擎。
给定碎片化的云、SaaS 和身份遥测数据——控制平面日志、登录事件、令牌与授权活动——NV 重建入侵最可能如何在账户与服务之间移动,枚举它可能采取的其他最可能路径,并以经过校准、可解释的置信度报告每一步。它拒绝断言证据无法支持的内容。
检测告诉你某件事发生了。Nimbus Vestige 告诉你它是如何发生的——以及还有什么。
现代入侵不会“破门而入”。它们登录进来。身份如今是主要的攻击载体,在绝大多数云 IR 调查中都有涉及,而且大多数入侵横跨多个层面——身份加云加 SaaS 加端点。记录这些入侵的遥测数据碎片化且不一致,这已经迫使响应人员从残缺的数据中手工重建故事。
这种手工重建很慢,而且会以可预测的方式失败:响应人员锚定在第一个看似合理的叙事上,从而错过真正的那个。NV 自动化重建,并通过始终呈现按等级排序的合理路径空间(而非单一故事)来直接对抗这种失败模式。
关键在于,NV 以诚实作为产品来做到这一点。市场公认的敌人是黑箱式置信——一个断言结论却不展示其依据的工具。NV 产生的每一个数字都可追溯到为其赢得该数字的具体证据,任何它无法支持的内容都会被保留而非猜测。
NV 不是检测器、扫描器、审计器或攻击执行器。那些工具告诉你某件事发生了并对其进行度量。NV 向后工作——从模式中对机制进行溯因重建,横跨碎片化的身份资产。
蓝队再生;红队商品化。 攻击性工具发现的是有限、可修补的漏洞集合,并正在融入自动化 CI/CD 安全流水线。重建入侵如何发生永远不会终结——攻击者不断发明新手段,因此需求是永久且自我更新的。
引擎与底层基质无关。 核心逻辑——从模式中重建机制,带有校准置信度和停止护栏——首先致力于云/身份领域,但之后可移植到网络、端点和 OT。目标可以改变而无需重写论点。
重建使加固成为可能。 一旦你知道他们如何进入——以及还有哪些门是敞开的——你就能构建防御。未采取但合理的路径是一份加固待办清单,其价值往往超过重建本身,因为大多数入侵利用的是可预防的暴露,而非新颖的战术手法。
它在新颖入侵——最重要的那类事件——上优雅降级。 签名/规则引擎在零日攻击面前失明;NV 的溯因核心仍能产生一条最可能路径,并诚实地标记为较低置信度。
诚实是护城河。 带有排序替代方案的、经过校准的基于案例的置信度,正是市场声称想要的,也是黑箱竞争对手在结构上无法在不重新设计的情况下提供的。
原始提供商日志 → 规范化的事件/实体图 → 重建引擎 → JSON → GUI。
O365 / Entra audit logs nv_extract_identity_events.py
AWS CloudTrail (IAM/STS/S3) nv_extract_cloudtrail_events.py (ingest + normalize)
▼
normalized identity events (JSONL) ← one event model, any substrate
│ nv/graph.py (typed per-actor timelines)
▼
reconstruction engine
├─ nv/patterns.py known layer: ATT&CK identity pattern library
├─ nv/providers.py provider packs: per-substrate op→ATT&CK vocab (Entra + AWS)
├─ nv/validation.py Phase 3: provenance + integrity gate on every pattern
├─ nv/feeds.py live ATT&CK STIX / TAXII / Sigma clients + scheduler
├─ nv/confidence.py evidence-corroboration scorer (calibratable weights)
├─ nv/calibration.py fit + measure confidence against labeled ground truth
├─ nv/reconstruct.py most-likely chain + ranked competing paths + trust floor
└─ nv/scope.py authorized-scope gate (refuses unauthorized tenants/accounts)
│ run_nv.py
▼
reconstruction.json ──► nv_gui.html (embedding canvas + two-column view + trust slider)
GUI 是一个纯视图层——它渲染引擎输出,并允许分析人员调整信任下限。它本身不包含任何重建逻辑。
每一步都携带一个位于 [0, 1] 区间内的置信度,该置信度通过证据佐证构建:
confidence = per-technique base rate
+ bonus for each independent corroborating signal
(source IP, device, successful outcome, temporal adjacency, broad-consent flags)
− penalty for missing signals (e.g. no source IP to corroborate origin)
路径分数通过几何平均组合其各条链接,因此一条弱链接会如实地拉低整条路径,而不是被平均掉。
上述每一个权重——每种技术的基础率、每个信号的加成、缺失惩罚——都存放在 nv/confidence.py 中的单个 PARAMS 表里。这些是 NV 的 v1 先验,并且它们是可校准的:nv/calibration.py 可以针对带标签的真实基准重新拟合它们,引擎会加载结果,在未提供校准结果时回退到 v1 先验(见下文)。
扣留规则已内置于引擎的输出契约中——而不仅仅是 UI。低于下限时,某一步骤会被扣留。拖动下限就是诚实性与覆盖率之间权衡的具体化:上调则严格/高信任,下调则宽松/高覆盖。默认下限是一个编辑决策,决定在分析师接触之前 NV 所处的位置。
v1 权重是站得住脚的,但它们只是先验。nv/calibration.py 针对带标签的真实基准调整它们——即我们已知哪些事件属于入侵、哪些属于良性的事件——并且关键的是,衡量调整是否真正有帮助,因此只有当校准降低了校准误差、而非仅仅挪动数字时,校准才会被采纳。
校准后的置信度意味着一点:NV 对某一步骤的置信度应等于该步骤确实位于攻击路径上的概率。因此,每个带标签事件的置信度被当作预测概率,测试框架拟合:
verified/withheld 阈值属于策略,而非校准,保持不变。
它报告Brier 分数、对数损失、ECE、AUC、一张可靠性表,以及一次信任下限扫描(在每个下限下的真实步骤召回率与良性假阳性率),涵盖调整前后——因此诚实性与覆盖率之间的权衡清晰可读,而非只是一个数字。
# calibrate against a real, labeled BadZure / MAAD-AF run
python3 calibrate.py --labeled run_labeled.jsonl --origin live \
--run-label "badzure-2026-07 tenant-x" --out calibration.json
# demonstrate on the bundled documented-shape proxy corpus
python3 calibrate.py --out calibration.json
# run the engine on calibrated weights (opt-in; v1 priors are the default)
python3 run_nv.py events.jsonl out.json --authorize t.onmicrosoft.com \
--calibration calibration.json
真实基准来源。 诚实的输入是来自真实租户中 BadZure / MAAD-AF 运行的遥测数据,其标注方式是将统一审计日志与攻击工具自身的活动记录进行关联(该工具引发的每个事件都在链上;其他一切均为良性——参见 nv/calibration.py 中的 LABELED_SCHEMA)。在你将其指向真实运行之前,eval/calibration_cases.py 会提供一个具有文档化形态的代理语料库,并且从该语料库拟合出的每个产物都会在其来源信息中标记为 source="proxy",因此代理拟合永远不会被误认为是真实拟合。calibration.proxy.json 就是那个随附的代理产物。
在随附的代理语料库上,拟合效果有明显改善——Brier 0.398 → 0.045,ECE 0.576 → 0.081,AUC 0.81 → 0.91,并且在 0.60 下限处的良性误报率从 0.69 骤降至 0.00(v1 的先验在良性活动上严重过度自信)。代理语料库被刻意设计得保守;真实租户运行才会产生你实际应当部署的权重。
两项一等控制措施保护重建过程免受不良输入的影响:
授权范围门控(nv/scope.py)——NV 拒绝在任何不在授权列表上的资产环境中运行。对于你被允许调查的每个资产环境,使用 --authorize <tenant-domain> 运行。门控无法归入任何资产环境的行(没有域且没有租户或账户的行为者)也会被拒绝,因为无法证明其处于范围内。仅当你被允许调查这些行时才传入 --allow-unscoped;它们仍会被计入输出。
模式源验证(nv/validation.py)——任何模式都必须通过以下检查才能进入库:(1) 可信源允许列表,(2) 必需的内容校验和,(3) 模式格式良好性,(4) 有效的 ATT&CK 风格技术 ID。每个被接纳的模式都会保留审计记录;被拒绝的模式会记录原因。格式错误或不符合策略的馈送条目会在上游被拦截,以免其制造虚假重建。这与引擎对证据所采用的怀疑态度相同,只是将其转移到了模式本身。
校验和是一种一致性检查:它在构建模式时根据模式自身的内容计算得出,因此能捕获之后发生变化的内容(被编辑的缓存或已提交的库文件)。它无法捕获在源头就被投毒的馈送。为此,请使用 update_feeds.py --pin URL=SHA256 固定你信任的馈送载荷的原始 sha256,该 URL 上的任何其他载荷都会在验证之前被拒绝。
重建引擎的时效性取决于其模式库,以及其处理模式库从未见过的情况的能力。NV 在设计上从两个独立方向应对陈旧性问题:
该库并非硬编码——nv/patterns.py 通过 nv/validation.py 加载每个模式(包括内置集合),因此外部馈送通过相同的可信路径进入。按计划拉取这些馈送的实时网络客户端在 nv/feeds.py 中实现,并由 update_feeds.py 驱动。来源按信任层级划分:
no known match),而不是携带过时的
分类体系。NV 保留对操作→技术绑定和基础率
先验的所有权;ATT&CK 拥有分类体系。attack.tXXXX 标签的云/身份规则都会成为一个
操作→技术候选,其基础率由规则自身的严重性
level 推导得出,然后按社区来源权重进行缩放。无法映射的规则
会被跳过并附上原因,绝不猜测。在操作冲突时,合并集会在提交前按信任度升序排序,因此
权威的 ATT&CK 绑定总是胜过社区绑定;较低层级的内容
仍会被接纳,并作为佐证记录在审计中。update_library(candidates)
会通过验证重新摄取整个集合,并以原子方式重建库,因此
被投毒的 feed 永远不会让库处于半更新状态。
从更新到重建。 一次已提交的运行会将库保存到
.nv_feeds/library.json。当你向 run_nv.py 传入 --feeds-state .nv_feeds
时,引擎会使用它;不带该标志时,它使用内置库。无论哪种方式,启动
输出都会说明运行的是哪个库,并且 reconstruction.json 会在 library 下记录它。
保存的模式在加载时会再次经过验证,如果任何一项失败(文件在提交后
发生了更改),引擎会拒绝运行,而不是使用其中的一部分。
两个允许列表,而非一个。 验证已经基于来源标签进行把关;连接器
增加了一个网络层的主机允许列表,因此连接器只能通过 TLS 从受信任的主机
获取数据 —— 这是来源允许列表在传输层的等价物,也是针对
被劫持或输入错误的 feed URL 的防护。每次获取都会将原始载荷的 sha256、URL 和时间
记录为来源信息,并且 update_feeds.py 会打印每个 sha256,以便你可以固定一个你
信任的载荷。一个被固定的 URL 如果返回任何其他内容,其拉取就会失败,库会
保留该 feed 上一次的良好候选。
为什么随着 feed 增长,验证层变得更加重要: 你越是自动化 更新,自动更新流水线就越会成为一种伪装的信任风险 —— 一个错误或 被投毒的 feed 会注入错误的模式并制造虚假的重建。NV 将 摄取视为对抗性的:对每个模式、每次更新都进行允许列表、校验和、模式 和审计追踪。
Feed 总是滞后于最新的战术手法。溯因核心是这种对冲:当一个
观察到的操作与任何库模式都不匹配时,NV 不会丢弃它 —— 它会从第一性原理
重建最可能的路径,并以降低的置信度将其标记为 no known match。这在
评估套件(eval/ground_truth_cases.py)中得到了验证,其中一个
故意未知的托管身份令牌窃取步骤被捕获、正确排序,
并诚实地降权到信任下限以下。
综合来看,这意味着 NV 永远不会完全过时:已知层通过一个 抗投毒的流水线跟踪已发布的前沿,而溯因层则让该工具 不会在前沿尚未赶上的入侵面前失明。
nv/feeds.py 携带每个 feed 的节奏,update_feeds.py 只拉取到期的内容,
因此它可以直接放入 cron 或 systemd 定时器中:
# run whatever is due, keeping state under ./.nv_feeds (cron-friendly)
python3 update_feeds.py
# force all feeds now but only report what would change
python3 update_feeds.py --force --dry-run
# run fully offline against captured fixtures (no network)
python3 update_feeds.py --offline eval/fixtures --force