## 它补的是 L1 结构上记不到的那块
L1 是 agent 自己记的,只看得见经过自己 RPC 的调用。攻击者绕过 agent
直接跑系统命令、或干脆把 agent 停掉 —— L1 全部盲区。L2 采集操作系统
自己的审计事件,补的正是这块。设计见 docs/AUDIT-L2.md。
## 必须先说清的信任边界(不夸大作用)
采集器与 agent 同进程同权限 —— 攻击者能停 agent 就能停采集器。
故本地 audit-l2.log 只是一份**归一化副本**,防篡改价值有限。
真正的独立性来自**把记录转发出机**(与 SECURITY.md §4.8 锚定同一思路)。
本实现不自动转发,只提供导出接口;这一点已写入文档与能力描述。
## 三端
- Windows:**落地**。选内置 auditpol 而非 Sysmon —— Sysmon 是第三方驱动,
需单独分发、需重启生效、版本行为有差异;内置策略零依赖即可立起来。
检测到装了 Sysmon 则并用,未装不尝试下载。
- status:查策略是否启用,并提示两个关键开关
* SCENoApplyLegacyAuditPolicy —— 不设它,组策略旧式类别策略会盖掉
subcategory,表现为「auditpol 显示已启用但事件日志里没有 4688」
* ProcessCreationIncludeCmdLine_Enabled —— 不开则 4688 只有进程名
- enable:开启进程创建 4688 / 登录 4624,4625 / 进程终止 4689(需管理员)
- collect:Get-WinEvent 拉取 → 归一化 → 独立落盘(带哈希链)
- verify:L2 链完整性校验
- Linux:仅设计(auditd,需预装),未落地
- macOS:仅设计,ESF 需 Apple entitlement 且必须是签名 System Extension,
个人自签无法启用 —— 不写假装能用的代码
## 落盘与 schema
独立文件 audit-l2.log,不与 L1 混 —— 混了就无法区分「agent 记的」与
「OS 记的」。记录含 ts(OS 事件时间)、src、event_id(保留原始便于核对)、
kind(归一化类别)、collected_at(采集时刻,与 ts 差过大说明曾离线)。
## 实测
- 新增 8 条单测,含两条故障注入:篡改内容检出「哈希不符」、
删中间条目检出「链断裂」
- 既有契约守卫两次报红并已被修复:manifest 新增 lkg 字段 → SystemInfo 接口缺声明;
新增 system.audit.l2 → 能力清单断言过期(51→52)
- 全量单测 349 → 357 全绿
7.5 KiB
L2 系统级审计兜底(设计 + Windows 落地)
本文是
docs/DELIVERY-PIPELINE.md与docs/SECURITY.md§4.8 的配套件。 编写日期:2026-10-17。基线提交:2bf45fd。
1. L1 与 L2 的分工(先界定问题)
| L1(已实现) | L2(本文) | |
|---|---|---|
| 记录者 | nodeagent 自己(apps/agent/src/audit.ts) |
操作系统 |
| 落盘位置 | %NODEAGENT_HOME%/audit.log |
OS 审计子系统(Windows 事件日志 / Linux auditd / macOS ESF) |
| 防篡改 | 哈希链 + 外部锚定 | 由 OS 内核写入,agent 无权改写 |
| 覆盖内容 | 「谁调用了什么能力」 | 「这台机器上发生了什么」(进程创建、登录、文件访问…) |
为什么还需要 L2
L1 的哈希链能发现链内篡改,外部锚定能发现整链重写。但两者都有一个前提: agent 还在记录。
一旦攻击者拿到被控端的 root:
- 可以直接停掉 agent(或让它不再写审计),此后发生的一切 L1 都看不到;
- 即使不停,L1 记的是「agent 知道的事」——攻击者绕过 agent、直接用系统命令做的事, L1 结构上就记不到(它只看得见经过自己 RPC 的调用)。
L2 补的就是这块:agent 不在场时,机器自己留下了记录。
2. 必须先说清的信任边界(不要夸大 L2 的作用)
L2 采集器本身不提供独立性。
采集器是 agent 的一部分,与被审计对象同权限、同进程。攻击者既然能停 agent, 就能停采集器。真正的独立性来自「记录不留在本地」:
OS 审计子系统 ──(只读)──> 采集器 ──> ① 本地 audit-l2.log(副本,仅便利)
└─> ② 转发出机(控制端 / 云盘) ← 独立性在这里
- ① 只是归一化副本,方便查询,防篡改价值有限(我们写的,我们能改)。
- ② 才是 L2 的意义所在 —— 与 §4.8 的锚定思路一致:留在攻击者碰不到的地方。
因此本文的落地范围是「①采集与归一化 + ②导出接口」, 自动转发属于部署侧配置(与锚点外发共用同一条通道),不在本文实现。
3. 三端设计
3.1 Windows —— 本次落地
Windows 有两条可选路径:
| 方案 | 依赖 | 覆盖 | 取舍 |
|---|---|---|---|
A. 内置高级审计策略(auditpol.exe) |
零依赖 | 进程创建 4688、登录 4624/4625、进程退出 4689、对象访问 4663 | ✅ 选它:无需安装任何东西 |
| B. Sysmon(Sysinternals) | 需下载安装 Sysmon 本体 + 配置 XML | 更丰富(镜像加载、命名管道、WMI、DNS) | 可选增强:检测到已装则并用 |
为什么先选 A:Sysmon 是第三方驱动,需单独分发、需重启生效、且不同版本行为有差异; 内置策略是 Windows 自带的、开箱即用的那层。能用零依赖方案先立起来,Sysmon 作为可选增强。
启用方式(需管理员):
:: 让「高级审计策略」覆盖旧的「类别级策略」—— 否则 subcategory 设置不生效
auditpol /set /subcategory:"进程创建" /success:enable /failure:enable
auditpol /set /subcategory:"登录" /success:enable /failure:enable
auditpol /set /subcategory:"进程终止" /success:enable
reg add HKLM\SYSTEM\CurrentControlSet\Control\Lsa /v SCENoApplyLegacyAuditPolicy /t REG_DWORD /d 1 /f
⚠️
SCENoApplyLegacyAuditPolicy是关键:不设这个,组策略里的旧式类别策略会盖掉 subcategory 设置,表现为「auditpol 显示已启用但事件日志里没有 4688」。
采集:Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688,4624,4625; StartTime=…}
(PowerShell 层,避免额外依赖)。
3.2 Linux —— 设计,本次不落地
- 机制:
auditd+auditctl规则。 - 规则示例:
-a always,exit -F arch=b64 -S execve(进程执行)、-w /etc/passwd -p wa(关键文件)。 - 读取:
ausearch或/var/log/audit/audit.log。 - 为什么本次不落地:需要
auditd已安装且运行;容器/精简发行版常常没有。 属部署侧前置条件,与 Windows 的「零依赖」不同。
3.3 macOS —— 设计,存在硬性限制
- 机制:Endpoint Security Framework(ESF)+ OpenBSM。
- ⚠️ ESF 属 Apple 受限权限:需向 Apple 申请
com.apple.developer.endpoint-security.cliententitlement,且客户端必须是签名的 System Extension。个人/自签分发无法启用。 - OpenBSM(
auditd)在 macOS 上仍可用但自 macOS 11 起被弱化,事件覆盖有限。 - 结论:macOS 侧本文只出设计不落地,如实标注可行性限制,不写「假装能用」的代码。
4. 归一化 schema
L2 记录与 L1 分开落盘(audit-l2.log),schema 独立 —— 因为来源与可信度不同,
混进 L1 会让「agent 记的」与「OS 记的」无法区分。
{
"ts": 1760000000000, // 事件发生时间(来自 OS,不是采集时刻)
"src": "windows-security", // 来源:windows-security | sysmon | linux-auditd | macos-esf
"event_id": 4688, // OS 原始事件 ID(保留原始,便于人工核对)
"kind": "process.create", // 归一化后的类别
"subject": "DOMAIN\\User", // 主体
"object": "C:\\Windows\\...", // 客体(进程映像 / 文件路径 / 目标)
"detail": { "...": "..." }, // 平台特有字段(命令行、PID、父进程…)
"collected_at": 1760000001234, // 采集时刻 —— ts 与它相差过大说明曾离线
"prev": "...", "hash": "..." // 与 L1 同构的哈希链
}
kind 取值(归一化枚举):
| kind | Windows | Linux | macOS |
|---|---|---|---|
process.create |
4688 | execve |
ESF exec |
process.exit |
4689 | — | ESF exit |
auth.success |
4624 | — | ESF authentication |
auth.failure |
4625 | — | — |
file.access |
4663 | -w ... -p wa |
ESF open |
5. 落地范围(本次)
| 项 | 状态 |
|---|---|
| 设计文档(三端 + 信任边界 + 可行性限制) | ✅ 本文 |
| Windows 状态检查(策略是否已启用) | ✅ system.audit.l2 action=status |
| Windows 启用(auditpol + 关 legacy 覆盖) | ✅ action=enable(需管理员) |
| Windows 采集(Security 日志 → 归一化 → 落盘) | ✅ action=collect |
| L2 链校验 | ✅ action=verify |
| Linux auditd | ⚠️ 仅设计,未落地 |
| macOS ESF | ⚠️ 仅设计,受 Apple entitlement 限制,不可自签启用 |
| 自动转发出机 | ❌ 不在本文范围(与锚点外发共用通道) |
6. 已知限制(如实记录,不掩饰)
- 采集器不提供独立性 —— 同进程同权限,攻击者能停 agent 就能停采集器。 真正独立性依赖转发出机(§2)。
- Windows 启用需管理员 —— 已提权时可用;未提权返回明确错误,不静默失败。
- 4688 默认不含完整命令行 —— 需额外开
HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit\ProcessCreationIncludeCmdLine_Enabled才能记命令行,否则只能看到进程名。采集器会检测并提示。 - 量大 —— Security 日志可能极多;采集必须带
since与limit,默认窗口 24 小时、上限 1000 条。 - Sysmon 未自动安装 —— 检测到则并用,未检测到不尝试下载(不在本仓库职责内)。