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:
183
gateway/internal/compliance/rules/auth_query_test.go
Normal file
183
gateway/internal/compliance/rules/auth_query_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
177
gateway/internal/compliance/rules/cred_direct_test.go
Normal file
177
gateway/internal/compliance/rules/cred_direct_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
233
gateway/internal/compliance/rules/cred_expose_test.go
Normal file
233
gateway/internal/compliance/rules/cred_expose_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
231
gateway/internal/compliance/rules/cred_ingress_test.go
Normal file
231
gateway/internal/compliance/rules/cred_ingress_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
137
gateway/internal/compliance/rules/engine.go
Normal file
137
gateway/internal/compliance/rules/engine.go
Normal 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
|
||||
}
|
||||
139
gateway/internal/compliance/rules/loader.go
Normal file
139
gateway/internal/compliance/rules/loader.go
Normal 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)
|
||||
}
|
||||
164
gateway/internal/compliance/rules/loader_test.go
Normal file
164
gateway/internal/compliance/rules/loader_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user