scrubber_rb 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.
- checksums.yaml +7 -0
- data/.cargo/config.toml +21 -0
- data/CHANGELOG.md +59 -0
- data/Cargo.lock +626 -0
- data/Cargo.toml +10 -0
- data/LICENSE +21 -0
- data/README.md +298 -0
- data/SECURITY.md +54 -0
- data/ext/scrubber_rb/Cargo.toml +41 -0
- data/ext/scrubber_rb/build.rs +6 -0
- data/ext/scrubber_rb/extconf.rb +6 -0
- data/ext/scrubber_rb/src/detectors/checksum.rs +222 -0
- data/ext/scrubber_rb/src/detectors/mod.rs +636 -0
- data/ext/scrubber_rb/src/detectors/upi.rs +138 -0
- data/ext/scrubber_rb/src/detectors/validate.rs +339 -0
- data/ext/scrubber_rb/src/engine.rs +716 -0
- data/ext/scrubber_rb/src/lib.rs +247 -0
- data/ext/scrubber_rb/src/nogvl.rs +84 -0
- data/ext/scrubber_rb/src/offsets.rs +118 -0
- data/ext/scrubber_rb/src/pattern.rs +319 -0
- data/ext/scrubber_rb/src/replace.rs +238 -0
- data/lib/scrubber/instance.rb +223 -0
- data/lib/scrubber/llm_guard.rb +79 -0
- data/lib/scrubber/log_formatter.rb +40 -0
- data/lib/scrubber/match.rb +27 -0
- data/lib/scrubber/middleware.rb +124 -0
- data/lib/scrubber/version.rb +5 -0
- data/lib/scrubber_rb.rb +109 -0
- data/sig/scrubber_rb.rbs +150 -0
- metadata +96 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
//! The detector registry.
|
|
2
|
+
//!
|
|
3
|
+
//! A *detector* is a public key the user enables (`:email`, `:credit_card`).
|
|
4
|
+
//! A *rule* is one compiled regex. Most detectors are a single rule; a few
|
|
5
|
+
//! (`:api_key`, `:phone`, `:password_pair`) are a family of rules that all
|
|
6
|
+
//! report the same type, because expressing them as one alternation would make
|
|
7
|
+
//! capture-group bookkeeping unreadable.
|
|
8
|
+
|
|
9
|
+
pub mod checksum;
|
|
10
|
+
pub mod upi;
|
|
11
|
+
pub mod validate;
|
|
12
|
+
|
|
13
|
+
pub use validate::{Candidate, Validator};
|
|
14
|
+
|
|
15
|
+
use crate::replace::MaskKind;
|
|
16
|
+
|
|
17
|
+
/// One compiled-at-startup pattern.
|
|
18
|
+
pub struct Rule {
|
|
19
|
+
/// Detector key this rule reports as, e.g. `"credit_card"`.
|
|
20
|
+
pub kind: &'static str,
|
|
21
|
+
/// The Rust `regex` source.
|
|
22
|
+
pub pattern: &'static str,
|
|
23
|
+
/// Literal substrings, any one of which must be present for this rule to
|
|
24
|
+
/// have a chance of matching. Empty means "always run this rule".
|
|
25
|
+
/// Used to build the stage-1 Aho-Corasick prefilter.
|
|
26
|
+
pub anchors: &'static [&'static str],
|
|
27
|
+
/// Which capture group to redact. 0 is the whole match; `password_pair` and
|
|
28
|
+
/// `url_credentials` redact only group 1 so the surrounding key stays
|
|
29
|
+
/// readable in logs.
|
|
30
|
+
pub capture: usize,
|
|
31
|
+
pub validator: Option<Validator>,
|
|
32
|
+
/// Lower wins when two matches overlap.
|
|
33
|
+
pub priority: u8,
|
|
34
|
+
pub mask: MaskKind,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Plain rule: redact the whole match, no post-validation.
|
|
38
|
+
const fn rule(
|
|
39
|
+
kind: &'static str,
|
|
40
|
+
pattern: &'static str,
|
|
41
|
+
anchors: &'static [&'static str],
|
|
42
|
+
priority: u8,
|
|
43
|
+
mask: MaskKind,
|
|
44
|
+
) -> Rule {
|
|
45
|
+
Rule {
|
|
46
|
+
kind,
|
|
47
|
+
pattern,
|
|
48
|
+
anchors,
|
|
49
|
+
capture: 0,
|
|
50
|
+
validator: None,
|
|
51
|
+
priority,
|
|
52
|
+
mask,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Rule with a checksum or context validator.
|
|
57
|
+
const fn checked(
|
|
58
|
+
kind: &'static str,
|
|
59
|
+
pattern: &'static str,
|
|
60
|
+
anchors: &'static [&'static str],
|
|
61
|
+
priority: u8,
|
|
62
|
+
mask: MaskKind,
|
|
63
|
+
validator: Validator,
|
|
64
|
+
) -> Rule {
|
|
65
|
+
Rule {
|
|
66
|
+
kind,
|
|
67
|
+
pattern,
|
|
68
|
+
anchors,
|
|
69
|
+
capture: 0,
|
|
70
|
+
validator: Some(validator),
|
|
71
|
+
priority,
|
|
72
|
+
mask,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Rule that redacts only capture group 1, leaving the surrounding key visible.
|
|
77
|
+
const fn captured(
|
|
78
|
+
kind: &'static str,
|
|
79
|
+
pattern: &'static str,
|
|
80
|
+
anchors: &'static [&'static str],
|
|
81
|
+
priority: u8,
|
|
82
|
+
) -> Rule {
|
|
83
|
+
Rule {
|
|
84
|
+
kind,
|
|
85
|
+
pattern,
|
|
86
|
+
anchors,
|
|
87
|
+
capture: 1,
|
|
88
|
+
validator: None,
|
|
89
|
+
priority,
|
|
90
|
+
mask: MaskKind::Full,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Capture-group rule with a validator that sees the captured span.
|
|
95
|
+
const fn captured_checked(
|
|
96
|
+
kind: &'static str,
|
|
97
|
+
pattern: &'static str,
|
|
98
|
+
anchors: &'static [&'static str],
|
|
99
|
+
priority: u8,
|
|
100
|
+
validator: Validator,
|
|
101
|
+
) -> Rule {
|
|
102
|
+
Rule {
|
|
103
|
+
kind,
|
|
104
|
+
pattern,
|
|
105
|
+
anchors,
|
|
106
|
+
capture: 1,
|
|
107
|
+
validator: Some(validator),
|
|
108
|
+
priority,
|
|
109
|
+
mask: MaskKind::Full,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Priorities. Secrets beat structured IDs beat loose numeric shapes, so that
|
|
114
|
+
// when spans overlap the more specific reading wins (behaviour contract S2).
|
|
115
|
+
// A provider-specific secret rule and the generic `key = value` rule usually
|
|
116
|
+
// match the same span. Ranking the specific one first means you get
|
|
117
|
+
// `[API_KEY]` rather than `[PASSWORD_PAIR]` — same redaction, better forensics.
|
|
118
|
+
const P_PRIVATE_KEY: u8 = 1;
|
|
119
|
+
const P_AWS: u8 = 2;
|
|
120
|
+
const P_API_KEY: u8 = 3;
|
|
121
|
+
const P_JWT: u8 = 4;
|
|
122
|
+
const P_URL_CREDS: u8 = 5;
|
|
123
|
+
const P_PASSWORD: u8 = 6;
|
|
124
|
+
pub const P_CUSTOM: u8 = 8;
|
|
125
|
+
const P_CARD: u8 = 20;
|
|
126
|
+
const P_IBAN: u8 = 21;
|
|
127
|
+
const P_AADHAAR: u8 = 22;
|
|
128
|
+
const P_SSN: u8 = 23;
|
|
129
|
+
const P_PAN: u8 = 24;
|
|
130
|
+
const P_UPI: u8 = 25;
|
|
131
|
+
const P_EMAIL: u8 = 26;
|
|
132
|
+
const P_MAC: u8 = 40;
|
|
133
|
+
const P_IPV6: u8 = 41;
|
|
134
|
+
const P_IPV4: u8 = 42;
|
|
135
|
+
const P_PHONE_IN: u8 = 50;
|
|
136
|
+
const P_PHONE: u8 = 51;
|
|
137
|
+
|
|
138
|
+
/// Every rule in the library, grouped by the detector key that enables it.
|
|
139
|
+
pub static REGISTRY: &[(&str, &[Rule])] = &[
|
|
140
|
+
(
|
|
141
|
+
"email",
|
|
142
|
+
&[checked(
|
|
143
|
+
"email",
|
|
144
|
+
r"[A-Za-z0-9._%+\-]+@[A-Za-z0-9](?:[A-Za-z0-9.\-]*[A-Za-z0-9])?\.[A-Za-z]{2,24}\b",
|
|
145
|
+
&[],
|
|
146
|
+
P_EMAIL,
|
|
147
|
+
MaskKind::Email,
|
|
148
|
+
validate::email,
|
|
149
|
+
)],
|
|
150
|
+
),
|
|
151
|
+
(
|
|
152
|
+
"phone",
|
|
153
|
+
&[
|
|
154
|
+
// +country (space/dot/dash) NNN NNN NNNN
|
|
155
|
+
rule(
|
|
156
|
+
"phone",
|
|
157
|
+
r"\+[1-9]\d{0,2}[ .\-]?\(?\d{3}\)?[ .\-]?\d{3}[ .\-]?\d{4}\b",
|
|
158
|
+
&[],
|
|
159
|
+
P_PHONE,
|
|
160
|
+
MaskKind::Tail4,
|
|
161
|
+
),
|
|
162
|
+
// Bare E.164
|
|
163
|
+
rule("phone", r"\+[1-9]\d{7,14}\b", &[], P_PHONE, MaskKind::Tail4),
|
|
164
|
+
// NNN-NNN-NNNN / NNN.NNN.NNNN / NNN NNN NNNN
|
|
165
|
+
checked(
|
|
166
|
+
"phone",
|
|
167
|
+
r"\b\d{3}[ .\-]\d{3}[ .\-]\d{4}\b",
|
|
168
|
+
&[],
|
|
169
|
+
P_PHONE,
|
|
170
|
+
MaskKind::Tail4,
|
|
171
|
+
validate::word_start,
|
|
172
|
+
),
|
|
173
|
+
// (NNN) NNN-NNNN
|
|
174
|
+
rule(
|
|
175
|
+
"phone",
|
|
176
|
+
r"\(\d{3}\)[ .\-]?\d{3}[ .\-]?\d{4}\b",
|
|
177
|
+
&[],
|
|
178
|
+
P_PHONE,
|
|
179
|
+
MaskKind::Tail4,
|
|
180
|
+
),
|
|
181
|
+
],
|
|
182
|
+
),
|
|
183
|
+
(
|
|
184
|
+
"phone_in",
|
|
185
|
+
&[
|
|
186
|
+
rule(
|
|
187
|
+
"phone_in",
|
|
188
|
+
r"\+91[ .\-]?[6-9]\d{9}\b",
|
|
189
|
+
&["+91"],
|
|
190
|
+
P_PHONE_IN,
|
|
191
|
+
MaskKind::Tail4,
|
|
192
|
+
),
|
|
193
|
+
checked(
|
|
194
|
+
"phone_in",
|
|
195
|
+
r"\b0?[6-9]\d{9}\b",
|
|
196
|
+
&[],
|
|
197
|
+
P_PHONE_IN,
|
|
198
|
+
MaskKind::Tail4,
|
|
199
|
+
validate::word_start,
|
|
200
|
+
),
|
|
201
|
+
],
|
|
202
|
+
),
|
|
203
|
+
(
|
|
204
|
+
"credit_card",
|
|
205
|
+
&[checked(
|
|
206
|
+
"credit_card",
|
|
207
|
+
r"\b\d(?:[ \-]?\d){12,18}\b",
|
|
208
|
+
&[],
|
|
209
|
+
P_CARD,
|
|
210
|
+
MaskKind::Tail4,
|
|
211
|
+
validate::credit_card,
|
|
212
|
+
)],
|
|
213
|
+
),
|
|
214
|
+
(
|
|
215
|
+
"aadhaar",
|
|
216
|
+
&[checked(
|
|
217
|
+
"aadhaar",
|
|
218
|
+
r"\b[2-9]\d{3}[ \-]?\d{4}[ \-]?\d{4}\b",
|
|
219
|
+
&[],
|
|
220
|
+
P_AADHAAR,
|
|
221
|
+
MaskKind::Tail4,
|
|
222
|
+
validate::aadhaar,
|
|
223
|
+
)],
|
|
224
|
+
),
|
|
225
|
+
(
|
|
226
|
+
"pan",
|
|
227
|
+
&[checked(
|
|
228
|
+
"pan",
|
|
229
|
+
r"\b[A-Z]{5}[0-9]{4}[A-Z]\b",
|
|
230
|
+
&[],
|
|
231
|
+
P_PAN,
|
|
232
|
+
MaskKind::Tail4,
|
|
233
|
+
validate::pan,
|
|
234
|
+
)],
|
|
235
|
+
),
|
|
236
|
+
(
|
|
237
|
+
"upi",
|
|
238
|
+
&[checked(
|
|
239
|
+
"upi",
|
|
240
|
+
r"\b[A-Za-z0-9][A-Za-z0-9.\-_]{1,60}@[A-Za-z]{2,64}\b",
|
|
241
|
+
&[],
|
|
242
|
+
P_UPI,
|
|
243
|
+
MaskKind::Full,
|
|
244
|
+
validate::upi_vpa,
|
|
245
|
+
)],
|
|
246
|
+
),
|
|
247
|
+
(
|
|
248
|
+
"ssn",
|
|
249
|
+
&[
|
|
250
|
+
checked(
|
|
251
|
+
"ssn",
|
|
252
|
+
r"\b\d{3}-\d{2}-\d{4}\b",
|
|
253
|
+
&[],
|
|
254
|
+
P_SSN,
|
|
255
|
+
MaskKind::Tail4,
|
|
256
|
+
validate::ssn,
|
|
257
|
+
),
|
|
258
|
+
checked(
|
|
259
|
+
"ssn",
|
|
260
|
+
r"\b\d{3} \d{2} \d{4}\b",
|
|
261
|
+
&[],
|
|
262
|
+
P_SSN,
|
|
263
|
+
MaskKind::Tail4,
|
|
264
|
+
validate::ssn,
|
|
265
|
+
),
|
|
266
|
+
],
|
|
267
|
+
),
|
|
268
|
+
(
|
|
269
|
+
"iban",
|
|
270
|
+
&[checked(
|
|
271
|
+
"iban",
|
|
272
|
+
r"\b[A-Z]{2}\d{2}(?:[ \-]?[A-Z0-9]){11,30}\b",
|
|
273
|
+
&[],
|
|
274
|
+
P_IBAN,
|
|
275
|
+
MaskKind::Tail4,
|
|
276
|
+
validate::iban,
|
|
277
|
+
)],
|
|
278
|
+
),
|
|
279
|
+
(
|
|
280
|
+
"ip",
|
|
281
|
+
&[checked(
|
|
282
|
+
"ip",
|
|
283
|
+
r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b",
|
|
284
|
+
&[],
|
|
285
|
+
P_IPV4,
|
|
286
|
+
MaskKind::Tail4,
|
|
287
|
+
validate::ipv4,
|
|
288
|
+
)],
|
|
289
|
+
),
|
|
290
|
+
(
|
|
291
|
+
"ipv6",
|
|
292
|
+
&[checked(
|
|
293
|
+
"ipv6",
|
|
294
|
+
r"(?:[0-9A-Fa-f]{0,4}:){2,7}(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|[0-9A-Fa-f]{0,4})",
|
|
295
|
+
&[],
|
|
296
|
+
P_IPV6,
|
|
297
|
+
MaskKind::Full,
|
|
298
|
+
validate::ipv6,
|
|
299
|
+
)],
|
|
300
|
+
),
|
|
301
|
+
(
|
|
302
|
+
"mac",
|
|
303
|
+
&[
|
|
304
|
+
rule(
|
|
305
|
+
"mac",
|
|
306
|
+
r"\b[0-9A-Fa-f]{2}(?:[:\-][0-9A-Fa-f]{2}){5}\b",
|
|
307
|
+
&[],
|
|
308
|
+
P_MAC,
|
|
309
|
+
MaskKind::Tail4,
|
|
310
|
+
),
|
|
311
|
+
// Cisco three-group form.
|
|
312
|
+
rule(
|
|
313
|
+
"mac",
|
|
314
|
+
r"\b[0-9A-Fa-f]{4}(?:\.[0-9A-Fa-f]{4}){2}\b",
|
|
315
|
+
&[],
|
|
316
|
+
P_MAC,
|
|
317
|
+
MaskKind::Tail4,
|
|
318
|
+
),
|
|
319
|
+
],
|
|
320
|
+
),
|
|
321
|
+
(
|
|
322
|
+
"jwt",
|
|
323
|
+
&[rule(
|
|
324
|
+
"jwt",
|
|
325
|
+
r"\beyJ[A-Za-z0-9_\-]{6,}\.[A-Za-z0-9_\-]{6,}\.[A-Za-z0-9_\-]*",
|
|
326
|
+
&["eyJ"],
|
|
327
|
+
P_JWT,
|
|
328
|
+
MaskKind::Full,
|
|
329
|
+
)],
|
|
330
|
+
),
|
|
331
|
+
(
|
|
332
|
+
"aws_key",
|
|
333
|
+
&[rule(
|
|
334
|
+
"aws_key",
|
|
335
|
+
r"\bA(?:KIA|SIA|IDA|ROA|GPA|NPA|NVA|PKA|IPA)[0-9A-Z]{16}\b",
|
|
336
|
+
&[
|
|
337
|
+
"AKIA", "ASIA", "AIDA", "AROA", "AGPA", "ANPA", "ANVA", "APKA", "AIPA",
|
|
338
|
+
],
|
|
339
|
+
P_AWS,
|
|
340
|
+
MaskKind::Full,
|
|
341
|
+
)],
|
|
342
|
+
),
|
|
343
|
+
(
|
|
344
|
+
"private_key",
|
|
345
|
+
&[rule(
|
|
346
|
+
"private_key",
|
|
347
|
+
r"(?s)-----BEGIN[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----.*?-----END[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----",
|
|
348
|
+
&["-----BEGIN"],
|
|
349
|
+
P_PRIVATE_KEY,
|
|
350
|
+
MaskKind::Full,
|
|
351
|
+
)],
|
|
352
|
+
),
|
|
353
|
+
("api_key", API_KEY_RULES),
|
|
354
|
+
(
|
|
355
|
+
"password_pair",
|
|
356
|
+
&[
|
|
357
|
+
captured_checked(
|
|
358
|
+
"password_pair",
|
|
359
|
+
r#"(?i)(?:password|passwd|pwd|secret|api[_\-]?key|apikey|access[_\-]?token|auth[_\-]?token|token)["']?\s*[:=]\s*"([^"\n]{1,256})""#,
|
|
360
|
+
PASSWORD_ANCHORS,
|
|
361
|
+
P_PASSWORD,
|
|
362
|
+
validate::not_redaction_token,
|
|
363
|
+
),
|
|
364
|
+
captured_checked(
|
|
365
|
+
"password_pair",
|
|
366
|
+
r#"(?i)(?:password|passwd|pwd|secret|api[_\-]?key|apikey|access[_\-]?token|auth[_\-]?token|token)["']?\s*[:=]\s*'([^'\n]{1,256})'"#,
|
|
367
|
+
PASSWORD_ANCHORS,
|
|
368
|
+
P_PASSWORD,
|
|
369
|
+
validate::not_redaction_token,
|
|
370
|
+
),
|
|
371
|
+
captured_checked(
|
|
372
|
+
"password_pair",
|
|
373
|
+
r#"(?i)(?:password|passwd|pwd|secret|api[_\-]?key|apikey|access[_\-]?token|auth[_\-]?token|token)["']?\s*[:=]\s*([^\s,;&"'}\]]{1,256})"#,
|
|
374
|
+
PASSWORD_ANCHORS,
|
|
375
|
+
P_PASSWORD + 1,
|
|
376
|
+
validate::not_redaction_token,
|
|
377
|
+
),
|
|
378
|
+
],
|
|
379
|
+
),
|
|
380
|
+
(
|
|
381
|
+
"url_credentials",
|
|
382
|
+
&[captured(
|
|
383
|
+
"url_credentials",
|
|
384
|
+
r"[A-Za-z][A-Za-z0-9+.\-]*://[^\s/:@]+:([^\s/@]+)@",
|
|
385
|
+
&["://"],
|
|
386
|
+
P_URL_CREDS,
|
|
387
|
+
)],
|
|
388
|
+
),
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
const PASSWORD_ANCHORS: &[&str] = &[
|
|
392
|
+
"password", "passwd", "pwd", "secret", "apikey", "api_key", "api-key", "token",
|
|
393
|
+
];
|
|
394
|
+
|
|
395
|
+
/// A curated subset of gitleaks-style provider rules. Every entry here is a
|
|
396
|
+
/// vendor-documented, structurally unambiguous credential format — no
|
|
397
|
+
/// entropy heuristics, because those false-positive on hashes and UUIDs.
|
|
398
|
+
static API_KEY_RULES: &[Rule] = &[
|
|
399
|
+
// GitHub classic PAT / OAuth / user-to-server / server-to-server / refresh
|
|
400
|
+
rule(
|
|
401
|
+
"api_key",
|
|
402
|
+
r"\bgh[pousr]_[A-Za-z0-9]{36,255}\b",
|
|
403
|
+
&["ghp_", "gho_", "ghu_", "ghs_", "ghr_"],
|
|
404
|
+
P_API_KEY,
|
|
405
|
+
MaskKind::Full,
|
|
406
|
+
),
|
|
407
|
+
// GitHub fine-grained PAT
|
|
408
|
+
rule(
|
|
409
|
+
"api_key",
|
|
410
|
+
r"\bgithub_pat_[A-Za-z0-9_]{60,120}\b",
|
|
411
|
+
&["github_pat_"],
|
|
412
|
+
P_API_KEY,
|
|
413
|
+
MaskKind::Full,
|
|
414
|
+
),
|
|
415
|
+
// GitLab PAT
|
|
416
|
+
rule(
|
|
417
|
+
"api_key",
|
|
418
|
+
r"\bglpat-[A-Za-z0-9_\-]{20,64}\b",
|
|
419
|
+
&["glpat-"],
|
|
420
|
+
P_API_KEY,
|
|
421
|
+
MaskKind::Full,
|
|
422
|
+
),
|
|
423
|
+
// Slack tokens
|
|
424
|
+
rule(
|
|
425
|
+
"api_key",
|
|
426
|
+
r"\bxox[baprse]-[A-Za-z0-9\-]{10,72}\b",
|
|
427
|
+
&["xoxb-", "xoxa-", "xoxp-", "xoxr-", "xoxs-", "xoxe-"],
|
|
428
|
+
P_API_KEY,
|
|
429
|
+
MaskKind::Full,
|
|
430
|
+
),
|
|
431
|
+
// Slack incoming webhook
|
|
432
|
+
rule(
|
|
433
|
+
"api_key",
|
|
434
|
+
r"https://hooks\.slack\.com/services/[A-Za-z0-9_/\-]{20,}",
|
|
435
|
+
&["hooks.slack.com"],
|
|
436
|
+
P_API_KEY,
|
|
437
|
+
MaskKind::Full,
|
|
438
|
+
),
|
|
439
|
+
// Stripe secret / restricted / publishable
|
|
440
|
+
rule(
|
|
441
|
+
"api_key",
|
|
442
|
+
r"\b[srp]k_(?:live|test)_[A-Za-z0-9]{16,247}\b",
|
|
443
|
+
&[
|
|
444
|
+
"sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_",
|
|
445
|
+
],
|
|
446
|
+
P_API_KEY,
|
|
447
|
+
MaskKind::Full,
|
|
448
|
+
),
|
|
449
|
+
// Anthropic
|
|
450
|
+
rule(
|
|
451
|
+
"api_key",
|
|
452
|
+
r"\bsk-ant-[A-Za-z0-9_\-]{20,120}\b",
|
|
453
|
+
&["sk-ant-"],
|
|
454
|
+
P_API_KEY,
|
|
455
|
+
MaskKind::Full,
|
|
456
|
+
),
|
|
457
|
+
// OpenAI (classic and project-scoped)
|
|
458
|
+
rule(
|
|
459
|
+
"api_key",
|
|
460
|
+
r"\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,160}\b",
|
|
461
|
+
&["sk-"],
|
|
462
|
+
P_API_KEY,
|
|
463
|
+
MaskKind::Full,
|
|
464
|
+
),
|
|
465
|
+
// Google API key
|
|
466
|
+
rule(
|
|
467
|
+
"api_key",
|
|
468
|
+
r"\bAIza[0-9A-Za-z_\-]{35}\b",
|
|
469
|
+
&["AIza"],
|
|
470
|
+
P_API_KEY,
|
|
471
|
+
MaskKind::Full,
|
|
472
|
+
),
|
|
473
|
+
// SendGrid
|
|
474
|
+
rule(
|
|
475
|
+
"api_key",
|
|
476
|
+
r"\bSG\.[A-Za-z0-9_\-]{16,32}\.[A-Za-z0-9_\-]{16,64}\b",
|
|
477
|
+
&["SG."],
|
|
478
|
+
P_API_KEY,
|
|
479
|
+
MaskKind::Full,
|
|
480
|
+
),
|
|
481
|
+
// Twilio API key / account SID
|
|
482
|
+
rule(
|
|
483
|
+
"api_key",
|
|
484
|
+
r"\b(?:SK|AC)[0-9a-fA-F]{32}\b",
|
|
485
|
+
&[],
|
|
486
|
+
P_API_KEY,
|
|
487
|
+
MaskKind::Full,
|
|
488
|
+
),
|
|
489
|
+
// npm
|
|
490
|
+
rule(
|
|
491
|
+
"api_key",
|
|
492
|
+
r"\bnpm_[A-Za-z0-9]{36}\b",
|
|
493
|
+
&["npm_"],
|
|
494
|
+
P_API_KEY,
|
|
495
|
+
MaskKind::Full,
|
|
496
|
+
),
|
|
497
|
+
// PyPI upload token
|
|
498
|
+
rule(
|
|
499
|
+
"api_key",
|
|
500
|
+
r"\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_\-]{50,}",
|
|
501
|
+
&["pypi-AgEIcHlwaS5vcmc"],
|
|
502
|
+
P_API_KEY,
|
|
503
|
+
MaskKind::Full,
|
|
504
|
+
),
|
|
505
|
+
// Shopify access tokens
|
|
506
|
+
rule(
|
|
507
|
+
"api_key",
|
|
508
|
+
r"\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b",
|
|
509
|
+
&["shpat_", "shpca_", "shppa_", "shpss_"],
|
|
510
|
+
P_API_KEY,
|
|
511
|
+
MaskKind::Full,
|
|
512
|
+
),
|
|
513
|
+
// Square
|
|
514
|
+
rule(
|
|
515
|
+
"api_key",
|
|
516
|
+
r"\bsq0(?:atp|csp|idp)-[A-Za-z0-9_\-]{22,64}\b",
|
|
517
|
+
&["sq0atp-", "sq0csp-", "sq0idp-"],
|
|
518
|
+
P_API_KEY,
|
|
519
|
+
MaskKind::Full,
|
|
520
|
+
),
|
|
521
|
+
// Mailgun
|
|
522
|
+
rule(
|
|
523
|
+
"api_key",
|
|
524
|
+
r"\bkey-[0-9a-f]{32}\b",
|
|
525
|
+
&["key-"],
|
|
526
|
+
P_API_KEY,
|
|
527
|
+
MaskKind::Full,
|
|
528
|
+
),
|
|
529
|
+
// DigitalOcean
|
|
530
|
+
rule(
|
|
531
|
+
"api_key",
|
|
532
|
+
r"\bdop_v1_[a-f0-9]{64}\b",
|
|
533
|
+
&["dop_v1_"],
|
|
534
|
+
P_API_KEY,
|
|
535
|
+
MaskKind::Full,
|
|
536
|
+
),
|
|
537
|
+
// Telegram bot token
|
|
538
|
+
rule(
|
|
539
|
+
"api_key",
|
|
540
|
+
r"\b\d{8,10}:AA[A-Za-z0-9_\-]{33}\b",
|
|
541
|
+
&[":AA"],
|
|
542
|
+
P_API_KEY,
|
|
543
|
+
MaskKind::Full,
|
|
544
|
+
),
|
|
545
|
+
// AWS secret access key, which only has shape in context
|
|
546
|
+
captured(
|
|
547
|
+
"api_key",
|
|
548
|
+
r#"(?i)aws[_\-]?(?:secret[_\-]?)?access[_\-]?key["']?\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})"#,
|
|
549
|
+
&["aws"],
|
|
550
|
+
P_API_KEY,
|
|
551
|
+
),
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
/// Detector keys enabled by default: everything except the region packs.
|
|
555
|
+
pub const DEFAULTS: &[&str] = &[
|
|
556
|
+
"email",
|
|
557
|
+
"phone",
|
|
558
|
+
"credit_card",
|
|
559
|
+
"ssn",
|
|
560
|
+
"iban",
|
|
561
|
+
"ip",
|
|
562
|
+
"ipv6",
|
|
563
|
+
"mac",
|
|
564
|
+
"jwt",
|
|
565
|
+
"aws_key",
|
|
566
|
+
"api_key",
|
|
567
|
+
"private_key",
|
|
568
|
+
"password_pair",
|
|
569
|
+
"url_credentials",
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
/// The opt-in India pack (DPDP Act shaped).
|
|
573
|
+
pub const INDIA: &[&str] = &["phone_in", "aadhaar", "pan", "upi"];
|
|
574
|
+
|
|
575
|
+
/// Rules for a detector key, or `None` if the key is unknown.
|
|
576
|
+
pub fn rules_for(key: &str) -> Option<&'static [Rule]> {
|
|
577
|
+
REGISTRY
|
|
578
|
+
.iter()
|
|
579
|
+
.find(|(k, _)| *k == key)
|
|
580
|
+
.map(|(_, rules)| *rules)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// Every detector key, for error messages and `Scrubber.detectors`.
|
|
584
|
+
pub fn all_keys() -> Vec<&'static str> {
|
|
585
|
+
REGISTRY.iter().map(|(k, _)| *k).collect()
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
#[cfg(test)]
|
|
589
|
+
mod tests {
|
|
590
|
+
use super::*;
|
|
591
|
+
|
|
592
|
+
#[test]
|
|
593
|
+
fn every_registry_pattern_compiles() {
|
|
594
|
+
for (key, rules) in REGISTRY {
|
|
595
|
+
for (i, r) in rules.iter().enumerate() {
|
|
596
|
+
regex::Regex::new(r.pattern)
|
|
597
|
+
.unwrap_or_else(|e| panic!("{key} rule {i} failed to compile: {e}"));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#[test]
|
|
603
|
+
fn capture_groups_exist_where_declared() {
|
|
604
|
+
for (key, rules) in REGISTRY {
|
|
605
|
+
for (i, r) in rules.iter().enumerate() {
|
|
606
|
+
let re = regex::Regex::new(r.pattern).unwrap();
|
|
607
|
+
assert!(
|
|
608
|
+
re.captures_len() > r.capture,
|
|
609
|
+
"{key} rule {i} declares capture {} but has {} groups",
|
|
610
|
+
r.capture,
|
|
611
|
+
re.captures_len() - 1
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#[test]
|
|
618
|
+
fn defaults_and_india_are_registered_and_disjoint() {
|
|
619
|
+
for key in DEFAULTS.iter().chain(INDIA.iter()) {
|
|
620
|
+
assert!(rules_for(key).is_some(), "{key} is not in the registry");
|
|
621
|
+
}
|
|
622
|
+
for key in INDIA {
|
|
623
|
+
assert!(!DEFAULTS.contains(key), "{key} must be opt-in");
|
|
624
|
+
}
|
|
625
|
+
assert_eq!(DEFAULTS.len() + INDIA.len(), REGISTRY.len());
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
#[test]
|
|
629
|
+
fn rule_kinds_match_their_registry_key() {
|
|
630
|
+
for (key, rules) in REGISTRY {
|
|
631
|
+
for r in rules.iter() {
|
|
632
|
+
assert_eq!(r.kind, *key);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|