@coo-quack/sensitive-canary 0.7.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 +791 -0
- package/README.md +142 -45
- 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 +155 -46
- 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 +202 -365
- 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 -289
- package/src/lib/__tests__/rules.test.ts +0 -1370
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contextWindow": 3,
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"id": "aws-access-key",
|
|
6
|
+
"description": "AWS Access Key ID",
|
|
7
|
+
"regex": "\\b(A3T[A-Z0-9]|AKIA|ABIA|ACCA|APKA|ASCA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\\b",
|
|
8
|
+
"category": "secret",
|
|
9
|
+
"validate": "aws-key"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "gcp-api-key",
|
|
13
|
+
"description": "Google Cloud API Key",
|
|
14
|
+
"regex": "AIza[0-9A-Za-z_-]{35}",
|
|
15
|
+
"category": "secret"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "private-key",
|
|
19
|
+
"description": "PEM Private Key",
|
|
20
|
+
"regex": "-----BEGIN ((RSA|EC|DSA|PGP|OPENSSH|ENCRYPTED|SSH2 ENCRYPTED) )?PRIVATE KEY",
|
|
21
|
+
"category": "secret"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "github-pat",
|
|
25
|
+
"description": "GitHub Personal Access Token",
|
|
26
|
+
"regex": "gh[pousr]_[A-Za-z0-9]{36,255}",
|
|
27
|
+
"category": "secret"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "github-fine-grained",
|
|
31
|
+
"description": "GitHub Fine-Grained Token",
|
|
32
|
+
"regex": "github_pat_[A-Za-z0-9_]{82}",
|
|
33
|
+
"category": "secret"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "gitlab-pat",
|
|
37
|
+
"description": "GitLab Personal Access Token",
|
|
38
|
+
"regex": "(?:gl(?:pat|dt|rt|cbt|imt|agent|oas|ptt|soat|ffct|ft)-[A-Za-z0-9_=-]{20,}|GR1348941[A-Za-z0-9_-]{20})",
|
|
39
|
+
"category": "secret"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "npm-token",
|
|
43
|
+
"description": "npm Access Token",
|
|
44
|
+
"regex": "npm_[A-Za-z0-9]{36}",
|
|
45
|
+
"category": "secret"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "slack-token",
|
|
49
|
+
"description": "Slack Token",
|
|
50
|
+
"regex": "(?:xox[abeprs]|xapp|xwfp)-[0-9a-zA-Z-]{10,72}",
|
|
51
|
+
"category": "secret"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "slack-webhook",
|
|
55
|
+
"description": "Slack Webhook URL",
|
|
56
|
+
"regex": "https:\\/\\/hooks\\.slack\\.com\\/services\\/T[A-Za-z0-9_]{8,10}\\/B[A-Za-z0-9_]{8,12}\\/[A-Za-z0-9_]{23,24}",
|
|
57
|
+
"category": "secret"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "discord-webhook",
|
|
61
|
+
"description": "Discord Webhook URL",
|
|
62
|
+
"regex": "https:\\/\\/discord(?:app)?\\.com\\/api\\/webhooks\\/[0-9]{17,20}\\/[A-Za-z0-9_-]{68}",
|
|
63
|
+
"category": "secret"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "telegram-bot-token",
|
|
67
|
+
"description": "Telegram Bot Token",
|
|
68
|
+
"regex": "\\b[0-9]{6,12}:AA[0-9A-Za-z_-]{30,128}\\b",
|
|
69
|
+
"category": "secret"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "twilio-sid",
|
|
73
|
+
"description": "Twilio Account SID",
|
|
74
|
+
"regex": "(?<![A-Za-z0-9])(?:AC|SK)[0-9a-fA-F]{32}(?![A-Za-z0-9])",
|
|
75
|
+
"category": "secret"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "sendgrid-key",
|
|
79
|
+
"description": "SendGrid API Key",
|
|
80
|
+
"regex": "SG\\.[A-Za-z0-9_-]{20,24}\\.[A-Za-z0-9_-]{39,50}",
|
|
81
|
+
"category": "secret"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "mailgun-key",
|
|
85
|
+
"description": "Mailgun API Key",
|
|
86
|
+
"regex": "key-[0-9a-zA-Z]{32}",
|
|
87
|
+
"category": "secret"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "mailchimp-key",
|
|
91
|
+
"description": "Mailchimp API Key",
|
|
92
|
+
"regex": "[0-9a-f]{32}-us[0-9]{1,2}",
|
|
93
|
+
"category": "secret"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "stripe-secret-key",
|
|
97
|
+
"description": "Stripe Secret Key",
|
|
98
|
+
"regex": "(?:sk_(?:live|test|org))_[0-9a-zA-Z]{24}|whsec_[0-9a-zA-Z]{32,}",
|
|
99
|
+
"category": "secret"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "stripe-restricted-key",
|
|
103
|
+
"description": "Stripe Restricted Key",
|
|
104
|
+
"regex": "rk_(live|test)_[0-9a-zA-Z]{24}",
|
|
105
|
+
"category": "secret"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "openai-key",
|
|
109
|
+
"description": "OpenAI API Key (legacy)",
|
|
110
|
+
"regex": "sk-(?!proj-|ant-)[A-Za-z0-9]{48}",
|
|
111
|
+
"category": "secret"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"id": "openai-project-key",
|
|
115
|
+
"description": "OpenAI Project API Key",
|
|
116
|
+
"regex": "sk-proj-[A-Za-z0-9_-]{40,}",
|
|
117
|
+
"category": "secret",
|
|
118
|
+
"entropyThreshold": 3.5
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "anthropic-key",
|
|
122
|
+
"description": "Anthropic API Key",
|
|
123
|
+
"regex": "sk-ant-[A-Za-z0-9_-]{95,}",
|
|
124
|
+
"category": "secret"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"id": "replicate-token",
|
|
128
|
+
"description": "Replicate API Token",
|
|
129
|
+
"regex": "r8_[0-9A-Za-z_-]{37}",
|
|
130
|
+
"category": "secret"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "huggingface-token",
|
|
134
|
+
"description": "Hugging Face Access Token",
|
|
135
|
+
"regex": "(?:hf_[a-zA-Z0-9]{34}|api_org_[a-zA-Z0-9]{34})",
|
|
136
|
+
"category": "secret"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"id": "groq-key",
|
|
140
|
+
"description": "Groq API Key",
|
|
141
|
+
"regex": "gsk_[A-Za-z0-9]{52}",
|
|
142
|
+
"category": "secret"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "openrouter-key",
|
|
146
|
+
"description": "OpenRouter API Key",
|
|
147
|
+
"regex": "sk-or-v1-[0-9a-f]{64}",
|
|
148
|
+
"category": "secret"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"id": "xai-key",
|
|
152
|
+
"description": "xAI (Grok) API Key",
|
|
153
|
+
"regex": "xai-[0-9A-Za-z_]{80}",
|
|
154
|
+
"category": "secret"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "perplexity-key",
|
|
158
|
+
"description": "Perplexity API Key",
|
|
159
|
+
"regex": "pplx-[a-zA-Z0-9]{48}",
|
|
160
|
+
"category": "secret"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"id": "digitalocean-pat",
|
|
164
|
+
"description": "DigitalOcean Personal Access Token",
|
|
165
|
+
"regex": "do[opr]_v1_[a-f0-9]{64}",
|
|
166
|
+
"category": "secret"
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": "square-access-token",
|
|
170
|
+
"description": "Square Access Token",
|
|
171
|
+
"regex": "(?<![A-Za-z0-9+/])(?:EAAA[a-zA-Z0-9\\-_+=]{60,1024}|sq0(?:atp|csp)-[A-Za-z0-9_-]{22,128})(?![A-Za-z0-9+/=_-])",
|
|
172
|
+
"category": "secret"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "mapbox-token",
|
|
176
|
+
"description": "Mapbox Token",
|
|
177
|
+
"regex": "(?<![A-Za-z0-9_-])(?:pk|sk|tk)\\.eyJ[A-Za-z0-9_-]{10,1024}\\.[A-Za-z0-9_-]{10,1024}",
|
|
178
|
+
"category": "secret"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"id": "sentry-user-token",
|
|
182
|
+
"description": "Sentry User Auth Token",
|
|
183
|
+
"regex": "sntryu_[a-f0-9]{64}",
|
|
184
|
+
"category": "secret"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"id": "sentry-org-token",
|
|
188
|
+
"description": "Sentry Organization Auth Token",
|
|
189
|
+
"regex": "(?<![A-Za-z0-9_-])sntrys_[A-Za-z0-9+/=]{10,1024}_[A-Za-z0-9]{43}",
|
|
190
|
+
"category": "secret"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "atlassian-token",
|
|
194
|
+
"description": "Atlassian API Token",
|
|
195
|
+
"regex": "ATATT3[A-Za-z0-9_\\-=]{180,}",
|
|
196
|
+
"category": "secret"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"id": "linear-key",
|
|
200
|
+
"description": "Linear API Key",
|
|
201
|
+
"regex": "lin_api_[a-zA-Z0-9]{40}",
|
|
202
|
+
"category": "secret"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "postman-key",
|
|
206
|
+
"description": "Postman API Key",
|
|
207
|
+
"regex": "PMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}",
|
|
208
|
+
"category": "secret"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"id": "supabase-key",
|
|
212
|
+
"description": "Supabase Personal Access Token",
|
|
213
|
+
"regex": "sbp_[a-z0-9]{40}",
|
|
214
|
+
"category": "secret"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"id": "jwt",
|
|
218
|
+
"description": "JSON Web Token (JWT)",
|
|
219
|
+
"regex": "(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]{10,4096}\\.eyJ[A-Za-z0-9_-]{10,4096}\\.[A-Za-z0-9_-]{10,4096}",
|
|
220
|
+
"category": "secret"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"id": "generic-secret",
|
|
224
|
+
"description": "Generic API Key / Secret",
|
|
225
|
+
"regex": "(api[_-]?key|secret[_-]?key|access[_-]?token|api[_-]?secret)\\s*[:=]\\s*['\"]?([A-Za-z0-9\\-_.]{20,})",
|
|
226
|
+
"category": "secret",
|
|
227
|
+
"flags": "gi",
|
|
228
|
+
"secretGroup": 2,
|
|
229
|
+
"entropyThreshold": 3.5
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"id": "env-assignment",
|
|
233
|
+
"description": ".env style secret assignment",
|
|
234
|
+
"regex": "(?:(?:^|\\n)[ \\t-]{0,64}[\"']?[A-Za-z0-9_]{0,64}(?:SECRET|PASSWORD|PASSWD|(?<![A-Za-z])PASS(?![A-Za-z])|TOKEN|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Za-z0-9_]{0,64}[\"']?[ \\t]*:|\\b[A-Za-z0-9_]{0,64}(?:SECRET|PASSWORD|PASSWD|(?<![A-Za-z])PASS(?![A-Za-z])|TOKEN|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Za-z0-9_]{0,64}[ \\t]*=)[ \\t]*[\"']?(?!\\$)(?=([^\\s\"'\\[\\]{}();,<>]{8,4096}))\\1(?:(?=$|[\\s\"';,)\\]}])|(?=[^\\s\"'\\[\\]{}();,<>]))",
|
|
235
|
+
"flags": "gi",
|
|
236
|
+
"category": "secret",
|
|
237
|
+
"secretGroup": 1,
|
|
238
|
+
"entropyThreshold": 3
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"id": "connection-string",
|
|
242
|
+
"description": "Database Connection String with credentials",
|
|
243
|
+
"regex": "(mongodb(?:\\+srv)?|mysql|postgres|postgresql|redis):\\/\\/[^:\\s]{1,1024}:[^@\\s]{1,1024}@",
|
|
244
|
+
"category": "secret"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"id": "pii-email",
|
|
248
|
+
"description": "Email Address",
|
|
249
|
+
"regex": "(?<!\\b(?:ssh|scp|rsync|sftp)\\b(?:[ \\t]+[^\\s@,;()]+){0,2}[ \\t]+)\\b(?!(?:git|hg|svn)@)[A-Za-z0-9._%+-]{1,64}@(?![0-9]+\\.)(?!example\\.(?:com|net|org|edu)\\b)(?![A-Za-z0-9.-]{0,255}\\.(?:test|invalid|localhost|local|example)\\b)(?:[A-Za-z0-9-]+\\.)+(?!(?:png|jpg|jpeg|gif|webp|svg|ico|bmp|tiff|tif|css|json|html|htm|xml|yaml|yml|lock|woff|ttf|otf|eot|webm|wav|flac|pdf|tgz|bz|xz)(?![A-Za-z]))[A-Za-z]{2,}(?![A-Za-z0-9.-])(?!:\\S)",
|
|
250
|
+
"category": "pii"
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"id": "pii-credit-card",
|
|
254
|
+
"description": "Credit Card Number",
|
|
255
|
+
"regex": "\\b(?:4[0-9]{3}(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{3}|4[0-9]{3}(?:[\\s-]?[0-9]{4}){3}|4[0-9]{3}(?:[\\s-]?[0-9]{4}){2}[\\s-]?[0-9]{1}|5[1-5][0-9]{2}(?:[\\s-]?[0-9]{4}){3}|2(?:22[1-9]|2[3-9][0-9]|[3-6][0-9]{2}|7[01][0-9]|720)(?:[\\s-]?[0-9]{4}){3}|3[47][0-9]{2}(?:[\\s-]?[0-9]{4}){2}[\\s-]?[0-9]{3}|3[47][0-9]{2}[\\s-]?[0-9]{6}[\\s-]?[0-9]{5}|6(?:011|4[4-9][0-9]|5[0-9]{2})(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{3}|6(?:011|4[4-9][0-9]|5[0-9]{2})(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{2}|6(?:011|4[4-9][0-9]|5[0-9]{2})(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{1}|6(?:011|4[4-9][0-9]|5[0-9]{2})(?:[\\s-]?[0-9]{4}){3}|35(?:2[89]|[3-8][0-9])(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{3}|35(?:2[89]|[3-8][0-9])(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{2}|35(?:2[89]|[3-8][0-9])(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{1}|35(?:2[89]|[3-8][0-9])(?:[\\s-]?[0-9]{4}){3}|3(?:0[0-5]|[689][0-9])[0-9](?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{3}|3(?:0[0-5]|[689][0-9])[0-9](?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{2}|3(?:0[0-5]|[689][0-9])[0-9](?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{1}|3(?:0[0-5]|[689][0-9])[0-9](?:[\\s-]?[0-9]{4}){3}|3(?:0[0-5]|[689][0-9])[0-9](?:[\\s-]?[0-9]{4}){2}[\\s-]?[0-9]{2}|3(?:0[0-5]|[689][0-9])[0-9][\\s-]?[0-9]{6}[\\s-]?[0-9]{4}|(?:62|81)[0-9]{2}(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{3}|(?:62|81)[0-9]{2}(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{2}|(?:62|81)[0-9]{2}(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{1}|(?:62|81)[0-9]{2}(?:[\\s-]?[0-9]{4}){3}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{3}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{2}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){3}[\\s-]?[0-9]{1}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){3}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){2}[\\s-]?[0-9]{3}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){2}[\\s-]?[0-9]{2}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){2}[\\s-]?[0-9]{1}|(?:5018|5020|5038|6304|6759|676[1-3])(?:[\\s-]?[0-9]{4}){2})\\b",
|
|
256
|
+
"category": "pii",
|
|
257
|
+
"validate": "luhn"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"id": "pii-ssn",
|
|
261
|
+
"description": "US Social Security Number",
|
|
262
|
+
"regex": "\\b(?!000|666|9\\d{2})\\d{3}[- ](?!00)\\d{2}[- ](?!0000)\\d{4}\\b",
|
|
263
|
+
"category": "pii"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"id": "pii-phone-us",
|
|
267
|
+
"description": "US Phone Number",
|
|
268
|
+
"regex": "\\b(\\+1[\\s.-]?)?\\(?\\d{3}\\)?[\\s.-]\\d{3}[\\s.-]\\d{4}\\b",
|
|
269
|
+
"category": "pii"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"id": "pii-phone-jp",
|
|
273
|
+
"description": "Japanese Phone Number",
|
|
274
|
+
"regex": "\\b0\\d{1,4}[\\s-]\\d{1,4}[\\s-]\\d{4}\\b",
|
|
275
|
+
"category": "pii",
|
|
276
|
+
"validate": "phone-jp"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"id": "pii-postal-jp",
|
|
280
|
+
"description": "Japanese Postal Code",
|
|
281
|
+
"regex": "\u3012\\d{3}[\\s-]\\d{4}",
|
|
282
|
+
"category": "pii"
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"id": "pii-mynumber-jp",
|
|
286
|
+
"description": "Japanese Individual Number (My Number)",
|
|
287
|
+
"regex": "\\b\\d{12}\\b",
|
|
288
|
+
"category": "pii",
|
|
289
|
+
"validate": "mynumber-jp"
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"id": "pii-nir-fr",
|
|
293
|
+
"description": "French NIR / Social Security Number",
|
|
294
|
+
"regex": "\\b[12]\\d{2}(?:0[1-9]|1[0-2]|20)(?:\\d{5}|2[AB]\\d{3})\\d{3}\\s?\\d{2}\\b",
|
|
295
|
+
"category": "pii",
|
|
296
|
+
"flags": "gi",
|
|
297
|
+
"validate": "nir-fr"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"id": "pii-codice-fiscale-it",
|
|
301
|
+
"description": "Italian Codice Fiscale",
|
|
302
|
+
"regex": "\\b[A-Z]{6}[0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]\\b",
|
|
303
|
+
"category": "pii",
|
|
304
|
+
"validate": "codice-fiscale-it"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"id": "pii-steuer-id-de",
|
|
308
|
+
"description": "German Steuer-Identifikationsnummer",
|
|
309
|
+
"regex": "\\b[1-9]\\d{10}\\b",
|
|
310
|
+
"category": "pii",
|
|
311
|
+
"validate": "steuer-id-de"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"id": "pii-dni-nie-es",
|
|
315
|
+
"description": "Spanish DNI / NIE",
|
|
316
|
+
"regex": "\\b(?:\\d{8}[A-Z]|[XYZ]\\d{7}[A-Z])\\b",
|
|
317
|
+
"category": "pii",
|
|
318
|
+
"validate": "dni-nie-es"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"id": "pii-phone-fr",
|
|
322
|
+
"description": "French Phone Number",
|
|
323
|
+
"regex": "\\b0[1-9](?:[\\s.-]?\\d{2}){4}\\b",
|
|
324
|
+
"category": "pii",
|
|
325
|
+
"requireContext": true,
|
|
326
|
+
"contextWords": [
|
|
327
|
+
"t\u00e9l",
|
|
328
|
+
"tel",
|
|
329
|
+
"t\u00e9l\u00e9phone",
|
|
330
|
+
"telephone",
|
|
331
|
+
"phone",
|
|
332
|
+
"mobile",
|
|
333
|
+
"portable",
|
|
334
|
+
"fax"
|
|
335
|
+
]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"id": "pii-phone-it",
|
|
339
|
+
"description": "Italian Phone Number",
|
|
340
|
+
"regex": "\\b(?:0\\d{8,9}|3\\d{8,9})\\b",
|
|
341
|
+
"category": "pii",
|
|
342
|
+
"requireContext": true,
|
|
343
|
+
"contextWords": ["telefono", "tel", "cellulare", "mobile", "phone", "fax"]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"id": "pii-phone-de",
|
|
347
|
+
"description": "German Phone Number",
|
|
348
|
+
"regex": "\\b0[1-9]\\d{6,11}\\b",
|
|
349
|
+
"category": "pii",
|
|
350
|
+
"requireContext": true,
|
|
351
|
+
"contextWords": ["telefon", "tel", "handy", "mobil", "phone", "fax"]
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"id": "pii-phone-es",
|
|
355
|
+
"description": "Spanish Phone Number",
|
|
356
|
+
"regex": "\\b[67]\\d{8}\\b",
|
|
357
|
+
"category": "pii",
|
|
358
|
+
"requireContext": true,
|
|
359
|
+
"contextWords": [
|
|
360
|
+
"tel\u00e9fono",
|
|
361
|
+
"telefono",
|
|
362
|
+
"tel",
|
|
363
|
+
"m\u00f3vil",
|
|
364
|
+
"movil",
|
|
365
|
+
"phone",
|
|
366
|
+
"fax"
|
|
367
|
+
]
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"id": "pii-postal-code",
|
|
371
|
+
"description": "Postal Code (US ZIP / EU / KR)",
|
|
372
|
+
"regex": "\\b\\d{5}(?:-\\d{4})?\\b",
|
|
373
|
+
"category": "pii",
|
|
374
|
+
"requireContext": true,
|
|
375
|
+
"contextWords": [
|
|
376
|
+
"zip",
|
|
377
|
+
"postal",
|
|
378
|
+
"postale",
|
|
379
|
+
"postcode",
|
|
380
|
+
"plz",
|
|
381
|
+
"postleitzahl",
|
|
382
|
+
"\uc6b0\ud3b8\ubc88\ud638",
|
|
383
|
+
"cap"
|
|
384
|
+
],
|
|
385
|
+
"excludeContext": ["buffer", "bytes", "byte", "memory", "cache"]
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"id": "pii-rrn-kr",
|
|
389
|
+
"description": "Korean Resident Registration Number",
|
|
390
|
+
"regex": "\\b\\d{6}[-\\s]?\\d{7}\\b",
|
|
391
|
+
"category": "pii",
|
|
392
|
+
"validate": "rrn-kr",
|
|
393
|
+
"requireContext": true,
|
|
394
|
+
"contextWords": [
|
|
395
|
+
"\uc8fc\ubbfc\ub4f1\ub85d\ubc88\ud638",
|
|
396
|
+
"\uc8fc\ubbfc\ubc88\ud638",
|
|
397
|
+
"rrn",
|
|
398
|
+
"resident",
|
|
399
|
+
"registration",
|
|
400
|
+
"\uc2e0\ubd84\uc99d",
|
|
401
|
+
"\uac1c\uc778\uc815\ubcf4"
|
|
402
|
+
]
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"id": "pii-brn-kr",
|
|
406
|
+
"description": "Korean Business Registration Number",
|
|
407
|
+
"regex": "\\b\\d{3}[-\\s]?\\d{2}[-\\s]?\\d{5}\\b",
|
|
408
|
+
"category": "pii",
|
|
409
|
+
"validate": "brn-kr",
|
|
410
|
+
"requireContext": true,
|
|
411
|
+
"contextWords": [
|
|
412
|
+
"\uc0ac\uc5c5\uc790\ub4f1\ub85d\ubc88\ud638",
|
|
413
|
+
"\uc0ac\uc5c5\uc790\ubc88\ud638",
|
|
414
|
+
"brn",
|
|
415
|
+
"business",
|
|
416
|
+
"registration",
|
|
417
|
+
"\ubc95\uc778",
|
|
418
|
+
"\uc138\uae08\uacc4\uc0b0\uc11c"
|
|
419
|
+
]
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"id": "pii-resident-id-cn",
|
|
423
|
+
"description": "Chinese Resident Identity Card",
|
|
424
|
+
"regex": "\\b\\d{17}[\\dXx]\\b",
|
|
425
|
+
"category": "pii",
|
|
426
|
+
"validate": "resident-id-cn"
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
"id": "pii-phone-kr",
|
|
430
|
+
"description": "Korean Phone Number",
|
|
431
|
+
"regex": "(?:\\+82[-\\s]?)?(?:01[016789]|0\\d{1,2})[-\\s]?\\d{3,4}[-\\s]?\\d{4}",
|
|
432
|
+
"category": "pii",
|
|
433
|
+
"requireContext": true,
|
|
434
|
+
"contextWords": [
|
|
435
|
+
"\uc804\ud654",
|
|
436
|
+
"\ud578\ub4dc\ud3f0",
|
|
437
|
+
"\ud734\ub300",
|
|
438
|
+
"tel",
|
|
439
|
+
"phone",
|
|
440
|
+
"mobile",
|
|
441
|
+
"fax"
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"id": "pii-phone-cn",
|
|
446
|
+
"description": "Chinese Phone Number",
|
|
447
|
+
"regex": "(?:\\+86[-\\s]?)?(?:1[3-9]\\d{9}|0\\d{2,3}[-\\s]?\\d{7,8})",
|
|
448
|
+
"category": "pii",
|
|
449
|
+
"requireContext": true,
|
|
450
|
+
"contextWords": [
|
|
451
|
+
"\u7535\u8bdd",
|
|
452
|
+
"\u624b\u673a",
|
|
453
|
+
"tel",
|
|
454
|
+
"phone",
|
|
455
|
+
"mobile",
|
|
456
|
+
"fax"
|
|
457
|
+
]
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
"id": "pii-postal-cn",
|
|
461
|
+
"description": "Chinese Postal Code",
|
|
462
|
+
"regex": "\\b[1-9]\\d{5}\\b",
|
|
463
|
+
"category": "pii",
|
|
464
|
+
"requireContext": true,
|
|
465
|
+
"contextWords": [
|
|
466
|
+
"\u90ae\u7f16",
|
|
467
|
+
"\u90ae\u653f\u7f16\u7801",
|
|
468
|
+
"postal",
|
|
469
|
+
"postcode",
|
|
470
|
+
"zip"
|
|
471
|
+
]
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"id": "pii-ipv4-public",
|
|
475
|
+
"description": "Public IPv4 Address",
|
|
476
|
+
"regex": "\\b\\d{1,3}(?:\\.\\d{1,3}){3}\\b",
|
|
477
|
+
"category": "pii",
|
|
478
|
+
"requireContext": true,
|
|
479
|
+
"contextWords": ["ip", "ipv4"],
|
|
480
|
+
"validate": "public-ipv4"
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
"id": "pii-ipv6",
|
|
484
|
+
"description": "IPv6 Address",
|
|
485
|
+
"regex": "\\b[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{0,4}){2,7}\\b",
|
|
486
|
+
"category": "pii",
|
|
487
|
+
"requireContext": true,
|
|
488
|
+
"contextWords": ["ipv6", "ip"],
|
|
489
|
+
"validate": "public-ipv6"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"id": "openai-service-key",
|
|
493
|
+
"description": "OpenAI Service Account / Admin Key",
|
|
494
|
+
"regex": "sk-(?:svcacct|admin)-[A-Za-z0-9_-]{20,}",
|
|
495
|
+
"category": "secret"
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
"id": "azure-storage-key",
|
|
499
|
+
"description": "Azure Storage Account Key",
|
|
500
|
+
"regex": "AccountKey=[A-Za-z0-9+/]{86}==",
|
|
501
|
+
"category": "secret"
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"id": "flyio-token",
|
|
505
|
+
"description": "Fly.io API Token",
|
|
506
|
+
"regex": "(?:fo1_[A-Za-z0-9_-]{43}|fm1[ar]_[A-Za-z0-9+/_-]{40,}={0,3}|fm2_[A-Za-z0-9+/_-]{40,}={0,3})",
|
|
507
|
+
"category": "secret"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"id": "databricks-token",
|
|
511
|
+
"description": "Databricks Personal Access Token",
|
|
512
|
+
"regex": "\\bdapi[0-9a-f]{32}\\b",
|
|
513
|
+
"category": "secret"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"id": "vault-token",
|
|
517
|
+
"description": "HashiCorp Vault Token",
|
|
518
|
+
"regex": "\\bhv[sb]\\.[A-Za-z0-9_-]{24,}",
|
|
519
|
+
"category": "secret"
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"id": "shopify-token",
|
|
523
|
+
"description": "Shopify Access Token",
|
|
524
|
+
"regex": "\\bshp(?:at|ss|ca|pa)_[0-9a-fA-F]{32}\\b",
|
|
525
|
+
"category": "secret"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"id": "doppler-token",
|
|
529
|
+
"description": "Doppler Token",
|
|
530
|
+
"regex": "\\bdp\\.(?:pt|st|ct|sa|scim)\\.[A-Za-z0-9]{40,}",
|
|
531
|
+
"category": "secret"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"id": "grafana-token",
|
|
535
|
+
"description": "Grafana Cloud / Service Account Token",
|
|
536
|
+
"regex": "\\bgl(?:c|sa)_[A-Za-z0-9+/=_-]{32,}",
|
|
537
|
+
"category": "secret"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"id": "notion-token",
|
|
541
|
+
"description": "Notion Integration Token",
|
|
542
|
+
"regex": "\\b(?:ntn_[A-Za-z0-9]{40,}|secret_[A-Za-z0-9]{43})",
|
|
543
|
+
"category": "secret"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"id": "azure-sas-key",
|
|
547
|
+
"description": "Azure Shared Access Key (Service Bus, Event Hubs, IoT Hub)",
|
|
548
|
+
"regex": "SharedAccessKey=[A-Za-z0-9+/]{22,86}={0,2}",
|
|
549
|
+
"category": "secret"
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"id": "google-oauth-secret",
|
|
553
|
+
"description": "Google OAuth Client Secret",
|
|
554
|
+
"regex": "\\bGOCSPX-[A-Za-z0-9_-]{20,}",
|
|
555
|
+
"category": "secret"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
"id": "private-key-base64",
|
|
559
|
+
"description": "PEM Private Key, base64-encoded",
|
|
560
|
+
"regex": "(?:LS0tLS1CRUdJTi|0tLS0tQkVHSU4g|tLS0tLUJFR0lOI)[A-Za-z0-9+/=]{40,}",
|
|
561
|
+
"category": "secret"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
"id": "url-basic-auth",
|
|
565
|
+
"description": "Credentials embedded in an http(s) URL",
|
|
566
|
+
"regex": "\\bhttps?://[A-Za-z0-9._~%+-]{1,64}:[^@/\\s\\\"'`<>]{3,256}@[A-Za-z0-9.-]+",
|
|
567
|
+
"category": "secret"
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// How a file's bytes are read as text.
|
|
2
|
+
//
|
|
3
|
+
// A file is not labelled with its encoding, so these decide. The decisions are
|
|
4
|
+
// deliberately loose and the caller reads the bytes both ways when the verdict
|
|
5
|
+
// is a guess: the alternative is a wrong guess that decodes a credential into
|
|
6
|
+
// characters no rule matches, which is the whole scan switched off for that
|
|
7
|
+
// file.
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
export function detectUtf16(raw) {
|
|
10
|
+
if (raw.length < 4)
|
|
11
|
+
return null;
|
|
12
|
+
// Swapping is done on a copy: `raw` is a view into the shared read buffer.
|
|
13
|
+
const swapped = () => Buffer.from(raw)
|
|
14
|
+
.subarray(0, raw.length & ~1)
|
|
15
|
+
.swap16();
|
|
16
|
+
if (raw[0] === 0xff && raw[1] === 0xfe)
|
|
17
|
+
return { bytes: raw, fromBom: true };
|
|
18
|
+
if (raw[0] === 0xfe && raw[1] === 0xff)
|
|
19
|
+
return { bytes: swapped(), fromBom: true };
|
|
20
|
+
// Far enough in to reach a newline or a space. Five hundred pairs of Japanese
|
|
21
|
+
// carry no zero byte at all, and that prefix decided the whole file.
|
|
22
|
+
const pairs = Math.min(raw.length >> 1, 8192);
|
|
23
|
+
let evenNuls = 0;
|
|
24
|
+
let oddNuls = 0;
|
|
25
|
+
for (let i = 0; i < pairs; i++) {
|
|
26
|
+
if (raw[i * 2] === 0)
|
|
27
|
+
evenNuls++;
|
|
28
|
+
if (raw[i * 2 + 1] === 0)
|
|
29
|
+
oddNuls++;
|
|
30
|
+
}
|
|
31
|
+
// Several, not one: a single stray NUL is not an encoding.
|
|
32
|
+
const MINIMUM_NULS = 8;
|
|
33
|
+
if (Math.max(evenNuls, oddNuls) < MINIMUM_NULS)
|
|
34
|
+
return null;
|
|
35
|
+
// How much the minority side holds is not asked. Characters in the U+xx00
|
|
36
|
+
// rows put a NUL on that side, and Japanese is full of them — `一` is U+4E00,
|
|
37
|
+
// and a full-width space is U+3000 — so any threshold tight enough to exclude
|
|
38
|
+
// a binary also excludes an ordinary Japanese document. The counts cannot
|
|
39
|
+
// separate those two cases, so the question is left to `readsAsText`, and
|
|
40
|
+
// being wrong costs only a pass: the caller reads the bytes both ways
|
|
41
|
+
// whenever the verdict did not come from a byte-order mark.
|
|
42
|
+
//
|
|
43
|
+
// Which side leads picks the order the two byte orders are tried in, not
|
|
44
|
+
// whether they are tried.
|
|
45
|
+
const orderings = oddNuls >= evenNuls ? [raw, swapped()] : [swapped(), raw];
|
|
46
|
+
for (const candidate of orderings) {
|
|
47
|
+
if (readsAsText(candidate))
|
|
48
|
+
return { bytes: candidate, fromBom: false };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// Whether the runs of text between the NUL bytes read as something someone
|
|
53
|
+
// wrote. A `.env` written by a tool that terminates its values, and a log with
|
|
54
|
+
// a stray zero in it, both pass; a JPEG's compressed bytes do not.
|
|
55
|
+
export function readsAsUtf8Text(raw) {
|
|
56
|
+
const sample = utf8Runs(raw.subarray(0, 4096));
|
|
57
|
+
if (sample.length === 0)
|
|
58
|
+
return false;
|
|
59
|
+
let bad = 0;
|
|
60
|
+
for (const ch of sample) {
|
|
61
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
62
|
+
const isControl = (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) ||
|
|
63
|
+
code === 0x7f;
|
|
64
|
+
if (isControl || code === 0xfffd)
|
|
65
|
+
bad++;
|
|
66
|
+
}
|
|
67
|
+
return bad / sample.length < 0.05;
|
|
68
|
+
}
|
|
69
|
+
// Every run of text between the NUL bytes, joined by newlines so that no rule
|
|
70
|
+
// matches across two unrelated runs.
|
|
71
|
+
export function utf8Runs(raw) {
|
|
72
|
+
const text = raw.toString("utf8");
|
|
73
|
+
return text.indexOf("\0") === -1 ? text : text.split("\0").join("\n");
|
|
74
|
+
}
|
|
75
|
+
// Whether these bytes decoded as UTF-16 look like something someone wrote. A
|
|
76
|
+
// binary file can have its NULs on one side by chance; text decoded from the
|
|
77
|
+
// wrong encoding is mostly control characters and replacement characters, and
|
|
78
|
+
// this is what separates the two.
|
|
79
|
+
export function readsAsText(le) {
|
|
80
|
+
const sample = le.subarray(0, 4096).toString("utf16le");
|
|
81
|
+
if (sample.length === 0)
|
|
82
|
+
return false;
|
|
83
|
+
let bad = 0;
|
|
84
|
+
for (const ch of sample) {
|
|
85
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
86
|
+
const isControl = (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) ||
|
|
87
|
+
code === 0x7f;
|
|
88
|
+
if (isControl || code === 0xfffd || (code >= 0xe000 && code <= 0xf8ff))
|
|
89
|
+
bad++;
|
|
90
|
+
}
|
|
91
|
+
return bad / sample.length < 0.05;
|
|
92
|
+
}
|
|
93
|
+
// Whether the first few kilobytes hold a NUL byte that UTF-16 does not explain.
|
|
94
|
+
// UTF-16 text is half NUL by construction, and a `.env` written by PowerShell is
|
|
95
|
+
// the file a sweep least wants to skip.
|
|
96
|
+
const BINARY_SNIFF_BYTES = 4096;
|
|
97
|
+
export function looksBinary(filePath) {
|
|
98
|
+
let fd;
|
|
99
|
+
try {
|
|
100
|
+
fd = fs.openSync(filePath, "r");
|
|
101
|
+
const head = Buffer.alloc(BINARY_SNIFF_BYTES);
|
|
102
|
+
const read = fs.readSync(fd, head, 0, head.length, 0);
|
|
103
|
+
const bytes = head.subarray(0, read);
|
|
104
|
+
if (!bytes.includes(0))
|
|
105
|
+
return false;
|
|
106
|
+
// Neither reading, rather than the UTF-16 verdict alone. That verdict is
|
|
107
|
+
// deliberately loose — the file scan covers a wrong guess by reading the
|
|
108
|
+
// bytes both ways — and a sweep has no such second chance, so it asks the
|
|
109
|
+
// question the answer is wanted for: does anything here read as text?
|
|
110
|
+
const utf16 = detectUtf16(bytes);
|
|
111
|
+
if (utf16 !== null && readsAsText(utf16.bytes))
|
|
112
|
+
return false;
|
|
113
|
+
return !readsAsUtf8Text(bytes);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Unreadable. Nothing to sweep, and the named-file path still guards it.
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (fd !== undefined)
|
|
121
|
+
fs.closeSync(fd);
|
|
122
|
+
}
|
|
123
|
+
}
|