审计日志
三个构建模块:auditEnricher 填充上下文,auditOnly 将审计路由到专用 drain,而 signed 添加可检测篡改的完整性。每个都是可选启用且可替换的。
auditEnricher()
auditEnricher() 会填充 event.audit.context.{requestId, traceId, ip, userAgent, tenantId}。如果你的策略不同,可以跳过它并提供自定义 enricher。
server/plugins/evlog.ts
import { auditEnricher } from 'evlog'
nitro.hooks.hook('evlog:enrich', auditEnricher())
对于多租户应用和自定义会话桥接,请传入选项:
nitro.hooks.hook('evlog:enrich', auditEnricher({
tenantId: ctx => ctx.event.tenant as string | undefined,
bridge: { getSession: async ctx => readSessionActor(ctx.headers) },
}))
不使用 auditEnricher 时,audit.context 会保持为空,而审计人员和事件响应人员至少需要 requestId 和 ip 才能对记录的操作进行交叉定位。
auditOnly()
为什么要将审计过滤到单独的 drain? 有三个原因:成本(与产品遥测相比,审计量很小,因此将它们分开可以避免保留成本失控)、权限(审计数据集应对工程师只读,而应用只能写入)以及保留期限(审计通常需要保存 7 年以上,而产品日志很少保存超过 90 天)。
auditOnly(drain) 只转发带有 audit 字段的事件。可与任何 drain 组合:
import { auditOnly } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
// 将审计发送到专用的 Axiom 数据集:
nitro.hooks.hook('evlog:drain', auditOnly(
createAxiomDrain({ dataset: 'audit', apiKey: process.env.AXIOM_AUDIT_API_KEY }),
))
设置 await: true 可使审计写入同步执行(审计不会 fire-and-forget,因此默认可安全应对崩溃):
auditOnly(createFsDrain({ dir: '.audit' }), { await: true })
await: true 标志会为每个记录审计的请求带来一点点延迟(一次同步 drain 调用),但能保证在响应发送之前审计已经落盘。对于合规级审计来说,这个权衡总是值得的。
signed()
signed(drain, opts) 会添加可检测篡改的完整性。它有两种策略:
| 策略 | 它增加的内容 | 使用场景 |
|---|---|---|
'hmac' | event.audit.signature(规范化事件的 HMAC) | 单事件完整性检查(任何后续修改都会验证失败)。 |
'hash-chain' | event.audit.prevHash 和 event.audit.hash | 可验证的链——删除和重排也会变得可检测。 |
signed() 实际上能带来什么。 检测,而非预防。任何拥有底层 drain 写入权限的人仍然可以删除文件或表,但该链可以在事后证明哪些事件被删除或修改过。如果你已经写入仅追加/WORM 存储(S3 Object Lock、具有行级不可变性的 Postgres、仅追加的 BigQuery 表),请跳过 signed();重复添加完整性层只会增加延迟,却不会提高门槛。HMAC
每个事件都会获得一个签名。篡改某一行会破坏该行的验证,但不会破坏后续行。
import { signed } from 'evlog'
signed(drain, { strategy: 'hmac', secret: process.env.AUDIT_SECRET! })
哈希链
每个事件都引用前一个事件的哈希。删除任何一行都会破坏该点之后的链,因此验证器可以精确定位被篡改的那一行。
signed(drain, {
strategy: 'hash-chain',
state: {
load: () => fs.readFile('.audit/head', 'utf8').catch(() => null),
save: (h) => fs.writeFile('.audit/head', h),
},
})
对于跨进程或持久化链路,state 配置是必需的:在每个事件之前,从你自己的存储(Redis、Postgres、文件)中加载上一个 head hash,并在之后保存新的 head。
audit chain·idle
- #1invoice.refund·usr_42·successpendingprev:∅hash:3f2c8e1a
- #2user.update·usr_42·deniedpendingprev:3f2c8e1ahash:9a1b4d7c
- #3apiKey.revoke·usr_42·successpendingprev:9a1b4d7chash:c4e7f2b9
link ok tamper detected chain verified · 3 events intact
用于遍历并验证链的 CLI(
evlog audit verify)已列入路线图。在此之前,请通过重新计算已存储事件的哈希,并将每个 prevHash 与前一个事件的 hash 进行比较来验证。
::。