审计日志
三个构建模块: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()
为什么要把审计过滤到单独的 sink? 有三个原因:成本(审计量相对于产品遥测来说很小——把它们分开可以避免保留成本失控)、权限(审计数据集应当对工程师只读、对应用只写)、以及保留(审计通常保留 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() 实际带来的好处。 是检测,不是预防。任何对底层 sink 有写入权限的人仍然可以直接删掉文件或表——但这条链能在事后证明哪些事件被删除或修改了。如果你已经写入的是仅追加 / 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 进行比较来验证。