# 审计日志增强设计方案(P1) - 版本:v1.0 - 日期:2026-04-02 - 状态:草稿 - 目标:为 M-013~M-016 指标提供完整的审计基础设施支撑 --- ## 1. 现状分析 ### 1.1 现有实现 #### supply-api/internal/audit/audit.go ```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 - 查询响应时间 < 500ms(1000条记录) - 支持至少 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 统一审计事件结构 ```go // 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 指标专用字段 ```go // 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 表结构 ```sql -- 统一审计事件表 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 缓存(热点数据) ```json { "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 ``` ```json { "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 ``` ```json { "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 ``` ```json { "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 ``` ```json { "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 层改造 ```go // 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" ) ``` #### 审计中间件集成 ```go // 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) } ``` #### 凭证暴露检测集成 ```go // 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 认证审计增强 ```go // 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 脱敏扫描集成 ```go // 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 计算 ```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_token,20个使用query key(被拒绝) - M-014 = 80/80 = 100%(分母只计算platform_token请求) - M-016 = 20/20 = 100%(分母计算所有query key请求) #### SQL 计算 ```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_type(ip_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 计算 ```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 计算 ```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 脚本 ```bash #!/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 测试用例 ```go // 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_INVALID`:Token无效 - `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`