@coo-quack/sensitive-canary 0.6.0 → 0.8.0
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +830 -0
- package/README.md +269 -43
- package/dist/lib/bash-commands.js +405 -0
- package/dist/lib/command-tables.js +462 -0
- package/dist/lib/default-config.json +570 -0
- package/dist/lib/encoding.js +123 -0
- package/dist/lib/fail-closed.js +31 -0
- package/dist/lib/inspector.js +0 -0
- package/dist/lib/rules.js +399 -0
- package/dist/lib/shapes.js +161 -0
- package/dist/lib/shell.js +436 -0
- package/dist/lib/tool-inputs.js +217 -0
- package/dist/lib/transcript.js +115 -0
- package/dist/lib/validators.js +435 -0
- package/dist/pre-tool-use-hook.js +773 -0
- package/dist/user-prompt-submit-hook.js +105 -0
- package/hooks/hooks.json +1 -1
- package/package.json +25 -11
- package/src/lib/bash-commands.ts +455 -0
- package/src/lib/command-tables.ts +518 -0
- package/src/lib/default-config.json +570 -0
- package/src/lib/encoding.ts +135 -0
- package/src/lib/fail-closed.ts +36 -0
- package/src/lib/inspector.ts +0 -0
- package/src/lib/rules.ts +482 -267
- package/src/lib/shapes.ts +175 -0
- package/src/lib/shell.ts +512 -0
- package/src/lib/tool-inputs.ts +235 -0
- package/src/lib/transcript.ts +142 -0
- package/src/lib/validators.ts +435 -0
- package/src/pre-tool-use-hook.ts +774 -198
- package/src/user-prompt-submit-hook.ts +60 -18
- package/src/__tests__/pre-tool-use-hook.test.ts +0 -779
- package/src/__tests__/user-prompt-submit-hook.test.ts +0 -297
- package/src/lib/__tests__/inspector.test.ts +0 -281
- package/src/lib/__tests__/rules.test.ts +0 -448
|
@@ -1,448 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
enabledCategoriesFromEnv,
|
|
4
|
-
entropy,
|
|
5
|
-
luhn,
|
|
6
|
-
parseCategories,
|
|
7
|
-
redact,
|
|
8
|
-
scan,
|
|
9
|
-
} from "../rules.ts";
|
|
10
|
-
|
|
11
|
-
// ── luhn ──────────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
describe("luhn", () => {
|
|
14
|
-
it("passes a valid Visa number", () => {
|
|
15
|
-
expect(luhn("4111111111111111")).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("passes a valid Mastercard number", () => {
|
|
19
|
-
expect(luhn("5500005555555559")).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("fails an invalid number", () => {
|
|
23
|
-
expect(luhn("1234567890123456")).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("ignores spaces and dashes", () => {
|
|
27
|
-
expect(luhn("4111 1111 1111 1111")).toBe(true);
|
|
28
|
-
expect(luhn("4111-1111-1111-1111")).toBe(true);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// ── entropy ───────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
describe("entropy", () => {
|
|
35
|
-
it("returns 0 for a single repeated character", () => {
|
|
36
|
-
expect(entropy("aaaa")).toBe(0);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns a higher value for a more varied string", () => {
|
|
40
|
-
expect(entropy("abcdefgh")).toBeGreaterThan(entropy("aaaabbbb"));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("'password' entropy is below 3.0 (env-assignment threshold)", () => {
|
|
44
|
-
expect(entropy("password")).toBeLessThan(3.0);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("random-looking value entropy is above 3.5 (generic-secret threshold)", () => {
|
|
48
|
-
expect(entropy("Xk9mP2qR7vL4nW1s")).toBeGreaterThan(3.5);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// ── redact ────────────────────────────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
describe("redact", () => {
|
|
55
|
-
it("masks strings of 8 chars or fewer completely", () => {
|
|
56
|
-
expect(redact("abc")).toBe("****");
|
|
57
|
-
expect(redact("12345678")).toBe("****");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("shows first 4 and last 4 chars for strings longer than 8 chars", () => {
|
|
61
|
-
expect(redact("123456789")).toBe("1234****6789");
|
|
62
|
-
expect(redact("AKIAIOSFODNN7EXAMPLE")).toBe("AKIA****MPLE");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("handles empty string", () => {
|
|
66
|
-
expect(redact("")).toBe("****");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// ── scan: secrets ─────────────────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
describe("scan — secrets", () => {
|
|
73
|
-
it("detects an AWS Access Key ID", () => {
|
|
74
|
-
const findings = scan("key=AKIAIOSFODNN7EXAMPLE");
|
|
75
|
-
expect(findings.some((f) => f.ruleId === "aws-access-key")).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("detects a GCP API key", () => {
|
|
79
|
-
const findings = scan(`key=AIzaSyC${"A".repeat(32)}`);
|
|
80
|
-
expect(findings.some((f) => f.ruleId === "gcp-api-key")).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("does not flag a string starting with AIza but too short", () => {
|
|
84
|
-
const findings = scan("AIzaSyC_short");
|
|
85
|
-
expect(findings.some((f) => f.ruleId === "gcp-api-key")).toBe(false);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("detects an npm access token", () => {
|
|
89
|
-
const findings = scan(`npm_${"A".repeat(36)}`);
|
|
90
|
-
expect(findings.some((f) => f.ruleId === "npm-token")).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("does not flag npm_ with insufficient length", () => {
|
|
94
|
-
const findings = scan("npm_shorttoken");
|
|
95
|
-
expect(findings.some((f) => f.ruleId === "npm-token")).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("detects a PEM private key header (RSA)", () => {
|
|
99
|
-
const findings = scan("-----BEGIN RSA PRIVATE KEY-----");
|
|
100
|
-
expect(findings.some((f) => f.ruleId === "private-key")).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("detects an OpenSSH private key header via private-key rule", () => {
|
|
104
|
-
const findings = scan("-----BEGIN OPENSSH PRIVATE KEY-----");
|
|
105
|
-
expect(findings.some((f) => f.ruleId === "private-key")).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("detects a GitHub PAT", () => {
|
|
109
|
-
const findings = scan(`token=ghp_${"A".repeat(36)}`);
|
|
110
|
-
expect(findings.some((f) => f.ruleId === "github-pat")).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("detects a GitHub fine-grained token", () => {
|
|
114
|
-
const findings = scan(`github_pat_${"A".repeat(82)}`);
|
|
115
|
-
expect(findings.some((f) => f.ruleId === "github-fine-grained")).toBe(true);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("detects a GitLab PAT", () => {
|
|
119
|
-
const findings = scan(`token=glpat-${"A".repeat(20)}`);
|
|
120
|
-
expect(findings.some((f) => f.ruleId === "gitlab-pat")).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("detects a Slack token", () => {
|
|
124
|
-
const findings = scan("xoxb-123456789012-ABCDEFGHIJ");
|
|
125
|
-
expect(findings.some((f) => f.ruleId === "slack-token")).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("detects a Slack webhook URL", () => {
|
|
129
|
-
const findings = scan(
|
|
130
|
-
`https://hooks.slack.com/services/TABCDEFGH/BABCDEFGHIJ/${"A".repeat(24)}`,
|
|
131
|
-
);
|
|
132
|
-
expect(findings.some((f) => f.ruleId === "slack-webhook")).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("detects a Discord webhook URL", () => {
|
|
136
|
-
const findings = scan(
|
|
137
|
-
`https://discord.com/api/webhooks/123456789012345678/${"A".repeat(68)}`,
|
|
138
|
-
);
|
|
139
|
-
expect(findings.some((f) => f.ruleId === "discord-webhook")).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("detects a Telegram bot token", () => {
|
|
143
|
-
const findings = scan(`12345678:AA${"A".repeat(33)}`);
|
|
144
|
-
expect(findings.some((f) => f.ruleId === "telegram-bot-token")).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("detects a Twilio Account SID", () => {
|
|
148
|
-
const findings = scan(`AC${"a".repeat(32)}`);
|
|
149
|
-
expect(findings.some((f) => f.ruleId === "twilio-sid")).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("detects a SendGrid API key", () => {
|
|
153
|
-
const findings = scan(`SG.${"A".repeat(22)}.${"B".repeat(43)}`);
|
|
154
|
-
expect(findings.some((f) => f.ruleId === "sendgrid-key")).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("detects a Mailgun API key", () => {
|
|
158
|
-
const findings = scan(`key-${"a".repeat(32)}`);
|
|
159
|
-
expect(findings.some((f) => f.ruleId === "mailgun-key")).toBe(true);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("detects a Mailchimp API key", () => {
|
|
163
|
-
const findings = scan(`${"a".repeat(32)}-us1`);
|
|
164
|
-
expect(findings.some((f) => f.ruleId === "mailchimp-key")).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("detects a Stripe secret key", () => {
|
|
168
|
-
const findings = scan(`sk_live_${"A".repeat(24)}`);
|
|
169
|
-
expect(findings.some((f) => f.ruleId === "stripe-secret-key")).toBe(true);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("detects a Stripe restricted key", () => {
|
|
173
|
-
const findings = scan(`rk_test_${"A".repeat(24)}`);
|
|
174
|
-
expect(findings.some((f) => f.ruleId === "stripe-restricted-key")).toBe(
|
|
175
|
-
true,
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("detects an OpenAI legacy API key", () => {
|
|
180
|
-
const findings = scan(`sk-${"A".repeat(48)}`);
|
|
181
|
-
expect(findings.some((f) => f.ruleId === "openai-key")).toBe(true);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("does not flag sk-proj-* as openai-key (legacy)", () => {
|
|
185
|
-
const findings = scan("sk-proj-Xk9mP2qR7vL4nW1sYj3cBz8dEf5gHiKoNpQuTxMn");
|
|
186
|
-
expect(findings.some((f) => f.ruleId === "openai-key")).toBe(false);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("does not flag sk-ant-* as openai-key (legacy)", () => {
|
|
190
|
-
const findings = scan(`sk-ant-${"A".repeat(95)}`);
|
|
191
|
-
expect(findings.some((f) => f.ruleId === "openai-key")).toBe(false);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("detects an OpenAI project API key", () => {
|
|
195
|
-
const findings = scan("sk-proj-Xk9mP2qR7vL4nW1sYj3cBz8dEf5gHiKoNpQuTxMn");
|
|
196
|
-
expect(findings.some((f) => f.ruleId === "openai-project-key")).toBe(true);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("detects an Anthropic API key", () => {
|
|
200
|
-
const findings = scan(`sk-ant-${"A".repeat(95)}`);
|
|
201
|
-
expect(findings.some((f) => f.ruleId === "anthropic-key")).toBe(true);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("detects a JWT", () => {
|
|
205
|
-
const jwt =
|
|
206
|
-
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
|
|
207
|
-
const findings = scan(jwt);
|
|
208
|
-
expect(findings.some((f) => f.ruleId === "jwt")).toBe(true);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("detects a generic API key assignment with sufficient entropy", () => {
|
|
212
|
-
const findings = scan("api_key=Xk9mP2qR7vL4nW1sYj3cBz8dEf5g");
|
|
213
|
-
expect(findings.some((f) => f.ruleId === "generic-secret")).toBe(true);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("does not flag a low-entropy generic API key value", () => {
|
|
217
|
-
const findings = scan("api_key=placeholder");
|
|
218
|
-
expect(findings.some((f) => f.ruleId === "generic-secret")).toBe(false);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("detects a database connection string with credentials", () => {
|
|
222
|
-
const findings = scan("postgres://user:password@localhost/mydb");
|
|
223
|
-
expect(findings.some((f) => f.ruleId === "connection-string")).toBe(true);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("detects an .env style assignment with sufficient entropy", () => {
|
|
227
|
-
const findings = scan("DATABASE_PASSWORD=Xk9mP2qR7vL4nW1s");
|
|
228
|
-
expect(findings.some((f) => f.ruleId === "env-assignment")).toBe(true);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("does not flag a low-entropy .env value", () => {
|
|
232
|
-
const findings = scan("DATABASE_PASSWORD=password");
|
|
233
|
-
expect(findings.some((f) => f.ruleId === "env-assignment")).toBe(false);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("returns no findings for clean text", () => {
|
|
237
|
-
expect(scan("hello world, nothing sensitive here")).toHaveLength(0);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// ── scan: PII ─────────────────────────────────────────────────────────────────
|
|
242
|
-
|
|
243
|
-
describe("scan — PII", () => {
|
|
244
|
-
it("detects an email address", () => {
|
|
245
|
-
const findings = scan("contact: user@example.com");
|
|
246
|
-
expect(findings.some((f) => f.ruleId === "pii-email")).toBe(true);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("detects a valid credit card number — no separators", () => {
|
|
250
|
-
const findings = scan("card: 4111111111111111");
|
|
251
|
-
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("detects a valid credit card number — space separated", () => {
|
|
255
|
-
const findings = scan("card: 4111 1111 1111 1111");
|
|
256
|
-
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("detects a valid credit card number — hyphen separated", () => {
|
|
260
|
-
const findings = scan("card: 4111-1111-1111-1111");
|
|
261
|
-
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("does not flag an invalid credit card number", () => {
|
|
265
|
-
const findings = scan("card: 4111111111111112");
|
|
266
|
-
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(false);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("detects a US SSN", () => {
|
|
270
|
-
const findings = scan("ssn: 123-45-6789");
|
|
271
|
-
expect(findings.some((f) => f.ruleId === "pii-ssn")).toBe(true);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("does not flag an SSN with area 000", () => {
|
|
275
|
-
expect(scan("ssn: 000-45-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
276
|
-
false,
|
|
277
|
-
);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("does not flag an SSN with area 666", () => {
|
|
281
|
-
expect(scan("ssn: 666-45-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
282
|
-
false,
|
|
283
|
-
);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("does not flag an SSN with area 9xx", () => {
|
|
287
|
-
expect(scan("ssn: 900-45-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
288
|
-
false,
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("does not flag an SSN with group 00", () => {
|
|
293
|
-
expect(scan("ssn: 123-00-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
294
|
-
false,
|
|
295
|
-
);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it("does not flag an SSN with serial 0000", () => {
|
|
299
|
-
expect(scan("ssn: 123-45-0000").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
300
|
-
false,
|
|
301
|
-
);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("detects a US phone number", () => {
|
|
305
|
-
const findings = scan("call: (555) 123-4567");
|
|
306
|
-
expect(findings.some((f) => f.ruleId === "pii-phone-us")).toBe(true);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("detects a Japanese phone number", () => {
|
|
310
|
-
const findings = scan("tel: 03-1234-5678");
|
|
311
|
-
expect(findings.some((f) => f.ruleId === "pii-phone-jp")).toBe(true);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("detects a Japanese postal code with 〒 prefix", () => {
|
|
315
|
-
const findings = scan("address: 〒150-0001");
|
|
316
|
-
expect(findings.some((f) => f.ruleId === "pii-postal-jp")).toBe(true);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("does not flag a postal-like number without 〒", () => {
|
|
320
|
-
const findings = scan("zip: 150-0001");
|
|
321
|
-
expect(findings.some((f) => f.ruleId === "pii-postal-jp")).toBe(false);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("detects a 192.168.x.x private IPv4 address", () => {
|
|
325
|
-
const findings = scan("server: 192.168.1.100");
|
|
326
|
-
expect(findings.some((f) => f.ruleId === "pii-ipv4")).toBe(true);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it("detects a 10.x.x.x private IPv4 address", () => {
|
|
330
|
-
const findings = scan("server: 10.0.0.1");
|
|
331
|
-
expect(findings.some((f) => f.ruleId === "pii-ipv4")).toBe(true);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("detects a 172.16–31.x.x private IPv4 address", () => {
|
|
335
|
-
expect(scan("host: 172.16.0.1").some((f) => f.ruleId === "pii-ipv4")).toBe(
|
|
336
|
-
true,
|
|
337
|
-
);
|
|
338
|
-
expect(
|
|
339
|
-
scan("host: 172.31.255.255").some((f) => f.ruleId === "pii-ipv4"),
|
|
340
|
-
).toBe(true);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("does not flag 172.15.x.x (outside private range)", () => {
|
|
344
|
-
expect(scan("host: 172.15.1.1").some((f) => f.ruleId === "pii-ipv4")).toBe(
|
|
345
|
-
false,
|
|
346
|
-
);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("does not flag 172.32.x.x (outside private range)", () => {
|
|
350
|
-
expect(scan("host: 172.32.1.1").some((f) => f.ruleId === "pii-ipv4")).toBe(
|
|
351
|
-
false,
|
|
352
|
-
);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("does not flag a public IPv4 address", () => {
|
|
356
|
-
const findings = scan("server: 8.8.8.8");
|
|
357
|
-
expect(findings.some((f) => f.ruleId === "pii-ipv4")).toBe(false);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// ── parseCategories ───────────────────────────────────────────────────────────
|
|
362
|
-
|
|
363
|
-
describe("parseCategories", () => {
|
|
364
|
-
it("defaults to all categories when unset", () => {
|
|
365
|
-
expect(parseCategories(undefined)).toEqual(new Set(["secret", "pii"]));
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("defaults to all categories when empty", () => {
|
|
369
|
-
expect(parseCategories("")).toEqual(new Set(["secret", "pii"]));
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it("parses a single category", () => {
|
|
373
|
-
expect(parseCategories("secret")).toEqual(new Set(["secret"]));
|
|
374
|
-
expect(parseCategories("pii")).toEqual(new Set(["pii"]));
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("parses a comma-separated list", () => {
|
|
378
|
-
expect(parseCategories("secret,pii")).toEqual(new Set(["secret", "pii"]));
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("treats 'all' as every category", () => {
|
|
382
|
-
expect(parseCategories("all")).toEqual(new Set(["secret", "pii"]));
|
|
383
|
-
expect(parseCategories("secret,all")).toEqual(new Set(["secret", "pii"]));
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it("is case-insensitive and trims whitespace", () => {
|
|
387
|
-
expect(parseCategories(" Secret , PII ")).toEqual(
|
|
388
|
-
new Set(["secret", "pii"]),
|
|
389
|
-
);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("falls back to all when no valid token is present", () => {
|
|
393
|
-
expect(parseCategories("foo,bar")).toEqual(new Set(["secret", "pii"]));
|
|
394
|
-
});
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// ── scan: category filter ─────────────────────────────────────────────────────
|
|
398
|
-
|
|
399
|
-
describe("scan — category filter", () => {
|
|
400
|
-
const text = "key=AKIAIOSFODNN7EXAMPLE card: 4111111111111111";
|
|
401
|
-
|
|
402
|
-
it("scans all categories by default", () => {
|
|
403
|
-
const findings = scan(text);
|
|
404
|
-
expect(findings.some((f) => f.ruleId === "aws-access-key")).toBe(true);
|
|
405
|
-
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it("scans only secrets when limited to the secret category", () => {
|
|
409
|
-
const findings = scan(text, new Set(["secret"]));
|
|
410
|
-
expect(findings.some((f) => f.ruleId === "aws-access-key")).toBe(true);
|
|
411
|
-
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(false);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("scans only PII when limited to the pii category", () => {
|
|
415
|
-
const findings = scan(text, new Set(["pii"]));
|
|
416
|
-
expect(findings.some((f) => f.ruleId === "aws-access-key")).toBe(false);
|
|
417
|
-
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("returns nothing when no categories are enabled", () => {
|
|
421
|
-
expect(scan(text, new Set())).toEqual([]);
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
// ── enabledCategoriesFromEnv ──────────────────────────────────────────────────
|
|
426
|
-
|
|
427
|
-
describe("enabledCategoriesFromEnv", () => {
|
|
428
|
-
const ENV_KEY = "SENSITIVE_CANARY_CATEGORIES";
|
|
429
|
-
const original = process.env[ENV_KEY];
|
|
430
|
-
|
|
431
|
-
afterEach(() => {
|
|
432
|
-
if (original === undefined) {
|
|
433
|
-
delete process.env[ENV_KEY];
|
|
434
|
-
} else {
|
|
435
|
-
process.env[ENV_KEY] = original;
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it("returns all categories when the env var is unset", () => {
|
|
440
|
-
delete process.env[ENV_KEY];
|
|
441
|
-
expect(enabledCategoriesFromEnv()).toEqual(new Set(["secret", "pii"]));
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it("returns the parsed categories when the env var is set", () => {
|
|
445
|
-
process.env[ENV_KEY] = "pii";
|
|
446
|
-
expect(enabledCategoriesFromEnv()).toEqual(new Set(["pii"]));
|
|
447
|
-
});
|
|
448
|
-
});
|