Files
lijiaoqiao/docs/audit_log_enhancement_design_v1_2026-04-02.md
Your Name 89104bd0db feat(P1/P2): 完成TDD开发及P1/P2设计文档
## 设计文档
- multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO)
- audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO)
- routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO)
- sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO)
- compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO)

## TDD开发成果
- IAM模块: supply-api/internal/iam/ (111个测试)
- 审计日志模块: supply-api/internal/audit/ (40+测试)
- 路由策略模块: gateway/internal/router/ (33+测试)
- 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/

## 规范文档
- parallel_agent_output_quality_standards: 并行Agent产出质量规范
- project_experience_summary: 项目经验总结 (v2)
- 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划

## 评审报告
- 5个CONDITIONAL GO设计文档评审报告
- fix_verification_report: 修复验证报告
- full_verification_report: 全面质量验证报告
- tdd_module_quality_verification: TDD模块质量验证
- tdd_execution_summary: TDD执行总结

依据: Superpowers执行框架 + TDD规范
2026-04-02 23:35:53 +08:00

42 KiB
Raw Blame History

审计日志增强设计方案P1

  • 版本v1.0
  • 日期2026-04-02
  • 状态:草稿
  • 目标:为 M-013~M-016 指标提供完整的审计基础设施支撑

1. 现状分析

1.1 现有实现

supply-api/internal/audit/audit.go

// 审计事件
type Event struct {
    EventID     string         `json:"event_id,omitempty"`
    TenantID    int64          `json:"tenant_id"`
    ObjectType  string         `json:"object_type"`
    ObjectID    int64          `json:"object_id"`
    Action      string         `json:"action"`
    BeforeState map[string]any `json:"before_state,omitempty"`
    AfterState  map[string]any `json:"after_state,omitempty"`
    RequestID   string         `json:"request_id,omitempty"`
    ResultCode  string         `json:"result_code"`
    ClientIP    string         `json:"client_ip,omitempty"`
    CreatedAt   time.Time      `json:"created_at"`
}
  • 仅内存存储MemoryAuditStore无持久化
  • 无事件分类体系
  • 无 M-013~M-016 指标映射能力
  • 无脱敏扫描能力

gateway/internal/middleware/audit.go

  • DatabaseAuditEmitter 实现PostgreSQL
  • 关注 Token 认证事件
  • 字段event_id, event_name, request_id, token_id, subject_id, route, result_code, client_ip, created_at
  • 与 supply-api 审计体系割裂

1.2 差距分析

维度 现有实现 M-013~M-016 要求 差距
凭证暴露事件 无专门记录 M-013: 凭证泄露事件=0需完整溯源 严重不足
凭证入站类型 无区分 M-014: 平台凭证覆盖率=100% 无追踪
直连绕过事件 M-015: 直连事件=0 无感知
query key 拒绝 M-016: 拒绝率=100% 无记录
事件分类 安全事件分类体系 缺失
存储 内存 持久化+可查询 需改造
溯源能力 基本 全链路追踪 不足

2. 设计目标

2.1 核心目标

  1. M-013 支撑:供应方上游凭证泄露事件追踪

    • 凭证相关操作完整记录
    • 脱敏扫描集成
    • 实时告警能力
  2. M-014 支撑:平台凭证入站覆盖率

    • 入站凭证类型标记
    • 覆盖率自动计算
    • 违规事件捕获
  3. M-015 支撑:需求方直连绕过追踪

    • 出网行为监控
    • 跨域调用检测
    • 异常模式识别
  4. M-016 支撑:外部 query key 拒绝率

    • query key 请求全记录
    • 拒绝原因分类
    • 拒绝率实时计算

2.2 非功能目标

  • 审计写入延迟 < 10ms
  • 查询响应时间 < 500ms1000条记录
  • 支持至少 10000 TPS 写入
  • 数据保留 365 天

3. 审计事件分类体系

3.1 事件大类

大类编码 大类名称 说明
CRED 凭证事件 凭证相关操作
AUTH 认证授权事件 身份验证与权限检查
DATA 数据访问事件 数据读写操作
CONFIG 配置变更事件 系统配置修改
SECURITY 安全相关事件 安全策略触发

3.2 凭证事件子类CRED

子类编码 子类名称 M-013 映射 记录场景
CRED-EXPOSE 凭证暴露 直接相关 响应/导出/日志中出现可复用凭证片段
CRED-INGRESS 凭证入站 直接相关 入站请求凭证类型校验
CRED-ROTATE 凭证轮换 间接相关 凭证主动轮换操作
CRED-REVOKE 凭证吊销 间接相关 凭证吊销/禁用操作
CRED-VALIDATE 凭证验证 间接相关 凭证验证结果
CRED-DIRECT 直连绕过 M-015 直接相关 需求方绕过平台直连供应方

3.3 认证授权事件子类AUTH

子类编码 子类名称 M-016 映射 记录场景
AUTH-TOKEN-OK Token认证成功 间接相关 平台Token认证通过
AUTH-TOKEN-FAIL Token认证失败 间接相关 Token无效/过期/格式错误
AUTH-QUERY-KEY query key 请求 M-016 直接相关 外部 query key 请求
AUTH-QUERY-REJECT query key 拒绝 M-016 直接相关 query key 被拒绝
AUTH-SCOPE-DENY Scope权限不足 间接相关 权限不足拒绝

3.4 数据访问事件子类DATA

子类编码 子类名称 说明
DATA-READ 数据读取 GET 请求
DATA-WRITE 数据写入 POST/PUT/PATCH 请求
DATA-DELETE 数据删除 DELETE 请求
DATA-EXPORT 数据导出 导出操作

3.5 配置变更事件子类CONFIG

子类编码 子类名称 说明
CONFIG-CREATE 配置创建 新增配置
CONFIG-UPDATE 配置更新 修改配置
CONFIG-DELETE 配置删除 删除配置

