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规范
This commit is contained in:
Your Name
2026-04-02 23:35:53 +08:00
parent ed0961d486
commit 89104bd0db
94 changed files with 24738 additions and 5 deletions

View File

@@ -0,0 +1,357 @@
package model
import (
"strings"
"time"
"github.com/google/uuid"
)
// 事件类别常量
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"
)
// 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"` // 违规类型列表
}
// NewSecurityFlags 创建默认安全标记
func NewSecurityFlags() *SecurityFlags {
return &SecurityFlags{
HasCredential: false,
CredentialExposed: false,
Desensitized: false,
Scanned: false,
ScanPassed: false,
ViolationTypes: []string{},
}
}
// HasViolation 检查是否有违规
func (sf *SecurityFlags) HasViolation() bool {
return len(sf.ViolationTypes) > 0
}
// HasViolationOfType 检查是否有指定类型的违规
func (sf *SecurityFlags) HasViolationOfType(violationType string) bool {
for _, v := range sf.ViolationTypes {
if v == violationType {
return true
}
}
return false
}
// AddViolationType 添加违规类型
func (sf *SecurityFlags) AddViolationType(violationType string) {
sf.ViolationTypes = append(sf.ViolationTypes, violationType)
}
// 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"` // 创建时间
}
// NewAuditEvent 创建审计事件
func NewAuditEvent(
eventName string,
eventCategory string,
eventSubCategory string,
metricName string,
requestID string,
traceID string,
operatorID int64,
operatorType string,
operatorRole string,
tenantID int64,
tenantType string,
objectType string,
objectID int64,
action string,
credentialType string,
sourceType string,
sourceIP string,
success bool,
resultCode string,
resultMessage string,
) *AuditEvent {
now := time.Now()
event := &AuditEvent{
EventID: uuid.New().String(),
EventName: eventName,
EventCategory: eventCategory,
EventSubCategory: eventSubCategory,
Timestamp: now,
TimestampMs: now.UnixMilli(),
RequestID: requestID,
TraceID: traceID,
OperatorID: operatorID,
OperatorType: operatorType,
OperatorRole: operatorRole,
TenantID: tenantID,
TenantType: tenantType,
ObjectType: objectType,
ObjectID: objectID,
Action: action,
CredentialType: credentialType,
SourceType: sourceType,
SourceIP: sourceIP,
Success: success,
ResultCode: resultCode,
ResultMessage: resultMessage,
Version: 1,
CreatedAt: now,
SecurityFlags: *NewSecurityFlags(),
ComplianceTags: []string{},
}
// 根据凭证类型设置安全标记
if credentialType != CredentialTypeNone && credentialType != "" {
event.SecurityFlags.HasCredential = true
}
// 根据事件名称设置凭证暴露标记M-013
if IsM013Event(eventName) {
event.SecurityFlags.CredentialExposed = true
}
// 根据事件名称设置指标名称到扩展字段
if metricName != "" {
if event.Extensions == nil {
event.Extensions = make(map[string]any)
}
event.Extensions["metric_name"] = metricName
}
return event
}
// NewAuditEventWithSecurityFlags 创建带完整安全标记的审计事件
func NewAuditEventWithSecurityFlags(
eventName string,
eventCategory string,
eventSubCategory string,
metricName string,
requestID string,
traceID string,
operatorID int64,
operatorType string,
operatorRole string,
tenantID int64,
tenantType string,
objectType string,
objectID int64,
action string,
credentialType string,
sourceType string,
sourceIP string,
success bool,
resultCode string,
resultMessage string,
securityFlags SecurityFlags,
riskScore int,
) *AuditEvent {
event := NewAuditEvent(
eventName,
eventCategory,
eventSubCategory,
metricName,
requestID,
traceID,
operatorID,
operatorType,
operatorRole,
tenantID,
tenantType,
objectType,
objectID,
action,
credentialType,
sourceType,
sourceIP,
success,
resultCode,
resultMessage,
)
event.SecurityFlags = securityFlags
event.RiskScore = riskScore
return event
}
// SetIdempotencyKey 设置幂等键
func (e *AuditEvent) SetIdempotencyKey(key string) {
e.IdempotencyKey = key
}
// SetTarget 设置目标信息用于M-015直连检测
func (e *AuditEvent) SetTarget(targetType, targetEndpoint string, targetDirect bool) {
e.TargetType = targetType
e.TargetEndpoint = targetEndpoint
e.TargetDirect = targetDirect
}
// SetInvariantRule 设置不变量规则用于SECURITY事件
func (e *AuditEvent) SetInvariantRule(rule string) {
e.InvariantRule = rule
// 添加合规标签
e.ComplianceTags = append(e.ComplianceTags, "XR-001")
}
// GetMetricName 获取指标名称
func (e *AuditEvent) GetMetricName() string {
if e.Extensions != nil {
if metricName, ok := e.Extensions["metric_name"].(string); ok {
return metricName
}
}
// 根据事件名称推断指标
switch e.EventName {
case "CRED-EXPOSE-RESPONSE", "CRED-EXPOSE-LOG", "CRED-EXPOSE":
return "supplier_credential_exposure_events"
case "CRED-INGRESS-PLATFORM", "CRED-INGRESS":
return "platform_credential_ingress_coverage_pct"
case "CRED-DIRECT-SUPPLIER", "CRED-DIRECT":
return "direct_supplier_call_by_consumer_events"
case "AUTH-QUERY-KEY", "AUTH-QUERY-REJECT", "AUTH-QUERY":
return "query_key_external_reject_rate_pct"
default:
return ""
}
}
// IsM013Event 判断是否为M-013凭证暴露事件
func IsM013Event(eventName string) bool {
return strings.HasPrefix(eventName, "CRED-EXPOSE")
}
// IsM014Event 判断是否为M-014凭证入站事件
func IsM014Event(eventName string) bool {
return strings.HasPrefix(eventName, "CRED-INGRESS")
}
// IsM015Event 判断是否为M-015直连绕过事件
func IsM015Event(eventName string) bool {
return strings.HasPrefix(eventName, "CRED-DIRECT")
}
// IsM016Event 判断是否为M-016 query key拒绝事件
func IsM016Event(eventName string) bool {
return strings.HasPrefix(eventName, "AUTH-QUERY")
}

