audit_log_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. package service
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. )
  7. func TestMaskAuditCredential(t *testing.T) {
  8. cases := []struct {
  9. name string
  10. in string
  11. want string
  12. }{
  13. {"empty", "", ""},
  14. {"short", "abc", "****"},
  15. {"boundary_14", "12345678901234", "****"},
  16. {"long", "sk-ant-api03-abcdefghijklmnop1234", "sk-ant****1234"},
  17. }
  18. for _, tc := range cases {
  19. t.Run(tc.name, func(t *testing.T) {
  20. got := MaskAuditCredential(tc.in)
  21. if got != tc.want {
  22. t.Fatalf("MaskAuditCredential(%q) = %q, want %q", tc.in, got, tc.want)
  23. }
  24. // 掩码结果绝不能包含原始凭证的中间部分。
  25. if len(tc.in) > 14 && strings.Contains(got, tc.in) {
  26. t.Fatalf("masked value leaks full credential: %q", got)
  27. }
  28. })
  29. }
  30. }
  31. func TestRedactAuditBody_JSONRedactsSecrets(t *testing.T) {
  32. raw := []byte(`{
  33. "name": "acc1",
  34. "base_url": "https://evil.example.com",
  35. "credentials": {"api_key": "sk-secret-123", "base_url": "https://evil.example.com"},
  36. "new_password": "hunter2",
  37. "totp_code": "123456",
  38. "nested": [{"access_token": "tok_abc"}]
  39. }`)
  40. out := RedactAuditBody(raw, "application/json")
  41. var parsed map[string]any
  42. if err := json.Unmarshal([]byte(out), &parsed); err != nil {
  43. t.Fatalf("output is not valid JSON: %v\n%s", err, out)
  44. }
  45. // 敏感字段被擦除。
  46. for _, secret := range []string{"sk-secret-123", "hunter2", "123456", "tok_abc"} {
  47. if strings.Contains(out, secret) {
  48. t.Fatalf("redacted body still contains secret %q: %s", secret, out)
  49. }
  50. }
  51. // 非敏感字段(base_url、name)保留以便追责。
  52. if !strings.Contains(out, "evil.example.com") {
  53. t.Fatalf("base_url should be preserved for accountability: %s", out)
  54. }
  55. if !strings.Contains(out, "acc1") {
  56. t.Fatalf("name should be preserved: %s", out)
  57. }
  58. }
  59. // 裸键 "session"(Ollama Cloud 会话保存的请求体字段)值整体就是浏览器 Cookie 明文,
  60. // 必须命中键级脱敏;session_id 等运行态标识不受影响,保留以便追责。
  61. func TestRedactAuditBody_BareSessionKeyRedacted(t *testing.T) {
  62. raw := []byte(`{"session": "wos-session=cookie-canary", "session_id": "sid-visible"}`)
  63. out := RedactAuditBody(raw, "application/json")
  64. if strings.Contains(out, "cookie-canary") {
  65. t.Fatalf("redacted body still contains the session cookie: %s", out)
  66. }
  67. if !strings.Contains(out, "sid-visible") {
  68. t.Fatalf("session_id should be preserved for accountability: %s", out)
  69. }
  70. }
  71. // TestRedactAuditBody_AuthoritativeTablesSynced 覆盖曾经漏网的凭证字段:
  72. // 账号 credentials 敏感子键、支付渠道无分隔符密钥、字符串值内嵌凭证的 proxy_key / custom_key,
  73. // 以及 camelCase 等命名变体(归一化比对)。
  74. func TestRedactAuditBody_AuthoritativeTablesSynced(t *testing.T) {
  75. raw := []byte(`{
  76. "credentials": {
  77. "session_key": "sk-session-aaa",
  78. "service_account_json": "{\"private_key\":\"pem-body-bbb\"}",
  79. "service_account": "sa-blob-ccc"
  80. },
  81. "proxy_key": "socks5|1.2.3.4|1080|proxyuser|proxypass-ddd",
  82. "custom_key": "sk-custom-eee",
  83. "config": {
  84. "pkey": "easypay-merchant-fff",
  85. "privateKey": "alipay-pem-ggg",
  86. "apiv3key": "wxpay-v3-hhh",
  87. "SecretKey": "stripe-sk-iii",
  88. "webhookSecret": "whsec-jjj"
  89. },
  90. "provider_key": "stripe",
  91. "name": "instance-1"
  92. }`)
  93. out := RedactAuditBody(raw, "application/json")
  94. for _, secret := range []string{
  95. "sk-session-aaa", "pem-body-bbb", "sa-blob-ccc",
  96. "proxypass-ddd", "sk-custom-eee",
  97. "easypay-merchant-fff", "alipay-pem-ggg", "wxpay-v3-hhh",
  98. "stripe-sk-iii", "whsec-jjj",
  99. } {
  100. if strings.Contains(out, secret) {
  101. t.Fatalf("redacted body still contains secret %q: %s", secret, out)
  102. }
  103. }
  104. // provider_key 是渠道标识而非密钥,必须保留以便追责。
  105. if !strings.Contains(out, `"provider_key":"stripe"`) {
  106. t.Fatalf("provider_key should be preserved for accountability: %s", out)
  107. }
  108. if !strings.Contains(out, "instance-1") {
  109. t.Fatalf("name should be preserved: %s", out)
  110. }
  111. }
  112. // SensitiveCredentialKeys 中的每个键都必须被审计脱敏判定命中(防两表漂移的守卫)。
  113. func TestAuditSensitiveKeys_CoverCredentialTable(t *testing.T) {
  114. for _, k := range SensitiveCredentialKeys {
  115. if !isAuditSensitiveBodyKey(k) {
  116. t.Fatalf("credential key %q is not covered by audit redaction", k)
  117. }
  118. }
  119. for provider, fields := range providerSensitiveConfigFields {
  120. for k := range fields {
  121. if !isAuditSensitiveBodyKey(k) {
  122. t.Fatalf("payment provider %q sensitive field %q is not covered by audit redaction", provider, k)
  123. }
  124. }
  125. }
  126. }
  127. func TestRedactAuditBody_NonJSONOmitted(t *testing.T) {
  128. out := RedactAuditBody([]byte("username=admin&password=secret"), "application/x-www-form-urlencoded")
  129. if strings.Contains(out, "secret") {
  130. t.Fatalf("non-json body must not leak content: %s", out)
  131. }
  132. if !strings.Contains(out, "omitted") {
  133. t.Fatalf("expected omission marker, got: %s", out)
  134. }
  135. }
  136. func TestRedactAuditBody_Empty(t *testing.T) {
  137. if got := RedactAuditBody(nil, "application/json"); got != "" {
  138. t.Fatalf("expected empty for nil body, got %q", got)
  139. }
  140. }
  141. func TestSessionBindingHash(t *testing.T) {
  142. a := &SessionBinding{IP: "1.2.3.4", UserAgent: "Mozilla/5.0"}
  143. b := &SessionBinding{IP: "1.2.3.4", UserAgent: "Mozilla/5.0"}
  144. if a.Hash() != b.Hash() {
  145. t.Fatalf("identical bindings must hash equal")
  146. }
  147. if a.Hash() == "" {
  148. t.Fatalf("non-empty binding must produce non-empty hash")
  149. }
  150. // IP 变化 → 哈希变化。
  151. c := &SessionBinding{IP: "5.6.7.8", UserAgent: "Mozilla/5.0"}
  152. if a.Hash() == c.Hash() {
  153. t.Fatalf("changing IP must change hash")
  154. }
  155. // UA 变化 → 哈希变化。
  156. d := &SessionBinding{IP: "1.2.3.4", UserAgent: "curl/8.0"}
  157. if a.Hash() == d.Hash() {
  158. t.Fatalf("changing UA must change hash")
  159. }
  160. // 空指纹 → 空哈希(旧 token 兼容)。
  161. empty := &SessionBinding{}
  162. if empty.Hash() != "" {
  163. t.Fatalf("empty binding must hash to empty string")
  164. }
  165. var nilBinding *SessionBinding
  166. if nilBinding.Hash() != "" {
  167. t.Fatalf("nil binding must hash to empty string")
  168. }
  169. }
  170. func TestParseAuditLogRetentionDays(t *testing.T) {
  171. cases := map[string]int{
  172. "": defaultAuditLogRetentionDays,
  173. "abc": defaultAuditLogRetentionDays,
  174. "90": 90,
  175. "0": 0,
  176. "-1": 0,
  177. " 30 ": 30,
  178. }
  179. for in, want := range cases {
  180. if got := parseAuditLogRetentionDays(in); got != want {
  181. t.Fatalf("parseAuditLogRetentionDays(%q) = %d, want %d", in, got, want)
  182. }
  183. }
  184. }