## 设计文档 - 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规范
312 lines
7.5 KiB
Go
312 lines
7.5 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"lijiaoqiao/supply-api/internal/audit/model"
|
||
)
|
||
|
||
// Metric 指标结构
|
||
type Metric struct {
|
||
MetricID string `json:"metric_id"`
|
||
MetricName string `json:"metric_name"`
|
||
Period *MetricPeriod `json:"period"`
|
||
Value float64 `json:"value"`
|
||
Unit string `json:"unit"`
|
||
Status string `json:"status"` // PASS/FAIL
|
||
Details map[string]interface{} `json:"details"`
|
||
}
|
||
|
||
// MetricPeriod 指标周期
|
||
type MetricPeriod struct {
|
||
Start time.Time `json:"start"`
|
||
End time.Time `json:"end"`
|
||
}
|
||
|
||
// MetricsService 指标服务
|
||
type MetricsService struct {
|
||
auditSvc *AuditService
|
||
}
|
||
|
||
// NewMetricsService 创建指标服务
|
||
func NewMetricsService(auditSvc *AuditService) *MetricsService {
|
||
return &MetricsService{
|
||
auditSvc: auditSvc,
|
||
}
|
||
}
|
||
|
||
// CalculateM013 计算M-013指标:凭证泄露事件数 = 0
|
||
func (s *MetricsService) CalculateM013(ctx context.Context, start, end time.Time) (*Metric, error) {
|
||
filter := &EventFilter{
|
||
StartTime: start,
|
||
EndTime: end,
|
||
Limit: 10000,
|
||
}
|
||
|
||
events, _, err := s.auditSvc.ListEventsWithFilter(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 统计CRED-EXPOSE事件数
|
||
exposureCount := 0
|
||
unresolvedCount := 0
|
||
for _, e := range events {
|
||
if model.IsM013Event(e.EventName) {
|
||
exposureCount++
|
||
// 检查是否已解决(通过扩展字段或标记判断)
|
||
if s.isEventUnresolved(e) {
|
||
unresolvedCount++
|
||
}
|
||
}
|
||
}
|
||
|
||
metric := &Metric{
|
||
MetricID: "M-013",
|
||
MetricName: "supplier_credential_exposure_events",
|
||
Period: &MetricPeriod{
|
||
Start: start,
|
||
End: end,
|
||
},
|
||
Value: float64(exposureCount),
|
||
Unit: "count",
|
||
Status: "PASS",
|
||
Details: map[string]interface{}{
|
||
"total_exposure_events": exposureCount,
|
||
"unresolved_events": unresolvedCount,
|
||
},
|
||
}
|
||
|
||
// 判断状态:M-013要求暴露事件数为0
|
||
if exposureCount > 0 {
|
||
metric.Status = "FAIL"
|
||
}
|
||
|
||
return metric, nil
|
||
}
|
||
|
||
// CalculateM014 计算M-014指标:平台凭证入站覆盖率 = 100%
|
||
// 分母定义:经平台凭证校验的入站请求(credential_type = 'platform_token'),不含被拒绝的无效请求
|
||
func (s *MetricsService) CalculateM014(ctx context.Context, start, end time.Time) (*Metric, error) {
|
||
filter := &EventFilter{
|
||
StartTime: start,
|
||
EndTime: end,
|
||
Limit: 10000,
|
||
}
|
||
|
||
events, _, err := s.auditSvc.ListEventsWithFilter(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 统计CRED-INGRESS-PLATFORM事件(只有这个才算入M-014)
|
||
var platformCount, totalIngressCount int
|
||
for _, e := range events {
|
||
// M-014只统计CRED-INGRESS-PLATFORM事件
|
||
if e.EventName == "CRED-INGRESS-PLATFORM" {
|
||
totalIngressCount++
|
||
// M-014分母:platform_token请求
|
||
if e.CredentialType == model.CredentialTypePlatformToken {
|
||
platformCount++
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算覆盖率
|
||
var coveragePct float64
|
||
if totalIngressCount > 0 {
|
||
coveragePct = float64(platformCount) / float64(totalIngressCount) * 100
|
||
} else {
|
||
coveragePct = 100.0 // 没有入站请求时,默认为100%
|
||
}
|
||
|
||
metric := &Metric{
|
||
MetricID: "M-014",
|
||
MetricName: "platform_credential_ingress_coverage_pct",
|
||
Period: &MetricPeriod{
|
||
Start: start,
|
||
End: end,
|
||
},
|
||
Value: coveragePct,
|
||
Unit: "percentage",
|
||
Status: "PASS",
|
||
Details: map[string]interface{}{
|
||
"platform_token_requests": platformCount,
|
||
"total_requests": totalIngressCount,
|
||
"non_compliant_requests": totalIngressCount - platformCount,
|
||
},
|
||
}
|
||
|
||
// 判断状态:M-014要求覆盖率为100%
|
||
if coveragePct < 100.0 {
|
||
metric.Status = "FAIL"
|
||
}
|
||
|
||
return metric, nil
|
||
}
|
||
|
||
// CalculateM015 计算M-015指标:直连绕过事件数 = 0
|
||
func (s *MetricsService) CalculateM015(ctx context.Context, start, end time.Time) (*Metric, error) {
|
||
filter := &EventFilter{
|
||
StartTime: start,
|
||
EndTime: end,
|
||
Limit: 10000,
|
||
}
|
||
|
||
events, _, err := s.auditSvc.ListEventsWithFilter(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 统计CRED-DIRECT事件数
|
||
directCallCount := 0
|
||
blockedCount := 0
|
||
for _, e := range events {
|
||
if model.IsM015Event(e.EventName) {
|
||
directCallCount++
|
||
// 检查是否被阻断
|
||
if s.isEventBlocked(e) {
|
||
blockedCount++
|
||
}
|
||
}
|
||
}
|
||
|
||
metric := &Metric{
|
||
MetricID: "M-015",
|
||
MetricName: "direct_supplier_call_by_consumer_events",
|
||
Period: &MetricPeriod{
|
||
Start: start,
|
||
End: end,
|
||
},
|
||
Value: float64(directCallCount),
|
||
Unit: "count",
|
||
Status: "PASS",
|
||
Details: map[string]interface{}{
|
||
"total_direct_call_events": directCallCount,
|
||
"blocked_events": blockedCount,
|
||
},
|
||
}
|
||
|
||
// 判断状态:M-015要求直连事件数为0
|
||
if directCallCount > 0 {
|
||
metric.Status = "FAIL"
|
||
}
|
||
|
||
return metric, nil
|
||
}
|
||
|
||
// CalculateM016 计算M-016指标:query key外部拒绝率 = 100%
|
||
// 分母定义:检测到的所有query key请求,含被拒绝的请求
|
||
func (s *MetricsService) CalculateM016(ctx context.Context, start, end time.Time) (*Metric, error) {
|
||
filter := &EventFilter{
|
||
StartTime: start,
|
||
EndTime: end,
|
||
Limit: 10000,
|
||
}
|
||
|
||
events, _, err := s.auditSvc.ListEventsWithFilter(ctx, filter)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 统计AUTH-QUERY-*事件
|
||
var totalQueryKey, rejectedCount int
|
||
rejectBreakdown := make(map[string]int)
|
||
for _, e := range events {
|
||
if model.IsM016Event(e.EventName) {
|
||
totalQueryKey++
|
||
if e.EventName == "AUTH-QUERY-REJECT" {
|
||
rejectedCount++
|
||
rejectBreakdown[e.ResultCode]++
|
||
}
|
||
}
|
||
}
|
||
|
||
// 计算拒绝率
|
||
var rejectRate float64
|
||
if totalQueryKey > 0 {
|
||
rejectRate = float64(rejectedCount) / float64(totalQueryKey) * 100
|
||
} else {
|
||
rejectRate = 100.0 // 没有query key请求时,默认为100%
|
||
}
|
||
|
||
metric := &Metric{
|
||
MetricID: "M-016",
|
||
MetricName: "query_key_external_reject_rate_pct",
|
||
Period: &MetricPeriod{
|
||
Start: start,
|
||
End: end,
|
||
},
|
||
Value: rejectRate,
|
||
Unit: "percentage",
|
||
Status: "PASS",
|
||
Details: map[string]interface{}{
|
||
"rejected_requests": rejectedCount,
|
||
"total_external_query_key_requests": totalQueryKey,
|
||
"reject_breakdown": rejectBreakdown,
|
||
},
|
||
}
|
||
|
||
// 判断状态:M-016要求拒绝率为100%(所有外部query key请求都被拒绝)
|
||
if rejectRate < 100.0 {
|
||
metric.Status = "FAIL"
|
||
}
|
||
|
||
return metric, nil
|
||
}
|
||
|
||
// isEventUnresolved 检查事件是否未解决
|
||
func (s *MetricsService) isEventUnresolved(e *model.AuditEvent) bool {
|
||
// 如果事件成功,表示已处理/已解决
|
||
// 如果事件失败,表示有问题/未解决
|
||
return !e.Success
|
||
}
|
||
|
||
// isEventBlocked 检查直连事件是否被阻断
|
||
func (s *MetricsService) isEventBlocked(e *model.AuditEvent) bool {
|
||
// 通过检查扩展字段或Success标志来判断是否被阻断
|
||
if e.Success {
|
||
return false // 成功表示未被阻断
|
||
}
|
||
|
||
// 检查扩展字段中的blocked标记
|
||
if e.Extensions != nil {
|
||
if blocked, ok := e.Extensions["blocked"].(bool); ok {
|
||
return blocked
|
||
}
|
||
}
|
||
|
||
// 通过结果码判断
|
||
switch e.ResultCode {
|
||
case "SEC_DIRECT_BYPASS", "SEC_DIRECT_BYPASS_BLOCKED":
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// GetAllMetrics 获取所有M-013~M-016指标
|
||
func (s *MetricsService) GetAllMetrics(ctx context.Context, start, end time.Time) ([]*Metric, error) {
|
||
m013, err := s.CalculateM013(ctx, start, end)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
m014, err := s.CalculateM014(ctx, start, end)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
m015, err := s.CalculateM015(ctx, start, end)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
m016, err := s.CalculateM016(ctx, start, end)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return []*Metric{m013, m014, m015, m016}, nil
|
||
} |