View File

@@ -0,0 +1,389 @@
package model
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestAuditEvent_NewEvent_ValidInput(t *testing.T) {
// 测试创建审计事件
event := NewAuditEvent(
"CRED-EXPOSE-RESPONSE",
"CRED",
"EXPOSE",
"supplier_credential_exposure_events",
"test-request-id",
"test-trace-id",
1001,
"user",
"admin",
2001,
"supplier",
"account",
12345,
"create",
"platform_token",
"api",
"192.168.1.1",
true,
"SEC_CRED_EXPOSED",
"Credential exposed in response",
)
// 验证字段
assert.NotEmpty(t, event.EventID, "EventID should not be empty")
assert.Equal(t, "CRED-EXPOSE-RESPONSE", event.EventName, "EventName should match")
assert.Equal(t, "CRED", event.EventCategory, "EventCategory should match")
assert.Equal(t, "EXPOSE", event.EventSubCategory, "EventSubCategory should match")
assert.Equal(t, "test-request-id", event.RequestID, "RequestID should match")
assert.Equal(t, "test-trace-id", event.TraceID, "TraceID should match")
assert.Equal(t, int64(1001), event.OperatorID, "OperatorID should match")
assert.Equal(t, "user", event.OperatorType, "OperatorType should match")
assert.Equal(t, "admin", event.OperatorRole, "OperatorRole should match")
assert.Equal(t, int64(2001), event.TenantID, "TenantID should match")
assert.Equal(t, "supplier", event.TenantType, "TenantType should match")
assert.Equal(t, "account", event.ObjectType, "ObjectType should match")
assert.Equal(t, int64(12345), event.ObjectID, "ObjectID should match")
assert.Equal(t, "create", event.Action, "Action should match")
assert.Equal(t, "platform_token", event.CredentialType, "CredentialType should match")
assert.Equal(t, "api", event.SourceType, "SourceType should match")
assert.Equal(t, "192.168.1.1", event.SourceIP, "SourceIP should match")
assert.True(t, event.Success, "Success should be true")
assert.Equal(t, "SEC_CRED_EXPOSED", event.ResultCode, "ResultCode should match")
assert.Equal(t, "Credential exposed in response", event.ResultMessage, "ResultMessage should match")
// 验证时间戳
assert.False(t, event.Timestamp.IsZero(), "Timestamp should not be zero")
assert.True(t, event.TimestampMs > 0, "TimestampMs should be positive")
assert.False(t, event.CreatedAt.IsZero(), "CreatedAt should not be zero")
// 验证版本
assert.Equal(t, 1, event.Version, "Version should be 1")
}
func TestAuditEvent_NewEvent_SecurityFlags(t *testing.T) {
// 验证SecurityFlags字段
event := NewAuditEvent(
"CRED-EXPOSE-RESPONSE",
"CRED",
"EXPOSE",
"supplier_credential_exposure_events",
"test-request-id",
"test-trace-id",
1001,
"user",
"admin",
2001,
"supplier",
"account",
12345,
"create",
"platform_token",
"api",
"192.168.1.1",
true,
"SEC_CRED_EXPOSED",
"Credential exposed in response",
)
// 验证安全标记
assert.NotNil(t, event.SecurityFlags, "SecurityFlags should not be nil")
assert.True(t, event.SecurityFlags.HasCredential, "HasCredential should be true")
assert.True(t, event.SecurityFlags.CredentialExposed, "CredentialExposed should be true")
assert.False(t, event.SecurityFlags.Desensitized, "Desensitized should be false by default")
assert.False(t, event.SecurityFlags.Scanned, "Scanned should be false by default")
assert.False(t, event.SecurityFlags.ScanPassed, "ScanPassed should be false by default")
assert.Empty(t, event.SecurityFlags.ViolationTypes, "ViolationTypes should be empty by default")
}
func TestAuditEvent_NewEvent_WithSecurityFlags(t *testing.T) {
// 测试带有完整安全标记的事件
securityFlags := SecurityFlags{
HasCredential: true,
CredentialExposed: true,
Desensitized: false,
Scanned: true,
ScanPassed: false,
ViolationTypes: []string{"api_key", "secret"},
}
event := NewAuditEventWithSecurityFlags(
"CRED-EXPOSE-RESPONSE",
"CRED",
"EXPOSE",
"supplier_credential_exposure_events",
"test-request-id",
"test-trace-id",
1001,
"user",
"admin",
2001,
"supplier",
"account",
12345,
"create",
"platform_token",
"api",
"192.168.1.1",
true,
"SEC_CRED_EXPOSED",
"Credential exposed in response",
securityFlags,
80,
)
// 验证安全标记
assert.Equal(t, true, event.SecurityFlags.HasCredential)
assert.Equal(t, true, event.SecurityFlags.CredentialExposed)
assert.Equal(t, false, event.SecurityFlags.Desensitized)
assert.Equal(t, true, event.SecurityFlags.Scanned)
assert.Equal(t, false, event.SecurityFlags.ScanPassed)
assert.Equal(t, []string{"api_key", "secret"}, event.SecurityFlags.ViolationTypes)
// 验证风险评分
assert.Equal(t, 80, event.RiskScore, "RiskScore should be 80")
}
func TestAuditEvent_NewAuditEventWithIdempotencyKey(t *testing.T) {
// 测试带幂等键的事件
event := NewAuditEvent(
"AUTH-QUERY-KEY",
"AUTH",
"QUERY",
"query_key_external_reject_rate_pct",
"test-request-id",
"test-trace-id",
1001,
"user",
"admin",
2001,
"supplier",
"account",
12345,
"query",
"query_key",
"api",
"192.168.1.1",
true,
"AUTH_QUERY_KEY",
"Query key request",
)
// 设置幂等键
event.SetIdempotencyKey("idem-key-12345")
assert.Equal(t, "idem-key-12345", event.IdempotencyKey, "IdempotencyKey should be set")
}
func TestAuditEvent_NewAuditEventWithTarget(t *testing.T) {
// 测试带目标信息的事件用于M-015直连检测
event := NewAuditEvent(
"CRED-DIRECT-SUPPLIER",
"CRED",
"DIRECT",
"direct_supplier_call_by_consumer_events",
"test-request-id",
"test-trace-id",
1001,
"user",
"admin",
2001,
"supplier",
"api",
12345,
"call",
"none",
"api",
"192.168.1.1",
false,
"SEC_DIRECT_BYPASS",
"Direct call detected",
)
// 设置直连目标
event.SetTarget("upstream_api", "https://supplier.example.com/v1/chat/completions", true)
assert.Equal(t, "upstream_api", event.TargetType, "TargetType should be set")
assert.Equal(t, "https://supplier.example.com/v1/chat/completions", event.TargetEndpoint, "TargetEndpoint should be set")
assert.True(t, event.TargetDirect, "TargetDirect should be true")
}
func TestAuditEvent_NewAuditEventWithInvariantRule(t *testing.T) {
// 测试不变量规则用于SECURITY事件
event := NewAuditEvent(
"INVARIANT-VIOLATION",
"SECURITY",
"VIOLATION",
"invariant_violation",
"test-request-id",
"test-trace-id",
1001,
"system",
"admin",
2001,
"supplier",
"settlement",
12345,
"withdraw",
"platform_token",
"api",
"192.168.1.1",
false,
"SEC_INV_SET_001",
"Settlement cannot be revoked",
)
// 设置不变量规则
event.SetInvariantRule("INV-SET-001")
assert.Equal(t, "INV-SET-001", event.InvariantRule, "InvariantRule should be set")
assert.Contains(t, event.ComplianceTags, "XR-001", "ComplianceTags should contain XR-001")
}
func TestSecurityFlags_HasViolation(t *testing.T) {
// 测试安全标记的违规检测
sf := NewSecurityFlags()
// 初始状态无违规
assert.False(t, sf.HasViolation(), "Should have no violation initially")
// 添加违规类型
sf.AddViolationType("api_key")
assert.True(t, sf.HasViolation(), "Should have violation after adding type")
assert.True(t, sf.HasViolationOfType("api_key"), "Should have api_key violation")
assert.False(t, sf.HasViolationOfType("password"), "Should not have password violation")
}
func TestSecurityFlags_AddViolationType(t *testing.T) {
sf := NewSecurityFlags()
sf.AddViolationType("api_key")
sf.AddViolationType("secret")
sf.AddViolationType("password")
assert.Len(t, sf.ViolationTypes, 3, "Should have 3 violation types")
assert.Contains(t, sf.ViolationTypes, "api_key")
assert.Contains(t, sf.ViolationTypes, "secret")
assert.Contains(t, sf.ViolationTypes, "password")
}
func TestAuditEvent_MetricName(t *testing.T) {
// 测试事件与指标的映射
testCases := []struct {
eventName string
expectedMetric string
}{
{"CRED-EXPOSE-RESPONSE", "supplier_credential_exposure_events"},
{"CRED-EXPOSE-LOG", "supplier_credential_exposure_events"},
{"CRED-INGRESS-PLATFORM", "platform_credential_ingress_coverage_pct"},
{"CRED-DIRECT-SUPPLIER", "direct_supplier_call_by_consumer_events"},
{"AUTH-QUERY-KEY", "query_key_external_reject_rate_pct"},
{"AUTH-QUERY-REJECT", "query_key_external_reject_rate_pct"},
}
for _, tc := range testCases {
t.Run(tc.eventName, func(t *testing.T) {
event := &AuditEvent{
EventName: tc.eventName,
}
assert.Equal(t, tc.expectedMetric, event.GetMetricName(), "MetricName should match for %s", tc.eventName)
})
}
}
func TestAuditEvent_IsM013Event(t *testing.T) {
// M-013: 凭证暴露事件
assert.True(t, IsM013Event("CRED-EXPOSE-RESPONSE"), "CRED-EXPOSE-RESPONSE is M-013 event")
assert.True(t, IsM013Event("CRED-EXPOSE-LOG"), "CRED-EXPOSE-LOG is M-013 event")
assert.True(t, IsM013Event("CRED-EXPOSE"), "CRED-EXPOSE is M-013 event")
assert.False(t, IsM013Event("CRED-INGRESS-PLATFORM"), "CRED-INGRESS-PLATFORM is not M-013 event")
assert.False(t, IsM013Event("AUTH-QUERY-KEY"), "AUTH-QUERY-KEY is not M-013 event")
}
func TestAuditEvent_IsM014Event(t *testing.T) {
// M-014: 凭证入站事件
assert.True(t, IsM014Event("CRED-INGRESS-PLATFORM"), "CRED-INGRESS-PLATFORM is M-014 event")
assert.True(t, IsM014Event("CRED-INGRESS"), "CRED-INGRESS is M-014 event")
assert.False(t, IsM014Event("CRED-EXPOSE-RESPONSE"), "CRED-EXPOSE-RESPONSE is not M-014 event")
}
func TestAuditEvent_IsM015Event(t *testing.T) {
// M-015: 直连绕过事件
assert.True(t, IsM015Event("CRED-DIRECT-SUPPLIER"), "CRED-DIRECT-SUPPLIER is M-015 event")
assert.True(t, IsM015Event("CRED-DIRECT"), "CRED-DIRECT is M-015 event")
assert.False(t, IsM015Event("CRED-INGRESS-PLATFORM"), "CRED-INGRESS-PLATFORM is not M-015 event")
}
func TestAuditEvent_IsM016Event(t *testing.T) {
// M-016: query key拒绝事件
assert.True(t, IsM016Event("AUTH-QUERY-KEY"), "AUTH-QUERY-KEY is M-016 event")
assert.True(t, IsM016Event("AUTH-QUERY-REJECT"), "AUTH-QUERY-REJECT is M-016 event")
assert.True(t, IsM016Event("AUTH-QUERY"), "AUTH-QUERY is M-016 event")
assert.False(t, IsM016Event("CRED-EXPOSE-RESPONSE"), "CRED-EXPOSE-RESPONSE is not M-016 event")
}
func TestAuditEvent_CredentialType(t *testing.T) {
// 测试凭证类型常量
assert.Equal(t, "platform_token", CredentialTypePlatformToken)
assert.Equal(t, "query_key", CredentialTypeQueryKey)
assert.Equal(t, "upstream_api_key", CredentialTypeUpstreamAPIKey)
assert.Equal(t, "none", CredentialTypeNone)
}
func TestAuditEvent_OperatorType(t *testing.T) {
// 测试操作者类型常量
assert.Equal(t, "user", OperatorTypeUser)
assert.Equal(t, "system", OperatorTypeSystem)
assert.Equal(t, "admin", OperatorTypeAdmin)
}
func TestAuditEvent_TenantType(t *testing.T) {
// 测试租户类型常量
assert.Equal(t, "supplier", TenantTypeSupplier)
assert.Equal(t, "consumer", TenantTypeConsumer)
assert.Equal(t, "platform", TenantTypePlatform)
}
func TestAuditEvent_Category(t *testing.T) {
// 测试事件类别常量
assert.Equal(t, "CRED", CategoryCRED)
assert.Equal(t, "AUTH", CategoryAUTH)
assert.Equal(t, "DATA", CategoryDATA)
assert.Equal(t, "CONFIG", CategoryCONFIG)
assert.Equal(t, "SECURITY", CategorySECURITY)
}
func TestAuditEvent_NewAuditEventTimestamp(t *testing.T) {
// 测试时间戳自动生成
before := time.Now()
event := NewAuditEvent(
"CRED-EXPOSE-RESPONSE",
"CRED",
"EXPOSE",
"supplier_credential_exposure_events",
"test-request-id",
"test-trace-id",
1001,
"user",
"admin",
2001,
"supplier",
"account",
12345,
"create",
"platform_token",
"api",
"192.168.1.1",
true,
"SEC_CRED_EXPOSED",
"Credential exposed in response",
)
after := time.Now()
// 验证时间戳在合理范围内
assert.True(t, event.Timestamp.After(before) || event.Timestamp.Equal(before), "Timestamp should be after or equal to before")
assert.True(t, event.Timestamp.Before(after) || event.Timestamp.Equal(after), "Timestamp should be before or equal to after")
assert.Equal(t, event.Timestamp.UnixMilli(), event.TimestampMs, "TimestampMs should match Timestamp")
}

