2026-04-02 23:35:53 +08:00
|
|
|
|
package sanitizer
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-03 09:39:32 +08:00
|
|
|
|
"regexp"
|
2026-04-02 23:35:53 +08:00
|
|
|
|
"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)
|
2026-04-03 09:39:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
// P2-03: regexp.MustCompile可能panic,应该使用regexp.Compile并处理错误
|
|
|
|
|
|
func TestP2_03_NewCredentialScanner_InvalidRegex(t *testing.T) {
|
|
|
|
|
|
// 测试一个无效的正则表达式
|
|
|
|
|
|
// 由于NewCredentialScanner内部使用MustCompile,这里我们测试在初始化时是否会panic
|
|
|
|
|
|
|
|
|
|
|
|
// 创建一个会panic的场景:无效正则应该被Compile检测而不是MustCompile
|
|
|
|
|
|
// 通过检查NewCredentialScanner是否能正常创建(不panic)来验证
|
|
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
|
t.Errorf("P2-03 BUG: NewCredentialScanner panicked with invalid regex: %v", r)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
// 这里如果正则都是有效的,应该不会panic
|
|
|
|
|
|
scanner := NewCredentialScanner()
|
|
|
|
|
|
if scanner == nil {
|
|
|
|
|
|
t.Error("scanner should not be nil")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 但我们无法在测试中模拟无效正则,因为MustCompile在编译时就panic了
|
|
|
|
|
|
// 所以这个测试更多是文档性质的
|
|
|
|
|
|
t.Logf("P2-03: NewCredentialScanner uses MustCompile which panics on invalid regex - should use Compile with error handling")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// P2-03: 验证MustCompile在无效正则时会panic
|
|
|
|
|
|
// 这个测试演示了问题:使用无效正则会导致panic
|
|
|
|
|
|
func TestP2_03_MustCompile_PanicsOnInvalidRegex(t *testing.T) {
|
|
|
|
|
|
invalidRegex := "[invalid" // 无效的正则,缺少结束括号
|
|
|
|
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
|
t.Logf("P2-03 CONFIRMED: MustCompile panics on invalid regex: %v", r)
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
|
|
// 这行会panic
|
|
|
|
|
|
_ = regexp.MustCompile(invalidRegex)
|
|
|
|
|
|
t.Error("Should have panicked")
|
|
|
|
|
|
}
|