@coo-quack/sensitive-canary 0.4.2
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/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +11 -0
- package/CHANGELOG.md +73 -0
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/hooks/hooks.json +26 -0
- package/package.json +42 -0
- package/src/__tests__/pre-tool-use-hook.test.ts +646 -0
- package/src/__tests__/user-prompt-submit-hook.test.ts +255 -0
- package/src/lib/__tests__/inspector.test.ts +281 -0
- package/src/lib/__tests__/rules.test.ts +322 -0
- package/src/lib/inspector.ts +113 -0
- package/src/lib/rules.ts +308 -0
- package/src/pre-tool-use-hook.ts +316 -0
- package/src/user-prompt-submit-hook.ts +106 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { entropy, luhn, redact, scan } from "../rules.ts";
|
|
3
|
+
|
|
4
|
+
// ── luhn ──────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
describe("luhn", () => {
|
|
7
|
+
it("passes a valid Visa number", () => {
|
|
8
|
+
expect(luhn("4111111111111111")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("passes a valid Mastercard number", () => {
|
|
12
|
+
expect(luhn("5500005555555559")).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("fails an invalid number", () => {
|
|
16
|
+
expect(luhn("1234567890123456")).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("ignores spaces and dashes", () => {
|
|
20
|
+
expect(luhn("4111 1111 1111 1111")).toBe(true);
|
|
21
|
+
expect(luhn("4111-1111-1111-1111")).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ── entropy ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe("entropy", () => {
|
|
28
|
+
it("returns 0 for a single repeated character", () => {
|
|
29
|
+
expect(entropy("aaaa")).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns a higher value for a more varied string", () => {
|
|
33
|
+
expect(entropy("abcdefgh")).toBeGreaterThan(entropy("aaaabbbb"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("'password' entropy is below 3.0 (env-assignment threshold)", () => {
|
|
37
|
+
expect(entropy("password")).toBeLessThan(3.0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("random-looking value entropy is above 3.5 (generic-secret threshold)", () => {
|
|
41
|
+
expect(entropy("Xk9mP2qR7vL4nW1s")).toBeGreaterThan(3.5);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── redact ────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
describe("redact", () => {
|
|
48
|
+
it("masks strings of 8 chars or fewer completely", () => {
|
|
49
|
+
expect(redact("abc")).toBe("****");
|
|
50
|
+
expect(redact("12345678")).toBe("****");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("shows first 4 and last 4 chars for strings longer than 8 chars", () => {
|
|
54
|
+
expect(redact("123456789")).toBe("1234****6789");
|
|
55
|
+
expect(redact("AKIAIOSFODNN7EXAMPLE")).toBe("AKIA****MPLE");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("handles empty string", () => {
|
|
59
|
+
expect(redact("")).toBe("****");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ── scan: secrets ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
describe("scan — secrets", () => {
|
|
66
|
+
it("detects an AWS Access Key ID", () => {
|
|
67
|
+
const findings = scan("key=AKIAIOSFODNN7EXAMPLE");
|
|
68
|
+
expect(findings.some((f) => f.ruleId === "aws-access-key")).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("detects a PEM private key header (RSA)", () => {
|
|
72
|
+
const findings = scan("-----BEGIN RSA PRIVATE KEY-----");
|
|
73
|
+
expect(findings.some((f) => f.ruleId === "private-key")).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("detects an OpenSSH private key header via private-key rule", () => {
|
|
77
|
+
const findings = scan("-----BEGIN OPENSSH PRIVATE KEY-----");
|
|
78
|
+
expect(findings.some((f) => f.ruleId === "private-key")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("detects a GitHub PAT", () => {
|
|
82
|
+
const findings = scan(`token=ghp_${"A".repeat(36)}`);
|
|
83
|
+
expect(findings.some((f) => f.ruleId === "github-pat")).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("detects a GitHub fine-grained token", () => {
|
|
87
|
+
const findings = scan(`github_pat_${"A".repeat(82)}`);
|
|
88
|
+
expect(findings.some((f) => f.ruleId === "github-fine-grained")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("detects a GitLab PAT", () => {
|
|
92
|
+
const findings = scan(`token=glpat-${"A".repeat(20)}`);
|
|
93
|
+
expect(findings.some((f) => f.ruleId === "gitlab-pat")).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("detects a Slack token", () => {
|
|
97
|
+
const findings = scan("xoxb-123456789012-ABCDEFGHIJ");
|
|
98
|
+
expect(findings.some((f) => f.ruleId === "slack-token")).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("detects a Slack webhook URL", () => {
|
|
102
|
+
const findings = scan(
|
|
103
|
+
`https://hooks.slack.com/services/TABCDEFGH/BABCDEFGHIJ/${"A".repeat(24)}`,
|
|
104
|
+
);
|
|
105
|
+
expect(findings.some((f) => f.ruleId === "slack-webhook")).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("detects a Discord webhook URL", () => {
|
|
109
|
+
const findings = scan(
|
|
110
|
+
`https://discord.com/api/webhooks/123456789012345678/${"A".repeat(68)}`,
|
|
111
|
+
);
|
|
112
|
+
expect(findings.some((f) => f.ruleId === "discord-webhook")).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("detects a Telegram bot token", () => {
|
|
116
|
+
const findings = scan(`12345678:AA${"A".repeat(33)}`);
|
|
117
|
+
expect(findings.some((f) => f.ruleId === "telegram-bot-token")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("detects a Twilio Account SID", () => {
|
|
121
|
+
const findings = scan(`AC${"a".repeat(32)}`);
|
|
122
|
+
expect(findings.some((f) => f.ruleId === "twilio-sid")).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("detects a SendGrid API key", () => {
|
|
126
|
+
const findings = scan(`SG.${"A".repeat(22)}.${"B".repeat(43)}`);
|
|
127
|
+
expect(findings.some((f) => f.ruleId === "sendgrid-key")).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("detects a Mailgun API key", () => {
|
|
131
|
+
const findings = scan(`key-${"a".repeat(32)}`);
|
|
132
|
+
expect(findings.some((f) => f.ruleId === "mailgun-key")).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("detects a Mailchimp API key", () => {
|
|
136
|
+
const findings = scan(`${"a".repeat(32)}-us1`);
|
|
137
|
+
expect(findings.some((f) => f.ruleId === "mailchimp-key")).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("detects a Stripe secret key", () => {
|
|
141
|
+
const findings = scan(`sk_live_${"A".repeat(24)}`);
|
|
142
|
+
expect(findings.some((f) => f.ruleId === "stripe-secret-key")).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("detects a Stripe restricted key", () => {
|
|
146
|
+
const findings = scan(`rk_test_${"A".repeat(24)}`);
|
|
147
|
+
expect(findings.some((f) => f.ruleId === "stripe-restricted-key")).toBe(
|
|
148
|
+
true,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("detects an OpenAI legacy API key", () => {
|
|
153
|
+
const findings = scan(`sk-${"A".repeat(48)}`);
|
|
154
|
+
expect(findings.some((f) => f.ruleId === "openai-key")).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("detects an OpenAI project API key", () => {
|
|
158
|
+
const findings = scan("sk-proj-Xk9mP2qR7vL4nW1sYj3cBz8dEf5gHiKoNpQuTxMn");
|
|
159
|
+
expect(findings.some((f) => f.ruleId === "openai-project-key")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("detects an Anthropic API key", () => {
|
|
163
|
+
const findings = scan(`sk-ant-${"A".repeat(95)}`);
|
|
164
|
+
expect(findings.some((f) => f.ruleId === "anthropic-key")).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("detects a JWT", () => {
|
|
168
|
+
const jwt =
|
|
169
|
+
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMTIzIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
|
|
170
|
+
const findings = scan(jwt);
|
|
171
|
+
expect(findings.some((f) => f.ruleId === "jwt")).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("detects a generic API key assignment with sufficient entropy", () => {
|
|
175
|
+
const findings = scan("api_key=Xk9mP2qR7vL4nW1sYj3cBz8dEf5g");
|
|
176
|
+
expect(findings.some((f) => f.ruleId === "generic-secret")).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("does not flag a low-entropy generic API key value", () => {
|
|
180
|
+
const findings = scan("api_key=placeholder");
|
|
181
|
+
expect(findings.some((f) => f.ruleId === "generic-secret")).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("detects a database connection string with credentials", () => {
|
|
185
|
+
const findings = scan("postgres://user:password@localhost/mydb");
|
|
186
|
+
expect(findings.some((f) => f.ruleId === "connection-string")).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("detects an .env style assignment with sufficient entropy", () => {
|
|
190
|
+
const findings = scan("DATABASE_PASSWORD=Xk9mP2qR7vL4nW1s");
|
|
191
|
+
expect(findings.some((f) => f.ruleId === "env-assignment")).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("does not flag a low-entropy .env value", () => {
|
|
195
|
+
const findings = scan("DATABASE_PASSWORD=password");
|
|
196
|
+
expect(findings.some((f) => f.ruleId === "env-assignment")).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("returns no findings for clean text", () => {
|
|
200
|
+
expect(scan("hello world, nothing sensitive here")).toHaveLength(0);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── scan: PII ─────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
describe("scan — PII", () => {
|
|
207
|
+
it("detects an email address", () => {
|
|
208
|
+
const findings = scan("contact: user@example.com");
|
|
209
|
+
expect(findings.some((f) => f.ruleId === "pii-email")).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("detects a valid credit card number — no separators", () => {
|
|
213
|
+
const findings = scan("card: 4111111111111111");
|
|
214
|
+
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("detects a valid credit card number — space separated", () => {
|
|
218
|
+
const findings = scan("card: 4111 1111 1111 1111");
|
|
219
|
+
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("detects a valid credit card number — hyphen separated", () => {
|
|
223
|
+
const findings = scan("card: 4111-1111-1111-1111");
|
|
224
|
+
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("does not flag an invalid credit card number", () => {
|
|
228
|
+
const findings = scan("card: 4111111111111112");
|
|
229
|
+
expect(findings.some((f) => f.ruleId === "pii-credit-card")).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("detects a US SSN", () => {
|
|
233
|
+
const findings = scan("ssn: 123-45-6789");
|
|
234
|
+
expect(findings.some((f) => f.ruleId === "pii-ssn")).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("does not flag an SSN with area 000", () => {
|
|
238
|
+
expect(scan("ssn: 000-45-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
239
|
+
false,
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("does not flag an SSN with area 666", () => {
|
|
244
|
+
expect(scan("ssn: 666-45-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
245
|
+
false,
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("does not flag an SSN with area 9xx", () => {
|
|
250
|
+
expect(scan("ssn: 900-45-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
251
|
+
false,
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("does not flag an SSN with group 00", () => {
|
|
256
|
+
expect(scan("ssn: 123-00-6789").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
257
|
+
false,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("does not flag an SSN with serial 0000", () => {
|
|
262
|
+
expect(scan("ssn: 123-45-0000").some((f) => f.ruleId === "pii-ssn")).toBe(
|
|
263
|
+
false,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("detects a US phone number", () => {
|
|
268
|
+
const findings = scan("call: (555) 123-4567");
|
|
269
|
+
expect(findings.some((f) => f.ruleId === "pii-phone-us")).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("detects a Japanese phone number", () => {
|
|
273
|
+
const findings = scan("tel: 03-1234-5678");
|
|
274
|
+
expect(findings.some((f) => f.ruleId === "pii-phone-jp")).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("detects a Japanese postal code with 〒 prefix", () => {
|
|
278
|
+
const findings = scan("address: 〒150-0001");
|
|
279
|
+
expect(findings.some((f) => f.ruleId === "pii-postal-jp")).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("does not flag a postal-like number without 〒", () => {
|
|
283
|
+
const findings = scan("zip: 150-0001");
|
|
284
|
+
expect(findings.some((f) => f.ruleId === "pii-postal-jp")).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("detects a 192.168.x.x private IPv4 address", () => {
|
|
288
|
+
const findings = scan("server: 192.168.1.100");
|
|
289
|
+
expect(findings.some((f) => f.ruleId === "pii-ipv4")).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("detects a 10.x.x.x private IPv4 address", () => {
|
|
293
|
+
const findings = scan("server: 10.0.0.1");
|
|
294
|
+
expect(findings.some((f) => f.ruleId === "pii-ipv4")).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("detects a 172.16–31.x.x private IPv4 address", () => {
|
|
298
|
+
expect(scan("host: 172.16.0.1").some((f) => f.ruleId === "pii-ipv4")).toBe(
|
|
299
|
+
true,
|
|
300
|
+
);
|
|
301
|
+
expect(
|
|
302
|
+
scan("host: 172.31.255.255").some((f) => f.ruleId === "pii-ipv4"),
|
|
303
|
+
).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("does not flag 172.15.x.x (outside private range)", () => {
|
|
307
|
+
expect(scan("host: 172.15.1.1").some((f) => f.ruleId === "pii-ipv4")).toBe(
|
|
308
|
+
false,
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("does not flag 172.32.x.x (outside private range)", () => {
|
|
313
|
+
expect(scan("host: 172.32.1.1").some((f) => f.ruleId === "pii-ipv4")).toBe(
|
|
314
|
+
false,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("does not flag a public IPv4 address", () => {
|
|
319
|
+
const findings = scan("server: 8.8.8.8");
|
|
320
|
+
expect(findings.some((f) => f.ruleId === "pii-ipv4")).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Finding } from "./rules.ts";
|
|
2
|
+
|
|
3
|
+
const BIRD_EMOJIS = ["🐦", "🐧", "🐤", "🐔"];
|
|
4
|
+
|
|
5
|
+
export function randomBird(): string {
|
|
6
|
+
return BIRD_EMOJIS[Math.floor(Math.random() * BIRD_EMOJIS.length)] ?? "🐦";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type { Finding };
|
|
10
|
+
|
|
11
|
+
type TextBlock = { type: "text"; text: string };
|
|
12
|
+
type ToolResultBlock = {
|
|
13
|
+
type: "tool_result";
|
|
14
|
+
content: string | ContentBlock[];
|
|
15
|
+
};
|
|
16
|
+
type ToolUseBlock = { type: "tool_use"; input: Record<string, unknown> };
|
|
17
|
+
type ContentBlock = TextBlock | ToolResultBlock | ToolUseBlock;
|
|
18
|
+
|
|
19
|
+
export interface Message {
|
|
20
|
+
role: string;
|
|
21
|
+
content: string | ContentBlock[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseTagsOfType(prefix: string, messages: Message[]): Set<string> {
|
|
25
|
+
const tags = new Set<string>();
|
|
26
|
+
const pattern = new RegExp(`\\[${prefix}-([^\\]]+)\\]`, "gi");
|
|
27
|
+
|
|
28
|
+
for (const msg of messages) {
|
|
29
|
+
if (msg.role !== "user") continue;
|
|
30
|
+
const texts =
|
|
31
|
+
typeof msg.content === "string"
|
|
32
|
+
? [msg.content]
|
|
33
|
+
: msg.content
|
|
34
|
+
.filter((b): b is TextBlock => b.type === "text")
|
|
35
|
+
.map((b) => b.text);
|
|
36
|
+
|
|
37
|
+
for (const text of texts) {
|
|
38
|
+
for (const [, tag] of text.matchAll(pattern)) {
|
|
39
|
+
if (tag) tags.add(tag.toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return tags;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseAllowTags(messages: Message[]): Set<string> {
|
|
47
|
+
return parseTagsOfType("allow", messages);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Resolve effective allow/mask tags based on first-occurrence priority.
|
|
51
|
+
// For each category dimension ("secret", "pii"), the first matching tag wins.
|
|
52
|
+
// [allow-all] / [mask-all] resolve both dimensions at once.
|
|
53
|
+
//
|
|
54
|
+
// "[allow-secret] [mask-secret] ..." → secret: allow
|
|
55
|
+
// "[mask-secret] [allow-secret] ..." → secret: mask
|
|
56
|
+
// "[allow-secret] [mask-pii] ..." → secret: allow, pii: mask
|
|
57
|
+
export function resolveTagPriority(prompt: string): {
|
|
58
|
+
effectiveAllow: Set<string>;
|
|
59
|
+
effectiveMask: Set<string>;
|
|
60
|
+
} {
|
|
61
|
+
const pattern = /\[(allow|mask)-(all|secret|pii)\]/gi;
|
|
62
|
+
const effectiveAllow = new Set<string>();
|
|
63
|
+
const effectiveMask = new Set<string>();
|
|
64
|
+
const resolved = new Set<string>();
|
|
65
|
+
|
|
66
|
+
for (const [, kind, tag] of prompt.matchAll(pattern)) {
|
|
67
|
+
if (!kind || !tag) continue;
|
|
68
|
+
const k = kind.toLowerCase() as "allow" | "mask";
|
|
69
|
+
const t = tag.toLowerCase();
|
|
70
|
+
const dims = t === "all" ? ["secret", "pii"] : [t];
|
|
71
|
+
|
|
72
|
+
for (const dim of dims) {
|
|
73
|
+
if (!resolved.has(dim)) {
|
|
74
|
+
resolved.add(dim);
|
|
75
|
+
(k === "allow" ? effectiveAllow : effectiveMask).add(dim);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (effectiveAllow.has("secret") && effectiveAllow.has("pii")) {
|
|
81
|
+
effectiveAllow.add("all");
|
|
82
|
+
}
|
|
83
|
+
if (effectiveMask.has("secret") && effectiveMask.has("pii")) {
|
|
84
|
+
effectiveMask.add("all");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { effectiveAllow, effectiveMask };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function applyAllowTags(
|
|
91
|
+
findings: Finding[],
|
|
92
|
+
allowTags: Set<string>,
|
|
93
|
+
): Finding[] {
|
|
94
|
+
if (allowTags.size === 0) return findings;
|
|
95
|
+
if (allowTags.has("all")) return [];
|
|
96
|
+
return findings.filter((f) => !allowTags.has(f.category));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function dedupeFindings(findings: Finding[]): Finding[] {
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
return findings.filter((f) => {
|
|
102
|
+
if (seen.has(f.secretValue)) return false;
|
|
103
|
+
seen.add(f.secretValue);
|
|
104
|
+
return true;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function findingsToLines(findings: Finding[]): string[] {
|
|
109
|
+
return findings.map((f) => {
|
|
110
|
+
const tag = f.category === "pii" ? "PII" : "Secret";
|
|
111
|
+
return ` [${tag}] ${f.description} (${f.ruleId}): ${f.matchRedacted}`;
|
|
112
|
+
});
|
|
113
|
+
}
|