3.6 安全相关事件子类SECURITY

子类编码 子类名称 M-013 映射 说明
INVARIANT-VIOLATION 不变量违反 直接相关 业务不变量检查失败依据XR-001要求所有不变量失败必须写入invariant_violation事件并携带rule_code
SECURITY-BREACH 安全突破 直接相关 安全机制被突破
SECURITY-ALERT 安全告警 间接相关 安全相关告警事件

3.6.1 invariant_violation 事件详细定义

根据XR-001要求所有不变量失败必须写入审计事件 invariant_violation,并携带 rule_code

规则ID 规则名称 触发场景 结果码
INV-PKG-001 供应方资质过期 资质验证 SEC_INV_PKG_001
INV-PKG-002 供应方余额为负 余额检查 SEC_INV_PKG_002
INV-PKG-003 售价不得低于保护价 发布/调价 SEC_INV_PKG_003
INV-SET-001 processing/completed 不可撤销 撤销申请 SEC_INV_SET_001
INV-SET-002 提现金额不得超过可提现余额 发起提现 SEC_INV_SET_002
INV-SET-003 结算单金额与余额流水必须平衡 结算入账 SEC_INV_SET_003

4. 审计字段标准化

4.1 统一审计事件结构

// AuditEvent 统一审计事件
type AuditEvent struct {
    // 基础标识
    EventID       string    `json:"event_id"`                // 事件唯一ID (UUID)
    EventName     string    `json:"event_name"`              // 事件名称 (e.g., "CRED-EXPOSE")
    EventCategory string    `json:"event_category"`          // 事件大类 (e.g., "CRED")
    EventSubCategory string `json:"event_sub_category"`      // 事件子类

    // 时间戳
    Timestamp     time.Time `json:"timestamp"`               // 事件发生时间
    TimestampMs  int64     `json:"timestamp_ms"`            // 毫秒时间戳

    // 请求上下文
    RequestID     string    `json:"request_id"`             // 请求追踪ID
    TraceID       string    `json:"trace_id"`               // 分布式追踪ID
    SpanID        string    `json:"span_id"`                // Span ID

    // 幂等性
    IdempotencyKey string   `json:"idempotency_key,omitempty"` // 幂等键

    // 操作者信息
    OperatorID    int64     `json:"operator_id"`            // 操作者ID
    OperatorType  string    `json:"operator_type"`          // 操作者类型 (user/system/admin)
    OperatorRole  string    `json:"operator_role"`          // 操作者角色

    // 租户信息
    TenantID      int64     `json:"tenant_id"`              // 租户ID
    TenantType    string    `json:"tenant_type"`            // 租户类型 (supplier/consumer/platform)

    // 对象信息
    ObjectType     string   `json:"object_type"`             // 对象类型 (account/package/settlement)
    ObjectID       int64    `json:"object_id"`              // 对象ID

    // 操作信息
    Action         string   `json:"action"`                 // 操作类型 (create/update/delete)
    ActionDetail   string   `json:"action_detail"`          // 操作详情

    // 凭证信息 (M-013/M-014/M-015/M-016 关键)
    CredentialType string   `json:"credential_type"`         // 凭证类型 (platform_token/query_key/upstream_api_key/none)
    CredentialID   string   `json:"credential_id,omitempty"` // 凭证标识 (脱敏)
    CredentialFingerprint string `json:"credential_fingerprint,omitempty"` // 凭证指纹

    // 来源信息
    SourceType     string   `json:"source_type"`            // 来源类型 (api/ui/cron/internal)
    SourceIP       string   `json:"source_ip"`              // 来源IP
    SourceRegion   string   `json:"source_region"`          // 来源区域
    UserAgent      string   `json:"user_agent,omitempty"`  // User Agent

    // 目标信息 (用于直连检测 M-015)
    TargetType     string   `json:"target_type,omitempty"` // 目标类型
    TargetEndpoint string   `json:"target_endpoint,omitempty"` // 目标端点
    TargetDirect   bool     `json:"target_direct"`          // 是否直连

    // 结果信息
    ResultCode     string   `json:"result_code"`            // 结果码
    ResultMessage  string   `json:"result_message,omitempty"` // 结果消息
    Success        bool     `json:"success"`               // 是否成功

    // 状态变更 (用于溯源)
    BeforeState    map[string]any `json:"before_state,omitempty"` // 操作前状态
    AfterState     map[string]any `json:"after_state,omitempty"` // 操作后状态

    // 安全标记 (M-013 关键)
    SecurityFlags  SecurityFlags `json:"security_flags"`      // 安全标记
    RiskScore      int          `json:"risk_score"`          // 风险评分 0-100

    // 合规信息
    ComplianceTags []string `json:"compliance_tags,omitempty"` // 合规标签 (e.g., ["GDPR", "SOC2"])
    InvariantRule string   `json:"invariant_rule,omitempty"` // 触发的不变量规则

    // 扩展字段
    Extensions     map[string]any `json:"extensions,omitempty"` // 扩展数据

    // 元数据
    Version        int       `json:"version"`               // 事件版本
    CreatedAt      time.Time `json:"created_at"`            // 创建时间
}

// SecurityFlags 安全标记
type SecurityFlags struct {
    HasCredential       bool     `json:"has_credential"`        // 是否包含凭证
    CredentialExposed  bool     `json:"credential_exposed"`   // 凭证是否暴露
    Desensitized        bool     `json:"desensitized"`          // 是否已脱敏
    Scanned             bool     `json:"scanned"`              // 是否已扫描
    ScanPassed          bool     `json:"scan_passed"`          // 扫描是否通过
    ViolationTypes      []string `json:"violation_types"`      // 违规类型列表
}

4.2 M-013~M-016 指标专用字段

