审计日志
完整性、脱敏预设、GDPR 与仅追加模式、保留窗口,以及将审计日志投入生产时最常见的陷阱。

合规框架(SOC2、HIPAA、GDPR、PCI)会对每一条审计日志提出同样的五个问题:谁、什么、何时、从哪里、以何种结果,以及我们如何知道它没有被篡改。evlog 通过组合现有的基础原语来回答每一个问题。

完整性

对审计日志进行哈希链处理,这样任何篡改都能被检测出来。每个事件的哈希都包含前一个哈希,因此删除某一行会破坏从该点开始向前的链。

auditOnly(
  signed(createFsDrain({ dir: '.audit' }), { strategy: 'hash-chain' }),
  { await: true },
)
请每年轮换用于 HMAC 签名审计的 secret 轮换时,在签名旁边嵌入一个密钥 ID(例如,通过 declare modulekeyId 扩展 AuditFields),这样旧事件仍可使用之前的 secret 进行验证。验证器应根据 ID 查找密钥,而不是假设只有一个全局 secret。

查看 排水器与完整性 以了解 HMAC 和哈希链之间的区别。

脱敏

审计事件会经过你现有的 RedactConfig。与严格的审计预设组合,以强化对 PII 的处理:

import { auditRedactPreset } from 'evlog'

initLogger({
  redact: {
    paths: [
      ...(auditRedactPreset.paths ?? []),
      'user.password',
    ],
  },
})

该预设会移除 Authorization / Cookie 请求头,以及常见的凭证字段名(passwordtokenapiKeycardNumbercvvssn)——无论它们出现在 audit.changes.before 还是 audit.changes.after 中。

GDPR 与仅追加模式

仅追加的审计日志与 GDPR 的“被遗忘权”相冲突。当前推荐的模式:

  1. 保持审计行不可变。
  2. 使用按行为主体区分的密钥加密 PII 字段(该密钥保存在审计存储之外)。
  3. 要“忘记”某个用户时,删除其密钥——审计行仍保留,链仍然有效,PII 则变得不可读。

内置的 cryptoShredding 帮助函数已在 后续路线图 中。

保留

保留策略本质上是存储层面的关注点。evlog 的审计层不会强制保留窗口,因为每个受支持的后端已经有更强且经过审计的机制来处理它。请选择与你的后端相匹配的方案:

后端保留机制
FScreateFsDrain({ maxFiles }) 与每日压缩器结合使用。
Postgres计划执行 DELETE FROM audit_events WHERE timestamp < now() - interval '7 years'
Axiom / Datadog / Loki在平台中设置数据集保留策略。
S3 Object Lock配置生命周期规则 + Object Lock 保留期限。

在你的安全策略中记录所选窗口。审计人员关注的是书面规则,而不是执行该规则的组件。

常见陷阱

  • 只记录成功。 审计人员最关心的是拒绝结果。始终在每个授权检查的否定分支同时调用 log.audit()log.audit.deny()
  • 通过 changes 泄露 PII。 auditDiff() 会经过你的 RedactConfig,但前提是字段路径已被列出。一次性在全局添加 passwordtokenapiKey 等,这样你以后就无需再考虑它们。
  • 把审计当作可观测性。 不要对审计事件进行采样、降采样或汇总。强制保留默认开启——不要禁用它。
  • actor.id 与会话 ID 混淆。 actor.id 是稳定的用户 ID(或系统身份)。通过 context.requestId / context.traceId 关联会话,绝不要通过 actor 关联。
  • 忘记独立任务。 定时任务、队列工作器和 CLI 也会触发值得审计的操作。使用 audit()(无请求)或 withAudit(),以确保与 HTTP 路由保持一致的覆盖范围。
  • 在审计排水器上跳过 await: true 如果不这样做,审计就变成了“发出即忘记”——当事件被发出到排水器完成刷新之间发生崩溃时,操作已经发生,但审计行却不存在。