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,183 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestAuthQueryKey 测试query key请求检测
func TestAuthQueryKey(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "AUTH-QUERY-KEY",
Name: "Query Key请求检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(key=|api_key=|token=|bearer=|authorization=)",
Target: "query_string",
Scope: "all",
},
},
Action: Action{
Primary: "reject",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "包含key参数",
input: "?key=sk-1234567890abcdefghijklmnopqrstuvwxyz",
shouldMatch: true,
},
{
name: "包含api_key参数",
input: "?api_key=sk-1234567890abcdefghijklmnopqrstuvwxyz",
shouldMatch: true,
},
{
name: "包含token参数",
input: "?token=bearer_1234567890abcdefghijklmnop",
shouldMatch: true,
},
{
name: "不包含认证参数",
input: "?query=hello&limit=10",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestAuthQueryInject 测试query key注入检测
func TestAuthQueryInject(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "AUTH-QUERY-INJECT",
Name: "Query Key注入检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(key=|api_key=|token=|bearer=|authorization=).*[a-zA-Z0-9]{20,}",
Target: "query_string",
Scope: "all",
},
},
Action: Action{
Primary: "reject",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "包含注入的key",
input: "?key=sk-1234567890abcdefghijklmnopqrstuvwxyz",
shouldMatch: true,
},
{
name: "包含空key值",
input: "?key=",
shouldMatch: false,
},
{
name: "包含短key值",
input: "?key=short",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestAuthQueryAudit 测试query key审计检测
func TestAuthQueryAudit(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "AUTH-QUERY-AUDIT",
Name: "Query Key审计检测",
Severity: "P1",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(query_key|qkey|query_token)",
Target: "internal_context",
Scope: "all",
},
},
Action: Action{
Primary: "alert",
Secondary: "log",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "包含query_key标记",
input: "internal: query_key=abc123",
shouldMatch: true,
},
{
name: "不包含query_key标记",
input: "internal: platform_token=xyz789",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestAuthQueryRuleIDFormat 测试规则ID格式
func TestAuthQueryRuleIDFormat(t *testing.T) {
loader := NewRuleLoader()
validIDs := []string{
"AUTH-QUERY-KEY",
"AUTH-QUERY-INJECT",
"AUTH-QUERY-AUDIT",
}
for _, id := range validIDs {
t.Run(id, func(t *testing.T) {
assert.True(t, loader.ValidateRuleID(id), "Rule ID %s should be valid", id)
})
}
}

View File

@@ -0,0 +1,177 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestCredDirectSupplier 测试直连供应商检测
func TestCredDirectSupplier(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-DIRECT-SUPPLIER",
Name: "直连供应商检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(api\\.openai\\.com|api\\.anthropic\\.com|api\\.minimax\\.chat)",
Target: "request_host",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "直连OpenAI API",
input: "api.openai.com",
shouldMatch: true,
},
{
name: "直连Anthropic API",
input: "api.anthropic.com",
shouldMatch: true,
},
{
name: "通过平台代理",
input: "gateway.platform.com",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredDirectAPI 测试直连API端点检测
func TestCredDirectAPI(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-DIRECT-API",
Name: "直连API端点检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "^/v1/(chat/completions|completions|embeddings)$",
Target: "request_path",
Scope: "all",
},
},
Action: Action{
Primary: "block",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "直接访问chat completions",
input: "/v1/chat/completions",
shouldMatch: true,
},
{
name: "直接访问completions",
input: "/v1/completions",
shouldMatch: true,
},
{
name: "平台代理路径",
input: "/api/platform/v1/chat/completions",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredDirectUnauth 测试未授权直连检测
func TestCredDirectUnauth(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-DIRECT-UNAUTH",
Name: "未授权直连检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(direct_ip| bypass_proxy| no_platform_auth)",
Target: "connection_metadata",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "检测到直连标记",
input: "direct_ip: 203.0.113.50, bypass_proxy: true",
shouldMatch: true,
},
{
name: "正常代理请求",
input: "via: platform_proxy, auth: platform_token",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredDirectRuleIDFormat 测试规则ID格式
func TestCredDirectRuleIDFormat(t *testing.T) {
loader := NewRuleLoader()
validIDs := []string{
"CRED-DIRECT-SUPPLIER",
"CRED-DIRECT-API",
"CRED-DIRECT-UNAUTH",
}
for _, id := range validIDs {
t.Run(id, func(t *testing.T) {
assert.True(t, loader.ValidateRuleID(id), "Rule ID %s should be valid", id)
})
}
}

View File

@@ -0,0 +1,233 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestCredExposeResponse 测试响应体凭证泄露检测
func TestCredExposeResponse(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
// 创建CRED-EXPOSE-RESPONSE规则
rule := Rule{
ID: "CRED-EXPOSE-RESPONSE",
Name: "响应体凭证泄露检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(sk-|ak-|api_key|secret|token).*[a-zA-Z0-9]{20,}",
Target: "response_body",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "包含sk-凭证",
input: `{"api_key": "sk-1234567890abcdefghijklmnopqrstuvwxyz"}`,
shouldMatch: true,
},
{
name: "包含ak-凭证",
input: `{"access_key": "ak-1234567890abcdefghijklmnopqrstuvwxyz"}`,
shouldMatch: true,
},
{
name: "包含api_key",
input: `{"result": "api_key_1234567890abcdefghijklmnopqr"}`,
shouldMatch: true,
},
{
name: "不包含凭证的正常响应",
input: `{"status": "success", "data": "hello world"}`,
shouldMatch: false,
},
{
name: "短token不匹配",
input: `{"token": "sk-short"}`,
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredExposeLog 测试日志凭证泄露检测
func TestCredExposeLog(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-EXPOSE-LOG",
Name: "日志凭证泄露检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(sk-|ak-|api_key|secret|token).*[a-zA-Z0-9]{20,}",
Target: "log",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "日志包含凭证",
input: "[INFO] Using API key: sk-1234567890abcdefghijklmnopqrstuvwxyz",
shouldMatch: true,
},
{
name: "日志不包含凭证",
input: "[INFO] Processing request from 192.168.1.1",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredExposeExport 测试导出凭证泄露检测
func TestCredExposeExport(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-EXPOSE-EXPORT",
Name: "导出凭证泄露检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(sk-|ak-|api_key|secret|token).*[a-zA-Z0-9]{20,}",
Target: "export",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "导出CSV包含凭证",
input: "api_key,secret\nsk-1234567890abcdefghijklmnopqrstuvwxyz,mysupersecret",
shouldMatch: true,
},
{
name: "导出CSV不包含凭证",
input: "id,name\n1,John Doe",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredExposeWebhook 测试Webhook凭证泄露检测
func TestCredExposeWebhook(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-EXPOSE-WEBHOOK",
Name: "Webhook凭证泄露检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(sk-|ak-|api_key|secret|token).*[a-zA-Z0-9]{20,}",
Target: "webhook",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "Webhook请求包含凭证",
input: `{"url": "https://example.com/callback", "token": "sk-1234567890abcdefghijklmnopqrstuvwxyz"}`,
shouldMatch: true,
},
{
name: "Webhook请求不包含凭证",
input: `{"url": "https://example.com/callback", "status": "ok"}`,
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredExposeRuleIDFormat 测试规则ID格式
func TestCredExposeRuleIDFormat(t *testing.T) {
loader := NewRuleLoader()
validIDs := []string{
"CRED-EXPOSE-RESPONSE",
"CRED-EXPOSE-LOG",
"CRED-EXPOSE-EXPORT",
"CRED-EXPOSE-WEBHOOK",
}
for _, id := range validIDs {
t.Run(id, func(t *testing.T) {
assert.True(t, loader.ValidateRuleID(id), "Rule ID %s should be valid", id)
})
}
}

View File

@@ -0,0 +1,231 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestCredIngressPlatform 测试平台凭证入站检测
func TestCredIngressPlatform(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-INGRESS-PLATFORM",
Name: "平台凭证入站检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "Authorization:\\s*Bearer\\s*ptk_[A-Za-z0-9]{20,}",
Target: "request_header",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "包含有效平台凭证",
input: "Authorization: Bearer ptk_1234567890abcdefghijklmnopqrst",
shouldMatch: true,
},
{
name: "不包含Authorization头",
input: "Content-Type: application/json",
shouldMatch: false,
},
{
name: "包含无效凭证格式",
input: "Authorization: Bearer invalid",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredIngressSupplier 测试供应商凭证入站检测
func TestCredIngressSupplier(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-INGRESS-SUPPLIER",
Name: "供应商凭证入站检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "(sk-|ak-|api_key).*[a-zA-Z0-9]{20,}",
Target: "request_header",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "请求头包含供应商凭证",
input: "X-API-Key: sk-1234567890abcdefghijklmnopqrstuvwxyz",
shouldMatch: true,
},
{
name: "请求头不包含供应商凭证",
input: "X-Request-ID: abc123",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredIngressFormat 测试凭证格式验证
func TestCredIngressFormat(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-INGRESS-FORMAT",
Name: "凭证格式验证",
Severity: "P1",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "^ptk_[A-Za-z0-9]{32,}$",
Target: "credential_format",
Scope: "all",
},
},
Action: Action{
Primary: "block",
Secondary: "alert",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "有效平台凭证格式",
input: "ptk_1234567890abcdefghijklmnopqrstuvwx",
shouldMatch: true,
},
{
name: "无效格式-缺少ptk_前缀",
input: "1234567890abcdefghijklmnopqrstuvwx",
shouldMatch: false,
},
{
name: "无效格式-太短",
input: "ptk_short",
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredIngressExpired 测试凭证过期检测
func TestCredIngressExpired(t *testing.T) {
loader := NewRuleLoader()
engine := NewRuleEngine(loader)
rule := Rule{
ID: "CRED-INGRESS-EXPIRED",
Name: "凭证过期检测",
Severity: "P0",
Matchers: []Matcher{
{
Type: "regex_match",
Pattern: "token_expired|token_invalid|TOKEN_EXPIRED|CredentialExpired",
Target: "error_response",
Scope: "all",
},
},
Action: Action{
Primary: "block",
},
}
testCases := []struct {
name string
input string
shouldMatch bool
}{
{
name: "包含token过期错误",
input: `{"error": "token_expired", "message": "Your token has expired"}`,
shouldMatch: true,
},
{
name: "包含CredentialExpired错误",
input: `{"error": "CredentialExpired", "message": "Credential has been revoked"}`,
shouldMatch: true,
},
{
name: "正常响应",
input: `{"status": "success", "data": "valid"}`,
shouldMatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
matchResult := engine.Match(rule, tc.input)
assert.Equal(t, tc.shouldMatch, matchResult.Matched, "Test case: %s", tc.name)
})
}
}
// TestCredIngressRuleIDFormat 测试规则ID格式
func TestCredIngressRuleIDFormat(t *testing.T) {
loader := NewRuleLoader()
validIDs := []string{
"CRED-INGRESS-PLATFORM",
"CRED-INGRESS-SUPPLIER",
"CRED-INGRESS-FORMAT",
"CRED-INGRESS-EXPIRED",
}
for _, id := range validIDs {
t.Run(id, func(t *testing.T) {
assert.True(t, loader.ValidateRuleID(id), "Rule ID %s should be valid", id)
})
}
}

View File

@@ -0,0 +1,137 @@
package rules
import (
"regexp"
)
// MatchResult 匹配结果
type MatchResult struct {
Matched bool
RuleID string
Matchers []MatcherResult
}
// MatcherResult 单个匹配器的结果
type MatcherResult struct {
MatcherIndex int
MatcherType string
Pattern string
MatchValue string
IsMatch bool
}
// RuleEngine 规则引擎
type RuleEngine struct {
loader *RuleLoader
compiledPatterns map[string][]*regexp.Regexp
}
// NewRuleEngine 创建新的规则引擎
func NewRuleEngine(loader *RuleLoader) *RuleEngine {
return &RuleEngine{
loader: loader,
compiledPatterns: make(map[string][]*regexp.Regexp),
}
}
// Match 执行规则匹配
func (e *RuleEngine) Match(rule Rule, content string) MatchResult {
result := MatchResult{
Matched: false,
RuleID: rule.ID,
Matchers: make([]MatcherResult, len(rule.Matchers)),
}
for i, matcher := range rule.Matchers {
matcherResult := MatcherResult{
MatcherIndex: i,
MatcherType: matcher.Type,
Pattern: matcher.Pattern,
IsMatch: false,
}
switch matcher.Type {
case "regex_match":
matcherResult.IsMatch = e.matchRegex(matcher.Pattern, content)
if matcherResult.IsMatch {
matcherResult.MatchValue = e.extractMatch(matcher.Pattern, content)
}
default:
// 未知匹配器类型,默认不匹配
}
result.Matchers[i] = matcherResult
if matcherResult.IsMatch {
result.Matched = true
}
}
return result
}
// matchRegex 执行正则表达式匹配
func (e *RuleEngine) matchRegex(pattern string, content string) bool {
// 编译并缓存正则表达式
regex, ok := e.compiledPatterns[pattern]
if !ok {
var err error
regex = make([]*regexp.Regexp, 1)
regex[0], err = regexp.Compile(pattern)
if err != nil {
return false
}
e.compiledPatterns[pattern] = regex
}
return regex[0].MatchString(content)
}
// extractMatch 提取匹配值
func (e *RuleEngine) extractMatch(pattern string, content string) string {
regex, ok := e.compiledPatterns[pattern]
if !ok {
regex = make([]*regexp.Regexp, 1)
regex[0], _ = regexp.Compile(pattern)
e.compiledPatterns[pattern] = regex
}
matches := regex[0].FindString(content)
return matches
}
// MatchFromConfig 从规则配置执行匹配
func (e *RuleEngine) MatchFromConfig(ruleID string, ruleConfig Rule, content string) (bool, error) {
// 验证规则
if err := e.validateRuleForMatch(ruleConfig); err != nil {
return false, err
}
result := e.Match(ruleConfig, content)
return result.Matched, nil
}
// validateRuleForMatch 验证规则是否可用于匹配
func (e *RuleEngine) validateRuleForMatch(rule Rule) error {
if rule.ID == "" {
return ErrInvalidRule
}
if len(rule.Matchers) == 0 {
return ErrNoMatchers
}
return nil
}
// Custom errors
var (
ErrInvalidRule = &RuleEngineError{"invalid rule: missing required fields"}
ErrNoMatchers = &RuleEngineError{"invalid rule: no matchers defined"}
)
// RuleEngineError 规则引擎错误
type RuleEngineError struct {
Message string
}
func (e *RuleEngineError) Error() string {
return e.Message
}

View File

@@ -0,0 +1,139 @@
package rules
import (
"fmt"
"os"
"regexp"
"gopkg.in/yaml.v3"
)
// Rule 定义合规规则结构
type Rule struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Severity string `yaml:"severity"`
Matchers []Matcher `yaml:"matchers"`
Action Action `yaml:"action"`
Audit Audit `yaml:"audit"`
}
// Matcher 定义规则匹配器
type Matcher struct {
Type string `yaml:"type"`
Pattern string `yaml:"pattern"`
Target string `yaml:"target"`
Scope string `yaml:"scope"`
}
// Action 定义规则动作
type Action struct {
Primary string `yaml:"primary"`
Secondary string `yaml:"secondary"`
}
// Audit 定义审计配置
type Audit struct {
EventName string `yaml:"event_name"`
EventCategory string `yaml:"event_category"`
EventSubCategory string `yaml:"event_sub_category"`
}
// RulesConfig YAML规则配置结构
type RulesConfig struct {
Rules []Rule `yaml:"rules"`
}
// RuleLoader 规则加载器
type RuleLoader struct {
ruleIDPattern *regexp.Regexp
}
// NewRuleLoader 创建新的规则加载器
func NewRuleLoader() *RuleLoader {
// 规则ID格式: {Category}-{SubCategory}[-{Detail}]
// Category: 大写字母, 2-4字符
// SubCategory: 大写字母, 2-10字符
// Detail: 可选, 大写字母+数字+连字符, 1-20字符
pattern := regexp.MustCompile(`^[A-Z]{2,4}-[A-Z]{2,10}(-[A-Z0-9-]{1,20})?$`)
return &RuleLoader{
ruleIDPattern: pattern,
}
}
// LoadFromFile 从YAML文件加载规则
func (l *RuleLoader) LoadFromFile(filePath string) ([]Rule, error) {
// 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil, fmt.Errorf("file not found: %s", filePath)
}
// 读取文件内容
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
// 解析YAML
var config RulesConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
// 验证规则
for _, rule := range config.Rules {
if err := l.validateRule(rule); err != nil {
return nil, err
}
}
return config.Rules, nil
}
// validateRule 验证规则完整性
func (l *RuleLoader) validateRule(rule Rule) error {
// 检查必需字段
if rule.ID == "" {
return fmt.Errorf("missing required field: id")
}
if rule.Name == "" {
return fmt.Errorf("missing required field: name for rule %s", rule.ID)
}
if rule.Severity == "" {
return fmt.Errorf("missing required field: severity for rule %s", rule.ID)
}
if len(rule.Matchers) == 0 {
return fmt.Errorf("missing required field: matchers for rule %s", rule.ID)
}
if rule.Action.Primary == "" {
return fmt.Errorf("missing required field: action.primary for rule %s", rule.ID)
}
// 验证规则ID格式
if !l.ValidateRuleID(rule.ID) {
return fmt.Errorf("invalid rule ID format: %s (expected format: {Category}-{SubCategory}[-{Detail}])", rule.ID)
}
// 验证每个匹配器
for i, matcher := range rule.Matchers {
if matcher.Type == "" {
return fmt.Errorf("missing required field: matchers[%d].type for rule %s", i, rule.ID)
}
if matcher.Pattern == "" {
return fmt.Errorf("missing required field: matchers[%d].pattern for rule %s", i, rule.ID)
}
// 验证正则表达式是否有效
if _, err := regexp.Compile(matcher.Pattern); err != nil {
return fmt.Errorf("invalid regex pattern in matchers[%d] for rule %s: %w", i, rule.ID, err)
}
}
return nil
}
// ValidateRuleID 验证规则ID格式
func (l *RuleLoader) ValidateRuleID(ruleID string) bool {
return l.ruleIDPattern.MatchString(ruleID)
}

View File

@@ -0,0 +1,164 @@
package rules
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestRuleLoader_ValidYaml 测试加载有效YAML
func TestRuleLoader_ValidYaml(t *testing.T) {
// 创建临时有效YAML文件
tmpfile, err := os.CreateTemp("", "valid_rule_*.yaml")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
validYAML := `
rules:
- id: "CRED-EXPOSE-RESPONSE"
name: "响应体凭证泄露检测"
description: "检测 API 响应中是否包含可复用的供应商凭证片段"
severity: "P0"
matchers:
- type: "regex_match"
pattern: "(sk-|ak-|api_key|secret|token).*[a-zA-Z0-9]{20,}"
target: "response_body"
scope: "all"
action:
primary: "block"
secondary: "alert"
audit:
event_name: "CRED-EXPOSE-RESPONSE"
event_category: "CRED"
event_sub_category: "EXPOSE"
`
_, err = tmpfile.WriteString(validYAML)
require.NoError(t, err)
tmpfile.Close()
// 测试加载
loader := NewRuleLoader()
rules, err := loader.LoadFromFile(tmpfile.Name())
assert.NoError(t, err)
assert.NotNil(t, rules)
assert.Len(t, rules, 1)
rule := rules[0]
assert.Equal(t, "CRED-EXPOSE-RESPONSE", rule.ID)
assert.Equal(t, "P0", rule.Severity)
assert.Equal(t, "block", rule.Action.Primary)
}
// TestRuleLoader_InvalidYaml 测试加载无效YAML
func TestRuleLoader_InvalidYaml(t *testing.T) {
// 创建临时无效YAML文件
tmpfile, err := os.CreateTemp("", "invalid_rule_*.yaml")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
invalidYAML := `
rules:
- id: "CRED-EXPOSE-RESPONSE"
name: "响应体凭证泄露检测"
severity: "P0"
# 缺少必需的matchers字段
action:
primary: "block"
`
_, err = tmpfile.WriteString(invalidYAML)
require.NoError(t, err)
tmpfile.Close()
// 测试加载
loader := NewRuleLoader()
rules, err := loader.LoadFromFile(tmpfile.Name())
assert.Error(t, err)
assert.Nil(t, rules)
}
// TestRuleLoader_MissingFields 测试缺少必需字段
func TestRuleLoader_MissingFields(t *testing.T) {
// 创建缺少必需字段的YAML
tmpfile, err := os.CreateTemp("", "missing_fields_*.yaml")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
// 缺少 id 字段
missingIDYAML := `
rules:
- name: "响应体凭证泄露检测"
severity: "P0"
matchers:
- type: "regex_match"
action:
primary: "block"
`
_, err = tmpfile.WriteString(missingIDYAML)
require.NoError(t, err)
tmpfile.Close()
loader := NewRuleLoader()
rules, err := loader.LoadFromFile(tmpfile.Name())
assert.Error(t, err)
assert.Nil(t, rules)
assert.Contains(t, err.Error(), "missing required field: id")
}
// TestRuleLoader_FileNotFound 测试文件不存在
func TestRuleLoader_FileNotFound(t *testing.T) {
loader := NewRuleLoader()
rules, err := loader.LoadFromFile("/nonexistent/path/rules.yaml")
assert.Error(t, err)
assert.Nil(t, rules)
}
// TestRuleLoader_ValidateRuleFormat 测试规则格式验证
func TestRuleLoader_ValidateRuleFormat(t *testing.T) {
tests := []struct {
name string
ruleID string
valid bool
}{
{"标准格式", "CRED-EXPOSE-RESPONSE", true},
{"带Detail格式", "CRED-EXPOSE-RESPONSE-DETAIL", true},
{"双连字符", "CRED--EXPOSE-RESPONSE", false},
{"小写字母", "cred-expose-response", false},
{"单字符Category", "C-EXPOSE-RESPONSE", false},
}
loader := NewRuleLoader()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
valid := loader.ValidateRuleID(tt.ruleID)
assert.Equal(t, tt.valid, valid)
})
}
}
// TestRuleLoader_EmptyRules 测试空规则列表
func TestRuleLoader_EmptyRules(t *testing.T) {
tmpfile, err := os.CreateTemp("", "empty_rules_*.yaml")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
emptyYAML := `
rules: []
`
_, err = tmpfile.WriteString(emptyYAML)
require.NoError(t, err)
tmpfile.Close()
loader := NewRuleLoader()
rules, err := loader.LoadFromFile(tmpfile.Name())
assert.NoError(t, err)
assert.NotNil(t, rules)
assert.Len(t, rules, 0)
}