// M-013: 凭证暴露事件专用
type CredentialExposureDetail struct {
    ExposureType     string   `json:"exposure_type"`      // exposed_in_response/exposed_in_log/exposed_in_export
    ExposureLocation string   `json:"exposure_location"`  // response_body/response_header/log_file/export_file
    ExposurePattern  string   `json:"exposure_pattern"`  // 匹配到的正则模式
    Exposed片段       string   `json:"exposed_fragment"`  // 暴露的片段(已脱敏)
    ScanRuleID       string   `json:"scan_rule_id"`      // 触发扫描规则ID
}

// M-014: 凭证入站类型专用
type CredentialIngressDetail struct {
    RequestCredentialType string `json:"request_credential_type"` // 请求中的凭证类型
    ExpectedCredentialType string `json:"expected_credential_type"` // 期望的凭证类型
    CoverageCompliant     bool   `json:"coverage_compliant"`      // 是否合规
    PlatformTokenPresent  bool   `json:"platform_token_present"`  // 平台Token是否存在
    UpstreamKeyPresent    bool   `json:"upstream_key_present"`    // 上游Key是否存在
}

// M-015: 直连绕过专用
type DirectCallDetail struct {
    ConsumerID       int64   `json:"consumer_id"`
    SupplierID        int64   `json:"supplier_id"`
    DirectEndpoint    string  `json:"direct_endpoint"`
    ViaPlatform      bool    `json:"via_platform"`
    BypassType       string  `json:"bypass_type"`         // ip_bypass/proxy_bypass/config_bypass
    DetectionMethod   string  `json:"detection_method"`   // how detected
}

// M-016: query key 拒绝专用
type QueryKeyRejectDetail struct {
    QueryKeyID       string  `json:"query_key_id"`
    RequestedEndpoint string  `json:"requested_endpoint"`
    RejectReason     string  `json:"reject_reason"`      // not_allowed/expired/malformed
    RejectCode       string  `json:"reject_code"`
}

5. 存储设计

5.1 PostgreSQL 表结构

