Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
201 lines
6.3 KiB
Go
201 lines
6.3 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestMaskAuditCredential(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
in string
|
||
want string
|
||
}{
|
||
{"empty", "", ""},
|
||
{"short", "abc", "****"},
|
||
{"boundary_14", "12345678901234", "****"},
|
||
{"long", "sk-ant-api03-abcdefghijklmnop1234", "sk-ant****1234"},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
got := MaskAuditCredential(tc.in)
|
||
if got != tc.want {
|
||
t.Fatalf("MaskAuditCredential(%q) = %q, want %q", tc.in, got, tc.want)
|
||
}
|
||
// 掩码结果绝不能包含原始凭证的中间部分。
|
||
if len(tc.in) > 14 && strings.Contains(got, tc.in) {
|
||
t.Fatalf("masked value leaks full credential: %q", got)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestRedactAuditBody_JSONRedactsSecrets(t *testing.T) {
|
||
raw := []byte(`{
|
||
"name": "acc1",
|
||
"base_url": "https://evil.example.com",
|
||
"credentials": {"api_key": "sk-secret-123", "base_url": "https://evil.example.com"},
|
||
"new_password": "hunter2",
|
||
"totp_code": "123456",
|
||
"nested": [{"access_token": "tok_abc"}]
|
||
}`)
|
||
out := RedactAuditBody(raw, "application/json")
|
||
|
||
var parsed map[string]any
|
||
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
||
t.Fatalf("output is not valid JSON: %v\n%s", err, out)
|
||
}
|
||
|
||
// 敏感字段被擦除。
|
||
for _, secret := range []string{"sk-secret-123", "hunter2", "123456", "tok_abc"} {
|
||
if strings.Contains(out, secret) {
|
||
t.Fatalf("redacted body still contains secret %q: %s", secret, out)
|
||
}
|
||
}
|
||
// 非敏感字段(base_url、name)保留以便追责。
|
||
if !strings.Contains(out, "evil.example.com") {
|
||
t.Fatalf("base_url should be preserved for accountability: %s", out)
|
||
}
|
||
if !strings.Contains(out, "acc1") {
|
||
t.Fatalf("name should be preserved: %s", out)
|
||
}
|
||
}
|
||
|
||
// 裸键 "session"(Ollama Cloud 会话保存的请求体字段)值整体就是浏览器 Cookie 明文,
|
||
// 必须命中键级脱敏;session_id 等运行态标识不受影响,保留以便追责。
|
||
func TestRedactAuditBody_BareSessionKeyRedacted(t *testing.T) {
|
||
raw := []byte(`{"session": "wos-session=cookie-canary", "session_id": "sid-visible"}`)
|
||
out := RedactAuditBody(raw, "application/json")
|
||
|
||
if strings.Contains(out, "cookie-canary") {
|
||
t.Fatalf("redacted body still contains the session cookie: %s", out)
|
||
}
|
||
if !strings.Contains(out, "sid-visible") {
|
||
t.Fatalf("session_id should be preserved for accountability: %s", out)
|
||
}
|
||
}
|
||
|
||
// TestRedactAuditBody_AuthoritativeTablesSynced 覆盖曾经漏网的凭证字段:
|
||
// 账号 credentials 敏感子键、支付渠道无分隔符密钥、字符串值内嵌凭证的 proxy_key / custom_key,
|
||
// 以及 camelCase 等命名变体(归一化比对)。
|
||
func TestRedactAuditBody_AuthoritativeTablesSynced(t *testing.T) {
|
||
raw := []byte(`{
|
||
"credentials": {
|
||
"session_key": "sk-session-aaa",
|
||
"service_account_json": "{\"private_key\":\"pem-body-bbb\"}",
|
||
"service_account": "sa-blob-ccc"
|
||
},
|
||
"proxy_key": "socks5|1.2.3.4|1080|proxyuser|proxypass-ddd",
|
||
"custom_key": "sk-custom-eee",
|
||
"config": {
|
||
"pkey": "easypay-merchant-fff",
|
||
"privateKey": "alipay-pem-ggg",
|
||
"apiv3key": "wxpay-v3-hhh",
|
||
"SecretKey": "stripe-sk-iii",
|
||
"webhookSecret": "whsec-jjj"
|
||
},
|
||
"provider_key": "stripe",
|
||
"name": "instance-1"
|
||
}`)
|
||
out := RedactAuditBody(raw, "application/json")
|
||
|
||
for _, secret := range []string{
|
||
"sk-session-aaa", "pem-body-bbb", "sa-blob-ccc",
|
||
"proxypass-ddd", "sk-custom-eee",
|
||
"easypay-merchant-fff", "alipay-pem-ggg", "wxpay-v3-hhh",
|
||
"stripe-sk-iii", "whsec-jjj",
|
||
} {
|
||
if strings.Contains(out, secret) {
|
||
t.Fatalf("redacted body still contains secret %q: %s", secret, out)
|
||
}
|
||
}
|
||
// provider_key 是渠道标识而非密钥,必须保留以便追责。
|
||
if !strings.Contains(out, `"provider_key":"stripe"`) {
|
||
t.Fatalf("provider_key should be preserved for accountability: %s", out)
|
||
}
|
||
if !strings.Contains(out, "instance-1") {
|
||
t.Fatalf("name should be preserved: %s", out)
|
||
}
|
||
}
|
||
|
||
// SensitiveCredentialKeys 中的每个键都必须被审计脱敏判定命中(防两表漂移的守卫)。
|
||
func TestAuditSensitiveKeys_CoverCredentialTable(t *testing.T) {
|
||
for _, k := range SensitiveCredentialKeys {
|
||
if !isAuditSensitiveBodyKey(k) {
|
||
t.Fatalf("credential key %q is not covered by audit redaction", k)
|
||
}
|
||
}
|
||
for provider, fields := range providerSensitiveConfigFields {
|
||
for k := range fields {
|
||
if !isAuditSensitiveBodyKey(k) {
|
||
t.Fatalf("payment provider %q sensitive field %q is not covered by audit redaction", provider, k)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRedactAuditBody_NonJSONOmitted(t *testing.T) {
|
||
out := RedactAuditBody([]byte("username=admin&password=secret"), "application/x-www-form-urlencoded")
|
||
if strings.Contains(out, "secret") {
|
||
t.Fatalf("non-json body must not leak content: %s", out)
|
||
}
|
||
if !strings.Contains(out, "omitted") {
|
||
t.Fatalf("expected omission marker, got: %s", out)
|
||
}
|
||
}
|
||
|
||
func TestRedactAuditBody_Empty(t *testing.T) {
|
||
if got := RedactAuditBody(nil, "application/json"); got != "" {
|
||
t.Fatalf("expected empty for nil body, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestSessionBindingHash(t *testing.T) {
|
||
a := &SessionBinding{IP: "1.2.3.4", UserAgent: "Mozilla/5.0"}
|
||
b := &SessionBinding{IP: "1.2.3.4", UserAgent: "Mozilla/5.0"}
|
||
if a.Hash() != b.Hash() {
|
||
t.Fatalf("identical bindings must hash equal")
|
||
}
|
||
if a.Hash() == "" {
|
||
t.Fatalf("non-empty binding must produce non-empty hash")
|
||
}
|
||
|
||
// IP 变化 → 哈希变化。
|
||
c := &SessionBinding{IP: "5.6.7.8", UserAgent: "Mozilla/5.0"}
|
||
if a.Hash() == c.Hash() {
|
||
t.Fatalf("changing IP must change hash")
|
||
}
|
||
// UA 变化 → 哈希变化。
|
||
d := &SessionBinding{IP: "1.2.3.4", UserAgent: "curl/8.0"}
|
||
if a.Hash() == d.Hash() {
|
||
t.Fatalf("changing UA must change hash")
|
||
}
|
||
|
||
// 空指纹 → 空哈希(旧 token 兼容)。
|
||
empty := &SessionBinding{}
|
||
if empty.Hash() != "" {
|
||
t.Fatalf("empty binding must hash to empty string")
|
||
}
|
||
var nilBinding *SessionBinding
|
||
if nilBinding.Hash() != "" {
|
||
t.Fatalf("nil binding must hash to empty string")
|
||
}
|
||
}
|
||
|
||
func TestParseAuditLogRetentionDays(t *testing.T) {
|
||
cases := map[string]int{
|
||
"": defaultAuditLogRetentionDays,
|
||
"abc": defaultAuditLogRetentionDays,
|
||
"90": 90,
|
||
"0": 0,
|
||
"-1": 0,
|
||
" 30 ": 30,
|
||
}
|
||
for in, want := range cases {
|
||
if got := parseAuditLogRetentionDays(in); got != want {
|
||
t.Fatalf("parseAuditLogRetentionDays(%q) = %d, want %d", in, got, want)
|
||
}
|
||
}
|
||
}
|