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

1355 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 审计日志增强设计方案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
- 查询响应时间 < 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 统一审计事件结构
```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_token20个使用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_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 计算
```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`