审计日志

审计 Schema

概览记录
AuditFields 类型、动作命名约定、主体类型、幂等性,以及该 schema 如何位于普通宽事件内部。

event.audit 是每个宽事件上的一个带类型字段。下游查询通过 audit IS NOT NULL 进行过滤,以便从普通日志中生成审计数据集。

AuditFields 类型

interface AuditFields {
  action: string                          // 'invoice.refund'
  actor: {
    type: 'user' | 'system' | 'api' | 'agent'
    id: string
    displayName?: string
    email?: string
    // 对于 type === 'agent',镜像 evlog/ai 字段:
    model?: string
    tools?: string[]
    reason?: string
    promptId?: string
  }
  target?: { type: string, id: string, [k: string]: unknown }
  outcome: 'success' | 'failure' | 'denied'
  reason?: string
  changes?: { before?: unknown, after?: unknown, patch?: AuditPatchOp[] }
  causationId?: string                    // 导致此动作的动作 ID
  correlationId?: string                  // 在一次操作中的每个动作共享
  version?: number                        // 默认为 1
  idempotencyKey?: string                 // 自动派生;在 drain 重试中保持安全
  context?: {                             // 由 auditEnricher 填充
    requestId?: string
    traceId?: string
    ip?: string
    userAgent?: string
    tenantId?: string
  }
  signature?: string                      // 由 signed({ strategy: 'hmac' }) 设置
  prevHash?: string                       // 由 signed({ strategy: 'hash-chain' }) 设置
  hash?: string
}

动作命名

action 的命名约定。 使用 noun.verbinvoice.refunduser.inviteapiKey.revoke)。如果动作已经发生,则使用过去时(invoice.refunded);如果 withAudit() 将解析结果,则使用现在时。将小型固定字典保存在一个文件中——审计员和 SIEM 规则会根据 action 进行查询,因此一个拼写错误就意味着漏报告警。

一个单独的字典文件可以让告警更容易实现:

src/audit/actions.ts
export const AUDIT_ACTIONS = {
  USER_INVITE: 'user.invite',
  USER_REMOVE: 'user.remove',
  USER_ROLE_CHANGE: 'user.role-change',
  INVOICE_REFUND: 'invoice.refund',
  API_KEY_REVOKE: 'apiKey.revoke',
} as const
对于超过少数几个动作的情况,建议使用 defineAuditCatalog 而不是普通对象字典——同样遵循 noun.verb 约定,但你还能免费获得动作联合类型的自动补全以及每个条目的 target 类型推断。

主体类型

不要伪造主体。 对于 cron 作业、队列 worker 和后台任务,使用 actor.type: 'system';对于使用令牌认证的机器对机器调用,使用 actor.type: 'api';对于 AI 工具调用,使用 actor.type: 'agent'。为系统操作记录一个伪造的 'user' 是最容易导致审计审查失败的做法。
actor.type适用场景
'user'通过你的常规认证流程完成身份验证的人类用户。
'system'Cron 作业、队列 worker、定时任务、内部后台进程。
'api'来自另一项服务、通过令牌认证的机器对机器调用。
'agent'AI 工具调用(可与 evlog/ai 字段结合,如 modeltoolspromptId)。

结果

outcome含义
'success'动作按要求完成。
'failure'动作已尝试但失败(下游错误、竞态条件等)。
'denied'动作被授权检查拒绝。

'failure''denied' 是不同的概念——审计员非常关注被拒绝的动作,因为它们通常意味着探测行为或访问控制配置错误。务必记录拒绝事件(参见 Recording Events)。

幂等性

idempotencyKeyactionactor.idtarget 和一个粗粒度时间戳的哈希自动派生。结果是:即使你的 drain 因网络抖动而重试审计插入,重复行也会在 ON CONFLICT DO NOTHING 下被折叠。你无需为此担心——它已经为你填好了。

将该字段用作 Postgres / Bigtable / DynamoDB 中的主键,这样重试就能从设计上保持安全。

因果与关联

字段使用场景
correlationId由属于同一操作的每个审计事件共享(例如:一次 HTTP 请求触发退款 + 邮件 + webhook)。
causationId导致此事件的前一个审计事件的 ID。适用于重建级联动作链条。

大多数团队会将 correlationId 设置为 requestIdcausationId 是可选的,只有在单个用户动作触发许多内部审计事件时才值得填写。