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:
357
supply-api/internal/audit/model/audit_event.go
Normal file
357
supply-api/internal/audit/model/audit_event.go
Normal 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")
|
||||
}
|
||||
389
supply-api/internal/audit/model/audit_event_test.go
Normal file
389
supply-api/internal/audit/model/audit_event_test.go
Normal 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")
|
||||
}
|
||||
220
supply-api/internal/audit/model/audit_metrics.go
Normal file
220
supply-api/internal/audit/model/audit_metrics.go
Normal 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"
|
||||
)
|
||||
459
supply-api/internal/audit/model/audit_metrics_test.go
Normal file
459
supply-api/internal/audit/model/audit_metrics_test.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user