Files
lijiaoqiao/supply-api/internal/audit/service/metrics_service_test.go

376 lines
11 KiB
Go
Raw Normal View History

package service
import (
"context"
"testing"
"time"
"lijiaoqiao/supply-api/internal/audit/model"
"github.com/stretchr/testify/assert"
)
func TestAuditMetrics_M013_CredentialExposure(t *testing.T) {
// M-013: supplier_credential_exposure_events = 0
ctx := context.Background()
svc := NewAuditService(NewInMemoryAuditStore())
metricsSvc := NewMetricsService(svc)
// 创建一些事件包括CRED-EXPOSE事件
events := []*model.AuditEvent{
{
EventName: "CRED-EXPOSE-RESPONSE",
EventCategory: "CRED",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "account",
ObjectID: 12345,
Action: "create",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: true,
ResultCode: "SEC_CRED_EXPOSED",
},
{
EventName: "AUTH-TOKEN-OK",
EventCategory: "AUTH",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "token",
ObjectID: 12345,
Action: "verify",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: true,
ResultCode: "AUTH_TOKEN_OK",
},
}
for _, e := range events {
svc.CreateEvent(ctx, e)
}
// 计算M-013指标
now := time.Now()
metric, err := metricsSvc.CalculateM013(ctx, now.Add(-24*time.Hour), now)
assert.NoError(t, err)
assert.NotNil(t, metric)
assert.Equal(t, "M-013", metric.MetricID)
assert.Equal(t, "supplier_credential_exposure_events", metric.MetricName)
assert.Equal(t, float64(1), metric.Value) // 有1个暴露事件
assert.Equal(t, "FAIL", metric.Status) // 暴露事件数 > 0应该是FAIL
}
func TestAuditMetrics_M014_IngressCoverage(t *testing.T) {
// M-014: platform_credential_ingress_coverage_pct = 100%
// 分母定义经平台凭证校验的入站请求credential_type = 'platform_token'),不含被拒绝的无效请求
ctx := context.Background()
svc := NewAuditService(NewInMemoryAuditStore())
metricsSvc := NewMetricsService(svc)
// 创建入站凭证事件
events := []*model.AuditEvent{
// 合规的platform_token请求
{
EventName: "CRED-INGRESS-PLATFORM",
EventCategory: "CRED",
EventSubCategory: "INGRESS",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "account",
ObjectID: 12345,
Action: "query",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: true,
ResultCode: "CRED_INGRESS_OK",
},
{
EventName: "CRED-INGRESS-PLATFORM",
EventCategory: "CRED",
EventSubCategory: "INGRESS",
OperatorID: 1002,
TenantID: 2001,
ObjectType: "account",
ObjectID: 12346,
Action: "query",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.2",
Success: true,
ResultCode: "CRED_INGRESS_OK",
},
// 非合规的query_key请求 - 不应该计入M-014的分母
{
EventName: "CRED-INGRESS-SUPPLIER",
EventCategory: "CRED",
EventSubCategory: "INGRESS",
OperatorID: 1003,
TenantID: 2001,
ObjectType: "account",
ObjectID: 12347,
Action: "query",
CredentialType: "query_key",
SourceType: "api",
SourceIP: "192.168.1.3",
Success: false,
ResultCode: "AUTH_QUERY_REJECT",
},
}
for _, e := range events {
svc.CreateEvent(ctx, e)
}
// 计算M-014指标
now := time.Now()
metric, err := metricsSvc.CalculateM014(ctx, now.Add(-24*time.Hour), now)
assert.NoError(t, err)
assert.NotNil(t, metric)
assert.Equal(t, "M-014", metric.MetricID)
assert.Equal(t, "platform_credential_ingress_coverage_pct", metric.MetricName)
// 2个platform_token / 2个总入站请求 = 100%
assert.Equal(t, 100.0, metric.Value)
assert.Equal(t, "PASS", metric.Status)
}
func TestAuditMetrics_M015_DirectCall(t *testing.T) {
// M-015: direct_supplier_call_by_consumer_events = 0
ctx := context.Background()
svc := NewAuditService(NewInMemoryAuditStore())
metricsSvc := NewMetricsService(svc)
// 创建直连事件
events := []*model.AuditEvent{
{
EventName: "CRED-DIRECT-SUPPLIER",
EventCategory: "CRED",
EventSubCategory: "DIRECT",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "api",
ObjectID: 12345,
Action: "call",
CredentialType: "none",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: false,
ResultCode: "SEC_DIRECT_BYPASS",
TargetDirect: true,
},
{
EventName: "AUTH-TOKEN-OK",
EventCategory: "AUTH",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "token",
ObjectID: 12345,
Action: "verify",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: true,
ResultCode: "AUTH_TOKEN_OK",
},
}
for _, e := range events {
svc.CreateEvent(ctx, e)
}
// 计算M-015指标
now := time.Now()
metric, err := metricsSvc.CalculateM015(ctx, now.Add(-24*time.Hour), now)
assert.NoError(t, err)
assert.NotNil(t, metric)
assert.Equal(t, "M-015", metric.MetricID)
assert.Equal(t, "direct_supplier_call_by_consumer_events", metric.MetricName)
assert.Equal(t, float64(1), metric.Value) // 有1个直连事件
assert.Equal(t, "FAIL", metric.Status) // 直连事件数 > 0应该是FAIL
}
func TestAuditMetrics_M016_QueryKeyRejectRate(t *testing.T) {
// M-016: query_key_external_reject_rate_pct = 100%
// 分母所有query key请求不含被拒绝的无效请求
ctx := context.Background()
svc := NewAuditService(NewInMemoryAuditStore())
metricsSvc := NewMetricsService(svc)
// 创建query key事件
events := []*model.AuditEvent{
// 被拒绝的query key请求
{
EventName: "AUTH-QUERY-REJECT",
EventCategory: "AUTH",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "query_key",
ObjectID: 12345,
Action: "query",
CredentialType: "query_key",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: false,
ResultCode: "QUERY_KEY_NOT_ALLOWED",
},
{
EventName: "AUTH-QUERY-REJECT",
EventCategory: "AUTH",
OperatorID: 1002,
TenantID: 2001,
ObjectType: "query_key",
ObjectID: 12346,
Action: "query",
CredentialType: "query_key",
SourceType: "api",
SourceIP: "192.168.1.2",
Success: false,
ResultCode: "QUERY_KEY_EXPIRED",
},
// query key请求
{
EventName: "AUTH-QUERY-KEY",
EventCategory: "AUTH",
OperatorID: 1003,
TenantID: 2001,
ObjectType: "query_key",
ObjectID: 12347,
Action: "query",
CredentialType: "query_key",
SourceType: "api",
SourceIP: "192.168.1.3",
Success: false,
ResultCode: "QUERY_KEY_EXPIRED",
},
// 非query key事件
{
EventName: "AUTH-TOKEN-OK",
EventCategory: "AUTH",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "token",
ObjectID: 12345,
Action: "verify",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: true,
ResultCode: "AUTH_TOKEN_OK",
},
}
for _, e := range events {
svc.CreateEvent(ctx, e)
}
// 计算M-016指标
now := time.Now()
metric, err := metricsSvc.CalculateM016(ctx, now.Add(-24*time.Hour), now)
assert.NoError(t, err)
assert.NotNil(t, metric)
assert.Equal(t, "M-016", metric.MetricID)
assert.Equal(t, "query_key_external_reject_rate_pct", metric.MetricName)
// 2个拒绝 / 3个query key总请求 = 66.67%
assert.InDelta(t, 66.67, metric.Value, 0.01)
assert.Equal(t, "FAIL", metric.Status) // 拒绝率 < 100%应该是FAIL
}
func TestAuditMetrics_M016_DifferentFromM014(t *testing.T) {
// M-014与M-016边界清晰分母不同无重叠
// M-014 分母经平台凭证校验的入站请求platform_token
// M-016 分母检测到的所有query key请求
ctx := context.Background()
svc := NewAuditService(NewInMemoryAuditStore())
metricsSvc := NewMetricsService(svc)
// 场景100个请求80个使用platform_token20个使用query key被拒绝
// M-014 = 80/80 = 100%分母只计算platform_token请求
// M-016 = 20/20 = 100%分母计算所有query key请求
// 创建80个platform_token请求
for i := 0; i < 80; i++ {
svc.CreateEvent(ctx, &model.AuditEvent{
EventName: "CRED-INGRESS-PLATFORM",
EventCategory: "CRED",
EventSubCategory: "INGRESS",
OperatorID: int64(1000 + i),
TenantID: 2001,
ObjectType: "account",
ObjectID: int64(i),
Action: "query",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: true,
ResultCode: "CRED_INGRESS_OK",
})
}
// 创建20个query key请求全部被拒绝
for i := 0; i < 20; i++ {
svc.CreateEvent(ctx, &model.AuditEvent{
EventName: "AUTH-QUERY-REJECT",
EventCategory: "AUTH",
OperatorID: int64(2000 + i),
TenantID: 2001,
ObjectType: "query_key",
ObjectID: int64(1000 + i),
Action: "query",
CredentialType: "query_key",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: false,
ResultCode: "QUERY_KEY_NOT_ALLOWED",
})
}
now := time.Now()
// 计算M-014
m014, err := metricsSvc.CalculateM014(ctx, now.Add(-24*time.Hour), now)
assert.NoError(t, err)
assert.Equal(t, 100.0, m014.Value) // 80/80 = 100%
// 计算M-016
m016, err := metricsSvc.CalculateM016(ctx, now.Add(-24*time.Hour), now)
assert.NoError(t, err)
assert.Equal(t, 100.0, m016.Value) // 20/20 = 100%
}
func TestAuditMetrics_M013_ZeroExposure(t *testing.T) {
// M-013: 当没有凭证暴露事件时应该为0状态PASS
ctx := context.Background()
svc := NewAuditService(NewInMemoryAuditStore())
metricsSvc := NewMetricsService(svc)
// 创建一些正常事件没有CRED-EXPOSE
svc.CreateEvent(ctx, &model.AuditEvent{
EventName: "AUTH-TOKEN-OK",
EventCategory: "AUTH",
OperatorID: 1001,
TenantID: 2001,
ObjectType: "token",
ObjectID: 12345,
Action: "verify",
CredentialType: "platform_token",
SourceType: "api",
SourceIP: "192.168.1.1",
Success: true,
ResultCode: "AUTH_TOKEN_OK",
})
now := time.Now()
metric, err := metricsSvc.CalculateM013(ctx, now.Add(-24*time.Hour), now)
assert.NoError(t, err)
assert.Equal(t, float64(0), metric.Value)
assert.Equal(t, "PASS", metric.Status)
}