@link-foundation/sensitive-data-sanitizer 0.0.0-stage → 0.11.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +530 -0
  2. package/LICENSE +24 -0
  3. package/README.md +184 -2
  4. package/bin/legacy-example-package-name.js +95 -0
  5. package/bin/sensitive-data-sanitizer.js +430 -0
  6. package/bin/stream.js +56 -0
  7. package/docs/API.md +336 -0
  8. package/docs/COVERAGE.md +136 -0
  9. package/docs/HISTORY-REMEDIATION.md +41 -0
  10. package/examples/combined-scanners.mjs +23 -0
  11. package/examples/presidio-bridge.py +32 -0
  12. package/examples/realistic-fakes.mjs +18 -0
  13. package/examples/sanitize-session.mjs +27 -0
  14. package/examples/shared-tool-logs.mjs +21 -0
  15. package/examples/stream-jsonl.mjs +17 -0
  16. package/package.json +124 -4
  17. package/src/adapters.js +204 -0
  18. package/src/arithmetic.js +11 -0
  19. package/src/confidence.js +65 -0
  20. package/src/context.js +424 -0
  21. package/src/credentials.js +54 -0
  22. package/src/detection.js +293 -0
  23. package/src/encoded.js +233 -0
  24. package/src/engines.js +348 -0
  25. package/src/entities.js +166 -0
  26. package/src/escape-spans.js +20 -0
  27. package/src/eslint-plugin.d.ts +10 -0
  28. package/src/eslint-plugin.js +193 -0
  29. package/src/external.js +169 -0
  30. package/src/fake-names.js +222 -0
  31. package/src/fake.js +524 -0
  32. package/src/files.js +131 -0
  33. package/src/gitleaks.js +194 -0
  34. package/src/history-rewrite.js +339 -0
  35. package/src/history.js +103 -0
  36. package/src/identity.js +274 -0
  37. package/src/index.d.ts +321 -0
  38. package/src/index.js +40 -0
  39. package/src/jsonl-stream.js +198 -0
  40. package/src/keywords.js +45 -0
  41. package/src/known.js +164 -0
  42. package/src/legacy.d.ts +3 -0
  43. package/src/legacy.js +2 -0
  44. package/src/locales.js +217 -0
  45. package/src/log-bytes.js +85 -0
  46. package/src/names.js +65 -0
  47. package/src/national-checks.js +129 -0
  48. package/src/national.js +128 -0
  49. package/src/outbound.js +28 -0
  50. package/src/personal-context.js +85 -0
  51. package/src/personal.js +248 -0
  52. package/src/projection.js +126 -0
  53. package/src/propagation.js +163 -0
  54. package/src/public-verifier.js +77 -0
  55. package/src/public.js +153 -0
  56. package/src/record-worker.js +20 -0
  57. package/src/rules.js +210 -0
  58. package/src/sanitizer.js +794 -0
  59. package/src/specimens.js +34 -0
  60. package/src/stream-identity.js +129 -0
  61. package/src/stream-worker.js +18 -0
  62. package/src/stream.js +449 -0
  63. package/src/structured.js +278 -0
  64. package/src/transforms.js +192 -0
  65. package/src/transliteration.js +75 -0
  66. package/src/utf8.js +60 -0
  67. package/src/vendor/entities/aws.json +42 -0
  68. package/src/vendor/entities/azure.json +182 -0
  69. package/src/vendor/entities/google.json +219 -0
  70. package/src/vendor/gitleaks/LICENSE +21 -0
  71. package/src/vendor/gitleaks/provenance.json +8 -0
  72. package/src/vendor/gitleaks/rules-0.json +268 -0
  73. package/src/vendor/gitleaks/rules-1.json +1794 -0
  74. package/src/vendor/gitleaks/rules-2.json +292 -0
  75. package/src/vendor/gitleaks/rules-3.json +263 -0
  76. package/src/vendor/gitleaks/rules-4.json +277 -0
  77. package/src/vendor/gitleaks/rules-5.json +154 -0
  78. package/src/worker-options.js +27 -0
