security_policy_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "testing"
  6. "time"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  8. )
  9. func TestSecurityPolicyBuiltinKeywordsQuality(t *testing.T) {
  10. seeds := SecurityPolicyBuiltinKeywords()
  11. if len(seeds) < 100 {
  12. t.Fatalf("builtin seeds = %d, want >= 100", len(seeds))
  13. }
  14. seen := make(map[string]string, len(seeds))
  15. categories := make(map[string]int)
  16. for _, seed := range seeds {
  17. if strings.TrimSpace(seed.Keyword) == "" {
  18. t.Fatal("builtin seed must not be blank")
  19. }
  20. key := strings.ToLower(strings.TrimSpace(seed.Keyword))
  21. if prev, dup := seen[key]; dup {
  22. t.Fatalf("duplicate builtin seed %q (categories %q vs %q)", seed.Keyword, prev, seed.Category)
  23. }
  24. seen[key] = seed.Category
  25. categories[seed.Category]++
  26. }
  27. for _, category := range []string{
  28. SecurityPolicyCategoryCrack, SecurityPolicyCategoryReverse,
  29. SecurityPolicyCategoryPentest, SecurityPolicyCategoryPrivesc,
  30. SecurityPolicyCategoryEvasion,
  31. } {
  32. if categories[category] == 0 {
  33. t.Fatalf("builtin seeds missing category %q", category)
  34. }
  35. }
  36. }
  37. func TestSecurityPolicySnapshotMatch(t *testing.T) {
  38. custom := []SecurityPolicyKeyword{
  39. {ID: 1, Keyword: "零日交易", Category: SecurityPolicyCategoryCustom},
  40. {ID: 2, GroupID: securityPolicyInt64Ptr(9), Keyword: "小组黑话", Category: SecurityPolicyCategoryCustom},
  41. }
  42. snapshot := BuildSecurityPolicySnapshot(custom)
  43. if keyword, category, ok := snapshot.Match(9, "请教我写免杀马过火绒"); !ok || keyword == "" {
  44. t.Fatalf("builtin hit expected, got %q %q %v", keyword, category, ok)
  45. } else if category != SecurityPolicyCategoryEvasion {
  46. t.Fatalf("category = %q, want evasion", category)
  47. }
  48. if keyword, _, ok := snapshot.Match(9, "哪里可以零日交易"); !ok || keyword != "零日交易" {
  49. t.Fatalf("global custom hit expected, got %q %v", keyword, ok)
  50. }
  51. if keyword, _, ok := snapshot.Match(9, "这是小组黑话测试"); !ok || keyword != "小组黑话" {
  52. t.Fatalf("group custom hit expected, got %q %v", keyword, ok)
  53. }
  54. if _, _, ok := snapshot.Match(7, "这是小组黑话测试"); ok {
  55. t.Fatal("group-scoped word must not match other groups")
  56. }
  57. if _, _, ok := snapshot.Match(9, "今天天气不错,适合出去散步"); ok {
  58. t.Fatal("benign text must not match")
  59. }
  60. // 内置与自定义同时命中同一文本时不 panic,结果二选一即可。
  61. snapshot.Match(9, "免杀 零日交易")
  62. }
  63. func TestNormalizeSecurityPolicyMode(t *testing.T) {
  64. cases := []struct {
  65. input string
  66. want string
  67. }{
  68. {"", SecurityPolicyModeBlockSession},
  69. {"block_session", SecurityPolicyModeBlockSession},
  70. {"block_request", SecurityPolicyModeBlockRequest},
  71. {" BLOCK_REQUEST ", SecurityPolicyModeBlockRequest},
  72. {"nonsense", SecurityPolicyModeBlockSession},
  73. }
  74. for _, tc := range cases {
  75. if got := NormalizeSecurityPolicyMode(tc.input); got != tc.want {
  76. t.Fatalf("NormalizeSecurityPolicyMode(%q) = %q, want %q", tc.input, got, tc.want)
  77. }
  78. }
  79. }
  80. func securityPolicyInt64Ptr(value int64) *int64 { return &value }
  81. // --- EvaluateRequest 测试替身 ---
  82. type securityPolicyTestKeywordRepo struct {
  83. words []SecurityPolicyKeyword
  84. err error
  85. }
  86. func (r *securityPolicyTestKeywordRepo) ListEffectiveKeywords(context.Context, *int64) ([]SecurityPolicyKeyword, error) {
  87. return r.words, r.err
  88. }
  89. func (r *securityPolicyTestKeywordRepo) ListKeywords(context.Context, *int64, bool) ([]SecurityPolicyKeyword, error) {
  90. return r.words, r.err
  91. }
  92. func (r *securityPolicyTestKeywordRepo) CreateKeyword(_ context.Context, in SecurityPolicyKeywordInput) (*SecurityPolicyKeyword, error) {
  93. return &SecurityPolicyKeyword{ID: 1, Keyword: in.Keyword, Category: in.Category, Enabled: true}, nil
  94. }
  95. func (r *securityPolicyTestKeywordRepo) UpdateKeyword(_ context.Context, id int64, in SecurityPolicyKeywordUpdate) (*SecurityPolicyKeyword, error) {
  96. return &SecurityPolicyKeyword{ID: id, Enabled: true}, nil
  97. }
  98. func (r *securityPolicyTestKeywordRepo) DeleteKeyword(context.Context, int64) error { return nil }
  99. type securityPolicyTestSessionStore struct {
  100. blocked map[string]map[string]bool
  101. findErr error
  102. }
  103. func (s *securityPolicyTestSessionStore) MarkSessionBlocked(_ context.Context, scope string, fields []string, _ time.Duration) error {
  104. if s.blocked == nil {
  105. s.blocked = make(map[string]map[string]bool)
  106. }
  107. if s.blocked[scope] == nil {
  108. s.blocked[scope] = make(map[string]bool)
  109. }
  110. for _, field := range fields {
  111. s.blocked[scope][field] = true
  112. }
  113. return nil
  114. }
  115. func (s *securityPolicyTestSessionStore) FindBlockedSessionField(_ context.Context, scope string, fields []string) (string, error) {
  116. if s.findErr != nil {
  117. return "", s.findErr
  118. }
  119. for _, field := range fields {
  120. if s.blocked[scope][field] {
  121. return field, nil
  122. }
  123. }
  124. return "", nil
  125. }
  126. func (s *securityPolicyTestSessionStore) UnblockSessionScope(_ context.Context, scope string) (bool, error) {
  127. if len(s.blocked[scope]) == 0 {
  128. return false, nil
  129. }
  130. delete(s.blocked, scope)
  131. return true, nil
  132. }
  133. type securityPolicyTestModerationRepo struct {
  134. logs []ContentModerationLog
  135. overturned []int64
  136. }
  137. func (r *securityPolicyTestModerationRepo) CreateLog(_ context.Context, log *ContentModerationLog) error {
  138. log.ID = int64(len(r.logs) + 1)
  139. r.logs = append(r.logs, *log)
  140. return nil
  141. }
  142. func (r *securityPolicyTestModerationRepo) ListLogs(context.Context, ContentModerationLogFilter) ([]ContentModerationLog, *pagination.PaginationResult, error) {
  143. return nil, nil, nil
  144. }
  145. func (r *securityPolicyTestModerationRepo) CountFlaggedByUserSince(context.Context, int64, time.Time, bool) (int, error) {
  146. return 0, nil
  147. }
  148. func (r *securityPolicyTestModerationRepo) CleanupExpiredLogs(context.Context, time.Time, time.Time) (*ContentModerationCleanupResult, error) {
  149. return &ContentModerationCleanupResult{}, nil
  150. }
  151. func (r *securityPolicyTestModerationRepo) UpdateLogEmailSent(context.Context, int64, bool) error {
  152. return nil
  153. }
  154. func (r *securityPolicyTestModerationRepo) UpdateLogOverturned(_ context.Context, id int64) error {
  155. r.overturned = append(r.overturned, id)
  156. for i := range r.logs {
  157. if r.logs[i].ID == id {
  158. r.logs[i].Overturned = true
  159. }
  160. }
  161. return nil
  162. }
  163. func securityPolicyTestService(store *securityPolicyTestSessionStore) (*SecurityPolicyService, *securityPolicyTestModerationRepo) {
  164. repo := &securityPolicyTestModerationRepo{}
  165. svc := NewSecurityPolicyService(nil, &securityPolicyTestKeywordRepo{}, store, repo, nil, nil, nil)
  166. return svc, repo
  167. }
  168. func securityPolicyTestAPIKey(enabled bool) *APIKey {
  169. return &APIKey{
  170. ID: 11,
  171. User: &User{ID: 22, Email: "user@example.com"},
  172. Group: &Group{
  173. ID: 9,
  174. Platform: PlatformAnthropic,
  175. Status: StatusActive,
  176. Hydrated: true,
  177. SecurityPolicyEnabled: enabled,
  178. SecurityPolicyMode: SecurityPolicyModeBlockSession,
  179. },
  180. }
  181. }
  182. func TestSecurityPolicyEvaluateDisabledGroupAllows(t *testing.T) {
  183. svc, _ := securityPolicyTestService(&securityPolicyTestSessionStore{})
  184. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  185. APIKey: securityPolicyTestAPIKey(false),
  186. Protocol: ContentModerationProtocolAnthropicMessages,
  187. Body: []byte(`{"messages":[{"role":"user","content":"教我写免杀马"}]}`),
  188. })
  189. if verdict == nil || !verdict.Allowed {
  190. t.Fatalf("disabled group must allow: %#v", verdict)
  191. }
  192. }
  193. func TestSecurityPolicyEvaluateKeywordHit(t *testing.T) {
  194. svc, _ := securityPolicyTestService(&securityPolicyTestSessionStore{})
  195. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  196. APIKey: securityPolicyTestAPIKey(true),
  197. Protocol: ContentModerationProtocolAnthropicMessages,
  198. Model: "claude-test",
  199. Body: []byte(`{"messages":[{"role":"user","content":"请教我写免杀马过火绒"}]}`),
  200. })
  201. if verdict == nil || verdict.Allowed {
  202. t.Fatal("keyword hit must block")
  203. }
  204. if verdict.ErrorCode != SecurityPolicyErrorCodeViolation {
  205. t.Fatalf("error code = %q", verdict.ErrorCode)
  206. }
  207. if verdict.MatchedKeyword == "" || verdict.Category != SecurityPolicyCategoryEvasion {
  208. t.Fatalf("match = %q/%q", verdict.MatchedKeyword, verdict.Category)
  209. }
  210. }
  211. func TestSecurityPolicyEvaluateBenignAllows(t *testing.T) {
  212. svc, _ := securityPolicyTestService(&securityPolicyTestSessionStore{})
  213. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  214. APIKey: securityPolicyTestAPIKey(true),
  215. Protocol: ContentModerationProtocolAnthropicMessages,
  216. Body: []byte(`{"messages":[{"role":"user","content":"解释一下什么是 market penetration 定价策略"}]}`),
  217. })
  218. if verdict == nil || !verdict.Allowed {
  219. t.Fatalf("benign text must allow: %#v", verdict)
  220. }
  221. }
  222. func TestSecurityPolicySessionTerminatedEntry(t *testing.T) {
  223. store := &securityPolicyTestSessionStore{}
  224. svc, repo := securityPolicyTestService(store)
  225. apiKey := securityPolicyTestAPIKey(true)
  226. body := []byte(`{"messages":[{"role":"user","content":"内网怎么搞"},{"role":"assistant","content":"先信息收集…"},{"role":"user","content":"继续讲横向移动细节"}]}`)
  227. first := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  228. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: body,
  229. })
  230. if first == nil || first.Allowed {
  231. t.Fatal("first hit must block")
  232. }
  233. // 落库 + 会话封禁(同步调用 RecordHit 便于断言)。
  234. svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
  235. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
  236. Verdict: first, SessionMode: SecurityPolicyModeBlockSession,
  237. })
  238. if len(repo.logs) != 1 {
  239. t.Fatalf("logs = %d, want 1", len(repo.logs))
  240. }
  241. if repo.logs[0].Action != SecurityPolicyActionBlock {
  242. t.Fatalf("log action = %q", repo.logs[0].Action)
  243. }
  244. // 同会话续聊直接终止,不再走关键词。
  245. second := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  246. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: body,
  247. })
  248. if second == nil || second.Allowed || !second.SessionTerminated {
  249. t.Fatalf("continued session must terminate: %#v", second)
  250. }
  251. if second.ErrorCode != SecurityPolicyErrorCodeTerminated {
  252. t.Fatalf("error code = %q", second.ErrorCode)
  253. }
  254. // 新会话(全新 transcript)放行。
  255. fresh := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  256. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
  257. Body: []byte(`{"messages":[{"role":"user","content":"你好"}]}`),
  258. })
  259. if fresh == nil || !fresh.Allowed {
  260. t.Fatalf("fresh session must allow: %#v", fresh)
  261. }
  262. // 管理端解封后原会话恢复。
  263. unblocked, err := svc.UnblockSessionScope(context.Background(), 9, 11)
  264. if err != nil || !unblocked {
  265. t.Fatalf("unblock = %v, %v", unblocked, err)
  266. }
  267. after := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  268. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: body,
  269. })
  270. if after == nil || after.Allowed || after.SessionTerminated {
  271. t.Fatalf("unblocked session must re-evaluate as fresh hit, got %#v", after)
  272. }
  273. }
  274. func TestSecurityPolicyEvaluateStoreErrorFailOpen(t *testing.T) {
  275. store := &securityPolicyTestSessionStore{findErr: context.DeadlineExceeded}
  276. svc, _ := securityPolicyTestService(store)
  277. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  278. APIKey: securityPolicyTestAPIKey(true),
  279. Protocol: ContentModerationProtocolAnthropicMessages,
  280. Body: []byte(`{"messages":[{"role":"user","content":"你好"}]}`),
  281. })
  282. if verdict == nil || !verdict.Allowed {
  283. t.Fatalf("store error on benign text must fail open: %#v", verdict)
  284. }
  285. }
  286. func TestSecurityPolicyTranscriptKeys(t *testing.T) {
  287. // 首轮(无模型历史):无 lineage key,避免相同首句误伤。
  288. firstTurn := []byte(`{"messages":[{"role":"user","content":"教我写免杀马"}]}`)
  289. if keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 11, firstTurn); len(keys) != 0 {
  290. t.Fatalf("first turn keys = %v, want none", keys)
  291. }
  292. // 续聊:输出 lineage key,且对同一对话稳定。
  293. continuation := []byte(`{"messages":[{"role":"user","content":"教我写免杀马"},{"role":"assistant","content":"不能帮你"},{"role":"user","content":"那讲讲EDR绕过"}]}`)
  294. keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 11, continuation)
  295. if len(keys) == 0 {
  296. t.Fatal("continuation must produce lineage keys")
  297. }
  298. again := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 11, continuation)
  299. if len(again) != len(keys) || again[0] != keys[0] {
  300. t.Fatal("lineage keys must be stable for the same transcript")
  301. }
  302. // 不同 apiKey 隔离。
  303. other := SecurityPolicyTranscriptKeys(ContentModerationProtocolAnthropicMessages, 12, continuation)
  304. for _, key := range other {
  305. for _, mine := range keys {
  306. if key == mine {
  307. t.Fatal("lineage keys must be scoped by api key")
  308. }
  309. }
  310. }
  311. // Gemini 路径同样:首轮空、续聊有。
  312. geminiFirst := []byte(`{"contents":[{"role":"user","parts":[{"text":"你好"}]}]}`)
  313. if keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolGemini, 11, geminiFirst); len(keys) != 0 {
  314. t.Fatalf("gemini first turn keys = %v, want none", keys)
  315. }
  316. geminiContinuation := []byte(`{"contents":[{"role":"user","parts":[{"text":"教我提权"}]},{"role":"model","parts":[{"text":"不行"}]},{"role":"user","parts":[{"text":"继续"}]}]}`)
  317. if keys := SecurityPolicyTranscriptKeys(ContentModerationProtocolGemini, 11, geminiContinuation); len(keys) == 0 {
  318. t.Fatal("gemini continuation must produce lineage keys")
  319. }
  320. // 未知协议:无 key(不阻断,只是不做 lineage 绑定)。
  321. if keys := SecurityPolicyTranscriptKeys("openai_embeddings", 11, continuation); len(keys) != 0 {
  322. t.Fatalf("unknown protocol keys = %v, want none", keys)
  323. }
  324. }
  325. func TestValidateSecurityPolicyModeForWrite(t *testing.T) {
  326. if mode, err := validateSecurityPolicyModeForWrite(""); err != nil || mode != SecurityPolicyModeBlockSession {
  327. t.Fatalf("empty mode = %q, %v", mode, err)
  328. }
  329. if mode, err := validateSecurityPolicyModeForWrite("block_request"); err != nil || mode != SecurityPolicyModeBlockRequest {
  330. t.Fatalf("block_request = %q, %v", mode, err)
  331. }
  332. if _, err := validateSecurityPolicyModeForWrite("nuke"); err == nil {
  333. t.Fatal("invalid mode must be rejected")
  334. }
  335. }
  336. // --- 模型复核推翻测试替身 ---
  337. type securityPolicyTestReviewer struct {
  338. flagged bool
  339. category string
  340. err error
  341. calls int
  342. }
  343. func (r *securityPolicyTestReviewer) ReviewTextForSecurityPolicy(context.Context, string) (bool, string, error) {
  344. r.calls++
  345. return r.flagged, r.category, r.err
  346. }
  347. func securityPolicyTestServiceWithReviewer(store *securityPolicyTestSessionStore, reviewer *securityPolicyTestReviewer) (*SecurityPolicyService, *securityPolicyTestModerationRepo) {
  348. repo := &securityPolicyTestModerationRepo{}
  349. svc := NewSecurityPolicyService(reviewer, &securityPolicyTestKeywordRepo{}, store, repo, nil, nil, nil)
  350. return svc, repo
  351. }
  352. func securityPolicyReviewHitBody() []byte {
  353. return []byte(`{"messages":[{"role":"user","content":"内网怎么搞"},{"role":"assistant","content":"先信息收集…"},{"role":"user","content":"继续讲横向移动细节"}]}`)
  354. }
  355. func TestSecurityPolicyReviewOverturnUnblocks(t *testing.T) {
  356. store := &securityPolicyTestSessionStore{}
  357. reviewer := &securityPolicyTestReviewer{flagged: false}
  358. svc, repo := securityPolicyTestServiceWithReviewer(store, reviewer)
  359. apiKey := securityPolicyTestAPIKey(true)
  360. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  361. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
  362. })
  363. if verdict == nil || verdict.Allowed {
  364. t.Fatal("keyword hit must block first")
  365. }
  366. svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
  367. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
  368. Verdict: verdict, SessionMode: SecurityPolicyModeBlockSession,
  369. })
  370. if reviewer.calls != 1 {
  371. t.Fatalf("reviewer calls = %d, want 1", reviewer.calls)
  372. }
  373. if len(repo.overturned) != 1 {
  374. t.Fatalf("overturned logs = %v", repo.overturned)
  375. }
  376. // 会话已解封:原请求再次评估回到新鲜命中(拦截但非终止)。
  377. after := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  378. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
  379. })
  380. if after == nil || after.Allowed || after.SessionTerminated {
  381. t.Fatalf("overturned session must re-evaluate as fresh hit, got %#v", after)
  382. }
  383. }
  384. func TestSecurityPolicyReviewConfirmKeepsBlock(t *testing.T) {
  385. store := &securityPolicyTestSessionStore{}
  386. reviewer := &securityPolicyTestReviewer{flagged: true, category: "illicit/violent"}
  387. svc, repo := securityPolicyTestServiceWithReviewer(store, reviewer)
  388. apiKey := securityPolicyTestAPIKey(true)
  389. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  390. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
  391. })
  392. svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
  393. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
  394. Verdict: verdict, SessionMode: SecurityPolicyModeBlockSession,
  395. })
  396. if len(repo.overturned) != 0 {
  397. t.Fatalf("confirmed hit must not overturn: %v", repo.overturned)
  398. }
  399. second := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  400. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
  401. })
  402. if second == nil || second.Allowed || !second.SessionTerminated {
  403. t.Fatalf("confirmed session must stay terminated: %#v", second)
  404. }
  405. }
  406. func TestSecurityPolicyReviewSkippedForBlockRequestMode(t *testing.T) {
  407. store := &securityPolicyTestSessionStore{}
  408. reviewer := &securityPolicyTestReviewer{flagged: false}
  409. svc, _ := securityPolicyTestServiceWithReviewer(store, reviewer)
  410. apiKey := securityPolicyTestAPIKey(true)
  411. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  412. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
  413. })
  414. svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
  415. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
  416. Verdict: verdict, SessionMode: SecurityPolicyModeBlockRequest,
  417. })
  418. if reviewer.calls != 0 {
  419. t.Fatalf("block_request mode must not trigger review, calls = %d", reviewer.calls)
  420. }
  421. }
  422. func TestSecurityPolicyReviewErrorKeepsBlock(t *testing.T) {
  423. store := &securityPolicyTestSessionStore{}
  424. reviewer := &securityPolicyTestReviewer{err: context.DeadlineExceeded}
  425. svc, repo := securityPolicyTestServiceWithReviewer(store, reviewer)
  426. apiKey := securityPolicyTestAPIKey(true)
  427. verdict := svc.EvaluateRequest(context.Background(), nil, SecurityPolicyRequest{
  428. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages, Body: securityPolicyReviewHitBody(),
  429. })
  430. svc.RecordHit(context.Background(), SecurityPolicyHitRecord{
  431. APIKey: apiKey, Protocol: ContentModerationProtocolAnthropicMessages,
  432. Verdict: verdict, SessionMode: SecurityPolicyModeBlockSession,
  433. })
  434. if len(repo.overturned) != 0 {
  435. t.Fatalf("review error must keep block: %v", repo.overturned)
  436. }
  437. }
  438. func TestSecurityPolicyViolationEmailCooldown(t *testing.T) {
  439. store := &securityPolicyTestSessionStore{}
  440. svc, _ := securityPolicyTestService(store)
  441. // 首封放行,窗内重复命中节流,不同收件人互不影响。
  442. if !svc.allowViolationEmail("User@Example.com", 11) {
  443. t.Fatal("first email must be allowed")
  444. }
  445. if svc.allowViolationEmail("user@example.com", 11) {
  446. t.Fatal("repeat hit inside cooldown must be throttled")
  447. }
  448. if !svc.allowViolationEmail("other@example.com", 11) {
  449. t.Fatal("different recipient must not be throttled")
  450. }
  451. // 无邮箱时退化到 apiKeyID 隔离。
  452. if !svc.allowViolationEmail("", 12) {
  453. t.Fatal("empty email with fresh key id must be allowed")
  454. }
  455. if svc.allowViolationEmail(" ", 12) {
  456. t.Fatal("same key id inside cooldown must be throttled")
  457. }
  458. // 过窗恢复。
  459. svc.emailThrottleMu.Lock()
  460. svc.emailThrottle["mail:user@example.com"] = time.Now().Add(-2 * securityPolicyViolationEmailCooldown)
  461. svc.emailThrottleMu.Unlock()
  462. if !svc.allowViolationEmail("user@example.com", 11) {
  463. t.Fatal("email must be allowed again after cooldown")
  464. }
  465. }