-- 统一审计事件表
CREATE TABLE IF NOT EXISTS audit_events (
    -- 基础标识
    event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    event_name VARCHAR(64) NOT NULL,
    event_category VARCHAR(32) NOT NULL,
    event_sub_category VARCHAR(32),

    -- 时间戳
    timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    timestamp_ms BIGINT NOT NULL,

    -- 请求上下文
    request_id VARCHAR(128),
    trace_id VARCHAR(128),
    span_id VARCHAR(64),
    idempotency_key VARCHAR(128),

    -- 操作者信息
    operator_id BIGINT NOT NULL,
    operator_type VARCHAR(32) NOT NULL,
    operator_role VARCHAR(64),

    -- 租户信息
    tenant_id BIGINT NOT NULL,
    tenant_type VARCHAR(32) NOT NULL,

    -- 对象信息
    object_type VARCHAR(64) NOT NULL,
    object_id BIGINT NOT NULL,

    -- 操作信息
    action VARCHAR(64) NOT NULL,
    action_detail TEXT,

    -- 凭证信息
    credential_type VARCHAR(32) NOT NULL,
    credential_id VARCHAR(128),
    credential_fingerprint VARCHAR(64),

    -- 来源信息
    source_type VARCHAR(32),
    source_ip INET,
    source_region VARCHAR(32),
    user_agent TEXT,

    -- 目标信息
    target_type VARCHAR(32),
    target_endpoint TEXT,
    target_direct BOOLEAN DEFAULT FALSE,

    -- 结果信息
    result_code VARCHAR(64) NOT NULL,
    result_message TEXT,
    success BOOLEAN NOT NULL DEFAULT TRUE,

    -- 状态变更 (JSONB)
    before_state JSONB,
    after_state JSONB,

    -- 安全标记 (JSONB)
    security_flags JSONB,

    -- 风险评分
    risk_score INT DEFAULT 0,

    -- 合规信息
    compliance_tags TEXT[],
    invariant_rule VARCHAR(128),

    -- 扩展字段 (JSONB)
    extensions JSONB,

    -- 元数据
    version INT DEFAULT 1,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- 索引策略
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_events(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_audit_request_id ON audit_events(request_id);
CREATE INDEX IF NOT EXISTS idx_audit_trace_id ON audit_events(trace_id);
CREATE INDEX IF NOT EXISTS idx_audit_tenant_id ON audit_events(tenant_id);
CREATE INDEX IF NOT EXISTS idx_audit_event_category ON audit_events(event_category);
CREATE INDEX IF NOT EXISTS idx_audit_event_name ON audit_events(event_name);
CREATE INDEX IF NOT EXISTS idx_audit_credential_type ON audit_events(credential_type);
CREATE INDEX IF NOT EXISTS idx_audit_object ON audit_events(object_type, object_id);
CREATE INDEX IF NOT EXISTS idx_audit_success ON audit_events(success) WHERE NOT success;
CREATE INDEX IF NOT EXISTS idx_audit_risk_score ON audit_events(risk_score) WHERE risk_score > 50;
CREATE INDEX IF NOT EXISTS idx_audit_security_flags ON audit_events((security_flags->>'credential_exposed')) WHERE security_flags->>'credential_exposed' = 'true';

-- M-013 专用索引
CREATE INDEX IF NOT EXISTS idx_audit_cred_exposure ON audit_events(event_name, timestamp DESC) WHERE event_name LIKE 'CRED-EXPOSE%';

-- M-014 专用索引
CREATE INDEX IF NOT EXISTS idx_audit_cred_ingress ON audit_events(credential_type, timestamp DESC) WHERE event_category = 'CRED' AND event_sub_category = 'INGRESS';

-- M-015 专用索引
CREATE INDEX IF NOT EXISTS idx_audit_direct_call ON audit_events(target_direct, timestamp DESC) WHERE target_direct = TRUE;

-- M-016 专用索引
CREATE INDEX IF NOT EXISTS idx_audit_query_key_reject ON audit_events(event_name, timestamp DESC) WHERE event_name LIKE 'AUTH-QUERY%';

-- 分区表(按月分区)
CREATE TABLE IF NOT EXISTS audit_events_partitioned () INHERITS (audit_events);

-- 创建分区函数
CREATE OR REPLACE FUNCTION create_audit_partition()
RETURNS void AS $$
DECLARE
    partition_date DATE;
    partition_name TEXT;
BEGIN
    partition_date := CURRENT_DATE;
    partition_name := 'audit_events_' || TO_CHAR(partition_date, 'YYYYMM');

    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS %I PARTITION OF audit_events_partitioned FOR VALUES FROM (%L) TO (%L)',
        partition_name,
        partition_date,
        partition_date + INTERVAL '1 month'
    );
END;
$$ LANGUAGE plpgsql;

-- 凭证暴露事件详情表 (M-013 专用)
CREATE TABLE IF NOT EXISTS credential_exposure_events (
    event_id UUID PRIMARY KEY REFERENCES audit_events(event_id),
    exposure_type VARCHAR(64) NOT NULL,
    exposure_location VARCHAR(64) NOT NULL,
    exposure_pattern VARCHAR(256),
    exposed_fragment TEXT,
    scan_rule_id VARCHAR(64),
    resolved BOOLEAN DEFAULT FALSE,
    resolved_at TIMESTAMPTZ,
    resolved_by BIGINT,
    resolution_notes TEXT
);

-- 凭证入站事件表 (M-014 专用)
CREATE TABLE IF NOT EXISTS credential_ingress_events (
    event_id UUID PRIMARY KEY REFERENCES audit_events(event_id),
    request_credential_type VARCHAR(32) NOT NULL,
    expected_credential_type VARCHAR(32) NOT NULL,
    coverage_compliant BOOLEAN NOT NULL,
    platform_token_present BOOLEAN NOT NULL,
    upstream_key_present BOOLEAN NOT NULL,
    reviewed BOOLEAN DEFAULT FALSE,
    reviewed_at TIMESTAMPTZ,
    reviewed_by BIGINT
);

-- 直连绕过事件表 (M-015 专用)
CREATE TABLE IF NOT EXISTS direct_call_events (
    event_id UUID PRIMARY KEY REFERENCES audit_events(event_id),
    consumer_id BIGINT NOT NULL,
    supplier_id BIGINT NOT NULL,
    direct_endpoint TEXT NOT NULL,
    via_platform BOOLEAN NOT NULL,
    bypass_type VARCHAR(32),
    detection_method VARCHAR(64),
    blocked BOOLEAN DEFAULT FALSE,
    blocked_at TIMESTAMPTZ,
    block_reason TEXT
);

-- query key 拒绝事件表 (M-016 专用)
CREATE TABLE IF NOT EXISTS query_key_reject_events (
    event_id UUID PRIMARY KEY REFERENCES audit_events(event_id),
    query_key_id VARCHAR(128) NOT NULL,
    requested_endpoint TEXT NOT NULL,
    reject_reason VARCHAR(64) NOT NULL,
    reject_code VARCHAR(64) NOT NULL,
    first_occurrence BOOLEAN DEFAULT TRUE,
    occurrence_count INT DEFAULT 1
);

-- 审计事件归档表 (历史数据)
CREATE TABLE IF NOT EXISTS audit_events_archive (
    LIKE audit_events INCLUDING ALL
);

-- 触发器:自动更新 updated_at
CREATE OR REPLACE FUNCTION update_created_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.created_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tr_audit_events_created_at
    BEFORE INSERT ON audit_events
    FOR EACH ROW
    EXECUTE FUNCTION update_created_at();

5.2 Redis 缓存(热点数据)

{
  "key_pattern": "audit:metric:{metric_type}:{date}",
  "ttl": 86400,
  "fields": {
    "m013_cred_exposure_count": 0,
    "m014_platform_ingress_count": 0,
    "m014_total_ingress_count": 0,
    "m015_direct_call_count": 0,
    "m016_query_key_reject_count": 0,
    "m016_query_key_total_count": 0
  }
}

6. API 设计

6.1 事件写入 API

POST /api/v1/audit/events
Content-Type: application/json
X-Request-Id: {request_id}
X-Idempotency-Key: {idempotency_key}

{
  "event": AuditEvent
}

幂等性响应语义

状态码 场景 响应体
201 首次成功 {"event_id": "...", "status": "created"}
202 处理中 {"status": "processing", "retry_after_ms": 1000}
409 重放异参 {"error": {"code": "IDEMPOTENCY_PAYLOAD_MISMATCH", "message": "Idempotency key reused with different payload"}}
200 重放同参 {"event_id": "...", "status": "duplicate", "original_created_at": "..."}

幂等性协议说明

  • 首次成功请求的幂等键从未使用过处理成功后返回201
  • 重放同参请求的幂等键已使用且payload相同返回200不重复创建
  • 重放异参请求的幂等键已使用但payload不同返回409冲突
  • 处理中请求的幂等键正在处理中异步场景返回202

6.2 事件查询 API

GET /api/v1/audit/events
参数 类型 说明
tenant_id int64 租户ID必填
start_date string 开始日期 ISO8601
end_date string 结束日期 ISO8601
event_category string 事件大类
event_name string 事件名称
object_type string 对象类型
object_id int64 对象ID
credential_type string 凭证类型
success bool 是否成功
risk_score_min int 最小风险评分
limit int 返回数量默认100最大1000
offset int 偏移量
GET /api/v1/audit/events/{event_id}

6.3 M-013~M-016 指标 API

GET /api/v1/audit/metrics/m013
{
  "metric_id": "M-013",
  "metric_name": "supplier_credential_exposure_events",
  "period": {
    "start": "2026-04-01T00:00:00Z",
    "end": "2026-04-02T00:00:00Z"
  },
  "value": 0,
  "unit": "count",
  "status": "PASS",
  "details": {
    "total_exposure_events": 0,
    "unresolved_events": 0,
    "recent_events": []
  }
}
GET /api/v1/audit/metrics/m014
{
  "metric_id": "M-014",
  "metric_name": "platform_credential_ingress_coverage_pct",
  "period": {
    "start": "2026-04-01T00:00:00Z",
    "end": "2026-04-02T00:00:00Z"
  },
  "value": 100.0,
  "unit": "percentage",
  "status": "PASS",
  "details": {
    "platform_token_requests": 10000,
    "total_requests": 10000,
    "non_compliant_requests": 0
  }
}
GET /api/v1/audit/metrics/m015
{
  "metric_id": "M-015",
  "metric_name": "direct_supplier_call_by_consumer_events",
  "period": {
    "start": "2026-04-01T00:00:00Z",
    "end": "2026-04-02T00:00:00Z"
  },
  "value": 0,
  "unit": "count",
  "status": "PASS",
  "details": {
    "total_direct_call_events": 0,
    "blocked_events": 0
  }
}
GET /api/v1/audit/metrics/m016
{
  "metric_id": "M-016",
  "metric_name": "query_key_external_reject_rate_pct",
  "period": {
    "start": "2026-04-01T00:00:00Z",
    "end": "2026-04-02T00:00:00Z"
  },
  "value": 100.0,
  "unit": "percentage",
  "status": "PASS",
  "details": {
    "rejected_requests": 0,
    "total_external_query_key_requests": 0,
    "reject_breakdown": {}
  }
}

6.4 告警配置 API

POST /api/v1/audit/alerts
GET /api/v1/audit/alerts
PUT /api/v1/audit/alerts/{alert_id}
DELETE /api/v1/audit/alerts/{alert_id}

7. 集成方案

7.1 supply-api 集成

Domain 层改造

// audit/event.go

package audit

// 事件类别常量
const (
    CategoryCRED = "CRED"
    CategoryAUTH = "AUTH"
    CategoryDATA = "DATA"
    CategoryCONFIG = "CONFIG"
    CategorySECURITY = "SECURITY"
)

// 凭证事件子类别
const (
    SubCategoryCredExpose   = "EXPOSE"
    SubCategoryCredIngress  = "INGRESS"
    SubCategoryCredRotate   = "ROTATE"
    SubCategoryCredRevoke   = "REVOKE"
    SubCategoryCredValidate = "VALIDATE"
    SubCategoryCredDirect   = "DIRECT"
)

// 凭证类型
const (
    CredentialTypePlatformToken  = "platform_token"
    CredentialTypeQueryKey       = "query_key"
    CredentialTypeUpstreamAPIKey  = "upstream_api_key"
    CredentialTypeNone            = "none"
)

// 操作者类型
const (
    OperatorTypeUser    = "user"
    OperatorTypeSystem = "system"
    OperatorTypeAdmin  = "admin"
)

// 租户类型
const (
    TenantTypeSupplier  = "supplier"
    TenantTypeConsumer  = "consumer"
    TenantTypePlatform  = "platform"
)

审计中间件集成

// httpapi/middleware/audit.go

package httpapi

import (
    "context"
    "supply-api/internal/audit"
)

type AuditMiddleware struct {
    auditStore audit.AuditStore
}

func (m *AuditMiddleware) Handle(ctx context.Context, req *Request, next Handler) (*Response, error) {
    // 创建审计上下文
    auditCtx := audit.WithContext(ctx, &audit.Context{
        RequestID:    req.Header.Get("X-Request-Id"),
        TraceID:      req.Header.Get("X-Trace-Id"),
        SpanID:       req.Header.Get("X-Span-Id"),
        OperatorID:   req.OperatorID,
        OperatorType: req.OperatorType,
        TenantID:     req.TenantID,
        TenantType:   req.TenantType,
        SourceIP:     req.ClientIP,
        UserAgent:    req.Header.Get("User-Agent"),
    })

    // 处理请求
    resp, err := next.Handle(auditCtx, req)

    // 记录审计事件
    m.emitFromResponse(auditCtx, req, resp, err)

    return resp, err
}

func (m *AuditMiddleware) emitFromResponse(ctx context.Context, req *Request, resp *Response, err error) {
    event := &audit.Event{
        EventName:     m.determineEventName(req),
        EventCategory: audit.CategoryAUTH,
        Timestamp:     time.Now(),
        RequestID:     req.Header.Get("X-Request-Id"),
        OperatorID:    req.OperatorID,
        TenantID:      req.TenantID,
        ObjectType:    m.determineObjectType(req),
        ObjectID:      req.ObjectID,
        Action:        req.Method,
        CredentialType: m.determineCredentialType(req),
        SourceIP:      req.ClientIP,
        ResultCode:    m.determineResultCode(resp, err),
        Success:       err == nil,
        RiskScore:     m.calculateRiskScore(req, resp, err),
    }

    m.auditStore.Emit(ctx, event)
}

凭证暴露检测集成

// security/credential_scanner.go

package security

type CredentialScanner struct {
    rules []ScanRule
}

type ScanRule struct {
    ID          string
    Pattern     *regexp.Regexp
    Severity    string
    Description string
}

func (s *CredentialScanner) Scan(content string) (*ScanResult, error) {
    result := &ScanResult{
        Violations: []Violation{},
    }

    for _, rule := range s.rules {
        if matches := rule.Pattern.FindAllString(content, -1); len(matches) > 0 {
            result.Violations = append(result.Violations, Violation{
                RuleID:     rule.ID,
                Matched:    matches,
                Severity:   rule.Severity,
                Described:  s.desensitize(matches),
            })
        }
    }

    return result, nil
}

func (s *CredentialScanner) desensitize(matches []string) []string {
    desensitized := make([]string, len(matches))
    for i, match := range matches {
        if len(match) > 8 {
            desensitized[i] = match[:4] + "****" + match[len(match)-4:]
        } else {
            desensitized[i] = "****"
        }
    }
    return desensitized
}

7.2 gateway 集成

Token 认证审计增强

// middleware/auth.go

func (m *AuthMiddleware) authn(ctx context.Context, req *Request) error {
    // ... 认证逻辑 ...

    // 审计事件
    event := &middleware.AuditEvent{
        EventID:    generateEventID(),
        EventName:  determineEventName(credType, success),
        RequestID:  req.Header.Get("X-Request-Id"),
        TokenID:    tokenID,
        SubjectID:  subjectID,
        Route:     req.URL.Path,
        ResultCode: resultCode,
        ClientIP:  req.ClientIP,
        CreatedAt: time.Now(),
        // 扩展字段
        Extensions: map[string]any{
            "credential_type": credType,
            "tenant_id":      tenantID,
            "m014_compliant":  credType == CredentialTypePlatformToken,
            "m016_query_key":  credType == CredentialTypeQueryKey,
        },
    }

    if err := m.Auditor.Emit(ctx, *event); err != nil {
        log.Errorf("failed to emit audit event: %v", err)
    }

    return nil
}

7.3 脱敏扫描集成

// security/desensitization.go

package security

// 脱敏规则
var DesensitizationRules = []DesensitizationRule{
    {
        Name:        "api_key",
        Pattern:     `sk-[a-zA-Z0-9]{20,}`,
        Replacement: "sk-****",
        Level:       LevelSensitive,
    },
    {
        Name:        "openai_key",
        Pattern:     `(sk-[a-zA-Z0-9]{20,})`,
        Replacement: "${1:0:4}****${1:-4}",
        Level:       LevelSensitive,
    },
    {
        Name:        "upstream_credential",
        Pattern:     `(sk-|api-|key-)[a-zA-Z0-9]{16,}`,
        Replacement: "${1}****",
        Level:       LevelSensitive,
    },
}

func Desensitize(content string) (string, []Violation) {
    result := content
    violations := []Violation{}

    for _, rule := range DesensitizationRules {
        if matches := rule.Pattern.FindAllString(result, -1); len(matches) > 0 {
            result = rule.Pattern.ReplaceAllString(result, rule.Replacement)
            violations = append(violations, Violation{
                Rule:   rule.Name,
                Count:  len(matches),
                Level:  rule.Level,
            })
        }
    }

    return result, violations
}

8. M-013~M-016 指标实现

8.1 M-013: 凭证泄露事件数 = 0

检测点

  1. 响应检测:所有 API 响应在返回前扫描凭证片段
  2. 日志检测:日志输出前扫描凭证片段
  3. 导出检测:导出文件生成前扫描凭证片段
  4. 实时告警:检测到立即告警

SQL 计算

SELECT COUNT(*) as exposure_count
FROM audit_events
WHERE event_name LIKE 'CRED-EXPOSE%'
    AND timestamp >= $start_date
    AND timestamp < $end_date;

8.2 M-014: 平台凭证入站覆盖率 = 100%

检测点

  1. 入站校验:每个入站请求记录凭证类型
  2. 覆盖率计算平台Token请求数 / 总请求数

M-014 与 M-016 边界说明

  • M-014 分母定义:经平台凭证校验的入站请求(credential_type = 'platform_token'不含被拒绝的无效请求
  • M-016 分母定义检测到的所有query key请求event_name LIKE 'AUTH-QUERY%'被拒绝的请求
  • 两者互不影响query key请求在通过平台认证前不会进入M-014的计数范围因此query key拒绝事件不会影响M-014的覆盖率计算

示例

  • 如果有100个请求其中80个使用platform_token20个使用query key被拒绝
  • M-014 = 80/80 = 100%分母只计算platform_token请求
  • M-016 = 20/20 = 100%分母计算所有query key请求

SQL 计算

WITH credential_stats AS (
    SELECT
        COUNT(*) FILTER (WHERE credential_type = 'platform_token') as platform_count,
        COUNT(*) as total_count
    FROM audit_events
    WHERE event_category = 'CRED'
        AND event_sub_category = 'INGRESS'
        AND timestamp >= $start_date
        AND timestamp < $end_date
)
SELECT
    CASE WHEN total_count = 0 THEN 100.0
         ELSE (platform_count::DECIMAL / total_count::DECIMAL) * 100
    END as coverage_pct
FROM credential_stats;

8.3 M-015: 直连事件数 = 0

检测点

  1. 出网监控:监控所有出站连接
  2. 直连识别:检测绕过平台的直接连接
  3. 模式识别:异常访问模式识别

M-015 直连检测机制详细设计

根据合规能力包C015-R01~C015-R03直连检测有以下机制

8.3.1 检测方法
检测方法 说明 实现位置
IP/域名白名单比对 请求目标为已知供应商IP/域名时标记为直连 Gateway层
上游API模式匹配 请求路径匹配 */v1/chat/completions 等上游端点 Gateway层
DNS解析监控 检测到Consumer直接解析Supplier域名 Network层
连接来源检测 出站连接直接来自Consumer IP而非平台代理 Network层
8.3.2 检测流程
直连检测流程 (M015-FLOW-01)

1. 请求发起
         │
         ▼
2. 检查请求目标
   - 若目标IP在供应商白名单 → 标记 target_direct = TRUE
   - 若目标域名解析指向供应商IP段 → 标记 target_direct = TRUE
         │
         ▼
3. 检查请求路径
   - 若路径匹配上游API模式如 */v1/chat/completions
   - 且来源不是平台代理 → 标记 target_direct = TRUE
         │
         ▼
4. 记录审计事件
   - 记录 target_direct = TRUE
   - 记录 bypass_typeip_bypass/proxy_bypass/config_bypass
   - 记录 detection_method检测方法
         │
         ▼
5. 触发阻断/告警
   - P0事件立即阻断
   - 发送告警到安全通道
8.3.3 target_direct 字段填充规则
场景 target_direct bypass_type detection_method
Consumer直接调用Supplier API TRUE ip_bypass upstream_api_pattern_match
Consumer DNS直解析Supplier TRUE dns_bypass dns_resolution_check
通过平台代理调用 FALSE - -
内部服务调用 FALSE - -

SQL 计算

SELECT COUNT(*) as direct_call_count
FROM audit_events
WHERE target_direct = TRUE
    AND timestamp >= $start_date
    AND timestamp < $end_date;

8.4 M-016: query key 拒绝率 = 100%

检测点

  1. 请求记录:所有 query key 请求
  2. 拒绝记录:所有拒绝事件
  3. 覆盖率计算:拒绝数 / 请求数

SQL 计算

WITH query_key_stats AS (
    SELECT
        COUNT(*) FILTER (WHERE event_name = 'AUTH-QUERY-KEY') as total_requests,
        COUNT(*) FILTER (WHERE event_name = 'AUTH-QUERY-REJECT') as rejected_requests
    FROM audit_events
    WHERE event_name LIKE 'AUTH-QUERY%'
        AND timestamp >= $start_date
        AND timestamp < $end_date
)
SELECT
    CASE WHEN total_requests = 0 THEN 100.0
         ELSE (rejected_requests::DECIMAL / total_requests::DECIMAL) * 100
    END as reject_rate_pct
FROM query_key_stats;

9. CI/CD 集成

9.1 Gate 脚本

#!/bin/bash
# scripts/ci/audit_metrics_gate.sh

set -e

METRICS_START_DATE=${METRICS_START_DATE:-$(date -d '1 day ago' +%Y-%m-%d)}
METRICS_END_DATE=${METRICS_END_DATE:-$(date +%Y-%m-%d)}

echo "=== M-013 凭证泄露事件数检查 ==="
M013_COUNT=$(psql -t -c "SELECT COUNT(*) FROM audit_events WHERE event_name LIKE 'CRED-EXPOSE%' AND timestamp >= '$METRICS_START_DATE' AND timestamp < '$METRICS_END_DATE';")
echo "M-013 凭证暴露事件数: $M013_COUNT"
if [ "$M013_COUNT" -gt 0 ]; then
    echo "FAIL: M-013 超标 (要求 = 0)"
    exit 1
fi
echo "PASS: M-013"

echo "=== M-014 平台凭证覆盖率检查 ==="
M014_RATE=$(psql -t -c "WITH stats AS (SELECT COUNT(*) FILTER (WHERE credential_type = 'platform_token') as p, COUNT(*) as t FROM audit_events WHERE event_category = 'CRED' AND event_sub_category = 'INGRESS' AND timestamp >= '$METRICS_START_DATE' AND timestamp < '$METRICS_END_DATE') SELECT CASE WHEN t = 0 THEN 100.0 ELSE (p::DECIMAL / t::DECIMAL) * 100 END FROM stats;")
echo "M-014 平台凭证覆盖率: $M014_RATE%"
if [ "$(echo "$M014_RATE < 100" | bc)" -eq 1 ]; then
    echo "FAIL: M-014 不达标 (要求 = 100%)"
    exit 1
fi
echo "PASS: M-014"

echo "=== M-015 直连绕过事件数检查 ==="
M015_COUNT=$(psql -t -c "SELECT COUNT(*) FROM audit_events WHERE target_direct = TRUE AND timestamp >= '$METRICS_START_DATE' AND timestamp < '$METRICS_END_DATE';")
echo "M-015 直连事件数: $M015_COUNT"
if [ "$M015_COUNT" -gt 0 ]; then
    echo "FAIL: M-015 超标 (要求 = 0)"
    exit 1
fi
echo "PASS: M-015"

echo "=== M-016 query key 拒绝率检查 ==="
M016_RATE=$(psql -t -c "WITH stats AS (SELECT COUNT(*) FILTER (WHERE event_name = 'AUTH-QUERY-KEY') as t, COUNT(*) FILTER (WHERE event_name = 'AUTH-QUERY-REJECT') as r FROM audit_events WHERE event_name LIKE 'AUTH-QUERY%' AND timestamp >= '$METRICS_START_DATE' AND timestamp < '$METRICS_END_DATE') SELECT CASE WHEN t = 0 THEN 100.0 ELSE (r::DECIMAL / t::DECIMAL) * 100 END FROM stats;")
echo "M-016 query key 拒绝率: $M016_RATE%"
if [ "$(echo "$M016_RATE < 100" | bc)" -eq 1 ]; then
    echo "FAIL: M-016 不达标 (要求 = 100%)"
    exit 1
fi
echo "PASS: M-016"

echo "=== 所有 M-013~M-016 检查通过 ==="

9.2 测试用例

// internal/audit/audit_test.go

package audit

import (
    "testing"
)

func TestM013_CredentialExposureDetection(t *testing.T) {
    scanner := NewCredentialScanner()

    testCases := []struct {
        name        string
        content     string
        expectFound bool
    }{
        {
            name:        "OpenAI API Key",
            content:     "sk-1234567890abcdefghijklmnopqrstuvwxyz",
            expectFound: true,
        },
        {
            name:        "Platform Token",
            content:     "platform_token_xxx",
            expectFound: false,
        },
        {
            name:        "Normal Text",
            content:     "This is normal text without credentials",
            expectFound: false,
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            result, err := scanner.Scan(tc.content)
            if err != nil {
                t.Fatalf("scan failed: %v", err)
            }
            if tc.expectFound && len(result.Violations) == 0 {
                t.Error("expected to find credential but none found")
            }
            if !tc.expectFound && len(result.Violations) > 0 {
                t.Errorf("expected no credential but found %d", len(result.Violations))
            }
        })
    }
}

func TestM014_PlatformCredentialIngressCoverage(t *testing.T) {
    store := NewTestStore()

    // 模拟入站请求
    testCases := []struct {
        credType    string
        shouldCount bool
    }{
        {CredentialTypePlatformToken, true},
        {CredentialTypeQueryKey, false},
        {CredentialTypeUpstreamAPIKey, false},
    }

    for _, tc := range testCases {
        event := &Event{
            EventCategory:    CategoryCRED,
            EventSubCategory: SubCategoryCredIngress,
            CredentialType:   tc.credType,
            Success:          true,
            Timestamp:        time.Now(),
        }
        store.Emit(context.Background(), *event)
    }

    // 计算覆盖率
    total := 0
    platformCount := 0
    events, _ := store.Query(context.Background(), EventFilter{})
    for _, e := range events {
        total++
        if e.CredentialType == CredentialTypePlatformToken {
            platformCount++
        }
    }

    coverage := float64(platformCount) / float64(total) * 100
    if coverage != 100.0 {
        t.Errorf("expected 100%% coverage, got %.2f%%", coverage)
    }
}

10. 实施计划

10.1 Phase 1: 基础设施1-2周

任务 依赖 负责人 验收标准
数据库表结构创建 - 后端 表创建成功,索引正常
统一 Event 结构体 - 后端 结构体定义完成
AuditStore 接口定义 - 后端 接口评审通过
PostgreSQL 实现 表结构 后端 单元测试通过

10.2 Phase 2: 核心功能2-3周

任务 依赖 负责人 验收标准
supply-api 审计中间件 Phase 1 后端 集成测试通过
凭证暴露扫描器 Phase 1 安全 扫描准确率 > 99%
脱敏规则库 Phase 1 安全 规则覆盖主要场景
API 实现 Phase 1 后端 API 测试通过
M-014 覆盖率计算 API 后端 指标计算正确

10.3 Phase 3: M-013~M-016 指标1-2周

任务 依赖 负责人 验收标准
M-013 事件记录 Phase 2 后端 事件正确分类
M-015 直连检测 Phase 2 安全 检测逻辑正确
M-016 拒绝记录 Phase 2 后端 记录完整
指标 API Phase 2 后端 API 正确返回
CI Gate 脚本 Phase 3 DevOps Gate 检查通过

10.4 Phase 4: 集成与优化1周

任务 依赖 负责人 验收标准
端到端测试 Phase 3 QA 测试通过
性能优化 Phase 3 后端 满足性能目标
文档完善 Phase 3 后端 文档完整
告警配置 Phase 3 运维 告警正常工作

11. 风险与缓解

风险 影响 概率 缓解措施
审计写入影响性能 异步写入,批量处理
数据量膨胀 分区表,定期归档
误报导致 M-014 误判 双校验机制
直连检测覆盖不全 多维度检测
历史数据迁移 分阶段迁移

12. 附录

12.1 事件名称规范

格式:{Category}-{SubCategory}[-{Detail}]

示例:

  • CRED-EXPOSE-RESPONSE
  • CRED-INGRESS-PLATFORM
  • AUTH-QUERY-KEY
  • AUTH-TOKEN-OK

12.1.1 事件名称与TOK-002对齐映射

为确保与TOK-002 Token中间件设计一致以下事件名称建立等价映射关系

设计文档事件名 TOK-002事件名 说明
AUTH-TOKEN-OK token.authn.success 平台Token认证成功
AUTH-TOKEN-FAIL token.authn.fail 平台Token认证失败
AUTH-SCOPE-DENY token.authz.denied Scope权限不足
AUTH-QUERY-REJECT token.query_key.rejected query key被拒绝
AUTH-QUERY-KEY - query key请求仅审计记录

命名风格说明

  • 设计文档使用 CATEGORY-SUBCATEGORY 格式(如 AUTH-TOKEN-OK适合数据库索引和SQL查询
  • TOK-002使用 token.category.action 格式(如 token.authn.success),适合日志和监控
  • 两种格式等价,系统内部统一使用设计文档格式,外部接口可转换

12.2 结果码规范

格式:{Domain}_{Code}

示例:

  • SEC_CRED_EXPOSED:凭证暴露
  • SEC_DIRECT_BYPASS:直连绕过
  • AUTH_TOKEN_INVALIDToken无效
  • AUTH_SCOPE_DENIED:权限不足

12.2.1 错误码体系对照表

本设计错误码与现有体系对齐:

错误码 来源 说明 对应事件
AUTH_MISSING_BEARER TOK-002 请求头缺失Bearer AUTH-TOKEN-FAIL
AUTH_INVALID_TOKEN TOK-002 Token无效/签名失败 AUTH-TOKEN-FAIL
AUTH_TOKEN_INACTIVE TOK-002 Token已吊销/过期 AUTH-TOKEN-FAIL
AUTH_SCOPE_DENIED TOK-002 权限不足 AUTH-SCOPE-DENY
QUERY_KEY_NOT_ALLOWED TOK-002 query key外部入站拒绝 AUTH-QUERY-REJECT
SEC_CRED_EXPOSED XR-001 凭证泄露 CRED-EXPOSE
SEC_DIRECT_BYPASS XR-001 直连绕过 CRED-DIRECT
SEC_INV_PKG_* XR-001 供应方不变量违反 INVARIANT-VIOLATION
SEC_INV_SET_* XR-001 结算不变量违反 INVARIANT-VIOLATION
SUP_PKG_* 供应侧 供应方包相关错误 CONFIG-*
SUP_SET_* 供应侧 结算相关错误 CONFIG-*

12.3 参考文档

  • docs/acceptance_gate_single_source_v1_2026-03-18.md
  • docs/supply_technical_design_enhanced_v1_2026-03-25.md
  • docs/security_solution_v1_2026-03-18.md
  • docs/supply_traceability_matrix_generation_rules_v1_2026-03-27.md