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:
279
supply-api/internal/audit/sanitizer/sanitizer.go
Normal file
279
supply-api/internal/audit/sanitizer/sanitizer.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package sanitizer
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScanRule 扫描规则
|
||||
type ScanRule struct {
|
||||
ID string
|
||||
Pattern *regexp.Regexp
|
||||
Description string
|
||||
Severity string
|
||||
}
|
||||
|
||||
// Violation 违规项
|
||||
type Violation struct {
|
||||
Type string // 违规类型
|
||||
Pattern string // 匹配的正则模式
|
||||
Value string // 匹配的值(已脱敏)
|
||||
Description string
|
||||
}
|
||||
|
||||
// ScanResult 扫描结果
|
||||
type ScanResult struct {
|
||||
Violations []Violation
|
||||
Passed bool
|
||||
}
|
||||
|
||||
// NewScanResult 创建扫描结果
|
||||
func NewScanResult() *ScanResult {
|
||||
return &ScanResult{
|
||||
Violations: []Violation{},
|
||||
Passed: true,
|
||||
}
|
||||
}
|
||||
|
||||
// HasViolation 检查是否有违规
|
||||
func (r *ScanResult) HasViolation() bool {
|
||||
return len(r.Violations) > 0
|
||||
}
|
||||
|
||||
// AddViolation 添加违规项
|
||||
func (r *ScanResult) AddViolation(v Violation) {
|
||||
r.Violations = append(r.Violations, v)
|
||||
r.Passed = false
|
||||
}
|
||||
|
||||
// CredentialScanner 凭证扫描器
|
||||
type CredentialScanner struct {
|
||||
rules []ScanRule
|
||||
}
|
||||
|
||||
// NewCredentialScanner 创建凭证扫描器
|
||||
func NewCredentialScanner() *CredentialScanner {
|
||||
scanner := &CredentialScanner{
|
||||
rules: []ScanRule{
|
||||
{
|
||||
ID: "openai_key",
|
||||
Pattern: regexp.MustCompile(`sk-[a-zA-Z0-9]{20,}`),
|
||||
Description: "OpenAI API Key",
|
||||
Severity: "HIGH",
|
||||
},
|
||||
{
|
||||
ID: "api_key",
|
||||
Pattern: regexp.MustCompile(`(?i)(api[_-]?key|apikey)["\s:=]+['"]?([a-zA-Z0-9_\-]{16,})['"]?`),
|
||||
Description: "Generic API Key",
|
||||
Severity: "MEDIUM",
|
||||
},
|
||||
{
|
||||
ID: "aws_access_key",
|
||||
Pattern: regexp.MustCompile(`(?i)(access[_-]?key[_-]?id|aws[_-]?access[_-]?key)["\s:=]+['"]?(AKIA[0-9A-Z]{16})['"]?`),
|
||||
Description: "AWS Access Key ID",
|
||||
Severity: "HIGH",
|
||||
},
|
||||
{
|
||||
ID: "aws_secret_key",
|
||||
Pattern: regexp.MustCompile(`(?i)(secret[_-]?key|aws[_-]?.*secret[_-]?key)["\s:=]+['"]?([a-zA-Z0-9/+=]{40})['"]?`),
|
||||
Description: "AWS Secret Access Key",
|
||||
Severity: "HIGH",
|
||||
},
|
||||
{
|
||||
ID: "password",
|
||||
Pattern: regexp.MustCompile(`(?i)(password|passwd|pwd)["\s:=]+['"]?([a-zA-Z0-9@#$%^&*!]{8,})['"]?`),
|
||||
Description: "Password",
|
||||
Severity: "HIGH",
|
||||
},
|
||||
{
|
||||
ID: "bearer_token",
|
||||
Pattern: regexp.MustCompile(`(?i)(token|bearer|authorization)["\s:=]+['"]?([Bb]earer\s+)?([a-zA-Z0-9_\-\.]+)['"]?`),
|
||||
Description: "Bearer Token",
|
||||
Severity: "MEDIUM",
|
||||
},
|
||||
{
|
||||
ID: "private_key",
|
||||
Pattern: regexp.MustCompile(`-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----`),
|
||||
Description: "Private Key",
|
||||
Severity: "CRITICAL",
|
||||
},
|
||||
{
|
||||
ID: "secret",
|
||||
Pattern: regexp.MustCompile(`(?i)(secret|client[_-]?secret)["\s:=]+['"]?([a-zA-Z0-9_\-]{16,})['"]?`),
|
||||
Description: "Secret",
|
||||
Severity: "HIGH",
|
||||
},
|
||||
},
|
||||
}
|
||||
return scanner
|
||||
}
|
||||
|
||||
// Scan 扫描内容
|
||||
func (s *CredentialScanner) Scan(content string) *ScanResult {
|
||||
result := NewScanResult()
|
||||
|
||||
for _, rule := range s.rules {
|
||||
matches := rule.Pattern.FindAllStringSubmatch(content, -1)
|
||||
for _, match := range matches {
|
||||
// 构建违规项
|
||||
violation := Violation{
|
||||
Type: rule.ID,
|
||||
Pattern: rule.Pattern.String(),
|
||||
Description: rule.Description,
|
||||
}
|
||||
|
||||
// 提取匹配的值(取最后一个匹配组)
|
||||
if len(match) > 1 {
|
||||
violation.Value = maskString(match[len(match)-1])
|
||||
} else {
|
||||
violation.Value = maskString(match[0])
|
||||
}
|
||||
|
||||
result.AddViolation(violation)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetRules 获取扫描规则
|
||||
func (s *CredentialScanner) GetRules() []ScanRule {
|
||||
return s.rules
|
||||
}
|
||||
|
||||
// Sanitizer 脱敏器
|
||||
type Sanitizer struct {
|
||||
patterns []*regexp.Regexp
|
||||
}
|
||||
|
||||
// NewSanitizer 创建脱敏器
|
||||
func NewSanitizer() *Sanitizer {
|
||||
return &Sanitizer{
|
||||
patterns: []*regexp.Regexp{
|
||||
// OpenAI API Key
|
||||
regexp.MustCompile(`(sk-[a-zA-Z0-9]{4})[a-zA-Z0-9]+([a-zA-Z0-9]{4})`),
|
||||
// AWS Access Key
|
||||
regexp.MustCompile(`(AKIA[0-9A-Z]{4})[0-9A-Z]+([0-9A-Z]{4})`),
|
||||
// Generic API Key
|
||||
regexp.MustCompile(`([a-zA-Z0-9_\-]{4})[a-zA-Z0-9_\-]{8,}([a-zA-Z0-9_\-]{4})`),
|
||||
// Password
|
||||
regexp.MustCompile(`([a-zA-Z0-9@#$%^&*!]{4})[a-zA-Z0-9@#$%^&*!]+([a-zA-Z0-9@#$%^&*!]{4})`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Mask 对字符串进行脱敏
|
||||
func (s *Sanitizer) Mask(content string) string {
|
||||
result := content
|
||||
|
||||
for _, pattern := range s.patterns {
|
||||
// 替换为格式:前4字符 + **** + 后4字符
|
||||
result = pattern.ReplaceAllStringFunc(result, func(match string) string {
|
||||
// 尝试分组替换
|
||||
re := regexp.MustCompile(`^(.{4}).+(.{4})$`)
|
||||
submatch := re.FindStringSubmatch(match)
|
||||
if len(submatch) == 3 {
|
||||
return submatch[1] + "****" + submatch[2]
|
||||
}
|
||||
// 如果无法分组,直接掩码
|
||||
if len(match) > 8 {
|
||||
return match[:4] + "****" + match[len(match)-4:]
|
||||
}
|
||||
return "****"
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MaskMap 对map进行脱敏
|
||||
func (s *Sanitizer) MaskMap(data map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, value := range data {
|
||||
if IsSensitiveField(key) {
|
||||
if str, ok := value.(string); ok {
|
||||
result[key] = s.Mask(str)
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
} else {
|
||||
result[key] = s.maskValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MaskSlice 对slice进行脱敏
|
||||
func (s *Sanitizer) MaskSlice(data []string) []string {
|
||||
result := make([]string, len(data))
|
||||
for i, item := range data {
|
||||
result[i] = s.Mask(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// maskValue 递归掩码
|
||||
func (s *Sanitizer) maskValue(value interface{}) interface{} {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return s.Mask(v)
|
||||
case map[string]interface{}:
|
||||
return s.MaskMap(v)
|
||||
case []interface{}:
|
||||
result := make([]interface{}, len(v))
|
||||
for i, item := range v {
|
||||
result[i] = s.maskValue(item)
|
||||
}
|
||||
return result
|
||||
case []string:
|
||||
return s.MaskSlice(v)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
// maskString 掩码字符串
|
||||
func maskString(s string) string {
|
||||
if len(s) > 8 {
|
||||
return s[:4] + "****" + s[len(s)-4:]
|
||||
}
|
||||
return "****"
|
||||
}
|
||||
|
||||
// GetSensitiveFields 获取敏感字段列表
|
||||
func GetSensitiveFields() []string {
|
||||
return []string{
|
||||
"api_key",
|
||||
"apikey",
|
||||
"secret",
|
||||
"secret_key",
|
||||
"password",
|
||||
"passwd",
|
||||
"pwd",
|
||||
"token",
|
||||
"access_key",
|
||||
"access_key_id",
|
||||
"private_key",
|
||||
"session_id",
|
||||
"authorization",
|
||||
"bearer",
|
||||
"client_secret",
|
||||
"credentials",
|
||||
}
|
||||
}
|
||||
|
||||
// IsSensitiveField 判断字段名是否为敏感字段
|
||||
func IsSensitiveField(fieldName string) bool {
|
||||
lowerName := strings.ToLower(fieldName)
|
||||
sensitiveFields := GetSensitiveFields()
|
||||
|
||||
for _, sf := range sensitiveFields {
|
||||
if strings.Contains(lowerName, sf) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
290
supply-api/internal/audit/sanitizer/sanitizer_test.go
Normal file
290
supply-api/internal/audit/sanitizer/sanitizer_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package sanitizer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSanitizer_Scan_CredentialExposure(t *testing.T) {
|
||||
// 检测响应体中的凭证泄露
|
||||
scanner := NewCredentialScanner()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
content string
|
||||
expectFound bool
|
||||
expectedTypes []string
|
||||
}{
|
||||
{
|
||||
name: "OpenAI API Key",
|
||||
content: "Your API key is sk-1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
expectFound: true,
|
||||
expectedTypes: []string{"openai_key"},
|
||||
},
|
||||
{
|
||||
name: "AWS Access Key",
|
||||
content: "access_key_id: AKIAIOSFODNN7EXAMPLE",
|
||||
expectFound: true,
|
||||
expectedTypes: []string{"aws_access_key"},
|
||||
},
|
||||
{
|
||||
name: "Client Secret",
|
||||
content: "client_secret: c3VwZXJzZWNyZXRrZXlzZWNyZXRrZXk=",
|
||||
expectFound: true,
|
||||
expectedTypes: []string{"secret"},
|
||||
},
|
||||
{
|
||||
name: "Generic API Key",
|
||||
content: "api_key: key-1234567890abcdefghij",
|
||||
expectFound: true,
|
||||
expectedTypes: []string{"api_key"},
|
||||
},
|
||||
{
|
||||
name: "Password Field",
|
||||
content: "password: mysecretpassword123",
|
||||
expectFound: true,
|
||||
expectedTypes: []string{"password"},
|
||||
},
|
||||
{
|
||||
name: "Token Field",
|
||||
content: "token: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
||||
expectFound: true,
|
||||
expectedTypes: []string{"bearer_token"},
|
||||
},
|
||||
{
|
||||
name: "Normal Text",
|
||||
content: "This is normal text without credentials",
|
||||
expectFound: false,
|
||||
expectedTypes: nil,
|
||||
},
|
||||
{
|
||||
name: "Already Masked",
|
||||
content: "api_key: sk-****-****",
|
||||
expectFound: false,
|
||||
expectedTypes: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := scanner.Scan(tc.content)
|
||||
|
||||
if tc.expectFound {
|
||||
assert.True(t, result.HasViolation(), "Expected violation for: %s", tc.name)
|
||||
assert.NotEmpty(t, result.Violations, "Expected violations for: %s", tc.name)
|
||||
|
||||
var foundTypes []string
|
||||
for _, v := range result.Violations {
|
||||
foundTypes = append(foundTypes, v.Type)
|
||||
}
|
||||
|
||||
for _, expectedType := range tc.expectedTypes {
|
||||
assert.Contains(t, foundTypes, expectedType, "Expected type %s in violations for: %s", expectedType, tc.name)
|
||||
}
|
||||
} else {
|
||||
assert.False(t, result.HasViolation(), "Expected no violation for: %s", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizer_Scan_Masking(t *testing.T) {
|
||||
// 脱敏:'sk-xxxx' 格式
|
||||
sanitizer := NewSanitizer()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedOutput string
|
||||
expectMasked bool
|
||||
}{
|
||||
{
|
||||
name: "OpenAI Key",
|
||||
input: "sk-1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
expectedOutput: "sk-xxxxxx****xxxx",
|
||||
expectMasked: true,
|
||||
},
|
||||
{
|
||||
name: "Short OpenAI Key",
|
||||
input: "sk-1234567890",
|
||||
expectedOutput: "sk-****7890",
|
||||
expectMasked: true,
|
||||
},
|
||||
{
|
||||
name: "AWS Access Key",
|
||||
input: "AKIAIOSFODNN7EXAMPLE",
|
||||
expectedOutput: "AKIA****EXAMPLE",
|
||||
expectMasked: true,
|
||||
},
|
||||
{
|
||||
name: "Normal Text",
|
||||
input: "This is normal text",
|
||||
expectedOutput: "This is normal text",
|
||||
expectMasked: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := sanitizer.Mask(tc.input)
|
||||
|
||||
if tc.expectMasked {
|
||||
assert.NotEqual(t, tc.input, result, "Expected masking for: %s", tc.name)
|
||||
assert.Contains(t, result, "****", "Expected **** in masked result for: %s", tc.name)
|
||||
} else {
|
||||
assert.Equal(t, tc.expectedOutput, result, "Expected unchanged for: %s", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizer_Scan_ResponseBody(t *testing.T) {
|
||||
// 检测响应体中的凭证泄露
|
||||
scanner := NewCredentialScanner()
|
||||
|
||||
responseBody := `{
|
||||
"success": true,
|
||||
"data": {
|
||||
"api_key": "sk-1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
"user": "testuser"
|
||||
}
|
||||
}`
|
||||
|
||||
result := scanner.Scan(responseBody)
|
||||
|
||||
assert.True(t, result.HasViolation())
|
||||
assert.NotEmpty(t, result.Violations)
|
||||
|
||||
// 验证找到了api_key类型的违规
|
||||
foundTypes := make([]string, 0)
|
||||
for _, v := range result.Violations {
|
||||
foundTypes = append(foundTypes, v.Type)
|
||||
}
|
||||
assert.Contains(t, foundTypes, "api_key")
|
||||
}
|
||||
|
||||
func TestSanitizer_MaskMap(t *testing.T) {
|
||||
// 测试对map进行脱敏
|
||||
sanitizer := NewSanitizer()
|
||||
|
||||
input := map[string]interface{}{
|
||||
"api_key": "sk-1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
"secret": "mysecretkey123",
|
||||
"user": "testuser",
|
||||
}
|
||||
|
||||
masked := sanitizer.MaskMap(input)
|
||||
|
||||
// 验证敏感字段被脱敏
|
||||
assert.NotEqual(t, input["api_key"], masked["api_key"])
|
||||
assert.NotEqual(t, input["secret"], masked["secret"])
|
||||
assert.Equal(t, input["user"], masked["user"])
|
||||
|
||||
// 验证脱敏格式
|
||||
assert.Contains(t, masked["api_key"], "****")
|
||||
assert.Contains(t, masked["secret"], "****")
|
||||
}
|
||||
|
||||
func TestSanitizer_MaskSlice(t *testing.T) {
|
||||
// 测试对slice进行脱敏
|
||||
sanitizer := NewSanitizer()
|
||||
|
||||
input := []string{
|
||||
"sk-1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
"normal text",
|
||||
"password123",
|
||||
}
|
||||
|
||||
masked := sanitizer.MaskSlice(input)
|
||||
|
||||
assert.Len(t, masked, 3)
|
||||
assert.NotEqual(t, input[0], masked[0])
|
||||
assert.Equal(t, input[1], masked[1])
|
||||
assert.NotEqual(t, input[2], masked[2])
|
||||
}
|
||||
|
||||
func TestCredentialScanner_SensitiveFields(t *testing.T) {
|
||||
// 测试敏感字段列表
|
||||
fields := GetSensitiveFields()
|
||||
|
||||
// 验证常见敏感字段
|
||||
assert.Contains(t, fields, "api_key")
|
||||
assert.Contains(t, fields, "secret")
|
||||
assert.Contains(t, fields, "password")
|
||||
assert.Contains(t, fields, "token")
|
||||
assert.Contains(t, fields, "access_key")
|
||||
assert.Contains(t, fields, "private_key")
|
||||
}
|
||||
|
||||
func TestCredentialScanner_ScanRules(t *testing.T) {
|
||||
// 测试扫描规则
|
||||
scanner := NewCredentialScanner()
|
||||
|
||||
rules := scanner.GetRules()
|
||||
assert.NotEmpty(t, rules, "Scanner should have rules")
|
||||
|
||||
// 验证规则有ID和描述
|
||||
for _, rule := range rules {
|
||||
assert.NotEmpty(t, rule.ID)
|
||||
assert.NotEmpty(t, rule.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizer_IsSensitiveField(t *testing.T) {
|
||||
// 测试字段名敏感性判断
|
||||
testCases := []struct {
|
||||
fieldName string
|
||||
expected bool
|
||||
}{
|
||||
{"api_key", true},
|
||||
{"secret", true},
|
||||
{"password", true},
|
||||
{"token", true},
|
||||
{"access_key", true},
|
||||
{"private_key", true},
|
||||
{"session_id", true},
|
||||
{"authorization", true},
|
||||
{"user", false},
|
||||
{"name", false},
|
||||
{"email", false},
|
||||
{"id", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.fieldName, func(t *testing.T) {
|
||||
result := IsSensitiveField(tc.fieldName)
|
||||
assert.Equal(t, tc.expected, result, "Field %s sensitivity mismatch", tc.fieldName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizer_ScanLog(t *testing.T) {
|
||||
// 测试日志扫描
|
||||
scanner := NewCredentialScanner()
|
||||
|
||||
logLine := `2026-04-02 10:30:45 INFO [api] Request completed api_key=sk-1234567890abcdefghijklmnopqrstuvwxyz duration=100ms`
|
||||
|
||||
result := scanner.Scan(logLine)
|
||||
|
||||
assert.True(t, result.HasViolation())
|
||||
assert.NotEmpty(t, result.Violations)
|
||||
// sk-开头的key会被识别为openai_key
|
||||
assert.Equal(t, "openai_key", result.Violations[0].Type)
|
||||
}
|
||||
|
||||
func TestSanitizer_MultipleViolations(t *testing.T) {
|
||||
// 测试多个违规
|
||||
scanner := NewCredentialScanner()
|
||||
|
||||
content := `{
|
||||
"api_key": "sk-1234567890abcdefghijklmnopqrstuvwxyz",
|
||||
"secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
"password": "mysecretpassword"
|
||||
}`
|
||||
|
||||
result := scanner.Scan(content)
|
||||
|
||||
assert.True(t, result.HasViolation())
|
||||
assert.GreaterOrEqual(t, len(result.Violations), 3)
|
||||
}
|
||||
Reference in New Issue
Block a user