mcp-rubber-duck 1.8.0 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/semantic-release.yml +12 -1
- package/.releaserc.json +6 -1
- package/CHANGELOG.md +30 -0
- package/README.md +158 -1
- package/audit-ci.json +3 -1
- package/dist/config/config.d.ts +2 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +144 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +1084 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +59 -0
- package/dist/config/types.js.map +1 -1
- package/dist/guardrails/context.d.ts +10 -0
- package/dist/guardrails/context.d.ts.map +1 -0
- package/dist/guardrails/context.js +35 -0
- package/dist/guardrails/context.js.map +1 -0
- package/dist/guardrails/errors.d.ts +26 -0
- package/dist/guardrails/errors.d.ts.map +1 -0
- package/dist/guardrails/errors.js +42 -0
- package/dist/guardrails/errors.js.map +1 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.d.ts.map +1 -0
- package/dist/guardrails/index.js +11 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/guardrails/plugins/base-plugin.d.ts +35 -0
- package/dist/guardrails/plugins/base-plugin.d.ts.map +1 -0
- package/dist/guardrails/plugins/base-plugin.js +70 -0
- package/dist/guardrails/plugins/base-plugin.js.map +1 -0
- package/dist/guardrails/plugins/index.d.ts +6 -0
- package/dist/guardrails/plugins/index.d.ts.map +1 -0
- package/dist/guardrails/plugins/index.js +6 -0
- package/dist/guardrails/plugins/index.js.map +1 -0
- package/dist/guardrails/plugins/pattern-blocker.d.ts +27 -0
- package/dist/guardrails/plugins/pattern-blocker.d.ts.map +1 -0
- package/dist/guardrails/plugins/pattern-blocker.js +140 -0
- package/dist/guardrails/plugins/pattern-blocker.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.d.ts +40 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.js +134 -0
- package/dist/guardrails/plugins/pii-redactor/detectors.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/index.d.ts +28 -0
- package/dist/guardrails/plugins/pii-redactor/index.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/index.js +157 -0
- package/dist/guardrails/plugins/pii-redactor/index.js.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts +33 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.d.ts.map +1 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js +70 -0
- package/dist/guardrails/plugins/pii-redactor/pseudonymizer.js.map +1 -0
- package/dist/guardrails/plugins/rate-limiter.d.ts +28 -0
- package/dist/guardrails/plugins/rate-limiter.d.ts.map +1 -0
- package/dist/guardrails/plugins/rate-limiter.js +91 -0
- package/dist/guardrails/plugins/rate-limiter.js.map +1 -0
- package/dist/guardrails/plugins/token-limiter.d.ts +30 -0
- package/dist/guardrails/plugins/token-limiter.d.ts.map +1 -0
- package/dist/guardrails/plugins/token-limiter.js +98 -0
- package/dist/guardrails/plugins/token-limiter.js.map +1 -0
- package/dist/guardrails/service.d.ts +38 -0
- package/dist/guardrails/service.d.ts.map +1 -0
- package/dist/guardrails/service.js +183 -0
- package/dist/guardrails/service.js.map +1 -0
- package/dist/guardrails/types.d.ts +96 -0
- package/dist/guardrails/types.d.ts.map +1 -0
- package/dist/guardrails/types.js +2 -0
- package/dist/guardrails/types.js.map +1 -0
- package/dist/providers/duck-provider-enhanced.d.ts +2 -1
- package/dist/providers/duck-provider-enhanced.d.ts.map +1 -1
- package/dist/providers/duck-provider-enhanced.js +55 -6
- package/dist/providers/duck-provider-enhanced.js.map +1 -1
- package/dist/providers/enhanced-manager.d.ts +2 -1
- package/dist/providers/enhanced-manager.d.ts.map +1 -1
- package/dist/providers/enhanced-manager.js +3 -3
- package/dist/providers/enhanced-manager.js.map +1 -1
- package/dist/providers/manager.d.ts +3 -1
- package/dist/providers/manager.d.ts.map +1 -1
- package/dist/providers/manager.js +4 -2
- package/dist/providers/manager.js.map +1 -1
- package/dist/providers/provider.d.ts +3 -1
- package/dist/providers/provider.d.ts.map +1 -1
- package/dist/providers/provider.js +43 -3
- package/dist/providers/provider.js.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +28 -6
- package/dist/server.js.map +1 -1
- package/dist/services/function-bridge.d.ts +3 -1
- package/dist/services/function-bridge.d.ts.map +1 -1
- package/dist/services/function-bridge.js +40 -1
- package/dist/services/function-bridge.js.map +1 -1
- package/package.json +5 -1
- package/src/config/config.ts +187 -1
- package/src/config/types.ts +73 -0
- package/src/guardrails/context.ts +37 -0
- package/src/guardrails/errors.ts +46 -0
- package/src/guardrails/index.ts +20 -0
- package/src/guardrails/plugins/base-plugin.ts +103 -0
- package/src/guardrails/plugins/index.ts +5 -0
- package/src/guardrails/plugins/pattern-blocker.ts +190 -0
- package/src/guardrails/plugins/pii-redactor/detectors.ts +200 -0
- package/src/guardrails/plugins/pii-redactor/index.ts +203 -0
- package/src/guardrails/plugins/pii-redactor/pseudonymizer.ts +91 -0
- package/src/guardrails/plugins/rate-limiter.ts +142 -0
- package/src/guardrails/plugins/token-limiter.ts +155 -0
- package/src/guardrails/service.ts +209 -0
- package/src/guardrails/types.ts +120 -0
- package/src/providers/duck-provider-enhanced.ts +76 -7
- package/src/providers/enhanced-manager.ts +5 -3
- package/src/providers/manager.ts +6 -3
- package/src/providers/provider.ts +57 -6
- package/src/server.ts +32 -6
- package/src/services/function-bridge.ts +53 -2
- package/tests/guardrails/config.test.ts +267 -0
- package/tests/guardrails/errors.test.ts +109 -0
- package/tests/guardrails/plugins/pattern-blocker.test.ts +309 -0
- package/tests/guardrails/plugins/pii-redactor.test.ts +1004 -0
- package/tests/guardrails/plugins/rate-limiter.test.ts +310 -0
- package/tests/guardrails/plugins/token-limiter.test.ts +216 -0
- package/tests/guardrails/service.test.ts +911 -0
- package/tests/mcp-bridge.test.ts +248 -0
- package/tests/providers.test.ts +739 -0
|
@@ -0,0 +1,1004 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
2
|
+
import { PIIRedactorPlugin } from '../../../src/guardrails/plugins/pii-redactor';
|
|
3
|
+
import { PIIDetector } from '../../../src/guardrails/plugins/pii-redactor/detectors';
|
|
4
|
+
import { Pseudonymizer } from '../../../src/guardrails/plugins/pii-redactor/pseudonymizer';
|
|
5
|
+
import { createGuardrailContext } from '../../../src/guardrails/context';
|
|
6
|
+
|
|
7
|
+
// Mock logger to avoid console noise during tests
|
|
8
|
+
jest.mock('../../../src/utils/logger');
|
|
9
|
+
|
|
10
|
+
describe('PIIDetector', () => {
|
|
11
|
+
describe('email detection', () => {
|
|
12
|
+
it('should detect email addresses', () => {
|
|
13
|
+
const detector = new PIIDetector({
|
|
14
|
+
detectEmails: true,
|
|
15
|
+
detectPhones: false,
|
|
16
|
+
detectSSN: false,
|
|
17
|
+
detectAPIKeys: false,
|
|
18
|
+
detectCreditCards: false,
|
|
19
|
+
detectIPAddresses: false,
|
|
20
|
+
customPatterns: [],
|
|
21
|
+
allowlist: [],
|
|
22
|
+
allowlistDomains: [],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const text = 'Contact me at john.doe@example.com for more info';
|
|
26
|
+
const detections = detector.detect(text);
|
|
27
|
+
|
|
28
|
+
expect(detections).toHaveLength(1);
|
|
29
|
+
expect(detections[0].type).toBe('email');
|
|
30
|
+
expect(detections[0].value).toBe('john.doe@example.com');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should detect multiple email addresses', () => {
|
|
34
|
+
const detector = new PIIDetector({
|
|
35
|
+
detectEmails: true,
|
|
36
|
+
detectPhones: false,
|
|
37
|
+
detectSSN: false,
|
|
38
|
+
detectAPIKeys: false,
|
|
39
|
+
detectCreditCards: false,
|
|
40
|
+
detectIPAddresses: false,
|
|
41
|
+
customPatterns: [],
|
|
42
|
+
allowlist: [],
|
|
43
|
+
allowlistDomains: [],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const text = 'Email alice@example.com or bob@company.org';
|
|
47
|
+
const detections = detector.detect(text);
|
|
48
|
+
|
|
49
|
+
expect(detections).toHaveLength(2);
|
|
50
|
+
expect(detections.map((d) => d.value)).toEqual(['alice@example.com', 'bob@company.org']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should not detect emails when disabled', () => {
|
|
54
|
+
const detector = new PIIDetector({
|
|
55
|
+
detectEmails: false,
|
|
56
|
+
detectPhones: false,
|
|
57
|
+
detectSSN: false,
|
|
58
|
+
detectAPIKeys: false,
|
|
59
|
+
detectCreditCards: false,
|
|
60
|
+
detectIPAddresses: false,
|
|
61
|
+
customPatterns: [],
|
|
62
|
+
allowlist: [],
|
|
63
|
+
allowlistDomains: [],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const text = 'Contact me at john@example.com';
|
|
67
|
+
const detections = detector.detect(text);
|
|
68
|
+
|
|
69
|
+
expect(detections).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('phone detection', () => {
|
|
74
|
+
it('should detect US phone numbers with dashes', () => {
|
|
75
|
+
const detector = new PIIDetector({
|
|
76
|
+
detectEmails: false,
|
|
77
|
+
detectPhones: true,
|
|
78
|
+
detectSSN: false,
|
|
79
|
+
detectAPIKeys: false,
|
|
80
|
+
detectCreditCards: false,
|
|
81
|
+
detectIPAddresses: false,
|
|
82
|
+
customPatterns: [],
|
|
83
|
+
allowlist: [],
|
|
84
|
+
allowlistDomains: [],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const text = 'Call me at 555-123-4567';
|
|
88
|
+
const detections = detector.detect(text);
|
|
89
|
+
|
|
90
|
+
expect(detections).toHaveLength(1);
|
|
91
|
+
expect(detections[0].type).toBe('phone');
|
|
92
|
+
expect(detections[0].value).toBe('555-123-4567');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should detect phone numbers with parentheses', () => {
|
|
96
|
+
const detector = new PIIDetector({
|
|
97
|
+
detectEmails: false,
|
|
98
|
+
detectPhones: true,
|
|
99
|
+
detectSSN: false,
|
|
100
|
+
detectAPIKeys: false,
|
|
101
|
+
detectCreditCards: false,
|
|
102
|
+
detectIPAddresses: false,
|
|
103
|
+
customPatterns: [],
|
|
104
|
+
allowlist: [],
|
|
105
|
+
allowlistDomains: [],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const text = 'Call me at (555) 123-4567';
|
|
109
|
+
const detections = detector.detect(text);
|
|
110
|
+
|
|
111
|
+
expect(detections).toHaveLength(1);
|
|
112
|
+
expect(detections[0].type).toBe('phone');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should detect phone numbers with country code', () => {
|
|
116
|
+
const detector = new PIIDetector({
|
|
117
|
+
detectEmails: false,
|
|
118
|
+
detectPhones: true,
|
|
119
|
+
detectSSN: false,
|
|
120
|
+
detectAPIKeys: false,
|
|
121
|
+
detectCreditCards: false,
|
|
122
|
+
detectIPAddresses: false,
|
|
123
|
+
customPatterns: [],
|
|
124
|
+
allowlist: [],
|
|
125
|
+
allowlistDomains: [],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const text = 'International: +1-555-123-4567';
|
|
129
|
+
const detections = detector.detect(text);
|
|
130
|
+
|
|
131
|
+
expect(detections).toHaveLength(1);
|
|
132
|
+
expect(detections[0].type).toBe('phone');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('SSN detection', () => {
|
|
137
|
+
it('should detect SSN with dashes', () => {
|
|
138
|
+
const detector = new PIIDetector({
|
|
139
|
+
detectEmails: false,
|
|
140
|
+
detectPhones: false,
|
|
141
|
+
detectSSN: true,
|
|
142
|
+
detectAPIKeys: false,
|
|
143
|
+
detectCreditCards: false,
|
|
144
|
+
detectIPAddresses: false,
|
|
145
|
+
customPatterns: [],
|
|
146
|
+
allowlist: [],
|
|
147
|
+
allowlistDomains: [],
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const text = 'SSN: 123-45-6789';
|
|
151
|
+
const detections = detector.detect(text);
|
|
152
|
+
|
|
153
|
+
expect(detections).toHaveLength(1);
|
|
154
|
+
expect(detections[0].type).toBe('ssn');
|
|
155
|
+
expect(detections[0].value).toBe('123-45-6789');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should detect SSN with spaces', () => {
|
|
159
|
+
const detector = new PIIDetector({
|
|
160
|
+
detectEmails: false,
|
|
161
|
+
detectPhones: false,
|
|
162
|
+
detectSSN: true,
|
|
163
|
+
detectAPIKeys: false,
|
|
164
|
+
detectCreditCards: false,
|
|
165
|
+
detectIPAddresses: false,
|
|
166
|
+
customPatterns: [],
|
|
167
|
+
allowlist: [],
|
|
168
|
+
allowlistDomains: [],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const text = 'SSN: 123 45 6789';
|
|
172
|
+
const detections = detector.detect(text);
|
|
173
|
+
|
|
174
|
+
expect(detections).toHaveLength(1);
|
|
175
|
+
expect(detections[0].type).toBe('ssn');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('API key detection', () => {
|
|
180
|
+
it('should detect OpenAI API keys (sk-)', () => {
|
|
181
|
+
const detector = new PIIDetector({
|
|
182
|
+
detectEmails: false,
|
|
183
|
+
detectPhones: false,
|
|
184
|
+
detectSSN: false,
|
|
185
|
+
detectAPIKeys: true,
|
|
186
|
+
detectCreditCards: false,
|
|
187
|
+
detectIPAddresses: false,
|
|
188
|
+
customPatterns: [],
|
|
189
|
+
allowlist: [],
|
|
190
|
+
allowlistDomains: [],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const text = 'API Key: sk-abc123def456ghi789jkl0';
|
|
194
|
+
const detections = detector.detect(text);
|
|
195
|
+
|
|
196
|
+
expect(detections).toHaveLength(1);
|
|
197
|
+
expect(detections[0].type).toBe('api_key');
|
|
198
|
+
expect(detections[0].value.startsWith('sk-')).toBe(true);
|
|
199
|
+
expect(detections[0].confidence).toBe(0.95);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should detect Groq API keys (gsk_)', () => {
|
|
203
|
+
const detector = new PIIDetector({
|
|
204
|
+
detectEmails: false,
|
|
205
|
+
detectPhones: false,
|
|
206
|
+
detectSSN: false,
|
|
207
|
+
detectAPIKeys: true,
|
|
208
|
+
detectCreditCards: false,
|
|
209
|
+
detectIPAddresses: false,
|
|
210
|
+
customPatterns: [],
|
|
211
|
+
allowlist: [],
|
|
212
|
+
allowlistDomains: [],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const text = 'Groq key: gsk_abc123def456ghi789jkl0';
|
|
216
|
+
const detections = detector.detect(text);
|
|
217
|
+
|
|
218
|
+
expect(detections).toHaveLength(1);
|
|
219
|
+
expect(detections[0].type).toBe('api_key');
|
|
220
|
+
expect(detections[0].value.startsWith('gsk_')).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should detect generic API keys with lower confidence', () => {
|
|
224
|
+
const detector = new PIIDetector({
|
|
225
|
+
detectEmails: false,
|
|
226
|
+
detectPhones: false,
|
|
227
|
+
detectSSN: false,
|
|
228
|
+
detectAPIKeys: true,
|
|
229
|
+
detectCreditCards: false,
|
|
230
|
+
detectIPAddresses: false,
|
|
231
|
+
customPatterns: [],
|
|
232
|
+
allowlist: [],
|
|
233
|
+
allowlistDomains: [],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Generic API key pattern (api_key + 16+ chars, not sk- or gsk_)
|
|
237
|
+
const text = 'Generic: api_key_abc123def456ghi7';
|
|
238
|
+
const detections = detector.detect(text);
|
|
239
|
+
|
|
240
|
+
expect(detections).toHaveLength(1);
|
|
241
|
+
expect(detections[0].type).toBe('api_key');
|
|
242
|
+
// Should have lower confidence (0.7) for generic keys
|
|
243
|
+
expect(detections[0].confidence).toBe(0.7);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('credit card detection', () => {
|
|
248
|
+
it('should detect Visa card numbers', () => {
|
|
249
|
+
const detector = new PIIDetector({
|
|
250
|
+
detectEmails: false,
|
|
251
|
+
detectPhones: false,
|
|
252
|
+
detectSSN: false,
|
|
253
|
+
detectAPIKeys: false,
|
|
254
|
+
detectCreditCards: true,
|
|
255
|
+
detectIPAddresses: false,
|
|
256
|
+
customPatterns: [],
|
|
257
|
+
allowlist: [],
|
|
258
|
+
allowlistDomains: [],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const text = 'Card: 4111111111111111';
|
|
262
|
+
const detections = detector.detect(text);
|
|
263
|
+
|
|
264
|
+
expect(detections).toHaveLength(1);
|
|
265
|
+
expect(detections[0].type).toBe('credit_card');
|
|
266
|
+
expect(detections[0].value).toBe('4111111111111111');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should detect Mastercard numbers', () => {
|
|
270
|
+
const detector = new PIIDetector({
|
|
271
|
+
detectEmails: false,
|
|
272
|
+
detectPhones: false,
|
|
273
|
+
detectSSN: false,
|
|
274
|
+
detectAPIKeys: false,
|
|
275
|
+
detectCreditCards: true,
|
|
276
|
+
detectIPAddresses: false,
|
|
277
|
+
customPatterns: [],
|
|
278
|
+
allowlist: [],
|
|
279
|
+
allowlistDomains: [],
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const text = 'Card: 5500000000000004';
|
|
283
|
+
const detections = detector.detect(text);
|
|
284
|
+
|
|
285
|
+
expect(detections).toHaveLength(1);
|
|
286
|
+
expect(detections[0].type).toBe('credit_card');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should detect American Express numbers', () => {
|
|
290
|
+
const detector = new PIIDetector({
|
|
291
|
+
detectEmails: false,
|
|
292
|
+
detectPhones: false,
|
|
293
|
+
detectSSN: false,
|
|
294
|
+
detectAPIKeys: false,
|
|
295
|
+
detectCreditCards: true,
|
|
296
|
+
detectIPAddresses: false,
|
|
297
|
+
customPatterns: [],
|
|
298
|
+
allowlist: [],
|
|
299
|
+
allowlistDomains: [],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const text = 'Amex: 340000000000009';
|
|
303
|
+
const detections = detector.detect(text);
|
|
304
|
+
|
|
305
|
+
expect(detections).toHaveLength(1);
|
|
306
|
+
expect(detections[0].type).toBe('credit_card');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('IP address detection', () => {
|
|
311
|
+
it('should detect IPv4 addresses', () => {
|
|
312
|
+
const detector = new PIIDetector({
|
|
313
|
+
detectEmails: false,
|
|
314
|
+
detectPhones: false,
|
|
315
|
+
detectSSN: false,
|
|
316
|
+
detectAPIKeys: false,
|
|
317
|
+
detectCreditCards: false,
|
|
318
|
+
detectIPAddresses: true,
|
|
319
|
+
customPatterns: [],
|
|
320
|
+
allowlist: [],
|
|
321
|
+
allowlistDomains: [],
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const text = 'Server IP: 192.168.1.100';
|
|
325
|
+
const detections = detector.detect(text);
|
|
326
|
+
|
|
327
|
+
expect(detections).toHaveLength(1);
|
|
328
|
+
expect(detections[0].type).toBe('ip_address');
|
|
329
|
+
expect(detections[0].value).toBe('192.168.1.100');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should not detect invalid IP addresses', () => {
|
|
333
|
+
const detector = new PIIDetector({
|
|
334
|
+
detectEmails: false,
|
|
335
|
+
detectPhones: false,
|
|
336
|
+
detectSSN: false,
|
|
337
|
+
detectAPIKeys: false,
|
|
338
|
+
detectCreditCards: false,
|
|
339
|
+
detectIPAddresses: true,
|
|
340
|
+
customPatterns: [],
|
|
341
|
+
allowlist: [],
|
|
342
|
+
allowlistDomains: [],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const text = 'Invalid: 999.999.999.999';
|
|
346
|
+
const detections = detector.detect(text);
|
|
347
|
+
|
|
348
|
+
expect(detections).toHaveLength(0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('allowlist', () => {
|
|
353
|
+
it('should not detect allowlisted emails', () => {
|
|
354
|
+
const detector = new PIIDetector({
|
|
355
|
+
detectEmails: true,
|
|
356
|
+
detectPhones: false,
|
|
357
|
+
detectSSN: false,
|
|
358
|
+
detectAPIKeys: false,
|
|
359
|
+
detectCreditCards: false,
|
|
360
|
+
detectIPAddresses: false,
|
|
361
|
+
customPatterns: [],
|
|
362
|
+
allowlist: ['support@company.com'],
|
|
363
|
+
allowlistDomains: [],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const text = 'Contact support@company.com or sales@company.com';
|
|
367
|
+
const detections = detector.detect(text);
|
|
368
|
+
|
|
369
|
+
expect(detections).toHaveLength(1);
|
|
370
|
+
expect(detections[0].value).toBe('sales@company.com');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should be case insensitive for allowlist', () => {
|
|
374
|
+
const detector = new PIIDetector({
|
|
375
|
+
detectEmails: true,
|
|
376
|
+
detectPhones: false,
|
|
377
|
+
detectSSN: false,
|
|
378
|
+
detectAPIKeys: false,
|
|
379
|
+
detectCreditCards: false,
|
|
380
|
+
detectIPAddresses: false,
|
|
381
|
+
customPatterns: [],
|
|
382
|
+
allowlist: ['SUPPORT@COMPANY.COM'],
|
|
383
|
+
allowlistDomains: [],
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const text = 'Contact support@company.com';
|
|
387
|
+
const detections = detector.detect(text);
|
|
388
|
+
|
|
389
|
+
expect(detections).toHaveLength(0);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('domain allowlist', () => {
|
|
394
|
+
it('should not detect emails from allowlisted domains', () => {
|
|
395
|
+
const detector = new PIIDetector({
|
|
396
|
+
detectEmails: true,
|
|
397
|
+
detectPhones: false,
|
|
398
|
+
detectSSN: false,
|
|
399
|
+
detectAPIKeys: false,
|
|
400
|
+
detectCreditCards: false,
|
|
401
|
+
detectIPAddresses: false,
|
|
402
|
+
customPatterns: [],
|
|
403
|
+
allowlist: [],
|
|
404
|
+
allowlistDomains: ['company.com'],
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const text = 'Contact anyone@company.com or external@gmail.com';
|
|
408
|
+
const detections = detector.detect(text);
|
|
409
|
+
|
|
410
|
+
expect(detections).toHaveLength(1);
|
|
411
|
+
expect(detections[0].value).toBe('external@gmail.com');
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('custom patterns', () => {
|
|
416
|
+
it('should detect custom patterns', () => {
|
|
417
|
+
const detector = new PIIDetector({
|
|
418
|
+
detectEmails: false,
|
|
419
|
+
detectPhones: false,
|
|
420
|
+
detectSSN: false,
|
|
421
|
+
detectAPIKeys: false,
|
|
422
|
+
detectCreditCards: false,
|
|
423
|
+
detectIPAddresses: false,
|
|
424
|
+
customPatterns: [
|
|
425
|
+
{ name: 'employee_id', pattern: 'EMP-[0-9]{6}', placeholder: 'EMPLOYEE' },
|
|
426
|
+
],
|
|
427
|
+
allowlist: [],
|
|
428
|
+
allowlistDomains: [],
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const text = 'Employee ID: EMP-123456';
|
|
432
|
+
const detections = detector.detect(text);
|
|
433
|
+
|
|
434
|
+
expect(detections).toHaveLength(1);
|
|
435
|
+
expect(detections[0].type).toBe('custom');
|
|
436
|
+
expect(detections[0].value).toBe('EMP-123456');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should not detect custom patterns that are in allowlist', () => {
|
|
440
|
+
const detector = new PIIDetector({
|
|
441
|
+
detectEmails: false,
|
|
442
|
+
detectPhones: false,
|
|
443
|
+
detectSSN: false,
|
|
444
|
+
detectAPIKeys: false,
|
|
445
|
+
detectCreditCards: false,
|
|
446
|
+
detectIPAddresses: false,
|
|
447
|
+
customPatterns: [
|
|
448
|
+
{ name: 'employee_id', pattern: 'EMP-[0-9]{6}', placeholder: 'EMPLOYEE' },
|
|
449
|
+
],
|
|
450
|
+
allowlist: ['EMP-123456'], // This employee ID is allowlisted
|
|
451
|
+
allowlistDomains: [],
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const text = 'Employee ID: EMP-123456 and EMP-789012';
|
|
455
|
+
const detections = detector.detect(text);
|
|
456
|
+
|
|
457
|
+
// Only EMP-789012 should be detected (EMP-123456 is allowlisted)
|
|
458
|
+
expect(detections).toHaveLength(1);
|
|
459
|
+
expect(detections[0].value).toBe('EMP-789012');
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should skip invalid regex patterns', () => {
|
|
463
|
+
const detector = new PIIDetector({
|
|
464
|
+
detectEmails: false,
|
|
465
|
+
detectPhones: false,
|
|
466
|
+
detectSSN: false,
|
|
467
|
+
detectAPIKeys: false,
|
|
468
|
+
detectCreditCards: false,
|
|
469
|
+
detectIPAddresses: false,
|
|
470
|
+
customPatterns: [
|
|
471
|
+
{ name: 'invalid', pattern: '[invalid(regex', placeholder: 'X' },
|
|
472
|
+
],
|
|
473
|
+
allowlist: [],
|
|
474
|
+
allowlistDomains: [],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const text = 'Some text';
|
|
478
|
+
const detections = detector.detect(text);
|
|
479
|
+
|
|
480
|
+
expect(detections).toHaveLength(0);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('multiple detections', () => {
|
|
485
|
+
it('should detect multiple PII types in same text', () => {
|
|
486
|
+
const detector = new PIIDetector({
|
|
487
|
+
detectEmails: true,
|
|
488
|
+
detectPhones: true,
|
|
489
|
+
detectSSN: true,
|
|
490
|
+
detectAPIKeys: false,
|
|
491
|
+
detectCreditCards: false,
|
|
492
|
+
detectIPAddresses: false,
|
|
493
|
+
customPatterns: [],
|
|
494
|
+
allowlist: [],
|
|
495
|
+
allowlistDomains: [],
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const text = 'Contact john@example.com at 555-123-4567. SSN: 123-45-6789';
|
|
499
|
+
const detections = detector.detect(text);
|
|
500
|
+
|
|
501
|
+
expect(detections).toHaveLength(3);
|
|
502
|
+
expect(detections.map((d) => d.type).sort()).toEqual(['email', 'phone', 'ssn']);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should return detections sorted by position', () => {
|
|
506
|
+
const detector = new PIIDetector({
|
|
507
|
+
detectEmails: true,
|
|
508
|
+
detectPhones: true,
|
|
509
|
+
detectSSN: false,
|
|
510
|
+
detectAPIKeys: false,
|
|
511
|
+
detectCreditCards: false,
|
|
512
|
+
detectIPAddresses: false,
|
|
513
|
+
customPatterns: [],
|
|
514
|
+
allowlist: [],
|
|
515
|
+
allowlistDomains: [],
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
const text = '555-123-4567 and john@example.com';
|
|
519
|
+
const detections = detector.detect(text);
|
|
520
|
+
|
|
521
|
+
expect(detections[0].type).toBe('phone');
|
|
522
|
+
expect(detections[1].type).toBe('email');
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe('Pseudonymizer', () => {
|
|
528
|
+
let pseudonymizer: Pseudonymizer;
|
|
529
|
+
|
|
530
|
+
beforeEach(() => {
|
|
531
|
+
pseudonymizer = new Pseudonymizer();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe('pseudonymize', () => {
|
|
535
|
+
it('should replace PII with numbered placeholders', () => {
|
|
536
|
+
const detections = [
|
|
537
|
+
{ type: 'email' as const, value: 'john@example.com', startIndex: 0, endIndex: 16, confidence: 0.9 },
|
|
538
|
+
];
|
|
539
|
+
|
|
540
|
+
const result = pseudonymizer.pseudonymize('john@example.com', detections);
|
|
541
|
+
|
|
542
|
+
expect(result.text).toBe('[EMAIL_1]');
|
|
543
|
+
expect(result.mappings.get('[EMAIL_1]')).toBe('john@example.com');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should number multiple instances of same type', () => {
|
|
547
|
+
const detections = [
|
|
548
|
+
{ type: 'email' as const, value: 'a@b.com', startIndex: 0, endIndex: 7, confidence: 0.9 },
|
|
549
|
+
{ type: 'email' as const, value: 'c@d.com', startIndex: 12, endIndex: 19, confidence: 0.9 },
|
|
550
|
+
];
|
|
551
|
+
|
|
552
|
+
const result = pseudonymizer.pseudonymize('a@b.com and c@d.com', detections);
|
|
553
|
+
|
|
554
|
+
expect(result.text).toBe('[EMAIL_1] and [EMAIL_2]');
|
|
555
|
+
expect(result.mappings.get('[EMAIL_1]')).toBe('a@b.com');
|
|
556
|
+
expect(result.mappings.get('[EMAIL_2]')).toBe('c@d.com');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should handle different PII types', () => {
|
|
560
|
+
const detections = [
|
|
561
|
+
{ type: 'email' as const, value: 'john@x.com', startIndex: 0, endIndex: 10, confidence: 0.9 },
|
|
562
|
+
{ type: 'phone' as const, value: '555-1234', startIndex: 15, endIndex: 23, confidence: 0.85 },
|
|
563
|
+
];
|
|
564
|
+
|
|
565
|
+
const result = pseudonymizer.pseudonymize('john@x.com and 555-1234', detections);
|
|
566
|
+
|
|
567
|
+
expect(result.text).toBe('[EMAIL_1] and [PHONE_1]');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should create correct placeholders for all types', () => {
|
|
571
|
+
const types = ['email', 'phone', 'ssn', 'api_key', 'credit_card', 'ip_address', 'custom'] as const;
|
|
572
|
+
const expectedLabels = ['EMAIL', 'PHONE', 'SSN', 'API_KEY', 'CARD', 'IP', 'REDACTED'];
|
|
573
|
+
|
|
574
|
+
types.forEach((type, i) => {
|
|
575
|
+
pseudonymizer.reset();
|
|
576
|
+
const detections = [
|
|
577
|
+
{ type, value: 'test', startIndex: 0, endIndex: 4, confidence: 0.9 },
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
const result = pseudonymizer.pseudonymize('test', detections);
|
|
581
|
+
expect(result.text).toBe(`[${expectedLabels[i]}_1]`);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
describe('restore', () => {
|
|
587
|
+
it('should restore placeholders to original values', () => {
|
|
588
|
+
const mappings = new Map<string, string>([
|
|
589
|
+
['[EMAIL_1]', 'john@example.com'],
|
|
590
|
+
]);
|
|
591
|
+
|
|
592
|
+
const result = pseudonymizer.restore('Contact [EMAIL_1] for help', mappings);
|
|
593
|
+
|
|
594
|
+
expect(result).toBe('Contact john@example.com for help');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should restore multiple occurrences of same placeholder', () => {
|
|
598
|
+
const mappings = new Map<string, string>([
|
|
599
|
+
['[EMAIL_1]', 'john@example.com'],
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
const result = pseudonymizer.restore('From [EMAIL_1] to [EMAIL_1]', mappings);
|
|
603
|
+
|
|
604
|
+
expect(result).toBe('From john@example.com to john@example.com');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('should restore multiple different placeholders', () => {
|
|
608
|
+
const mappings = new Map<string, string>([
|
|
609
|
+
['[EMAIL_1]', 'john@x.com'],
|
|
610
|
+
['[PHONE_1]', '555-1234'],
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
const result = pseudonymizer.restore('[EMAIL_1] at [PHONE_1]', mappings);
|
|
614
|
+
|
|
615
|
+
expect(result).toBe('john@x.com at 555-1234');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should return original text if no mappings match', () => {
|
|
619
|
+
const mappings = new Map<string, string>([
|
|
620
|
+
['[EMAIL_1]', 'john@example.com'],
|
|
621
|
+
]);
|
|
622
|
+
|
|
623
|
+
const result = pseudonymizer.restore('No placeholders here', mappings);
|
|
624
|
+
|
|
625
|
+
expect(result).toBe('No placeholders here');
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
describe('PIIRedactorPlugin', () => {
|
|
631
|
+
let plugin: PIIRedactorPlugin;
|
|
632
|
+
|
|
633
|
+
beforeEach(async () => {
|
|
634
|
+
plugin = new PIIRedactorPlugin();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe('initialization', () => {
|
|
638
|
+
it('should initialize with default values', async () => {
|
|
639
|
+
await plugin.initialize({ enabled: true });
|
|
640
|
+
|
|
641
|
+
expect(plugin.enabled).toBe(true);
|
|
642
|
+
expect(plugin.name).toBe('pii_redactor');
|
|
643
|
+
expect(plugin.phases).toContain('pre_request');
|
|
644
|
+
expect(plugin.phases).toContain('post_response');
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('should initialize detector with config', async () => {
|
|
648
|
+
await plugin.initialize({
|
|
649
|
+
enabled: true,
|
|
650
|
+
detect_emails: true,
|
|
651
|
+
detect_phones: false,
|
|
652
|
+
detect_ssn: false,
|
|
653
|
+
detect_api_keys: false,
|
|
654
|
+
detect_credit_cards: false,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const detector = plugin.getDetector();
|
|
658
|
+
const text = 'Email: test@example.com, Phone: 555-123-4567';
|
|
659
|
+
const detections = detector.detect(text);
|
|
660
|
+
|
|
661
|
+
// Should only detect email, not phone
|
|
662
|
+
expect(detections).toHaveLength(1);
|
|
663
|
+
expect(detections[0].type).toBe('email');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('should configure allowlist', async () => {
|
|
667
|
+
await plugin.initialize({
|
|
668
|
+
enabled: true,
|
|
669
|
+
detect_emails: true,
|
|
670
|
+
allowlist: ['allowed@example.com'],
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const detector = plugin.getDetector();
|
|
674
|
+
const detections = detector.detect('Contact allowed@example.com or other@example.com');
|
|
675
|
+
|
|
676
|
+
expect(detections).toHaveLength(1);
|
|
677
|
+
expect(detections[0].value).toBe('other@example.com');
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe('pre_request phase', () => {
|
|
682
|
+
it('should redact PII in prompt', async () => {
|
|
683
|
+
await plugin.initialize({
|
|
684
|
+
enabled: true,
|
|
685
|
+
detect_emails: true,
|
|
686
|
+
detect_phones: false,
|
|
687
|
+
detect_ssn: false,
|
|
688
|
+
detect_api_keys: false,
|
|
689
|
+
detect_credit_cards: false,
|
|
690
|
+
detect_ip_addresses: false,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const context = createGuardrailContext({
|
|
694
|
+
prompt: 'Contact john@example.com for help',
|
|
695
|
+
});
|
|
696
|
+
const result = await plugin.execute('pre_request', context);
|
|
697
|
+
|
|
698
|
+
expect(result.action).toBe('modify');
|
|
699
|
+
expect(context.prompt).toBe('Contact [EMAIL_1] for help');
|
|
700
|
+
expect(context.metadata.get('pii_mappings')).toBeDefined();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('should allow prompts without PII', async () => {
|
|
704
|
+
await plugin.initialize({
|
|
705
|
+
enabled: true,
|
|
706
|
+
detect_emails: true,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
const context = createGuardrailContext({
|
|
710
|
+
prompt: 'Hello world, no sensitive data here',
|
|
711
|
+
});
|
|
712
|
+
const result = await plugin.execute('pre_request', context);
|
|
713
|
+
|
|
714
|
+
expect(result.action).toBe('allow');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should record modifications', async () => {
|
|
718
|
+
await plugin.initialize({
|
|
719
|
+
enabled: true,
|
|
720
|
+
detect_emails: true,
|
|
721
|
+
detect_phones: true,
|
|
722
|
+
detect_ssn: false,
|
|
723
|
+
detect_api_keys: false,
|
|
724
|
+
detect_credit_cards: false,
|
|
725
|
+
detect_ip_addresses: false,
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const context = createGuardrailContext({
|
|
729
|
+
prompt: 'Email: a@b.com, Phone: 555-123-4567',
|
|
730
|
+
});
|
|
731
|
+
await plugin.execute('pre_request', context);
|
|
732
|
+
|
|
733
|
+
expect(context.modifications.length).toBeGreaterThan(0);
|
|
734
|
+
expect(context.modifications[0].pluginName).toBe('pii_redactor');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should update last message when present', async () => {
|
|
738
|
+
await plugin.initialize({
|
|
739
|
+
enabled: true,
|
|
740
|
+
detect_emails: true,
|
|
741
|
+
detect_phones: false,
|
|
742
|
+
detect_ssn: false,
|
|
743
|
+
detect_api_keys: false,
|
|
744
|
+
detect_credit_cards: false,
|
|
745
|
+
detect_ip_addresses: false,
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const context = createGuardrailContext({
|
|
749
|
+
prompt: 'Email: test@example.com',
|
|
750
|
+
messages: [
|
|
751
|
+
{ role: 'user', content: 'Email: test@example.com', timestamp: new Date() },
|
|
752
|
+
],
|
|
753
|
+
});
|
|
754
|
+
await plugin.execute('pre_request', context);
|
|
755
|
+
|
|
756
|
+
expect(context.messages[0].content).toBe('Email: [EMAIL_1]');
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
describe('pre_tool_input phase', () => {
|
|
761
|
+
it('should redact PII in tool arguments', async () => {
|
|
762
|
+
await plugin.initialize({
|
|
763
|
+
enabled: true,
|
|
764
|
+
detect_emails: true,
|
|
765
|
+
detect_phones: false,
|
|
766
|
+
detect_ssn: false,
|
|
767
|
+
detect_api_keys: false,
|
|
768
|
+
detect_credit_cards: false,
|
|
769
|
+
detect_ip_addresses: false,
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
const context = createGuardrailContext({
|
|
773
|
+
toolName: 'send_email',
|
|
774
|
+
toolArgs: { to: 'john@example.com', subject: 'Hello' },
|
|
775
|
+
});
|
|
776
|
+
const result = await plugin.execute('pre_tool_input', context);
|
|
777
|
+
|
|
778
|
+
expect(result.action).toBe('modify');
|
|
779
|
+
expect(JSON.stringify(context.toolArgs)).toContain('[EMAIL_1]');
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
describe('post_response phase', () => {
|
|
784
|
+
it('should not restore by default', async () => {
|
|
785
|
+
await plugin.initialize({
|
|
786
|
+
enabled: true,
|
|
787
|
+
restore_on_response: false,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const context = createGuardrailContext({
|
|
791
|
+
response: 'Contact [EMAIL_1] for help',
|
|
792
|
+
});
|
|
793
|
+
context.metadata.set('pii_mappings', new Map([['[EMAIL_1]', 'john@example.com']]));
|
|
794
|
+
|
|
795
|
+
const result = await plugin.execute('post_response', context);
|
|
796
|
+
|
|
797
|
+
expect(result.action).toBe('allow');
|
|
798
|
+
expect(context.response).toBe('Contact [EMAIL_1] for help');
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('should restore when configured', async () => {
|
|
802
|
+
await plugin.initialize({
|
|
803
|
+
enabled: true,
|
|
804
|
+
restore_on_response: true,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const context = createGuardrailContext({
|
|
808
|
+
response: 'Contact [EMAIL_1] for help',
|
|
809
|
+
});
|
|
810
|
+
context.metadata.set('pii_mappings', new Map([['[EMAIL_1]', 'john@example.com']]));
|
|
811
|
+
|
|
812
|
+
const result = await plugin.execute('post_response', context);
|
|
813
|
+
|
|
814
|
+
expect(result.action).toBe('modify');
|
|
815
|
+
expect(context.response).toBe('Contact john@example.com for help');
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('should not modify if no mappings exist', async () => {
|
|
819
|
+
await plugin.initialize({
|
|
820
|
+
enabled: true,
|
|
821
|
+
restore_on_response: true,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const context = createGuardrailContext({
|
|
825
|
+
response: 'Contact [EMAIL_1] for help',
|
|
826
|
+
});
|
|
827
|
+
// No mappings set
|
|
828
|
+
|
|
829
|
+
const result = await plugin.execute('post_response', context);
|
|
830
|
+
|
|
831
|
+
expect(result.action).toBe('allow');
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
describe('post_tool_output phase', () => {
|
|
836
|
+
it('should restore PII in tool result when configured', async () => {
|
|
837
|
+
await plugin.initialize({
|
|
838
|
+
enabled: true,
|
|
839
|
+
restore_on_response: true,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const context = createGuardrailContext({
|
|
843
|
+
toolResult: 'User email is [EMAIL_1]',
|
|
844
|
+
});
|
|
845
|
+
context.metadata.set('pii_mappings', new Map([['[EMAIL_1]', 'john@example.com']]));
|
|
846
|
+
|
|
847
|
+
const result = await plugin.execute('post_tool_output', context);
|
|
848
|
+
|
|
849
|
+
expect(result.action).toBe('modify');
|
|
850
|
+
expect(context.toolResult).toBe('User email is john@example.com');
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
describe('edge cases', () => {
|
|
855
|
+
it('should handle empty prompt', async () => {
|
|
856
|
+
await plugin.initialize({ enabled: true });
|
|
857
|
+
|
|
858
|
+
const context = createGuardrailContext({
|
|
859
|
+
prompt: '',
|
|
860
|
+
});
|
|
861
|
+
const result = await plugin.execute('pre_request', context);
|
|
862
|
+
|
|
863
|
+
expect(result.action).toBe('allow');
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('should handle undefined prompt', async () => {
|
|
867
|
+
await plugin.initialize({ enabled: true });
|
|
868
|
+
|
|
869
|
+
const context = createGuardrailContext({});
|
|
870
|
+
const result = await plugin.execute('pre_request', context);
|
|
871
|
+
|
|
872
|
+
expect(result.action).toBe('allow');
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('should handle unrecognized phase', async () => {
|
|
876
|
+
await plugin.initialize({ enabled: true });
|
|
877
|
+
|
|
878
|
+
const context = createGuardrailContext({});
|
|
879
|
+
const result = await plugin.execute('pre_cache', context);
|
|
880
|
+
|
|
881
|
+
expect(result.action).toBe('allow');
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('should handle JSON parse error gracefully when redacting toolArgs', async () => {
|
|
885
|
+
await plugin.initialize({
|
|
886
|
+
enabled: true,
|
|
887
|
+
detect_emails: true,
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// Create context with toolArgs that become invalid JSON after redaction
|
|
891
|
+
// When the email is replaced with placeholder, the JSON structure might break
|
|
892
|
+
const context = createGuardrailContext({
|
|
893
|
+
toolName: 'send_email',
|
|
894
|
+
toolArgs: { to: 'user@example.com' },
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
const result = await plugin.execute('pre_tool_input', context);
|
|
898
|
+
|
|
899
|
+
// Should still modify and store result appropriately
|
|
900
|
+
expect(result.action).toBe('modify');
|
|
901
|
+
expect(context.toolArgs).toBeDefined();
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it('should handle empty response when restoring', async () => {
|
|
905
|
+
await plugin.initialize({
|
|
906
|
+
enabled: true,
|
|
907
|
+
restore_on_response: true,
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
const context = createGuardrailContext({
|
|
911
|
+
response: '',
|
|
912
|
+
});
|
|
913
|
+
context.metadata.set('pii_mappings', new Map([['[EMAIL_1]', 'test@test.com']]));
|
|
914
|
+
|
|
915
|
+
const result = await plugin.execute('post_response', context);
|
|
916
|
+
|
|
917
|
+
// Empty response should just allow
|
|
918
|
+
expect(result.action).toBe('allow');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('should handle no placeholders found during restore', async () => {
|
|
922
|
+
await plugin.initialize({
|
|
923
|
+
enabled: true,
|
|
924
|
+
restore_on_response: true,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const context = createGuardrailContext({
|
|
928
|
+
response: 'The user has been notified.',
|
|
929
|
+
});
|
|
930
|
+
// Mappings exist but placeholders are not in the response
|
|
931
|
+
context.metadata.set('pii_mappings', new Map([['[EMAIL_1]', 'test@test.com']]));
|
|
932
|
+
|
|
933
|
+
const result = await plugin.execute('post_response', context);
|
|
934
|
+
|
|
935
|
+
// No changes to make, should allow
|
|
936
|
+
expect(result.action).toBe('allow');
|
|
937
|
+
expect(context.response).toBe('The user has been notified.');
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('should expose getPseudonymizer for testing', async () => {
|
|
941
|
+
await plugin.initialize({ enabled: true });
|
|
942
|
+
|
|
943
|
+
const pseudonymizer = plugin.getPseudonymizer();
|
|
944
|
+
expect(pseudonymizer).toBeDefined();
|
|
945
|
+
expect(typeof pseudonymizer.pseudonymize).toBe('function');
|
|
946
|
+
expect(typeof pseudonymizer.restore).toBe('function');
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('should expose getDetector for testing', async () => {
|
|
950
|
+
await plugin.initialize({ enabled: true });
|
|
951
|
+
|
|
952
|
+
const detector = plugin.getDetector();
|
|
953
|
+
expect(detector).toBeDefined();
|
|
954
|
+
expect(typeof detector.detect).toBe('function');
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
describe('edge cases', () => {
|
|
959
|
+
it('should handle tool args that become invalid JSON after redaction', async () => {
|
|
960
|
+
await plugin.initialize({
|
|
961
|
+
enabled: true,
|
|
962
|
+
detect_emails: true,
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// Create a context where toolArgs will have PII redacted in a way that
|
|
966
|
+
// could produce invalid JSON if not handled properly
|
|
967
|
+
const context = createGuardrailContext({
|
|
968
|
+
toolName: 'test_tool',
|
|
969
|
+
toolArgs: {
|
|
970
|
+
email: 'test@example.com',
|
|
971
|
+
nested: { value: 'data' },
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
const result = await plugin.execute('pre_tool_input', context);
|
|
976
|
+
|
|
977
|
+
expect(result.action).toBe('modify');
|
|
978
|
+
expect(context.toolArgs).toBeDefined();
|
|
979
|
+
// The redacted args should be valid (either parsed JSON or fallback)
|
|
980
|
+
expect(typeof context.toolArgs).toBe('object');
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it('should return default confidence for unknown PII types', () => {
|
|
984
|
+
const detector = new PIIDetector({
|
|
985
|
+
detectEmails: false,
|
|
986
|
+
detectPhones: false,
|
|
987
|
+
detectSSN: false,
|
|
988
|
+
detectAPIKeys: false,
|
|
989
|
+
detectCreditCards: false,
|
|
990
|
+
detectIPAddresses: false,
|
|
991
|
+
customPatterns: [
|
|
992
|
+
{ name: 'custom_unknown_type', pattern: '\\bTEST\\d+\\b', placeholder: '[CUSTOM]' },
|
|
993
|
+
],
|
|
994
|
+
allowlist: [],
|
|
995
|
+
allowlistDomains: [],
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const detections = detector.detect('Found TEST123 here');
|
|
999
|
+
expect(detections.length).toBe(1);
|
|
1000
|
+
// Custom patterns get default confidence
|
|
1001
|
+
expect(detections[0].confidence).toBeGreaterThanOrEqual(0.7);
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
});
|