package/src/context.js ADDED
@@ -0,0 +1,424 @@
1
+ import { labels } from './rules.js';
2
+ import { escapePattern, REDACTED } from './detection.js';
3
+ import { credentialPlaceholder, referencePrefix } from './credentials.js';
4
+ import { servicePath } from './public.js';
5
+ import { plausiblePerson } from './names.js';
6
+
7
+ // Bounded vocabularies are explicit so the short PASS/PW forms do not match
8
+ // unrelated words such as passport. Verb forms cover the supported locales.
9
+ export const credentialVerbs = [
10
+ 'is',
11
+ 'was',
12
+ 'equals',
13
+ 'es',
14
+ 'era',
15
+ 'est',
16
+ 'était',
17
+ 'ist',
18
+ 'war',
19
+ 'é',
20
+ 'era',
21
+ 'это',
22
+ 'равен',
23
+ 'был',
24
+ 'была',
25
+ 'было',
26
+ 'будет',
27
+ 'составляет',
28
+ '是',
29
+ '为',
30
+ '為',
31
+ 'は',
32
+ 'です',
33
+ '은',
34
+ '는',
35
+ '입니다',
36
+ 'هو',
37
+ 'هي',
38
+ 'היא',
39
+ 'הוא',
40
+ 'है',
41
+ 'था',
42
+ 'adalah',
43
+ 'ialah',
44
+ 'là',
45
+ 'คือ',
46
+ ];
47
+ const verbs = credentialVerbs.map(escapePattern).join('|');
48
+ const key = `(?:[\\p{L}\\p{N}]+[_.-])*(?:auth|authorization)(?:[_.-][\\p{L}\\p{N}]+)*|[\\p{L}\\p{N}_.-]{0,32}(?:${labels.credential
49
+ .filter((word) => !['auth', 'authorization'].includes(word))
50
+ .map(escapePattern)
51
+ .join(
52
+ '|'
53
+ )})[\\p{L}\\p{N}_.-]{0,32}|(?:[\\p{L}\\p{N}_.-]{1,32}[_-])?(?:pass|pw)(?:[_-][\\p{L}\\p{N}_.-]{1,32})?`;
54
+ // A qualifier such as "for staging" stays on one logical line: it never
55
+ // crosses an escaped line break, an escape sequence or an enclosing quote.
56
+ // Verbs are whole words, so "request" does not end in the verb "est".
57
+ const credentialPrefix = new RegExp(
58
+ `(?<![\\p{L}\\p{N}_])(?:\\\\*["'])?(${key})(?![\\p{L}\\p{N}_])(?:\\\\*["'])?(?:[ \\t]+(?:от|для|к|for|de|du|del)[ \\t]+[^:=\\r\\n"\\\\]{1,64})?[ \\t]*([:=:]|(?<![\\p{L}\\p{N}_])(?:${verbs})(?![\\p{L}\\p{N}_]))[ \\t]*`,
59
+ 'giu'
60
+ );
61
+ // Bare generic labels are common English nouns. After a verb they need a
62
+ // value that looks generated; "the token is valid" is prose, not a secret.
63
+ const genericLabel =
64
+ /^(?:token|sig|sas|secret|auth|authorization|cookie|секрет|токен)$/iu;
65
+ function generatedWord(text, range) {
66
+ const word = /^[^\s]+/u
67
+ .exec(text.slice(range.start, range.end))?.[0]
68
+ .replace(/[.,;:!?)]+$/u, '');
69
+ if (
70
+ !word ||
71
+ !(
72
+ (word.length >= 6 && /\p{L}/u.test(word) && /\d/.test(word)) ||
73
+ /^[\w+/=.-]{20,}$/.test(word)
74
+ )
75
+ ) {
76
+ return undefined;
77
+ }
78
+ return { start: range.start, end: range.start + word.length };
79
+ }
80
+ const ignored =
81
+ /^(?:true|false|null|undefined|await|async|function|none|nil)$/i;
82
+
83
+ function emitValue(text, start, end, emit, details, keyName = '') {
84
+ while (end > start && /[ \t\r]/.test(text[end - 1])) {
85
+ end--;
86
+ }
87
+ const value = text.slice(start, end);
88
+ if (!value || value.startsWith(REDACTED) || ignored.test(value)) {
89
+ return;
90
+ }
91
+ if (details.category === 'credential' && credentialPlaceholder(value)) {
92
+ return;
93
+ }
94
+ if (details.category === 'credential' && credentialMetadata(keyName, value)) {
95
+ return;
96
+ }
97
+ if (
98
+ /tokens$|token(?:count|limit|usage|budget)$|(?:password|passphrase)(?:length|count|size)$/.test(
99
+ keyName.replace(/[^\p{L}\p{N}]/gu, '').toLowerCase()
100
+ ) &&
101
+ /^\d+(?:\.\d+)?$/.test(value)
102
+ ) {
103
+ return;
104
+ }
105
+ emit({ start, end, ...details });
106
+ }
107
+
108
+ function credentialMetadata(keyName, value) {
109
+ return (
110
+ (/(?:^|[_.-])auth[_.-](?:mode|method|type)$/i.test(keyName) &&
111
+ /^(?:chatgpt|oauth2?|apikey|api_key|bearer|basic|none)$/i.test(value)) ||
112
+ (keyName === 'id-token' && /^(?:write|read|none)$/.test(value)) ||
113
+ // Package/version metadata cannot exempt exact credential labels.
114
+ (!labels.credential.includes(keyName.toLowerCase()) &&
115
+ /^(?:@[a-z0-9_.-]+\/)?[a-z0-9][a-z0-9_.-]*$/.test(keyName) &&
116
+ value.length <= 128 &&
117
+ /^[~^<>=* ]*v?\d+(?:\.[\dx*]+){0,2}(?:-[\w.-]+)?(?:\s*\|\|\s*[~^<>=* ]*v?\d+(?:\.[\dx*]+){0,2}(?:-[\w.-]+)?)*$/i.test(
118
+ value
119
+ ))
120
+ );
121
+ }
122
+
123
+ // Opening quotes support the escape depth of an outer AI-session string.
124
+ // Only a quote at the same escape depth closes it; embedded quotes belong to
125
+ // the value. Offsets cover escape bytes as well as the decoded characters.
126
+ export function valueRange(text, start, mode = 'token', maxLength = Infinity) {
127
+ const reference = referencePrefix(text.slice(start, start + 512));
128
+ if (reference) {
129
+ return { start, end: start + reference[0].length };
130
+ }
131
+ const opening = text.slice(start).match(/^(\\*)(["'])/);
132
+ if (opening) {
133
+ const range = quotedRange(text, start, opening, maxLength);
134
+ const outer = outerQuoteEnd(text, range.start, range.end);
135
+ return outer === undefined ? range : { ...range, end: outer };
136
+ }
137
+ if (/^[{[]/.test(text.slice(start, start + 1))) {
138
+ return undefined;
139
+ }
140
+ const tail = text.slice(start, start + maxLength);
141
+ const terminator =
142
+ mode === 'query'
143
+ ? /[\s&#"'<>]/
144
+ : mode === 'line'
145
+ ? /[\r\n<>"]|\\+[nrt"']/
146
+ : mode === 'prose'
147
+ ? /[\r\n<>]|\\+[nrt"']|(?<![ \t])[ \t]+(?:for the|for my|для|pour|para|für|لـ)(?:[ \t]|$)/iu
148
+ : /[\s,;}\]"'<>]|\\+[nrt"']/;
149
+ const stop = tail.search(terminator);
150
+ const end = stop < 0 ? start + tail.length : start + stop;
151
+ return { start, end: Math.min(end, outerQuoteEnd(text, start, end) ?? end) };
152
+ }
153
+ function quotedRange(text, start, opening, maxLength) {
154
+ const depth = opening[1].length,
155
+ quote = opening[2];
156
+ const valueStart = start + opening[0].length;
157
+ for (
158
+ let i = valueStart;
159
+ i < Math.min(text.length, valueStart + maxLength);
160
+ i++
161
+ ) {
162
+ if (
163
+ Number.isFinite(maxLength) &&
164
+ /^(?:[\r\n]|\\+[nrt])/.test(text.slice(i, i + 8))
165
+ ) {
166
+ return { start: valueStart, end: i };
167
+ }
168
+ if (text[i] !== quote) {
169
+ continue;
170
+ }
171
+ let slashes = 0;
172
+ for (let j = i - 1; j >= valueStart && text[j] === '\\'; j--) {
173
+ slashes++;
174
+ }
175
+ if (slashes === depth) {
176
+ return { start: valueStart, end: i - depth };
177
+ }
178
+ // A containing JSON string can close before an unfinished inner quote.
179
+ if (quote === '"' && slashes < depth && slashes % 2 === 0) {
180
+ return { start: valueStart, end: Math.max(valueStart, i) };
181
+ }
182
+ }
183
+ return {
184
+ start: valueStart,
185
+ end: Math.min(text.length, valueStart + maxLength),
186
+ };
187
+ }
188
+
189
+ function plausibleLabel(value, type) {
190
+ if (type === 'PERSON') {
191
+ return plausiblePerson(value.replace(/[,;\d].*$/s, '').trim());
192
+ }
193
+ if (['STATE', 'US_STATE', 'CITY', 'REGION', 'PROVINCE'].includes(type)) {
194
+ return (
195
+ /^[\p{L}\p{M}][\p{L}\p{M} .’'-]{1,79}$/u.test(value) &&
196
+ (!/\p{Ll}/u.test(value) || /\p{Lu}/u.test(value))
197
+ );
198
+ }
199
+ return true;
200
+ }
201
+
202
+ // Shared by native labels and the vendor catalogs. Code expressions and
203
+ // line escapes are not personal field values, even when a key is familiar.
204
+ export function personalValueAllowed(text, range, type, labelStart) {
205
+ const value = text.slice(range.start, range.end).trim();
206
+ if (!plausibleLabel(value, type)) {
207
+ return false;
208
+ }
209
+ if (['LOCATION', 'ADDRESS'].includes(type) && servicePath(value)) {
210
+ return false;
211
+ }
212
+ if (type === 'PHONE') {
213
+ return (
214
+ /^\+?[\d () .-]{7,40}$/.test(value) &&
215
+ /\d{3}/.test(value.replace(/\D/g, ''))
216
+ );
217
+ }
218
+ if (type === 'EMAIL') {
219
+ return /^[^\s@]+@[^\s@]+\.[\p{L}]{2,63}$/u.test(value);
220
+ }
221
+ if (/^(?:\\[nrt]|[\r\n])/.test(value)) {
222
+ return false;
223
+ }
224
+ if (
225
+ ['ADDRESS', 'STATE', 'US_STATE', 'CITY', 'REGION', 'PROVINCE'].includes(
226
+ type
227
+ )
228
+ ) {
229
+ if (!/\p{L}{2}|\d{4,}/u.test(value)) {
230
+ return false;
231
+ }
232
+ }
233
+ if (
234
+ ['PERSON', 'USERNAME'].includes(type) &&
235
+ !/["']/.test(text[range.start - 1] ?? '')
236
+ ) {
237
+ const before = text
238
+ .slice(Math.max(0, labelStart - 100), labelStart)
239
+ .split(/[\r\n]/)
240
+ .at(-1);
241
+ if (
242
+ /\b(?:const|let|var)\s+$/.test(before) ||
243
+ /\b[\p{L}_$][\p{L}\p{N}_$]*(?:\s*\(|\.[\p{L}_$])/u.test(value)
244
+ ) {
245
+ return false;
246
+ }
247
+ }
248
+ return true;
249
+ }
250
+
251
+ // Prose labels inside JSON strings stop at the enclosing quote.
252
+ function outerQuoteEnd(text, start, end) {
253
+ let opening = start - 1;
254
+ while (opening >= Math.max(0, start - 8192)) {
255
+ opening = text.lastIndexOf('"', opening);
256
+ if (opening < 0) {
257
+ return undefined;
258
+ }
259
+ if (backslashes(text, opening) % 2 === 0) {
260
+ break;
261
+ }
262
+ opening--;
263
+ }
264
+ // Structured batches put each JSON string on its own line, after an
265
+ // optional field prefix; values never continue into the next token.
266
+ if (
267
+ opening < 0 ||
268
+ (opening > 0 &&
269
+ text[opening - 1] !== '\n' &&
270
+ !/:\s*$/.test(text.slice(Math.max(0, opening - 8), opening)))
271
+ ) {
272
+ return undefined;
273
+ }
274
+ for (let i = start; i < end; i++) {
275
+ if (text[i] === '"' && backslashes(text, i) % 2 === 0) {
276
+ return i;
277
+ }
278
+ }
279
+ return undefined;
280
+ }
281
+ function backslashes(text, index) {
282
+ let count = 0;
283
+ while (index > 0 && text[--index] === '\\') {
284
+ count++;
285
+ }
286
+ return count;
287
+ }
288
+
289
+ function detectCredentials(text, emit) {
290
+ const details = { category: 'credential', type: 'SECRET', rule: 'context' };
291
+ for (const match of text.matchAll(credentialPrefix)) {
292
+ const start = match.index + match[0].length;
293
+ const prefix = text.slice(Math.max(0, match.index - 512), match.index);
294
+ const query =
295
+ /(?:https?:\/\/[^\s]*[?&]|^[?&])[^\s]*$/i.test(prefix) ||
296
+ /[?&]$/.test(prefix);
297
+ const mode = query
298
+ ? 'query'
299
+ : match[2] === '=' || /["']\s*[::]/.test(match[0])
300
+ ? 'token'
301
+ : /[::]/.test(match[2])
302
+ ? 'line'
303
+ : 'prose';
304
+ let range = valueRange(text, start, mode);
305
+ if (range && mode === 'prose' && genericLabel.test(match[1])) {
306
+ range = generatedWord(text, range);
307
+ }
308
+ if (range) {
309
+ emitValue(text, range.start, range.end, emit, details, match[1]);
310
+ }
311
+ }
312
+ const xml = new RegExp(`<(${key})[ \\t]*>([^<]+)</\\1[ \\t]*>`, 'giu');
313
+ for (const match of text.matchAll(xml)) {
314
+ const start = match.index + match[0].indexOf('>') + 1;
315
+ emitValue(text, start, start + match[2].length, emit, details, match[1]);
316
+ }
317
+ const flags = new RegExp(`--(${key})(?:[ \\t]+|=)`, 'giu');
318
+ for (const match of text.matchAll(flags)) {
319
+ const range = valueRange(text, match.index + match[0].length);
320
+ if (range) {
321
+ emitValue(text, range.start, range.end, emit, details, match[1]);
322
+ }
323
+ }
324
+ }
325
+
326
+ function detectPersonalLabels(text, emit) {
327
+ for (const [type, words] of Object.entries(labels)) {
328
+ if (type === 'credential') {
329
+ continue;
330
+ }
331
+ const separator =
332
+ type === 'ID'
333
+ ? `(?:[:=:]|(?:${verbs})(?![\\p{L}\\p{N}_]))?[ \\t]+|[:=:][ \\t]*`
334
+ : `(?:[:=:]|(?:${verbs})(?![\\p{L}\\p{N}_]))[ \\t]*`;
335
+ const prefix = new RegExp(
336
+ `(?<![\\p{L}\\p{N}_])(?:\\\\*["'])?(?:${words.map(escapePattern).join('|')})(?![\\p{L}\\p{N}_])(?:\\\\*["'])?[ \\t]*(?:${separator})`,
337
+ 'giu'
338
+ );
339
+ for (const match of text.matchAll(prefix)) {
340
+ const identity = type === 'ID';
341
+ const suffix = identity
342
+ ? (/^(?:(?:number|no\.?|номер)[ \t]*[:=:]?[ \t]*|[№#][ \t]*)/iu.exec(
343
+ text.slice(match.index + match[0].length)
344
+ )?.[0] ?? '')
345
+ : '';
346
+ const range = valueRange(
347
+ text,
348
+ match.index + match[0].length + suffix.length,
349
+ identity ? 'token' : 'line',
350
+ 512
351
+ );
352
+ if (range && personalValueAllowed(text, range, type, match.index)) {
353
+ if (type === 'PERSON') {
354
+ const delimiter = text.slice(range.start, range.end).search(/[,;\d]/);
355
+ if (delimiter >= 0) {
356
+ range.end = range.start + delimiter;
357
+ }
358
+ }
359
+ emitValue(text, range.start, range.end, emit, {
360
+ type:
361
+ identity &&
362
+ /passport|паспорт|pasaporte|Reisepass|passeport|passaporto|护照|パスポート|جواز/iu.test(
363
+ match[0]
364
+ )
365
+ ? 'PASSPORT_NUMBER'
366
+ : type,
367
+ category: 'personal',
368
+ rule: 'label',
369
+ });
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ function detectCommandPasswords(text, emit) {
376
+ const details = {
377
+ type: 'SECRET',
378
+ category: 'credential',
379
+ rule: 'cli-password',
380
+ };
381
+ // Scope short flags to programs whose documentation assigns that meaning.
382
+ // mysql -P and psql -p are port numbers and deliberately excluded.
383
+ for (const line of text.matchAll(/[^\r\n;|&]+/g)) {
384
+ const command = line[0];
385
+ let flag;
386
+ if (/(?:^|[\s/])(?:mysql|mariadb|sshpass)(?:\s|$)/.test(command)) {
387
+ flag = /(?:^|\s)-p[ \t]*/g;
388
+ } else if (/(?:^|[\s/])(?:7z|7za|unzip)(?:\s|$)/.test(command)) {
389
+ flag = /(?:^|\s)-(?:p|P)[ \t]*/g;
390
+ }
391
+ if (flag) {
392
+ for (const match of command.matchAll(flag)) {
393
+ const range = valueRange(
394
+ text,
395
+ line.index + match.index + match[0].length
396
+ );
397
+ if (range) {
398
+ emitValue(text, range.start, range.end, emit, details);
399
+ }
400
+ }
401
+ }
402
+ if (/(?:^|[\s/])curl(?:\s|$)/.test(command)) {
403
+ for (const match of command.matchAll(
404
+ /(?:^|\s)(?:-u|--user)(?:[ \t]+|=)/g
405
+ )) {
406
+ const range = valueRange(
407
+ text,
408
+ line.index + match.index + match[0].length
409
+ );
410
+ if (range) {
411
+ emitValue(text, range.start, range.end, emit, details);
412
+ }
413
+ }
414
+ }
415
+ }
416
+ }
417
+
418
+ export function detectContext(text, emit, credentialsOnly = false) {
419
+ detectCredentials(text, emit);
420
+ if (!credentialsOnly) {
421
+ detectPersonalLabels(text, emit);
422
+ }
423
+ detectCommandPasswords(text, emit);
424
+ }
@@ -0,0 +1,54 @@
1
+ import { collectMatches } from './detection.js';
2
+
3
+ export function credentialPlaceholder(value) {
4
+ return (
5
+ /^(?:\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*|<[^<>]+>|\[REDACTED\])$/.test(
6
+ value
7
+ ) || referencePrefix(value)?.[0] === value
8
+ );
9
+ }
10
+ // Only variable references and the documented gh lookup are exempt. Arbitrary
11
+ // shell commands and Actions expressions can contain literal credentials.
12
+ export function referencePrefix(value) {
13
+ return /^(?:(?:token|Bearer|Basic)\s+)?(?:\$\{\{\s*(?:secrets|env|vars|github|inputs)\.[A-Za-z_][A-Za-z0-9_.]*\s*\}\}|\$\(gh\s+auth\s+token(?:\s+--hostname\s+[a-z0-9.-]+)?\))(?=$|[\s"',;])/i.exec(
14
+ value
15
+ );
16
+ }
17
+ const details = (rule) => ({
18
+ type: 'SECRET',
19
+ category: 'credential',
20
+ rule,
21
+ confidence: 0.99,
22
+ });
23
+ export function detectCredentialFormats(text, emit) {
24
+ collectMatches(
25
+ text,
26
+ /(?<![\w])(?:machine[ \t]+[^\s"'<>]+|default)\s+login\s+[^\s"'<>]+\s+password\s+("[^"\r\n]+"|'[^'\r\n]+'|[^\s"'<>]+)/g,
27
+ emit,
28
+ details('netrc'),
29
+ 0,
30
+ (_value, match) =>
31
+ !credentialPlaceholder(match[1].replace(/^["']|["']$/g, ''))
32
+ );
33
+ collectMatches(
34
+ text,
35
+ /(?<![a-z0-9_])oy2[a-z0-9]{43}(?![a-z0-9_])/g,
36
+ emit,
37
+ details('nuget')
38
+ );
39
+ collectMatches(
40
+ text,
41
+ /\bdefine\s*\(\s*(['"])(?:AUTH|SECURE_AUTH|LOGGED_IN|NONCE)_(?:KEY|SALT)\1\s*,\s*(['"])((?:\\.|(?!\2)[^\\\r\n])+)\2\s*\)/g,
42
+ emit,
43
+ details('wordpress'),
44
+ 3,
45
+ (value) =>
46
+ !credentialPlaceholder(value) && value !== 'put your unique phrase here'
47
+ );
48
+ collectMatches(
49
+ text,
50
+ /PuTTY-User-Key-File-[23]:[^\r\n]*(?:\r\n|\r|\n)[\s\S]*?(?:Private-MAC:[ \t]*[a-fA-F0-9]+|$)/g,
51
+ emit,
52
+ details('putty-private-key')
53
+ );
54
+ }