合规框架(SOC2、HIPAA、GDPR、PCI)会对每一条审计日志提出同样的五个问题:谁、什么、何时、从哪里、以何种结果,以及我们如何知道它没有被篡改。evlog 通过组合现有的基础原语来回答每一个问题。
完整性
对审计日志进行哈希链处理,这样任何篡改都能被检测出来。每个事件的哈希都包含前一个哈希,因此删除某一行会破坏从该点开始向前的链。
auditOnly(
signed(createFsDrain({ dir: '.audit' }), { strategy: 'hash-chain' }),
{ await: true },
)
请每年轮换用于 HMAC 签名审计的
secret。 轮换时,在签名旁边嵌入一个密钥 ID(例如,通过 declare module 用 keyId 扩展 AuditFields),这样旧事件仍可使用之前的 secret 进行验证。验证器应根据 ID 查找密钥,而不是假设只有一个全局 secret。查看 排水器与完整性 以了解 HMAC 和哈希链之间的区别。
脱敏
审计事件会经过你现有的 RedactConfig。与严格的审计预设组合,以强化对 PII 的处理:
import { auditRedactPreset } from 'evlog'
initLogger({
redact: {
paths: [
...(auditRedactPreset.paths ?? []),
'user.password',
],
},
})
该预设会移除 Authorization / Cookie 请求头,以及常见的凭证字段名(password、token、apiKey、cardNumber、cvv、ssn)——无论它们出现在 audit.changes.before 还是 audit.changes.after 中。
GDPR 与仅追加模式
仅追加的审计日志与 GDPR 的“被遗忘权”相冲突。当前推荐的模式:
- 保持审计行不可变。
- 使用按行为主体区分的密钥加密 PII 字段(该密钥保存在审计存储之外)。
- 要“忘记”某个用户时,删除其密钥——审计行仍保留,链仍然有效,PII 则变得不可读。
内置的 cryptoShredding 帮助函数已在 后续路线图 中。
保留
保留策略本质上是存储层面的关注点。evlog 的审计层不会强制保留窗口,因为每个受支持的后端已经有更强且经过审计的机制来处理它。请选择与你的后端相匹配的方案:
| 后端 | 保留机制 |
|---|---|
| FS | 将 createFsDrain({ 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,但前提是字段路径已被列出。一次性在全局添加password、token、apiKey等,这样你以后就无需再考虑它们。 - 把审计当作可观测性。 不要对审计事件进行采样、降采样或汇总。强制保留默认开启——不要禁用它。
- 将
actor.id与会话 ID 混淆。actor.id是稳定的用户 ID(或系统身份)。通过context.requestId/context.traceId关联会话,绝不要通过 actor 关联。 - 忘记独立任务。 定时任务、队列工作器和 CLI 也会触发值得审计的操作。使用
audit()(无请求)或withAudit(),以确保与 HTTP 路由保持一致的覆盖范围。 - 在审计排水器上跳过
await: true。 如果不这样做,审计就变成了“发出即忘记”——当事件被发出到排水器完成刷新之间发生崩溃时,操作已经发生,但审计行却不存在。