## 设计文档 - 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规范
42 KiB
42 KiB
审计日志增强设计方案(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 核心目标
-
M-013 支撑:供应方上游凭证泄露事件追踪
- 凭证相关操作完整记录
- 脱敏扫描集成
- 实时告警能力
-
M-014 支撑:平台凭证入站覆盖率
- 入站凭证类型标记
- 覆盖率自动计算
- 违规事件捕获
-
M-015 支撑:需求方直连绕过追踪
- 出网行为监控
- 跨域调用检测
- 异常模式识别
-
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 统一审计事件结构
// 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
检测点
- 响应检测:所有 API 响应在返回前扫描凭证片段
- 日志检测:日志输出前扫描凭证片段
- 导出检测:导出文件生成前扫描凭证片段
- 实时告警:检测到立即告警
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%
检测点
- 入站校验:每个入站请求记录凭证类型
- 覆盖率计算:平台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 计算
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
检测点
- 出网监控:监控所有出站连接
- 直连识别:检测绕过平台的直接连接
- 模式识别:异常访问模式识别
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 计算
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%
检测点
- 请求记录:所有 query key 请求
- 拒绝记录:所有拒绝事件
- 覆盖率计算:拒绝数 / 请求数
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-RESPONSECRED-INGRESS-PLATFORMAUTH-QUERY-KEYAUTH-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.mddocs/supply_technical_design_enhanced_v1_2026-03-25.mddocs/security_solution_v1_2026-03-18.mddocs/supply_traceability_matrix_generation_rules_v1_2026-03-27.md