@coo-quack/sensitive-canary 0.7.0 → 0.8.1

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.
Files changed (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +798 -0
  3. package/README.md +142 -45
  4. package/dist/lib/bash-commands.js +405 -0
  5. package/dist/lib/command-tables.js +462 -0
  6. package/dist/lib/default-config.json +570 -0
  7. package/dist/lib/encoding.js +123 -0
  8. package/dist/lib/fail-closed.js +31 -0
  9. package/dist/lib/inspector.js +0 -0
  10. package/dist/lib/rules.js +399 -0
  11. package/dist/lib/shapes.js +161 -0
  12. package/dist/lib/shell.js +436 -0
  13. package/dist/lib/tool-inputs.js +217 -0
  14. package/dist/lib/transcript.js +115 -0
  15. package/dist/lib/validators.js +435 -0
  16. package/dist/pre-tool-use-hook.js +773 -0
  17. package/dist/user-prompt-submit-hook.js +105 -0
  18. package/hooks/hooks.json +1 -1
  19. package/package.json +25 -11
  20. package/src/lib/bash-commands.ts +455 -0
  21. package/src/lib/command-tables.ts +518 -0
  22. package/src/lib/default-config.json +155 -46
  23. package/src/lib/encoding.ts +135 -0
  24. package/src/lib/fail-closed.ts +36 -0
  25. package/src/lib/inspector.ts +0 -0
  26. package/src/lib/rules.ts +202 -365
  27. package/src/lib/shapes.ts +175 -0
  28. package/src/lib/shell.ts +512 -0
  29. package/src/lib/tool-inputs.ts +235 -0
  30. package/src/lib/transcript.ts +142 -0
  31. package/src/lib/validators.ts +435 -0
  32. package/src/pre-tool-use-hook.ts +774 -198
  33. package/src/user-prompt-submit-hook.ts +60 -18
  34. package/src/__tests__/pre-tool-use-hook.test.ts +0 -779
  35. package/src/__tests__/user-prompt-submit-hook.test.ts +0 -297
  36. package/src/lib/__tests__/inspector.test.ts +0 -289
  37. package/src/lib/__tests__/rules.test.ts +0 -1370
@@ -1,297 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const HOOK = new URL("../user-prompt-submit-hook.ts", import.meta.url).pathname;
5
- const NODE_FLAGS = ["--experimental-strip-types"];
6
-
7
- function runHook(prompt: string, opts?: { env?: Record<string, string> }) {
8
- const result = spawnSync("node", [...NODE_FLAGS, HOOK], {
9
- input: JSON.stringify({ prompt }),
10
- encoding: "utf8",
11
- env: { ...process.env, ...opts?.env },
12
- });
13
- return {
14
- exitCode: result.status ?? -1,
15
- stdout: result.stdout,
16
- stderr: result.stderr,
17
- };
18
- }
19
-
20
- describe("user-prompt-submit-hook — allow (exit 0)", () => {
21
- it("passes a clean prompt", () => {
22
- const { exitCode } = runHook("hello, can you help me?");
23
- expect(exitCode).toBe(0);
24
- });
25
-
26
- it("passes an empty prompt", () => {
27
- const { exitCode } = runHook("");
28
- expect(exitCode).toBe(0);
29
- });
30
-
31
- it("passes with [allow-all] tag even if secret is present", () => {
32
- const { exitCode } = runHook("[allow-all] my key is AKIAIOSFODNN7EXAMPLE");
33
- expect(exitCode).toBe(0);
34
- });
35
-
36
- it("passes with [allow-secret] tag when only secrets are present", () => {
37
- const { exitCode } = runHook(
38
- "[allow-secret] my key is AKIAIOSFODNN7EXAMPLE",
39
- );
40
- expect(exitCode).toBe(0);
41
- });
42
-
43
- it("passes with [allow-pii] tag when only PII is present", () => {
44
- const { exitCode } = runHook("[allow-pii] please email user@example.com");
45
- expect(exitCode).toBe(0);
46
- });
47
- });
48
-
49
- describe("user-prompt-submit-hook — block (exit 2)", () => {
50
- it("blocks a prompt with an AWS access key", () => {
51
- const { exitCode, stderr } = runHook("my key is AKIAIOSFODNN7EXAMPLE");
52
- expect(exitCode).toBe(2);
53
- expect(stderr).toContain("aws-access-key");
54
- expect(stderr).toContain("blocked");
55
- });
56
-
57
- it("blocks a prompt with a JWT", () => {
58
- const jwt =
59
- "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
60
- const { exitCode, stderr } = runHook(`token: ${jwt}`);
61
- expect(exitCode).toBe(2);
62
- expect(stderr).toContain("jwt");
63
- });
64
-
65
- it("blocks a prompt with an email address", () => {
66
- const { exitCode, stderr } = runHook("please email user@example.com");
67
- expect(exitCode).toBe(2);
68
- expect(stderr).toContain("pii-email");
69
- });
70
-
71
- it("blocks a prompt with a credit card number", () => {
72
- const { exitCode, stderr } = runHook("card: 4111111111111111");
73
- expect(exitCode).toBe(2);
74
- expect(stderr).toContain("pii-credit-card");
75
- });
76
-
77
- it("shows [allow-secret] and [allow-all] hints for a secret", () => {
78
- const { stderr } = runHook("key=AKIAIOSFODNN7EXAMPLE");
79
- expect(stderr).toContain("[allow-secret]");
80
- expect(stderr).toContain("[allow-all]");
81
- });
82
-
83
- it("shows [allow-pii] and [allow-all] hints for PII", () => {
84
- const { stderr } = runHook("email: user@example.com");
85
- expect(stderr).toContain("[allow-pii]");
86
- expect(stderr).toContain("[allow-all]");
87
- });
88
-
89
- it("shows both [allow-secret] and [allow-pii] hints when both are detected", () => {
90
- const { stderr } = runHook(
91
- "key=AKIAIOSFODNN7EXAMPLE and email user@example.com",
92
- );
93
- expect(stderr).toContain("[allow-secret]");
94
- expect(stderr).toContain("[allow-pii]");
95
- expect(stderr).toContain("[allow-all]");
96
- });
97
-
98
- it("deduplicates the same secret appearing multiple times", () => {
99
- const { stderr } = runHook("A=AKIAIOSFODNN7EXAMPLE B=AKIAIOSFODNN7EXAMPLE");
100
- // aws-access-key finding should appear only once in the output
101
- const count = (stderr ?? "").split("aws-access-key").length - 1;
102
- expect(count).toBe(1);
103
- });
104
-
105
- it("[allow-pii] does not bypass a secret block", () => {
106
- const { exitCode } = runHook("[allow-pii] my key is AKIAIOSFODNN7EXAMPLE");
107
- expect(exitCode).toBe(2);
108
- });
109
-
110
- it("[allow-secret] does not bypass a PII block", () => {
111
- const { exitCode } = runHook(
112
- "[allow-secret] please email user@example.com",
113
- );
114
- expect(exitCode).toBe(2);
115
- });
116
-
117
- it("[allow-secret] with mixed content still blocks PII", () => {
118
- const { exitCode } = runHook(
119
- "[allow-secret] key=AKIAIOSFODNN7EXAMPLE and email user@example.com",
120
- );
121
- expect(exitCode).toBe(2);
122
- });
123
- });
124
-
125
- describe("user-prompt-submit-hook — [mask-xxx] tags", () => {
126
- it("[mask-secret] with secret shows the actual tag in message", () => {
127
- const { exitCode, stderr } = runHook(
128
- "[mask-secret] my key is AKIAIOSFODNN7EXAMPLE",
129
- );
130
- expect(exitCode).toBe(2);
131
- expect(stderr).toContain("prompt masking is not supported");
132
- expect(stderr).toContain("[mask-secret]");
133
- expect(stderr).toContain("[allow-secret]");
134
- });
135
-
136
- it("[mask-pii] with PII shows the actual tag in message", () => {
137
- const { exitCode, stderr } = runHook(
138
- "[mask-pii] please email user@example.com",
139
- );
140
- expect(exitCode).toBe(2);
141
- expect(stderr).toContain("prompt masking is not supported");
142
- expect(stderr).toContain("[mask-pii]");
143
- expect(stderr).toContain("[allow-pii]");
144
- });
145
-
146
- it("[mask-all] with any sensitive data shows masking not supported", () => {
147
- const { exitCode, stderr } = runHook(
148
- "[mask-all] my key is AKIAIOSFODNN7EXAMPLE",
149
- );
150
- expect(exitCode).toBe(2);
151
- expect(stderr).toContain("prompt masking is not supported");
152
- expect(stderr).toContain("[mask-all]");
153
- });
154
-
155
- it("[mask-secret] with only PII falls through to normal block", () => {
156
- const { exitCode, stderr } = runHook(
157
- "[mask-secret] please email user@example.com",
158
- );
159
- expect(exitCode).toBe(2);
160
- expect(stderr).not.toContain("prompt masking is not supported");
161
- expect(stderr).toContain("sensitive data detected");
162
- });
163
-
164
- it("[mask-pii] with only secrets falls through to normal block", () => {
165
- const { exitCode, stderr } = runHook(
166
- "[mask-pii] my key is AKIAIOSFODNN7EXAMPLE",
167
- );
168
- expect(exitCode).toBe(2);
169
- expect(stderr).not.toContain("prompt masking is not supported");
170
- expect(stderr).toContain("sensitive data detected");
171
- });
172
-
173
- it("[mask-secret] with clean prompt passes through", () => {
174
- const { exitCode } = runHook("[mask-secret] hello, can you help me?");
175
- expect(exitCode).toBe(0);
176
- });
177
-
178
- it("[mask-unknown] with sensitive data falls through to normal block", () => {
179
- const { exitCode, stderr } = runHook(
180
- "[mask-unknown] my key is AKIAIOSFODNN7EXAMPLE",
181
- );
182
- expect(exitCode).toBe(2);
183
- expect(stderr).not.toContain("prompt masking is not supported");
184
- expect(stderr).toContain("sensitive data detected");
185
- });
186
- });
187
-
188
- describe("user-prompt-submit-hook — first-occurrence tag priority", () => {
189
- it("[allow-secret] before [mask-secret] → passes through (exit 0)", () => {
190
- const { exitCode } = runHook(
191
- "[allow-secret] [mask-secret] my key is AKIAIOSFODNN7EXAMPLE",
192
- );
193
- expect(exitCode).toBe(0);
194
- });
195
-
196
- it("[mask-secret] before [allow-secret] → shows masking not supported (exit 2)", () => {
197
- const { exitCode, stderr } = runHook(
198
- "[mask-secret] [allow-secret] my key is AKIAIOSFODNN7EXAMPLE",
199
- );
200
- expect(exitCode).toBe(2);
201
- expect(stderr).toContain("prompt masking is not supported");
202
- });
203
-
204
- it("[allow-all] before [mask-secret] → passes through (exit 0)", () => {
205
- const { exitCode } = runHook(
206
- "[allow-all] [mask-secret] my key is AKIAIOSFODNN7EXAMPLE",
207
- );
208
- expect(exitCode).toBe(0);
209
- });
210
-
211
- it("[mask-all] before [allow-secret] → shows masking not supported (exit 2)", () => {
212
- const { exitCode, stderr } = runHook(
213
- "[mask-all] [allow-secret] my key is AKIAIOSFODNN7EXAMPLE",
214
- );
215
- expect(exitCode).toBe(2);
216
- expect(stderr).toContain("prompt masking is not supported");
217
- });
218
-
219
- it("[allow-secret] [mask-pii] → secret allowed, pii masked → masking not supported (exit 2)", () => {
220
- const { exitCode, stderr } = runHook(
221
- "[allow-secret] [mask-pii] key AKIAIOSFODNN7EXAMPLE email user@example.com",
222
- );
223
- expect(exitCode).toBe(2);
224
- expect(stderr).toContain("prompt masking is not supported");
225
- expect(stderr).toContain("[mask-pii]");
226
- expect(stderr).not.toContain("[mask-secret]");
227
- });
228
-
229
- it("[mask-pii] [allow-secret] → secret: allow, pii: mask → masking not supported for email (exit 2)", () => {
230
- const { exitCode, stderr } = runHook(
231
- "[mask-pii] [allow-secret] key AKIAIOSFODNN7EXAMPLE email user@example.com",
232
- );
233
- expect(exitCode).toBe(2);
234
- expect(stderr).toContain("prompt masking is not supported");
235
- expect(stderr).toContain("[mask-pii]");
236
- });
237
-
238
- it("[allow-pii] before [mask-pii] → pii allowed, secret still blocked (exit 2)", () => {
239
- const { exitCode, stderr } = runHook(
240
- "[allow-pii] [mask-pii] key AKIAIOSFODNN7EXAMPLE email user@example.com",
241
- );
242
- expect(exitCode).toBe(2);
243
- expect(stderr).toContain("sensitive data detected");
244
- expect(stderr).not.toContain("prompt masking is not supported");
245
- });
246
- });
247
-
248
- describe("user-prompt-submit-hook — malformed input", () => {
249
- it("exits 0 on invalid JSON", () => {
250
- const result = spawnSync("node", [...NODE_FLAGS, HOOK], {
251
- input: "not json",
252
- encoding: "utf8",
253
- });
254
- expect(result.status).toBe(0);
255
- });
256
- });
257
-
258
- describe("user-prompt-submit-hook — SENSITIVE_CANARY_CATEGORIES", () => {
259
- it("pii-only: passes a prompt containing only secrets", () => {
260
- const { exitCode } = runHook("my key is AKIAIOSFODNN7EXAMPLE", {
261
- env: { SENSITIVE_CANARY_CATEGORIES: "pii" },
262
- });
263
- expect(exitCode).toBe(0);
264
- });
265
-
266
- it("pii-only: still blocks a prompt containing PII", () => {
267
- const { exitCode, stderr } = runHook("my card is 4111111111111111", {
268
- env: { SENSITIVE_CANARY_CATEGORIES: "pii" },
269
- });
270
- expect(exitCode).toBe(2);
271
- expect(stderr).toContain("sensitive data detected");
272
- });
273
-
274
- it("secret-only: passes a prompt containing only PII", () => {
275
- const { exitCode } = runHook("my card is 4111111111111111", {
276
- env: { SENSITIVE_CANARY_CATEGORIES: "secret" },
277
- });
278
- expect(exitCode).toBe(0);
279
- });
280
-
281
- it("secret-only: still blocks a prompt containing secrets", () => {
282
- const { exitCode, stderr } = runHook("my key is AKIAIOSFODNN7EXAMPLE", {
283
- env: { SENSITIVE_CANARY_CATEGORIES: "secret" },
284
- });
285
- expect(exitCode).toBe(2);
286
- expect(stderr).toContain("sensitive data detected");
287
- });
288
-
289
- it("unset: blocks both secrets and PII (default behavior)", () => {
290
- const { exitCode, stderr } = runHook(
291
- "key AKIAIOSFODNN7EXAMPLE card 4111111111111111",
292
- );
293
- expect(exitCode).toBe(2);
294
- expect(stderr).toContain("aws-access-key");
295
- expect(stderr).toContain("pii-credit-card");
296
- });
297
- });
@@ -1,289 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { Finding } from "../inspector.ts";
3
- import {
4
- applyAllowTags,
5
- dedupeFindings,
6
- findingsToLines,
7
- parseAllowTags,
8
- resolveTagPriority,
9
- } from "../inspector.ts";
10
-
11
- // ── parseAllowTags ────────────────────────────────────────────────────────────
12
-
13
- describe("parseAllowTags", () => {
14
- it("returns empty set when no tags are present", () => {
15
- const tags = parseAllowTags([{ role: "user", content: "hello" }]);
16
- expect(tags.size).toBe(0);
17
- });
18
-
19
- it("parses [allow-all]", () => {
20
- const tags = parseAllowTags([
21
- { role: "user", content: "[allow-all] send anyway" },
22
- ]);
23
- expect(tags.has("all")).toBe(true);
24
- });
25
-
26
- it("parses [allow-pii]", () => {
27
- const tags = parseAllowTags([{ role: "user", content: "[allow-pii] ok" }]);
28
- expect(tags.has("pii")).toBe(true);
29
- });
30
-
31
- it("parses [allow-secret]", () => {
32
- const tags = parseAllowTags([
33
- { role: "user", content: "[allow-secret] here is my key" },
34
- ]);
35
- expect(tags.has("secret")).toBe(true);
36
- });
37
-
38
- it("is case-insensitive", () => {
39
- const tags = parseAllowTags([{ role: "user", content: "[Allow-Secret]" }]);
40
- expect(tags.has("secret")).toBe(true);
41
- });
42
-
43
- it("ignores tags in assistant messages", () => {
44
- const tags = parseAllowTags([
45
- { role: "assistant", content: "[allow-all] this is the assistant" },
46
- ]);
47
- expect(tags.size).toBe(0);
48
- });
49
-
50
- it("parses tags from ContentBlock[] content", () => {
51
- const tags = parseAllowTags([
52
- {
53
- role: "user",
54
- content: [{ type: "text", text: "[allow-secret] check this" }],
55
- },
56
- ]);
57
- expect(tags.has("secret")).toBe(true);
58
- });
59
- });
60
-
61
- // ── resolveTagPriority ────────────────────────────────────────────────────────
62
-
63
- describe("resolveTagPriority", () => {
64
- it("returns empty sets when no tags are present", () => {
65
- const { effectiveAllow, effectiveMask } = resolveTagPriority("hello world");
66
- expect(effectiveAllow.size).toBe(0);
67
- expect(effectiveMask.size).toBe(0);
68
- });
69
-
70
- it("[allow-secret] → effectiveAllow has 'secret'", () => {
71
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
72
- "[allow-secret] key=abc",
73
- );
74
- expect(effectiveAllow.has("secret")).toBe(true);
75
- expect(effectiveMask.has("secret")).toBe(false);
76
- });
77
-
78
- it("[mask-secret] → effectiveMask has 'secret'", () => {
79
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
80
- "[mask-secret] key=abc",
81
- );
82
- expect(effectiveMask.has("secret")).toBe(true);
83
- expect(effectiveAllow.has("secret")).toBe(false);
84
- });
85
-
86
- it("[allow-secret] before [mask-secret] → allow wins for secret", () => {
87
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
88
- "[allow-secret] [mask-secret] key=abc",
89
- );
90
- expect(effectiveAllow.has("secret")).toBe(true);
91
- expect(effectiveMask.has("secret")).toBe(false);
92
- });
93
-
94
- it("[mask-secret] before [allow-secret] → mask wins for secret", () => {
95
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
96
- "[mask-secret] [allow-secret] key=abc",
97
- );
98
- expect(effectiveMask.has("secret")).toBe(true);
99
- expect(effectiveAllow.has("secret")).toBe(false);
100
- });
101
-
102
- it("[allow-all] before [mask-secret] → allow wins for both dimensions", () => {
103
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
104
- "[allow-all] [mask-secret] key=abc",
105
- );
106
- expect(effectiveAllow.has("secret")).toBe(true);
107
- expect(effectiveAllow.has("pii")).toBe(true);
108
- expect(effectiveMask.size).toBe(0);
109
- });
110
-
111
- it("[mask-all] before [allow-secret] → mask wins for both dimensions", () => {
112
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
113
- "[mask-all] [allow-secret] key=abc",
114
- );
115
- expect(effectiveMask.has("secret")).toBe(true);
116
- expect(effectiveMask.has("pii")).toBe(true);
117
- expect(effectiveMask.has("all")).toBe(true);
118
- expect(effectiveAllow.size).toBe(0);
119
- });
120
-
121
- it("[allow-secret] [mask-pii] → allow for secret, mask for pii", () => {
122
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
123
- "[allow-secret] [mask-pii] ...",
124
- );
125
- expect(effectiveAllow.has("secret")).toBe(true);
126
- expect(effectiveMask.has("pii")).toBe(true);
127
- expect(effectiveAllow.has("pii")).toBe(false);
128
- expect(effectiveMask.has("secret")).toBe(false);
129
- });
130
-
131
- it("[allow-all] → effectiveAllow has 'all'", () => {
132
- const { effectiveAllow } = resolveTagPriority("[allow-all] key=abc");
133
- expect(effectiveAllow.has("all")).toBe(true);
134
- });
135
-
136
- it("[mask-all] → effectiveMask has 'all'", () => {
137
- const { effectiveMask } = resolveTagPriority("[mask-all] key=abc");
138
- expect(effectiveMask.has("all")).toBe(true);
139
- });
140
-
141
- it("is case-insensitive", () => {
142
- const { effectiveAllow } = resolveTagPriority("[Allow-Secret] key=abc");
143
- expect(effectiveAllow.has("secret")).toBe(true);
144
- });
145
-
146
- it("unknown tag suffix is ignored", () => {
147
- const { effectiveAllow, effectiveMask } = resolveTagPriority(
148
- "[allow-unknown] key=abc",
149
- );
150
- expect(effectiveAllow.size).toBe(0);
151
- expect(effectiveMask.size).toBe(0);
152
- });
153
- });
154
-
155
- // ── applyAllowTags ────────────────────────────────────────────────────────────
156
-
157
- describe("applyAllowTags", () => {
158
- const findings: Finding[] = [
159
- {
160
- ruleId: "aws-access-key",
161
- description: "AWS",
162
- category: "secret",
163
- matchRedacted: "AKIA****",
164
- secretValue: "AKIATEST",
165
- score: 1,
166
- },
167
- {
168
- ruleId: "pii-email",
169
- description: "Email",
170
- category: "pii",
171
- matchRedacted: "user****",
172
- secretValue: "user@example.com",
173
- score: 1,
174
- },
175
- ];
176
-
177
- it("returns all findings when allow set is empty", () => {
178
- expect(applyAllowTags(findings, new Set())).toHaveLength(2);
179
- });
180
-
181
- it("returns empty array when findings is empty", () => {
182
- expect(applyAllowTags([], new Set(["all"]))).toHaveLength(0);
183
- });
184
-
185
- it("[allow-all] removes all findings", () => {
186
- expect(applyAllowTags(findings, new Set(["all"]))).toHaveLength(0);
187
- });
188
-
189
- it("[allow-secret] removes only secret findings", () => {
190
- const result = applyAllowTags(findings, new Set(["secret"]));
191
- expect(result).toHaveLength(1);
192
- expect(result[0]?.ruleId).toBe("pii-email");
193
- });
194
-
195
- it("[allow-pii] removes only PII findings", () => {
196
- const result = applyAllowTags(findings, new Set(["pii"]));
197
- expect(result).toHaveLength(1);
198
- expect(result[0]?.ruleId).toBe("aws-access-key");
199
- });
200
-
201
- it("an unknown tag has no effect", () => {
202
- const result = applyAllowTags(findings, new Set(["aws-access-key"]));
203
- expect(result).toHaveLength(2);
204
- });
205
- });
206
-
207
- // ── dedupeFindings ────────────────────────────────────────────────────────────
208
-
209
- describe("dedupeFindings", () => {
210
- it("removes duplicate findings with the same secretValue", () => {
211
- const findings: Finding[] = [
212
- {
213
- ruleId: "aws-access-key",
214
- description: "AWS",
215
- category: "secret",
216
- matchRedacted: "AKIA****",
217
- secretValue: "AKIATEST",
218
- score: 1,
219
- },
220
- {
221
- ruleId: "aws-access-key",
222
- description: "AWS",
223
- category: "secret",
224
- matchRedacted: "AKIA****",
225
- secretValue: "AKIATEST",
226
- score: 1,
227
- },
228
- ];
229
- expect(dedupeFindings(findings)).toHaveLength(1);
230
- });
231
-
232
- it("keeps findings with different secretValues", () => {
233
- const findings: Finding[] = [
234
- {
235
- ruleId: "aws-access-key",
236
- description: "AWS",
237
- category: "secret",
238
- matchRedacted: "AKIA****",
239
- secretValue: "AKIATEST1",
240
- score: 1,
241
- },
242
- {
243
- ruleId: "aws-access-key",
244
- description: "AWS",
245
- category: "secret",
246
- matchRedacted: "AKIA****",
247
- secretValue: "AKIATEST2",
248
- score: 1,
249
- },
250
- ];
251
- expect(dedupeFindings(findings)).toHaveLength(2);
252
- });
253
- });
254
-
255
- // ── findingsToLines ───────────────────────────────────────────────────────────
256
-
257
- describe("findingsToLines", () => {
258
- it("formats a secret finding", () => {
259
- const findings: Finding[] = [
260
- {
261
- ruleId: "aws-access-key",
262
- description: "AWS Access Key ID",
263
- category: "secret",
264
- matchRedacted: "AKIA****MPLE",
265
- secretValue: "AKIAIOSFODNN7EXAMPLE",
266
- score: 1,
267
- },
268
- ];
269
- const lines = findingsToLines(findings);
270
- expect(lines[0]).toBe(
271
- " [Secret] AWS Access Key ID (aws-access-key): AKIA****MPLE",
272
- );
273
- });
274
-
275
- it("formats a PII finding", () => {
276
- const findings: Finding[] = [
277
- {
278
- ruleId: "pii-email",
279
- description: "Email Address",
280
- category: "pii",
281
- matchRedacted: "user****",
282
- secretValue: "user@example.com",
283
- score: 1,
284
- },
285
- ];
286
- const lines = findingsToLines(findings);
287
- expect(lines[0]).toBe(" [PII] Email Address (pii-email): user****");
288
- });
289
- });