reposec 0.1.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/lib/rules.ts ADDED
@@ -0,0 +1,662 @@
1
+ import type { Severity, FindingCategory } from "./types";
2
+
3
+ export interface SecretPattern {
4
+ name: string;
5
+ needles: string[];
6
+ regex: RegExp;
7
+ severity: Severity;
8
+ description: string;
9
+ }
10
+
11
+ export const SECRET_PATTERNS: SecretPattern[] = [
12
+ {
13
+ name: "OpenAI API Key",
14
+ needles: ["sk-"],
15
+ regex: /\bsk-[A-Za-z0-9_-]{20,}/g,
16
+ severity: "critical",
17
+ description: "Looks like an OpenAI API key. Rotate immediately if real.",
18
+ },
19
+ {
20
+ name: "Anthropic API Key",
21
+ needles: ["sk-ant-"],
22
+ regex: /\bsk-ant-(?:api03-)?[A-Za-z0-9_-]{20,}/g,
23
+ severity: "critical",
24
+ description:
25
+ "Looks like an Anthropic API key. Rotate immediately if real.",
26
+ },
27
+ {
28
+ name: "Groq API Key",
29
+ needles: ["gsk_"],
30
+ regex: /\bgsk_[A-Za-z0-9]{20,}/g,
31
+ severity: "critical",
32
+ description: "Looks like a Groq API key. Rotate immediately if real.",
33
+ },
34
+ {
35
+ name: "HuggingFace Token",
36
+ needles: ["hf_"],
37
+ regex: /\bhf_[A-Za-z0-9]{30,}/g,
38
+ severity: "high",
39
+ description: "Looks like a HuggingFace token. Revoke it if real.",
40
+ },
41
+ {
42
+ name: "GitHub Token",
43
+ needles: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"],
44
+ regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}\b|github_pat_[A-Za-z0-9_]{20,}/g,
45
+ severity: "critical",
46
+ description:
47
+ "Looks like a GitHub personal access or OAuth token. Revoke it if real.",
48
+ },
49
+ {
50
+ name: "GitLab Token",
51
+ needles: ["glpat-", "gloas-", "glrt-"],
52
+ regex: /\bgl(?:pat|oas|rt)-[A-Za-z0-9_-]{20,}/g,
53
+ severity: "critical",
54
+ description: "Looks like a GitLab token. Revoke it if real.",
55
+ },
56
+ {
57
+ name: "npm Token",
58
+ needles: ["npm_"],
59
+ regex: /\bnpm_[A-Za-z0-9]{36,}/g,
60
+ severity: "critical",
61
+ description: "Looks like an npm publish token. Revoke it if real.",
62
+ },
63
+ {
64
+ name: "PyPI Token",
65
+ needles: ["pypi-"],
66
+ regex: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{20,}/g,
67
+ severity: "critical",
68
+ description: "Looks like a PyPI upload token. Revoke it if real.",
69
+ },
70
+ {
71
+ name: "Slack Token",
72
+ needles: ["xox"],
73
+ regex: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
74
+ severity: "high",
75
+ description: "Looks like a Slack token. Rotate it if real.",
76
+ },
77
+ {
78
+ name: "Discord Bot Token",
79
+ needles: [],
80
+ regex:
81
+ /\b[MN][A-Za-z0-9]{23,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{27,}\b/g,
82
+ severity: "critical",
83
+ description: "Looks like a Discord bot token. Regenerate the bot if real.",
84
+ },
85
+ {
86
+ name: "Telegram Bot Token",
87
+ needles: [],
88
+ regex: /\b\d{8,10}:[A-Za-z0-9_-]{35}\b/g,
89
+ severity: "critical",
90
+ description: "Looks like a Telegram bot token. Revoke via BotFather if real.",
91
+ },
92
+ {
93
+ name: "Twilio API Key",
94
+ needles: ["SK", "AC"],
95
+ regex: /\bSK[a-fA-F0-9]{32}\b/g,
96
+ severity: "high",
97
+ description: "Looks like a Twilio API key SID. Rotate it if real.",
98
+ },
99
+ {
100
+ name: "SendGrid API Key",
101
+ needles: ["SG."],
102
+ regex: /\bSG\.[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{43,}\b/g,
103
+ severity: "high",
104
+ description: "Looks like a SendGrid API key. Revoke it if real.",
105
+ },
106
+ {
107
+ name: "Mailgun API Key",
108
+ needles: ["key-"],
109
+ regex: /\bkey-[a-f0-9]{32}\b/g,
110
+ severity: "high",
111
+ description: "Looks like a Mailgun API key. Rotate it if real.",
112
+ },
113
+ {
114
+ name: "Stripe Secret Key",
115
+ needles: ["sk_live_"],
116
+ regex: /\bsk_live_[A-Za-z0-9]{20,}/g,
117
+ severity: "critical",
118
+ description: "Looks like a live Stripe secret key. Roll the key if real.",
119
+ },
120
+ {
121
+ name: "Stripe Test Key",
122
+ needles: ["sk_test_"],
123
+ regex: /\bsk_test_[A-Za-z0-9]{20,}/g,
124
+ severity: "medium",
125
+ description: "Looks like a Stripe test key. Keep test keys out of repos.",
126
+ },
127
+ {
128
+ name: "Google API Key",
129
+ needles: ["AIza"],
130
+ regex: /\bAIza[0-9A-Za-z\-_]{35}\b/g,
131
+ severity: "high",
132
+ description: "Looks like a Google API key. Restrict and rotate if real.",
133
+ },
134
+ {
135
+ name: "AWS Access Key ID",
136
+ needles: ["AKIA", "ASIA"],
137
+ regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
138
+ severity: "critical",
139
+ description: "Looks like an AWS access key. Disable and rotate if real.",
140
+ },
141
+ {
142
+ name: "AWS Secret Access Key",
143
+ needles: ["aws_secret", "aws_sk"],
144
+ regex:
145
+ /\baws[_-]?(?:secret|sk)[^A-Za-z0-9]{0,5}[A-Za-z0-9/+=]{40}\b/gi,
146
+ severity: "critical",
147
+ description: "Possible AWS secret access key near a key id.",
148
+ },
149
+ {
150
+ name: "Supabase Token",
151
+ needles: ["sbp_", "sb_secret_"],
152
+ regex: /\bsbp_[a-f0-9]{40,}\b|sb_secret_[A-Za-z0-9_-]{40,}/g,
153
+ severity: "critical",
154
+ description: "Looks like a Supabase service-role key. Rotate it if real.",
155
+ },
156
+ {
157
+ name: "Linear API Key",
158
+ needles: ["lin_api_"],
159
+ regex: /\blin_api_[A-Za-z0-9]{40,}/g,
160
+ severity: "high",
161
+ description: "Looks like a Linear API key. Revoke it if real.",
162
+ },
163
+ {
164
+ name: "Shopify Token",
165
+ needles: ["shpat_", "shpca_", "shppa_", "shpss_"],
166
+ regex: /\b(?:shpat|shpca|shppa|shpss)_[a-fA-F0-9]{32,}\b/g,
167
+ severity: "critical",
168
+ description: "Looks like a Shopify access token. Revoke it if real.",
169
+ },
170
+ {
171
+ name: "PlanetScale Token",
172
+ needles: ["pscale_tkn_"],
173
+ regex: /\bpscale_tkn_[A-Za-z0-9_-]{40,}/g,
174
+ severity: "critical",
175
+ description: "Looks like a PlanetScale token. Revoke it if real.",
176
+ },
177
+ {
178
+ name: "1Password Service Account",
179
+ needles: ["ops_"],
180
+ regex: /\bops_[A-Za-z0-9]{40,}/g,
181
+ severity: "critical",
182
+ description: "Looks like a 1Password service-account token. Revoke it if real.",
183
+ },
184
+ {
185
+ name: "Doppler CLI Token",
186
+ needles: ["dp.st."],
187
+ regex: /\bdp\.st\.[A-Za-z0-9_-]{40,}/g,
188
+ severity: "critical",
189
+ description: "Looks like a Doppler CLI token. Revoke it if real.",
190
+ },
191
+ {
192
+ name: "Postman API Key",
193
+ needles: ["PMAK-"],
194
+ regex: /\bPMAK-[A-Za-z0-9-]{60,}\b/g,
195
+ severity: "high",
196
+ description: "Looks like a Postman API key. Revoke it if real.",
197
+ },
198
+ {
199
+ name: "Mapbox Secret Token",
200
+ needles: ["sk.eyJ"],
201
+ regex: /\bsk\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g,
202
+ severity: "high",
203
+ description: "Looks like a Mapbox secret access token. Restrict and rotate if real.",
204
+ },
205
+ {
206
+ name: "Private Key Block",
207
+ needles: ["-----BEGIN"],
208
+ regex:
209
+ /-----BEGIN (?:RSA |OPENSSH |DSA |EC |PGP |ENCRYPTED )?PRIVATE KEY-----/g,
210
+ severity: "critical",
211
+ description:
212
+ "Private key block in source. Move out and rotate immediately.",
213
+ },
214
+ {
215
+ name: "SSH Public Key (informational)",
216
+ needles: ["ssh-rsa", "ssh-ed25519"],
217
+ regex: /\b(?:ssh-rsa|ssh-ed25519|ecdsa-sha2-[a-z0-9-]+) AAAA[A-Za-z0-9+/=]{50,}/g,
218
+ severity: "low",
219
+ description:
220
+ "SSH public key in source. Public keys are not secret, but review whether it should be committed.",
221
+ },
222
+ {
223
+ name: "Generic JWT",
224
+ needles: ["eyJ"],
225
+ regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
226
+ severity: "high",
227
+ description: "Looks like a JWT. Treat as a secret and rotate if real.",
228
+ },
229
+ {
230
+ name: "Database URL with Credentials",
231
+ needles: ["postgres://", "postgresql://", "mysql://", "mongodb://", "redis://"],
232
+ regex:
233
+ /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/[^\s"'<>]+:[^\s"'<>]+@/gi,
234
+ severity: "high",
235
+ description:
236
+ "Looks like a database URL with embedded credentials. Move to a secret manager.",
237
+ },
238
+ {
239
+ name: ".npmrc _authToken",
240
+ needles: ["_authToken"],
241
+ regex: /_authToken\s*=\s*[A-Za-z0-9._-]+/g,
242
+ severity: "critical",
243
+ description:
244
+ "npm auth token in .npmrc. Revoke the token on npmjs.com and remove this file from the repo.",
245
+ },
246
+ {
247
+ name: ".pypirc password",
248
+ needles: ["[pypi]", "password"],
249
+ regex: /^\s*password\s*=\s*[^\s#]+/gim,
250
+ severity: "critical",
251
+ description:
252
+ "PyPI password in .pypirc. Rotate the token and remove this file from the repo.",
253
+ },
254
+ {
255
+ name: ".netrc password",
256
+ needles: ["machine ", "password "],
257
+ regex: /^\s*password\s+[^\s#]+/gim,
258
+ severity: "critical",
259
+ description:
260
+ "Plaintext password in .netrc. Move credentials to a secret manager and remove this file from the repo.",
261
+ },
262
+ {
263
+ name: "Bearer Token in Header",
264
+ needles: ["Bearer "],
265
+ regex: /\bBearer\s+[A-Za-z0-9._\-+/=]{20,}/g,
266
+ severity: "high",
267
+ description:
268
+ "Bearer token in source. Move to a request signer and avoid checking tokens in.",
269
+ },
270
+ {
271
+ name: "Basic Auth in URL",
272
+ needles: ["://"],
273
+ regex: /\bhttps?:\/\/[A-Za-z0-9._~%-]+:[A-Za-z0-9._~%-]+@[^\s"'<>]+/g,
274
+ severity: "high",
275
+ description:
276
+ "Basic-auth credentials in URL. Move to request headers and use environment variables.",
277
+ },
278
+ {
279
+ name: "Generic API Key Assignment",
280
+ needles: ["api_key", "apikey", "secret", "token", "password", "passwd", "pwd"],
281
+ regex:
282
+ /\b(?:api[_-]?key|apikey|secret|token|password|passwd|pwd)\b\s*[:=]\s*["']([^"'\s]{12,})["']/gi,
283
+ severity: "high",
284
+ description:
285
+ "Hardcoded credential-like assignment. Review and move to environment variables.",
286
+ },
287
+ {
288
+ name: "JWT Secret Assignment",
289
+ needles: ["JWT_SECRET", "jwt-secret", "jwt_secret"],
290
+ regex: /\bJWT[_-]?SECRET\b\s*[:=]\s*["']([^"'\s]{6,})["']/gi,
291
+ severity: "high",
292
+ description: "JWT secret committed to source. Move to a secret manager.",
293
+ },
294
+ {
295
+ name: "Terraform Variable with secret name",
296
+ needles: ["secret", "password", "token"],
297
+ regex: /^\s*(?:api[_-]?key|secret|password|token|private[_-]?key)\s*=\s*["']([^"'\s]{12,})["']/gim,
298
+ severity: "high",
299
+ description:
300
+ "Hardcoded value in a Terraform variable. Move to a tfvars file that is gitignored, or use a secret manager.",
301
+ },
302
+
303
+ // ---- gitleaks-derived rules (ported from gitleaks/config/gitleaks.toml) ----
304
+
305
+ {
306
+ name: "Anthropic Admin API Key",
307
+ needles: ["sk-ant-admin01"],
308
+ regex: /\bsk-ant-admin01-[A-Za-z0-9_\-]{93}AA(?:[`'"\s;]|$)/g,
309
+ severity: "critical",
310
+ description:
311
+ "Looks like an Anthropic admin API key. Rotate immediately if real.",
312
+ },
313
+ {
314
+ name: "Perplexity API Key",
315
+ needles: ["pplx-"],
316
+ regex: /\bpplx-[A-Za-z0-9]{48}\b/g,
317
+ severity: "high",
318
+ description: "Looks like a Perplexity AI API key. Revoke it if real.",
319
+ },
320
+ {
321
+ name: "Notion API Token",
322
+ needles: ["ntn_"],
323
+ regex: /\bntn_\d{11}[A-Za-z0-9]{32}[A-Za-z0-9]{3}(?:[`'"\s;]|$)/g,
324
+ severity: "high",
325
+ description: "Looks like a Notion API token. Revoke it if real.",
326
+ },
327
+ {
328
+ name: "HuggingFace Org Token",
329
+ needles: ["api_org_"],
330
+ regex: /\bapi_org_[A-Za-z0-9]{34}(?:[`'"\s;]|$)/g,
331
+ severity: "high",
332
+ description: "Looks like a HuggingFace organization token. Revoke it if real.",
333
+ },
334
+ {
335
+ name: "AWS Bedrock Long-Lived API Key",
336
+ needles: ["ABSK"],
337
+ regex: /\bABSK[A-Za-z0-9+/]{109,269}={0,2}(?:[`'"\s;]|$)/g,
338
+ severity: "critical",
339
+ description: "Looks like an AWS Bedrock long-lived API key. Rotate it if real.",
340
+ },
341
+ {
342
+ name: "Azure AD Client Secret",
343
+ needles: ["Q~"],
344
+ regex: /(?:^|[`'"\s>=:(,])([A-Za-z0-9_~.]{3}\dQ~[A-Za-z0-9_~.-]{31,34})(?:$|[`'"\s<),])/g,
345
+ severity: "high",
346
+ description: "Looks like an Azure AD client secret. Rotate it if real.",
347
+ },
348
+ {
349
+ name: "Alibaba AccessKey ID",
350
+ needles: ["LTAI"],
351
+ regex: /\bLTAI[A-Za-z0-9]{20}(?:[`'"\s;]|$)/g,
352
+ severity: "critical",
353
+ description: "Looks like an Alibaba Cloud access key ID. Rotate it if real.",
354
+ },
355
+ {
356
+ name: "Heroku API Key v2",
357
+ needles: ["HRKU-AA"],
358
+ regex: /\bHRKU-AA[A-Za-z0-9_-]{58}(?:[`'"\s;]|$)/g,
359
+ severity: "high",
360
+ description: "Looks like a Heroku API key. Revoke it if real.",
361
+ },
362
+ {
363
+ name: "Pulumi API Token",
364
+ needles: ["pul-"],
365
+ regex: /\bpul-[a-f0-9]{40}(?:[`'"\s;]|$)/g,
366
+ severity: "high",
367
+ description: "Looks like a Pulumi API token. Revoke it if real.",
368
+ },
369
+ {
370
+ name: "Fly.io Access Token",
371
+ needles: ["fo1_", "fm1", "fm2_"],
372
+ regex: /\b(?:fo1_[\w-]{43}|fm1[ar]_[A-Za-z0-9+/]{100,}={0,3}|fm2_[A-Za-z0-9+/]{100,}={0,3})(?:[`'"\s;]|$)/g,
373
+ severity: "high",
374
+ description: "Looks like a Fly.io access token. Revoke it if real.",
375
+ },
376
+ {
377
+ name: "New Relic User API Key",
378
+ needles: ["NRAK-"],
379
+ regex: /\bNRAK-[a-z0-9]{27}\b/gi,
380
+ severity: "high",
381
+ description: "Looks like a New Relic user API key. Revoke it if real.",
382
+ },
383
+ {
384
+ name: "New Relic Insert Key",
385
+ needles: ["NRII-"],
386
+ regex: /\bNRII-[a-z0-9-]{32}\b/gi,
387
+ severity: "medium",
388
+ description: "Looks like a New Relic data insert key. Rotate it if real.",
389
+ },
390
+ {
391
+ name: "Dynatrace API Token",
392
+ needles: ["dt0c01."],
393
+ regex: /dt0c01\.[A-Za-z0-9]{24}\.[a-z0-9]{64}/g,
394
+ severity: "high",
395
+ description: "Looks like a Dynatrace API token. Revoke it if real.",
396
+ },
397
+ {
398
+ name: "Grafana Cloud API Token",
399
+ needles: ["glc_"],
400
+ regex: /\bglc_[A-Za-z0-9+/]{32,400}={0,3}\b/gi,
401
+ severity: "high",
402
+ description: "Looks like a Grafana Cloud API token. Revoke it if real.",
403
+ },
404
+ {
405
+ name: "Databricks API Token",
406
+ needles: ["dapi"],
407
+ regex: /\bdapi[a-f0-9]{32}(?:-\d)?(?:[`'"\s;]|$)/g,
408
+ severity: "critical",
409
+ description: "Looks like a Databricks personal access token. Revoke it if real.",
410
+ },
411
+ {
412
+ name: "HashiCorp Vault Service Token",
413
+ needles: ["hvs.", "s."],
414
+ regex: /\b(?:hvs\.[\w-]{90,120}|s\.[A-Za-z0-9]{24})(?:[`'"\s;]|$)/g,
415
+ severity: "critical",
416
+ description:
417
+ "Looks like a HashiCorp Vault service or static token. Revoke it if real.",
418
+ },
419
+ {
420
+ name: "HashiCorp Vault Batch Token",
421
+ needles: ["hvb."],
422
+ regex: /\bhvb\.[\w-]{138,300}(?:[`'"\s;]|$)/g,
423
+ severity: "critical",
424
+ description: "Looks like a HashiCorp Vault batch token. Revoke it if real.",
425
+ },
426
+ {
427
+ name: "HashiCorp Terraform Cloud API Token",
428
+ needles: ["atlasv1"],
429
+ regex: /[a-z0-9]{14}\.atlasv1\.[a-z0-9\-_=]{60,70}/gi,
430
+ severity: "high",
431
+ description:
432
+ "Looks like a Terraform Cloud API token (atlasv1). Revoke it if real.",
433
+ },
434
+ {
435
+ name: "EasyPost API Token",
436
+ needles: ["EZAK"],
437
+ regex: /\bEZAK[A-Za-z0-9]{54}\b/g,
438
+ severity: "high",
439
+ description: "Looks like an EasyPost API token. Revoke it if real.",
440
+ },
441
+ {
442
+ name: "ReadMe API Token",
443
+ needles: ["rdme_"],
444
+ regex: /\brdme_[a-z0-9]{70}(?:[`'"\s;]|$)/g,
445
+ severity: "high",
446
+ description: "Looks like a ReadMe.io API token. Revoke it if real.",
447
+ },
448
+ {
449
+ name: "Prefect API Token",
450
+ needles: ["pnu_"],
451
+ regex: /\bpnu_[A-Za-z0-9]{36}(?:[`'"\s;]|$)/g,
452
+ severity: "high",
453
+ description: "Looks like a Prefect API token. Revoke it if real.",
454
+ },
455
+ {
456
+ name: "Sourcegraph Access Token",
457
+ needles: ["sgp_"],
458
+ regex: /\b(?:sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40})\b/g,
459
+ severity: "high",
460
+ description: "Looks like a Sourcegraph access token. Revoke it if real.",
461
+ },
462
+ {
463
+ name: "Slack Webhook URL",
464
+ needles: ["hooks.slack.com"],
465
+ regex: /(?:https?:\/\/)?hooks\.slack\.com\/(?:services|workflows|triggers)\/[A-Za-z0-9+/]{43,56}/g,
466
+ severity: "high",
467
+ description:
468
+ "Slack incoming webhook URL in source. Delete the webhook and create a new one if real.",
469
+ },
470
+ {
471
+ name: "Microsoft Teams Webhook URL",
472
+ needles: ["webhook.office.com", "webhookb2"],
473
+ regex: /https:\/\/[a-z0-9]+\.webhook\.office\.com\/webhookb2\/[a-z0-9]{8}-(?:[a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-(?:[a-z0-9]{4}-){3}[a-z0-9]{12}\/IncomingWebhook\/[a-z0-9]{32}\/[a-z0-9]{8}-(?:[a-z0-9]{4}-){3}[a-z0-9]{12}/g,
474
+ severity: "high",
475
+ description:
476
+ "Microsoft Teams incoming webhook URL. Regenerate the webhook if real.",
477
+ },
478
+ {
479
+ name: "Curl Authorization Header",
480
+ needles: ["curl", "Authorization:", "Bearer", "X-Api-Key"],
481
+ regex: /\bcurl\b[\s\S]{0,300}?(?:-H|--header)\b[\s\S]{0,10}?["'](?:Authorization:\s*(?:Basic|Bearer|Token)\s+[A-Za-z0-9._~+/=@-]{8,}|X-[A-Za-z]+-?(?:Api-?)?Key:\s*[A-Za-z0-9._~+/=@-]{8,})["']/i,
482
+ severity: "high",
483
+ description:
484
+ "Authorization or API-key header in a curl command. Move credentials to environment variables or a request signer.",
485
+ },
486
+ {
487
+ name: "Curl Basic Auth User",
488
+ needles: ["curl"],
489
+ regex: /\bcurl\b[\s\S]{0,200}?(?:-u|--user)\b[\s\S]{0,5}?["'](?:[^:"\s]+:[^"\s]+|[^"'\s]+:[^"'\s]+)["']/i,
490
+ severity: "high",
491
+ description:
492
+ "Basic-auth user:pass in a curl command. Move credentials to environment variables or a .netrc file.",
493
+ },
494
+ {
495
+ name: "HashiCorp Terraform Password",
496
+ needles: ["password", "administrator_login_password"],
497
+ regex: /\b(?:administrator_login_password|password)\b[\s'"=]{0,5}["'][A-Za-z0-9=_\-]{8,}["']/gi,
498
+ severity: "high",
499
+ description:
500
+ "Hardcoded password in a Terraform (.tf/.hcl) file. Use a tfvars file that is gitignored, or fetch from a secret manager.",
501
+ },
502
+ ];
503
+
504
+ export const SECRET_NEEDLES: string[] = (() => {
505
+ const set = new Set<string>();
506
+ for (const p of SECRET_PATTERNS) {
507
+ for (const n of p.needles) set.add(n);
508
+ }
509
+ return [...set];
510
+ })();
511
+
512
+ export function fileContainsAnyNeedle(content: string): boolean {
513
+ for (const n of SECRET_NEEDLES) {
514
+ if (content.indexOf(n) !== -1) return true;
515
+ }
516
+ return false;
517
+ }
518
+
519
+ export interface RuleHit {
520
+ id: string;
521
+ title: string;
522
+ description: string;
523
+ severity: Severity;
524
+ category: FindingCategory;
525
+ file?: string;
526
+ line?: number;
527
+ evidence?: string;
528
+ fix: string;
529
+ fixPrompt?: string;
530
+ }
531
+
532
+ const PLACEHOLDER_VALUES = new Set([
533
+ "your-api-key",
534
+ "changeme",
535
+ "change-me",
536
+ "example",
537
+ "placeholder",
538
+ "xxx",
539
+ "xxxx",
540
+ "your-key",
541
+ "your-token",
542
+ "<your-key>",
543
+ "<your-token>",
544
+ "todo",
545
+ "fixme",
546
+ "null",
547
+ "nil",
548
+ "none",
549
+ "undefined",
550
+ "true",
551
+ "false",
552
+ ]);
553
+
554
+ const PLACEHOLDER_PATTERNS: RegExp[] = [
555
+ /^<[a-z][a-z0-9_.\-]{1,30}>$/i,
556
+ /^\[[a-z][a-z0-9_.\-]{1,30}\]$/i,
557
+ /^\{[a-z][a-z0-9_.\-]{1,30}\}$/i,
558
+ /^(?:your|my|the|some|insert|replace|enter)[-_][a-z][a-z0-9_.\-]{1,30}$/i,
559
+ /^[a-z][a-z0-9_.\-]{1,30}[-_](?:here|placeholder|example|change|todo|fixme|key|token|password|secret)$/i,
560
+ /^(?:x{3,}|\*{3,}|\.{3,}|-{3,})$/i,
561
+ /^\$\{[a-z][a-z0-9_.\-]{0,30}\}$/i,
562
+ /^process\.env\.[a-z][a-z0-9_]{0,30}$/i,
563
+ /^env\.[a-z][a-z0-9_]{0,30}$/i,
564
+ /^%[a-z][a-z0-9_]{0,30}%$/i,
565
+ ];
566
+
567
+ export function isLikelyPlaceholder(value: string): boolean {
568
+ const v = value.trim();
569
+ if (!v) return true;
570
+ const lower = v.toLowerCase();
571
+ if (PLACEHOLDER_VALUES.has(lower)) return true;
572
+ if (v.length < 8) return true;
573
+ for (const re of PLACEHOLDER_PATTERNS) {
574
+ if (re.test(v)) return true;
575
+ }
576
+ return false;
577
+ }
578
+
579
+ const COMMENT_PREFIXES_BY_EXT: Record<string, string[]> = {
580
+ "default": ["#", "//", "/*", "*", "<!--"],
581
+ "ts": ["//", "/*", "*"],
582
+ "tsx": ["//", "/*", "*"],
583
+ "js": ["//", "/*", "*"],
584
+ "jsx": ["//", "/*", "*"],
585
+ "mjs": ["//", "/*", "*"],
586
+ "cjs": ["//", "/*", "*"],
587
+ "java": ["//", "/*", "*"],
588
+ "kt": ["//", "/*", "*"],
589
+ "swift": ["//", "/*", "*"],
590
+ "go": ["//", "/*", "*"],
591
+ "rs": ["//", "/*", "*"],
592
+ "scala": ["//", "/*", "*"],
593
+ "clj": [";;", ";", "#"],
594
+ "dart": ["//", "/*", "*"],
595
+ "php": ["//", "/*", "*", "#"],
596
+ "sh": ["#"],
597
+ "bash": ["#"],
598
+ "zsh": ["#"],
599
+ "fish": ["#"],
600
+ "ps1": ["#"],
601
+ "py": ["#"],
602
+ "rb": ["#"],
603
+ "lua": ["--"],
604
+ "yml": ["#"],
605
+ "yaml": ["#"],
606
+ "toml": ["#"],
607
+ "ini": [";", "#"],
608
+ "cfg": [";", "#"],
609
+ "conf": [";", "#"],
610
+ "config": [";", "#"],
611
+ "env": ["#"],
612
+ "npmrc": ["#", ";"],
613
+ "pypirc": ["#"],
614
+ "netrc": ["#"],
615
+ "tf": ["#", "//"],
616
+ "tfvars": ["#", "//"],
617
+ "properties": ["#", "!"],
618
+ "xml": ["<!--"],
619
+ "html": ["<!--"],
620
+ "htm": ["<!--"],
621
+ "md": ["<!--"],
622
+ "json": [],
623
+ "pem": [],
624
+ "key": [],
625
+ "crt": [],
626
+ };
627
+
628
+ export function isCommentedLine(line: string, ext: string = "default"): boolean {
629
+ const t = line.trimStart();
630
+ if (!t) return false;
631
+ const prefixes =
632
+ COMMENT_PREFIXES_BY_EXT[ext.toLowerCase()] ??
633
+ COMMENT_PREFIXES_BY_EXT["default"];
634
+ for (const p of prefixes) {
635
+ if (t.startsWith(p)) return true;
636
+ }
637
+ return false;
638
+ }
639
+
640
+ export const SEVERITY_WEIGHT: Record<Severity, number> = {
641
+ critical: 25,
642
+ high: 15,
643
+ medium: 8,
644
+ low: 3,
645
+ info: 1,
646
+ };
647
+
648
+ export const SEVERITY_RANK: Record<Severity, number> = {
649
+ critical: 5,
650
+ high: 4,
651
+ medium: 3,
652
+ low: 2,
653
+ info: 1,
654
+ };
655
+
656
+ export const SEVERITY_LABELS: Record<Severity, string> = {
657
+ critical: "Critical",
658
+ high: "High",
659
+ medium: "Medium",
660
+ low: "Low",
661
+ info: "Info",
662
+ };