View File

@@ -0,0 +1,220 @@
package model
import (
"time"
)
// ==================== M-013: 凭证暴露事件详情 ====================
// CredentialExposureDetail M-013: 凭证暴露事件专用
type CredentialExposureDetail struct {
EventID string `json:"event_id"` // 事件ID关联audit_events
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"` // 匹配到的正则模式
ExposedFragment string `json:"exposed_fragment"` // 暴露的片段(已脱敏)
ScanRuleID string `json:"scan_rule_id"` // 触发扫描规则ID
Resolved bool `json:"resolved"` // 是否已解决
ResolvedAt *time.Time `json:"resolved_at"` // 解决时间
ResolvedBy *int64 `json:"resolved_by"` // 解决人
ResolutionNotes string `json:"resolution_notes"` // 解决备注
}
// NewCredentialExposureDetail 创建凭证暴露详情
func NewCredentialExposureDetail(
exposureType string,
exposureLocation string,
exposurePattern string,
exposedFragment string,
scanRuleID string,
) *CredentialExposureDetail {
return &CredentialExposureDetail{
ExposureType: exposureType,
ExposureLocation: exposureLocation,
ExposurePattern: exposurePattern,
ExposedFragment: exposedFragment,
ScanRuleID: scanRuleID,
Resolved: false,
}
}
// Resolve 标记为已解决
func (d *CredentialExposureDetail) Resolve(resolvedBy int64, notes string) {
now := time.Now()
d.Resolved = true
d.ResolvedAt = &now
d.ResolvedBy = &resolvedBy
d.ResolutionNotes = notes
}
// ==================== M-014: 凭证入站事件详情 ====================
// CredentialIngressDetail M-014: 凭证入站类型专用
type CredentialIngressDetail struct {
EventID string `json:"event_id"` // 事件ID
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是否存在
Reviewed bool `json:"reviewed"` // 是否已审核
ReviewedAt *time.Time `json:"reviewed_at"` // 审核时间
ReviewedBy *int64 `json:"reviewed_by"` // 审核人
}
// NewCredentialIngressDetail 创建凭证入站详情
func NewCredentialIngressDetail(
requestCredentialType string,
expectedCredentialType string,
coverageCompliant bool,
platformTokenPresent bool,
upstreamKeyPresent bool,
) *CredentialIngressDetail {
return &CredentialIngressDetail{
RequestCredentialType: requestCredentialType,
ExpectedCredentialType: expectedCredentialType,
CoverageCompliant: coverageCompliant,
PlatformTokenPresent: platformTokenPresent,
UpstreamKeyPresent: upstreamKeyPresent,
Reviewed: false,
}
}
// Review 标记为已审核
func (d *CredentialIngressDetail) Review(reviewedBy int64) {
now := time.Now()
d.Reviewed = true
d.ReviewedAt = &now
d.ReviewedBy = &reviewedBy
}
// ==================== M-015: 直连绕过事件详情 ====================
// DirectCallDetail M-015: 直连绕过专用
type DirectCallDetail struct {
EventID string `json:"event_id"` // 事件ID
ConsumerID int64 `json:"consumer_id"` // 消费者ID
SupplierID int64 `json:"supplier_id"` // 供应商ID
DirectEndpoint string `json:"direct_endpoint"` // 直连端点
ViaPlatform bool `json:"via_platform"` // 是否通过平台
BypassType string `json:"bypass_type"` // ip_bypass/proxy_bypass/config_bypass/dns_bypass
DetectionMethod string `json:"detection_method"` // 检测方法
Blocked bool `json:"blocked"` // 是否被阻断
BlockedAt *time.Time `json:"blocked_at"` // 阻断时间
BlockReason string `json:"block_reason"` // 阻断原因
}
// NewDirectCallDetail 创建直连详情
func NewDirectCallDetail(
consumerID int64,
supplierID int64,
directEndpoint string,
viaPlatform bool,
bypassType string,
detectionMethod string,
) *DirectCallDetail {
return &DirectCallDetail{
ConsumerID: consumerID,
SupplierID: supplierID,
DirectEndpoint: directEndpoint,
ViaPlatform: viaPlatform,
BypassType: bypassType,
DetectionMethod: detectionMethod,
Blocked: false,
}
}
// Block 标记为已阻断
func (d *DirectCallDetail) Block(reason string) {
now := time.Now()
d.Blocked = true
d.BlockedAt = &now
d.BlockReason = reason
}
// ==================== M-016: Query Key 拒绝事件详情 ====================
// QueryKeyRejectDetail M-016: query key 拒绝专用
type QueryKeyRejectDetail struct {
EventID string `json:"event_id"` // 事件ID
QueryKeyID string `json:"query_key_id"` // Query Key ID
RequestedEndpoint string `json:"requested_endpoint"` // 请求端点
RejectReason string `json:"reject_reason"` // not_allowed/expired/malformed/revoked/rate_limited
RejectCode string `json:"reject_code"` // 拒绝码
FirstOccurrence bool `json:"first_occurrence"` // 是否首次发生
OccurrenceCount int `json:"occurrence_count"` // 发生次数
}
// NewQueryKeyRejectDetail 创建Query Key拒绝详情
func NewQueryKeyRejectDetail(
queryKeyID string,
requestedEndpoint string,
rejectReason string,
rejectCode string,
) *QueryKeyRejectDetail {
return &QueryKeyRejectDetail{
QueryKeyID: queryKeyID,
RequestedEndpoint: requestedEndpoint,
RejectReason: rejectReason,
RejectCode: rejectCode,
FirstOccurrence: true,
OccurrenceCount: 1,
}
}
// RecordOccurrence 记录再次发生
func (d *QueryKeyRejectDetail) RecordOccurrence(firstOccurrence bool) {
d.FirstOccurrence = firstOccurrence
d.OccurrenceCount++
}
// ==================== 指标常量 ====================
// M-013 暴露类型常量
const (
ExposureTypeResponse = "exposed_in_response"
ExposureTypeLog = "exposed_in_log"
ExposureTypeExport = "exposed_in_export"
)
// M-013 暴露位置常量
const (
ExposureLocationResponseBody = "response_body"
ExposureLocationResponseHeader = "response_header"
ExposureLocationLogFile = "log_file"
ExposureLocationExportFile = "export_file"
)
// M-015 绕过类型常量
const (
BypassTypeIPBypass = "ip_bypass"
BypassTypeProxyBypass = "proxy_bypass"
BypassTypeConfigBypass = "config_bypass"
BypassTypeDNSBypass = "dns_bypass"
)
// M-015 检测方法常量
const (
DetectionMethodUpstreamAPIPattern = "upstream_api_pattern_match"
DetectionMethodDNSResolution = "dns_resolution_check"
DetectionMethodConnectionSource = "connection_source_check"
DetectionMethodIPWhitelist = "ip_whitelist_check"
)
// M-016 拒绝原因常量
const (
RejectReasonNotAllowed = "not_allowed"
RejectReasonExpired = "expired"
RejectReasonMalformed = "malformed"
RejectReasonRevoked = "revoked"
RejectReasonRateLimited = "rate_limited"
)
// M-016 拒绝码常量
const (
RejectCodeNotAllowed = "QUERY_KEY_NOT_ALLOWED"
RejectCodeExpired = "QUERY_KEY_EXPIRED"
RejectCodeMalformed = "QUERY_KEY_MALFORMED"
RejectCodeRevoked = "QUERY_KEY_REVOKED"
RejectCodeRateLimited = "QUERY_KEY_RATE_LIMITED"
)

View File

@@ -0,0 +1,459 @@
package model
import (
"testing"
"github.com/stretchr/testify/assert"
)
// ==================== M-013 凭证暴露事件详情 ====================
func TestCredentialExposureDetail_New(t *testing.T) {
// M-013: 凭证暴露事件专用
detail := NewCredentialExposureDetail(
"exposed_in_response",
"response_body",
"sk-[a-zA-Z0-9]{20,}",
"sk-xxxxxx****xxxx",
"SCAN-001",
)
assert.Equal(t, "exposed_in_response", detail.ExposureType)
assert.Equal(t, "response_body", detail.ExposureLocation)
assert.Equal(t, "sk-[a-zA-Z0-9]{20,}", detail.ExposurePattern)
assert.Equal(t, "sk-xxxxxx****xxxx", detail.ExposedFragment)
assert.Equal(t, "SCAN-001", detail.ScanRuleID)
assert.False(t, detail.Resolved)
assert.Nil(t, detail.ResolvedAt)
assert.Nil(t, detail.ResolvedBy)
assert.Empty(t, detail.ResolutionNotes)
}
func TestCredentialExposureDetail_Resolve(t *testing.T) {
detail := NewCredentialExposureDetail(
"exposed_in_response",
"response_body",
"sk-[a-zA-Z0-9]{20,}",
"sk-xxxxxx****xxxx",
"SCAN-001",
)
detail.Resolve(1001, "Fixed by adding masking")
assert.True(t, detail.Resolved)
assert.NotNil(t, detail.ResolvedAt)
assert.Equal(t, int64(1001), *detail.ResolvedBy)
assert.Equal(t, "Fixed by adding masking", detail.ResolutionNotes)
}
func TestCredentialExposureDetail_ExposureTypes(t *testing.T) {
// 验证暴露类型常量
validTypes := []string{
"exposed_in_response",
"exposed_in_log",
"exposed_in_export",
}
for _, exposureType := range validTypes {
detail := NewCredentialExposureDetail(
exposureType,
"response_body",
"pattern",
"fragment",
"SCAN-001",
)
assert.Equal(t, exposureType, detail.ExposureType)
}
}
func TestCredentialExposureDetail_ExposureLocations(t *testing.T) {
// 验证暴露位置常量
validLocations := []string{
"response_body",
"response_header",
"log_file",
"export_file",
}
for _, location := range validLocations {
detail := NewCredentialExposureDetail(
"exposed_in_response",
location,
"pattern",
"fragment",
"SCAN-001",
)
assert.Equal(t, location, detail.ExposureLocation)
}
}
// ==================== M-014 凭证入站事件详情 ====================
func TestCredentialIngressDetail_New(t *testing.T) {
// M-014: 凭证入站类型专用
detail := NewCredentialIngressDetail(
"platform_token",
"platform_token",
true,
true,
false,
)
assert.Equal(t, "platform_token", detail.RequestCredentialType)
assert.Equal(t, "platform_token", detail.ExpectedCredentialType)
assert.True(t, detail.CoverageCompliant)
assert.True(t, detail.PlatformTokenPresent)
assert.False(t, detail.UpstreamKeyPresent)
assert.False(t, detail.Reviewed)
assert.Nil(t, detail.ReviewedAt)
assert.Nil(t, detail.ReviewedBy)
}
func TestCredentialIngressDetail_NonCompliant(t *testing.T) {
// M-014 非合规场景:使用 query_key 而不是 platform_token
detail := NewCredentialIngressDetail(
"query_key",
"platform_token",
false,
false,
true,
)
assert.Equal(t, "query_key", detail.RequestCredentialType)
assert.Equal(t, "platform_token", detail.ExpectedCredentialType)
assert.False(t, detail.CoverageCompliant)
assert.False(t, detail.PlatformTokenPresent)
assert.True(t, detail.UpstreamKeyPresent)
}
func TestCredentialIngressDetail_Review(t *testing.T) {
detail := NewCredentialIngressDetail(
"platform_token",
"platform_token",
true,
true,
false,
)
detail.Review(1001)
assert.True(t, detail.Reviewed)
assert.NotNil(t, detail.ReviewedAt)
assert.Equal(t, int64(1001), *detail.ReviewedBy)
}
func TestCredentialIngressDetail_CredentialTypes(t *testing.T) {
// 验证凭证类型
testCases := []struct {
credType string
platformToken bool
upstreamKey bool
compliant bool
}{
{"platform_token", true, false, true},
{"query_key", false, false, false},
{"upstream_api_key", false, true, false},
{"none", false, false, false},
}
for _, tc := range testCases {
detail := NewCredentialIngressDetail(
tc.credType,
"platform_token",
tc.compliant,
tc.platformToken,
tc.upstreamKey,
)
assert.Equal(t, tc.compliant, detail.CoverageCompliant, "Compliance mismatch for %s", tc.credType)
}
}
// ==================== M-015 直连绕过事件详情 ====================
func TestDirectCallDetail_New(t *testing.T) {
// M-015: 直连绕过专用
detail := NewDirectCallDetail(
1001, // consumerID
2001, // supplierID
"https://supplier.example.com/v1/chat/completions",
false, // viaPlatform
"ip_bypass",
"upstream_api_pattern_match",
)
assert.Equal(t, int64(1001), detail.ConsumerID)
assert.Equal(t, int64(2001), detail.SupplierID)
assert.Equal(t, "https://supplier.example.com/v1/chat/completions", detail.DirectEndpoint)
assert.False(t, detail.ViaPlatform)
assert.Equal(t, "ip_bypass", detail.BypassType)
assert.Equal(t, "upstream_api_pattern_match", detail.DetectionMethod)
assert.False(t, detail.Blocked)
assert.Nil(t, detail.BlockedAt)
assert.Empty(t, detail.BlockReason)
}
func TestDirectCallDetail_Block(t *testing.T) {
detail := NewDirectCallDetail(
1001,
2001,
"https://supplier.example.com/v1/chat/completions",
false,
"ip_bypass",
"upstream_api_pattern_match",
)
detail.Block("P0 event - immediate block")
assert.True(t, detail.Blocked)
assert.NotNil(t, detail.BlockedAt)
assert.Equal(t, "P0 event - immediate block", detail.BlockReason)
}
func TestDirectCallDetail_BypassTypes(t *testing.T) {
// 验证绕过类型常量
validBypassTypes := []string{
"ip_bypass",
"proxy_bypass",
"config_bypass",
"dns_bypass",
}
for _, bypassType := range validBypassTypes {
detail := NewDirectCallDetail(
1001,
2001,
"https://example.com",
false,
bypassType,
"detection_method",
)
assert.Equal(t, bypassType, detail.BypassType)
}
}
func TestDirectCallDetail_DetectionMethods(t *testing.T) {
// 验证检测方法常量
validMethods := []string{
"upstream_api_pattern_match",
"dns_resolution_check",
"connection_source_check",
"ip_whitelist_check",
}
for _, method := range validMethods {
detail := NewDirectCallDetail(
1001,
2001,
"https://example.com",
false,
"ip_bypass",
method,
)
assert.Equal(t, method, detail.DetectionMethod)
}
}
func TestDirectCallDetail_ViaPlatform(t *testing.T) {
// 通过平台的调用不应该标记为直连
detail := NewDirectCallDetail(
1001,
2001,
"https://platform.example.com/v1/chat/completions",
true, // viaPlatform = true
"",
"platform_proxy",
)
assert.True(t, detail.ViaPlatform)
assert.False(t, detail.Blocked)
}
// ==================== M-016 Query Key 拒绝事件详情 ====================
func TestQueryKeyRejectDetail_New(t *testing.T) {
// M-016: query key 拒绝专用
detail := NewQueryKeyRejectDetail(
"qk-12345",
"/v1/chat/completions",
"not_allowed",
"QUERY_KEY_NOT_ALLOWED",
)
assert.Equal(t, "qk-12345", detail.QueryKeyID)
assert.Equal(t, "/v1/chat/completions", detail.RequestedEndpoint)
assert.Equal(t, "not_allowed", detail.RejectReason)
assert.Equal(t, "QUERY_KEY_NOT_ALLOWED", detail.RejectCode)
assert.True(t, detail.FirstOccurrence)
assert.Equal(t, 1, detail.OccurrenceCount)
}
func TestQueryKeyRejectDetail_RecordOccurrence(t *testing.T) {
detail := NewQueryKeyRejectDetail(
"qk-12345",
"/v1/chat/completions",
"not_allowed",
"QUERY_KEY_NOT_ALLOWED",
)
// 第二次发生
detail.RecordOccurrence(false)
assert.Equal(t, 2, detail.OccurrenceCount)
assert.False(t, detail.FirstOccurrence)
// 第三次发生
detail.RecordOccurrence(false)
assert.Equal(t, 3, detail.OccurrenceCount)
}
func TestQueryKeyRejectDetail_RejectReasons(t *testing.T) {
// 验证拒绝原因常量
validReasons := []string{
"not_allowed",
"expired",
"malformed",
"revoked",
"rate_limited",
}
for _, reason := range validReasons {
detail := NewQueryKeyRejectDetail(
"qk-12345",
"/v1/chat/completions",
reason,
"QUERY_KEY_REJECT",
)
assert.Equal(t, reason, detail.RejectReason)
}
}
func TestQueryKeyRejectDetail_RejectCodes(t *testing.T) {
// 验证拒绝码常量
validCodes := []string{
"QUERY_KEY_NOT_ALLOWED",
"QUERY_KEY_EXPIRED",
"QUERY_KEY_MALFORMED",
"QUERY_KEY_REVOKED",
"QUERY_KEY_RATE_LIMITED",
}
for _, code := range validCodes {
detail := NewQueryKeyRejectDetail(
"qk-12345",
"/v1/chat/completions",
"not_allowed",
code,
)
assert.Equal(t, code, detail.RejectCode)
}
}
// ==================== 指标计算辅助函数 ====================
func TestCalculateM013(t *testing.T) {
// M-013: 凭证泄露事件数 = 0
events := []struct {
eventName string
resolved bool
}{
{"CRED-EXPOSE-RESPONSE", true},
{"CRED-EXPOSE-RESPONSE", true},
{"CRED-EXPOSE-LOG", false},
{"AUTH-TOKEN-OK", true},
}
var unresolvedCount int
for _, e := range events {
if IsM013Event(e.eventName) && !e.resolved {
unresolvedCount++
}
}
assert.Equal(t, 1, unresolvedCount, "M-013 should have 1 unresolved event")
}
func TestCalculateM014(t *testing.T) {
// M-014: 平台凭证入站覆盖率 = 100%
events := []struct {
credentialType string
compliant bool
}{
{"platform_token", true},
{"platform_token", true},
{"query_key", false},
{"upstream_api_key", false},
{"platform_token", true},
}
var platformCount, totalCount int
for _, e := range events {
if IsM014Compliant(e.credentialType) {
platformCount++
}
totalCount++
}
coverage := float64(platformCount) / float64(totalCount) * 100
assert.Equal(t, 60.0, coverage, "M-014 coverage should be 60%%")
assert.Equal(t, 3, platformCount)
assert.Equal(t, 5, totalCount)
}
func TestCalculateM015(t *testing.T) {
// M-015: 直连事件数 = 0
events := []struct {
targetDirect bool
blocked bool
}{
{targetDirect: true, blocked: false},
{targetDirect: true, blocked: true},
{targetDirect: false, blocked: false},
{targetDirect: true, blocked: false},
}
var directCallCount, blockedCount int
for _, e := range events {
if e.targetDirect {
directCallCount++
if e.blocked {
blockedCount++
}
}
}
assert.Equal(t, 3, directCallCount, "M-015 should have 3 direct call events")
assert.Equal(t, 1, blockedCount, "M-015 should have 1 blocked event")
}
func TestCalculateM016(t *testing.T) {
// M-016: query key 拒绝率 = 100%
// 分母所有query key请求不含被拒绝的无效请求
events := []struct {
eventName string
}{
{"AUTH-QUERY-KEY"},
{"AUTH-QUERY-REJECT"},
{"AUTH-QUERY-KEY"},
{"AUTH-QUERY-REJECT"},
{"AUTH-TOKEN-OK"},
}
var totalQueryKey, rejectedCount int
for _, e := range events {
if IsM016Event(e.eventName) {
totalQueryKey++
if e.eventName == "AUTH-QUERY-REJECT" {
rejectedCount++
}
}
}
rejectRate := float64(rejectedCount) / float64(totalQueryKey) * 100
assert.Equal(t, 4, totalQueryKey, "M-016 should have 4 query key events")
assert.Equal(t, 2, rejectedCount, "M-016 should have 2 rejected events")
assert.Equal(t, 50.0, rejectRate, "M-016 reject rate should be 50%%")
}
// IsM014Compliant 检查凭证类型是否为M-014合规
func IsM014Compliant(credentialType string) bool {
return credentialType == CredentialTypePlatformToken
}