redac---n 0.0.3 → 0.0.5
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/README.md +11 -10
- package/dist/calls.d.ts +8 -0
- package/dist/calls.js +60 -3
- package/dist/checksums.js +4 -3
- package/dist/commands.d.ts +5 -0
- package/dist/commands.js +93 -14
- package/dist/context.d.ts +5 -2
- package/dist/context.js +36 -9
- package/dist/encoded.js +13 -6
- package/dist/environment.d.ts +5 -3
- package/dist/environment.js +38 -14
- package/dist/formats.js +15 -0
- package/dist/index.js +10 -4
- package/dist/json.js +29 -5
- package/dist/known.d.ts +7 -7
- package/dist/known.js +33 -25
- package/dist/named.d.ts +6 -0
- package/dist/named.js +421 -125
- package/dist/names.d.ts +5 -1
- package/dist/names.js +143 -18
- package/dist/normalize.d.ts +4 -1
- package/dist/normalize.js +37 -11
- package/dist/pem.d.ts +2 -0
- package/dist/pem.js +12 -0
- package/dist/policy.js +12 -5
- package/dist/public.js +35 -13
- package/dist/secretfile.d.ts +5 -0
- package/dist/secretfile.js +49 -20
- package/dist/shell.d.ts +12 -3
- package/dist/shell.js +51 -7
- package/dist/spans.d.ts +4 -3
- package/dist/spans.js +79 -8
- package/dist/sql.d.ts +12 -8
- package/dist/sql.js +94 -16
- package/dist/stream.d.ts +2 -1
- package/dist/stream.js +15 -3
- package/dist/structure.js +210 -61
- package/dist/syntax.d.ts +5 -2
- package/dist/syntax.js +7 -4
- package/dist/transcript.js +116 -28
- package/dist/unnamed.js +128 -20
- package/dist/urls.d.ts +8 -3
- package/dist/urls.js +16 -7
- package/dist/values.d.ts +2 -1
- package/dist/values.js +70 -25
- package/docs/holdout.svg +8 -8
- package/docs/layers.md +1 -1
- package/package.json +1 -1
package/dist/urls.js
CHANGED
|
@@ -20,9 +20,11 @@ const USERINFO = new RegExp(String.raw `${URL_SCHEME}:\/\/[^\s/@]*`, "y");
|
|
|
20
20
|
* Where a URL that starts at `at` ends: at a blank, a quote, an angle bracket, a backtick, a backslash (a JSON
|
|
21
21
|
* string's escaped quote), or a closing bracket it didn't open (a Markdown link's `)`); sentence punctuation at its
|
|
22
22
|
* end isn't part of it (`…/reset/<token>.`). A password's brackets are (`app:p)dgbs@db`, `admin:<Harbor2291x>@host`).
|
|
23
|
-
* However long the password: each look-ahead for its `@` is remembered to where it stopped (`reachesAt`).
|
|
23
|
+
* However long the password: each look-ahead for its `@` is remembered to where it stopped (`reachesAt`). With `enough`,
|
|
24
|
+
* it stops once it has read past that point (the answer is then only known to be past it): urlAround asks whether a
|
|
25
|
+
* URL reaches a position, and a line with no blank in it (a comma-separated list of hosts) reads, as one URL, to its end.
|
|
24
26
|
*/
|
|
25
|
-
export function urlEnd(text, at) {
|
|
27
|
+
export function urlEnd(text, at, enough = Infinity) {
|
|
26
28
|
const open = { ")": 0, "]": 0 };
|
|
27
29
|
const closerIsPassword = reachesAt(text, /[^\s"'<>`\\/@]*(@)?/y);
|
|
28
30
|
const quoteIsPassword = reachesAt(text, /[^\s"<>`\\/@,;]*(@)?/y);
|
|
@@ -33,6 +35,9 @@ export function urlEnd(text, at) {
|
|
|
33
35
|
const bracketed = /<[^\s<>@"'`]+>@/y;
|
|
34
36
|
let end = at;
|
|
35
37
|
for (; end < text.length && end - at < MAX_VALUE; end++) {
|
|
38
|
+
// (Sentence punctuation read last could still be trimmed off below.)
|
|
39
|
+
if (end > enough && !/[.,;:!?]/.test(text[end - 1]))
|
|
40
|
+
return end;
|
|
36
41
|
const c = text[end];
|
|
37
42
|
if (c === "(")
|
|
38
43
|
open[")"]++;
|
|
@@ -50,13 +55,14 @@ export function urlEnd(text, at) {
|
|
|
50
55
|
break;
|
|
51
56
|
end = bracketed.lastIndex - 1;
|
|
52
57
|
}
|
|
53
|
-
else if (
|
|
58
|
+
else if (/["'<>`\\]/.test(c) &&
|
|
54
59
|
quoteIsPassword(end + 1) &&
|
|
55
60
|
colon >= 0 &&
|
|
56
61
|
colon < end &&
|
|
57
62
|
end <= userinfoEnd &&
|
|
58
63
|
!endsInPort(text, end)) {
|
|
59
|
-
// (In a password, before the `@` that ends it: `postgres://app:Kx9m'Qv2@db`, `postgres://app:Kx9m"Qv2@db
|
|
64
|
+
// (In a password, before the `@` that ends it: `postgres://app:Kx9m'Qv2@db`, `postgres://app:Kx9m"Qv2@db`,
|
|
65
|
+
// `postgres://app:Qv2>Kx9m@db`; a JSON string's escape too, `app:Kx9m\u0026Qv2@db`.)
|
|
60
66
|
}
|
|
61
67
|
else if (/[\s"'<>`\\]/.test(c))
|
|
62
68
|
break;
|
|
@@ -136,7 +142,7 @@ export function urlAround(text, at, end = at, from = Math.max(0, at - 4096)) {
|
|
|
136
142
|
if (!scheme)
|
|
137
143
|
return -1;
|
|
138
144
|
const start = sep - scheme[0].length;
|
|
139
|
-
return urlEnd(text, start) >= end ? start : -1;
|
|
145
|
+
return urlEnd(text, start, end) >= end ? start : -1;
|
|
140
146
|
}
|
|
141
147
|
/**
|
|
142
148
|
* A URL's parts. The authority's user information, as clients read it: a user with no `/?#@:` (or an email address
|
|
@@ -224,8 +230,11 @@ function password(url, userStart, userEnd, hostEnd) {
|
|
|
224
230
|
}
|
|
225
231
|
/** A host as a URL names one: a domain with a TLD (`db.internal`, `example.com`), an IPv4 address, a name and a port. */
|
|
226
232
|
const isHost = (h) => /^(?:[\w-]+\.)+[A-Za-z][\w-]*(?::\d{1,5})?$|^\d{1,3}(?:\.\d{1,3}){3}(?::\d{1,5})?$|^[\w-]+:\d{1,5}$/.test(h);
|
|
227
|
-
/**
|
|
228
|
-
|
|
233
|
+
/**
|
|
234
|
+
* A parameter's start and name: after `?`, `&` (or HTML's `&`, or the `\u0026` and `\x26` that JSON encoders and JS
|
|
235
|
+
* write for it: Go's encoding/json escapes every `&`) or `#`, a name with PHP's `[]` allowed.
|
|
236
|
+
*/
|
|
237
|
+
export const PARAM_START = String.raw `(?:&|\\u0026|\\x26|[?&#])`;
|
|
229
238
|
export const PARAM_NAME = String.raw `[\w.\[\]-]{1,60}`;
|
|
230
239
|
/**
|
|
231
240
|
* The `name=value` pairs in `text`: a URL's query and fragment (`#access_token=…`), a form body. A value ends at `&`,
|
package/dist/values.d.ts
CHANGED
|
@@ -127,7 +127,8 @@ export declare function hasThreeClasses(value: string): boolean;
|
|
|
127
127
|
export declare function isMadePassword(value: string): boolean;
|
|
128
128
|
/**
|
|
129
129
|
* Names of algorithms, encodings and formats that sentences about credentials mention: `password (UTF-8)`, `SHA-256`;
|
|
130
|
-
* and password hashers as libraries name them: Django's `pbkdf2_sha256`, `bcrypt_sha256`, passlib's `sha512_crypt
|
|
130
|
+
* and password hashers as libraries name them: Django's `pbkdf2_sha256`, `bcrypt_sha256`, passlib's `sha512_crypt`;
|
|
131
|
+
* and the schemes a password is checked or sent by: `scram-sha-256`, `md5crypt`, `WPA2`, `NTLMv2`, `MSCHAPv2`.
|
|
131
132
|
*/
|
|
132
133
|
export declare const ALGORITHM: RegExp;
|
|
133
134
|
/** A number with the word that counts in CJK: `6位`, `8文字`, `3回`, `8자`, `2번`. */
|
package/dist/values.js
CHANGED
|
@@ -238,6 +238,9 @@ function isCommandSubstitution(v) {
|
|
|
238
238
|
const arithmetic = /^\$\$?\(\(([^()]*)\)\)$/.exec(v);
|
|
239
239
|
if (arithmetic)
|
|
240
240
|
return /^[\w\s+*/%$-]+$/.test(arithmetic[1]) && /[A-Za-z_]/.test(arithmetic[1]);
|
|
241
|
+
// A program a variable names, quoted, cut at its first blank or not: `$("$binpath" write …)`, `$("${VAULT_BIN}")`.
|
|
242
|
+
if (/^\$\$?\(\s*"\$\{?[A-Za-z_]\w*\}?"(?:\s|\)|$)/.test(v))
|
|
243
|
+
return true;
|
|
241
244
|
const body = /^\$\$?\(([\s\S]*)\)$/.exec(v)?.[1] ??
|
|
242
245
|
/^`([^`]{0,400})`$/.exec(v)?.[1] ??
|
|
243
246
|
/^\$\$?\(([A-Za-z][\w.-]{1,30}(?: [^\n]*)?)$/.exec(v)?.[1];
|
|
@@ -247,7 +250,8 @@ function isCommandSubstitution(v) {
|
|
|
247
250
|
return command !== undefined && command.split("/").every((p) => p === "" || p === "." || p === ".." || isNameLike(p));
|
|
248
251
|
}
|
|
249
252
|
/** Shapes the printf family writes; see `printf template` below. */
|
|
250
|
-
|
|
253
|
+
// (Java's `%n` is a line break; Python's `{0:04x}` a field with its format.)
|
|
254
|
+
const SPEC = /%[-+ #0]{0,3}(?:\d{1,3}|\*)?(?:\.(?:\d{1,3}|\*))?[sdvxfiun]|%\(\w+\)[sd]|\{\d*(?::\w+)?\}|\{:\w*\}/g;
|
|
251
255
|
/**
|
|
252
256
|
* Shapes of values that can't be credentials, by name: constants, paths, references to a value held elsewhere
|
|
253
257
|
* (variables, templates, secret managers), masks and placeholders, encrypted values, public identifiers.
|
|
@@ -269,8 +273,11 @@ const INERT_SHAPES = [
|
|
|
269
273
|
(v) => {
|
|
270
274
|
const m = /^[$%@&]\{([\w.-]{1,80})(?:(?::?[-?=+]|:)[^}]{0,80})?\}$/.exec(v);
|
|
271
275
|
const ps = /^\$(?:env:([A-Za-z_]\w{0,80})|\{env:([A-Za-z_]\w{0,80})\}|\(\$env:([A-Za-z_]\w{0,80})\))$/i.exec(v);
|
|
272
|
-
// (A long name reads as words: `$rqcy7duhdjbjv` is a password
|
|
273
|
-
|
|
276
|
+
// (A long name reads as words: `$rqcy7duhdjbjv` is a password; and a short one switches between letters and
|
|
277
|
+
// digits once at most, `$pass1`, `$x509`: `$kd9xqz2m` is a password.)
|
|
278
|
+
const reads = (name) => name.length <= 8
|
|
279
|
+
? (name.match(/[A-Za-z](?=\d)|\d(?=[A-Za-z])/g)?.length ?? 0) < 2
|
|
280
|
+
: naturalness(name.replace(/[_.-]+/g, " ")) > 0;
|
|
274
281
|
// (Lower case with digits closing it reads as a password: `$summer2024`, not `$db_pass2`.)
|
|
275
282
|
return ((!!m && isRefName(m[1])) ||
|
|
276
283
|
(!!ps && isRefName(ps[1] ?? ps[2] ?? ps[3])) ||
|
|
@@ -292,14 +299,16 @@ const INERT_SHAPES = [
|
|
|
292
299
|
// quote: `$(kubectl get secret … -o jsonpath="`.
|
|
293
300
|
["command substitution", isCommandSubstitution],
|
|
294
301
|
// `{{ var }}`, `${{ secrets.X }}`, `<%= pw %>`, `#{var}`, `@VAR@` (Maven, Autoconf): whole, a name inside. (Go's
|
|
295
|
-
// templates trim blanks with a `-` inside the braces: `{{- .Values.password -}}`, `{{- end }}`.)
|
|
302
|
+
// templates trim blanks with a `-` inside the braces: `{{- .Values.password -}}`, `{{- end }}`.) And the tokens a
|
|
303
|
+
// deployment replaces in a config file it ships (Octopus, Azure DevOps' Replace Tokens): `#{DbPassword}#`,
|
|
304
|
+
// `__DbPassword__`, `@@DbPassword@@`.
|
|
296
305
|
[
|
|
297
306
|
"template",
|
|
298
307
|
(v) => {
|
|
299
|
-
const m = /^(?:\$?\{\{-?(.{0,200}?)-?\}\}|<%=?([^%]{0,200})%>|#\{([^}]{0,200})\}
|
|
308
|
+
const m = /^(?:\$?\{\{-?(.{0,200}?)-?\}\}|<%=?([^%]{0,200})%>|#\{([^}]{0,200})\}#?|@@?([\w.-]{1,80})@@?|__([A-Za-z][\w.]{0,80}[A-Za-z0-9])__)$/.exec(v);
|
|
300
309
|
if (!m)
|
|
301
310
|
return false;
|
|
302
|
-
const inner = (m[1] ?? m[2] ?? m[3] ?? m[4]).trim();
|
|
311
|
+
const inner = (m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5]).trim();
|
|
303
312
|
return /^[A-Za-z_@$.]/.test(inner) && isNameLike(inner.split(/[^\w.]/)[0] || "x");
|
|
304
313
|
},
|
|
305
314
|
],
|
|
@@ -326,8 +335,14 @@ const INERT_SHAPES = [
|
|
|
326
335
|
if (m)
|
|
327
336
|
return (isReferencePath(m[3]) &&
|
|
328
337
|
(m[1] !== undefined || m[3].includes("/") || (m[2] === "env" && /^[A-Z_][A-Z0-9_]*$/.test(m[3]))));
|
|
338
|
+
// (An ARN's name as RDS makes it holds a `!`: `secret:rds!db-<uuid>-AbCdEf`. A Google resource name needs no
|
|
339
|
+
// scheme: `projects/123/secrets/db-password/versions/latest`. Azure's App Service reads Key Vault from
|
|
340
|
+
// `@Microsoft.KeyVault(SecretUri=…)` or `(VaultName=…;SecretName=…)`.)
|
|
341
|
+
const gcp = /^projects\/([\w-]{1,64})\/secrets\/([\w-]{1,255})(?:\/versions\/(?:\d+|latest))?$/.exec(v);
|
|
329
342
|
return (/^credentialsJSON:[0-9a-f-]{36}$/.test(v) ||
|
|
330
|
-
/^arn:aws[\w-]*:(?:secretsmanager|ssm|kms):[a-z0-9-]*:(?:\d{12})?:[\w/:+=.@*${}
|
|
343
|
+
/^arn:aws[\w-]*:(?:secretsmanager|ssm|kms):[a-z0-9-]*:(?:\d{12})?:[\w/:+=.@*${}!-]+$/.test(v) ||
|
|
344
|
+
(!!gcp && isReferencePath(`${gcp[1]}/${gcp[2]}`)) ||
|
|
345
|
+
/^@Microsoft\.KeyVault\((?:SecretUri=https:\/\/[\w.-]+\/secrets\/[\w-]+(?:\/[\w-]*)?|VaultName=[\w-]+;\s*SecretName=[\w-]+(?:;\s*SecretVersion=\w+)?)\)$/i.test(v));
|
|
331
346
|
},
|
|
332
347
|
],
|
|
333
348
|
// `****`, `xxxxxxxx`, `••••`, `[REDACTED]`, `<hidden>`, `ab****yz`; AWX's `$encrypted$`;
|
|
@@ -343,6 +358,10 @@ const INERT_SHAPES = [
|
|
|
343
358
|
/^x{3,}(?:[-_.\d]*$|[-_.]x)/i.test(v) ||
|
|
344
359
|
/^[a-z]{1,10}(?:[-_][a-z]{1,10}){0,2}[-_](?:x{4,}|X{4,}|\*{4,}|\.{3,})$/.test(v),
|
|
345
360
|
],
|
|
361
|
+
// A block comment the value opens with, whole: the annotations bundlers and compilers write after `=`
|
|
362
|
+
// (`/*#__PURE__*/`, `/* @__PURE__ */`, `/** @type {string} */`, `/* webpackChunkName: "auth" */`), then the code
|
|
363
|
+
// they annotate, glued on or not (`/*#__PURE__*/function`).
|
|
364
|
+
["comment", (v) => /^\/\*[^]{0,200}?\*\//.test(v)],
|
|
346
365
|
// An interpolation cut where the reader stopped, at a quote inside it: Ruby `#{ENV['`, `#{node['db'`, JS `${cfg[`.
|
|
347
366
|
[
|
|
348
367
|
"cut interpolation",
|
|
@@ -372,7 +391,11 @@ const INERT_SHAPES = [
|
|
|
372
391
|
/^\$__(?:env|file|vault)\{[^}\n]{1,200}\}$/.test(v) ||
|
|
373
392
|
/^%\{(?:::)?[A-Za-z_][\w:]*(?:\([^}\n]{0,200}\))?\}$/.test(v) ||
|
|
374
393
|
/^@@\{[^}\n]{1,100}\}@@$/.test(v) ||
|
|
375
|
-
/^\[%key:[\w:]{1,200}%\]$/.test(v)
|
|
394
|
+
/^\[%key:[\w:]{1,200}%\]$/.test(v) ||
|
|
395
|
+
// Concourse, BOSH and CredHub (`((db_pass))`, `((/main/db.password))`), and Azure Pipelines' runtime
|
|
396
|
+
// expressions (`$[variables.dbPassword]`): a path of names.
|
|
397
|
+
(/^\(\([\w./:-]{1,200}\)\)$/.test(v) && isReferencePath(v.slice(2, -2))) ||
|
|
398
|
+
/^\$\[\s*variables(?:\.[\w.-]{1,100}|\[['"][\w.-]{1,100}['"]\])\s*\]$/.test(v),
|
|
376
399
|
],
|
|
377
400
|
// A placeholder after a prefix: `sk.<ask-in-#atlas-dev>`, `ghp_<your token>`; in words: `<from vault: "Android upload keystore">`,
|
|
378
401
|
// `<your-key>`; names in brackets, a path of them too: `<SecretsManager:prod/db>` (not `<Xk9p>`); marked: `change-me`,
|
|
@@ -409,13 +432,17 @@ const INERT_SHAPES = [
|
|
|
409
432
|
return (((residue.match(/[A-Za-z]/g)?.length ?? 0) <= 4 ||
|
|
410
433
|
naturalness(residue.replace(/[^A-Za-z]+/g, " ").trim()) > 0) &&
|
|
411
434
|
residue.split(/[^A-Za-z]+/).every((w) => w.length <= 3 || (naturalness(w) > 0.6 && isNameLike(w))) &&
|
|
435
|
+
// (Quotes around a field, as a command line's template quotes one: `password='%s'`.)
|
|
412
436
|
v
|
|
437
|
+
.replace(/(["'])(%[^%'"\s]{1,8})\1/g, "$2")
|
|
413
438
|
.replace(SPEC, "")
|
|
414
439
|
.replace(/\\[nrt]/g, "")
|
|
415
440
|
.replace(/[\w.\-/: ]/g, "").length <= 1 &&
|
|
416
441
|
!(/[A-Z]/.test(v) && /\d/.test(v.replace(/%[sdv]/g, ""))) &&
|
|
417
|
-
|
|
418
|
-
|
|
442
|
+
// (A line break joins the words around it: `%n%nUsing generated security password: %s%n`.)
|
|
443
|
+
[...v.matchAll(SPEC)].every((m) => m[0] === "%n" ||
|
|
444
|
+
((!/[A-Za-z0-9]/.test(v[m.index - 1] ?? "") || !/[A-Za-z0-9]/.test(v[m.index + m[0].length] ?? "")) &&
|
|
445
|
+
!(m[0][0] === "%" && /[A-Za-z]/.test(v[m.index + m[0].length] ?? "")))));
|
|
419
446
|
},
|
|
420
447
|
],
|
|
421
448
|
["option", (v) => /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/.test(v)], // `--password-stdin`: a flag named, not a value
|
|
@@ -543,8 +570,9 @@ export function isPath(path) {
|
|
|
543
570
|
if (/^[a-z0-9_-]+(?:\/[a-z0-9_.-]+)*\/[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.[a-z0-9]{1,8}$/.test(value))
|
|
544
571
|
return value.split(/[/._-]/).every((seg) => (seg.match(/[a-z](?=\d)|\d(?=[a-z])/g)?.length ?? 0) <= 3);
|
|
545
572
|
// (Rooted at a variable too: `$HOME/.config`, `$PWD/.kube/config`, `${GITHUB_WORKSPACE}/out`. Braces only around a
|
|
546
|
-
// name, a variable's or a template's: `/srv/{app}/logs`, not `/srv/{4}/x`.
|
|
547
|
-
|
|
573
|
+
// name, a variable's or a template's: `/srv/{app}/logs`, not `/srv/{4}/x`. A drive's path with either slash:
|
|
574
|
+
// `C:\Users\me\.ssh\id_rsa`, `C:/Users/me/.ssh/id_rsa`.)
|
|
575
|
+
if (!/^(\.{1,2}\/|~\/|\/|\$\{?[A-Za-z_]\w*\}?\/|[A-Za-z]:[\\/])[\w.\-/+@${}\\ ]+$/.test(value) ||
|
|
548
576
|
!/[/\\]/.test(value.slice(1)) ||
|
|
549
577
|
/[{}]/.test(value.replace(/\$?\{[A-Za-z_]\w*\}/g, "")))
|
|
550
578
|
return false;
|
|
@@ -558,6 +586,10 @@ export function isPath(path) {
|
|
|
558
586
|
*/
|
|
559
587
|
export function isGeneratedSegment(seg) {
|
|
560
588
|
const mixed = /[a-z]/.test(seg) && /[A-Z]/.test(seg);
|
|
589
|
+
// (A long name of parts a person joined is judged by its parts: `Wildcard-Cert2024.pem`, `Build-Pipeline-v2`.)
|
|
590
|
+
const parts = seg.split(/[-_.]+/).filter(Boolean);
|
|
591
|
+
if (mixed && /\d/.test(seg) && seg.length >= 16 && !/[$+@ ]/.test(seg) && parts.length > 1)
|
|
592
|
+
return parts.some(isGeneratedSegment);
|
|
561
593
|
if (mixed && /\d/.test(seg) && (seg.length >= 16 || /[$+@ ]/.test(seg) || (seg.length >= 5 && !isNameLike(seg))))
|
|
562
594
|
return true;
|
|
563
595
|
if (mixed && /[A-Za-z0-9]\+[A-Za-z0-9]/.test(seg))
|
|
@@ -715,7 +747,8 @@ export function isBalanced(s) {
|
|
|
715
747
|
* (A bare variable in capitals, after no letter, digit or `$`: `Pa$$WORD`, `P@$SWORD1` keep letters of their own.)
|
|
716
748
|
*/
|
|
717
749
|
function isExpansionsOnly(v) {
|
|
718
|
-
|
|
750
|
+
// (A name switching between letters and digits twice or more is a password after a `$`: `$X7K2M9QP`.)
|
|
751
|
+
const rest = v.replace(/\$\{[^{}]{1,80}\}|(?<![\p{L}\p{N}$])\$[A-Z_][A-Z0-9_]{2,40}(?![\w$])|%[A-Z_][A-Z0-9_]{2,40}%/gu, (m) => (/^\$\w+$/.test(m) && (m.match(/[A-Z](?=\d)|\d(?=[A-Z])/g)?.length ?? 0) >= 2 ? m : ""));
|
|
719
752
|
// (One variable at least: `!@#$%^&*()` is a password of symbols.)
|
|
720
753
|
return rest !== v && !/[\p{L}\p{N}]/u.test(rest);
|
|
721
754
|
}
|
|
@@ -947,10 +980,12 @@ const CODE_SHAPES = [
|
|
|
947
980
|
(/^(["'`]).+\1$/.test(a) && isInert(a.slice(1, -1))))));
|
|
948
981
|
},
|
|
949
982
|
],
|
|
950
|
-
// Go's pointer to a qualified type or a slice of one, `*corev1.Secret`, `[]byte
|
|
983
|
+
// Go's pointer to a qualified type or a slice of one, `*corev1.Secret`, `[]byte`, or a slice of the package's own,
|
|
984
|
+
// a struct field's `TSigKeys []TSigKey`: no password starts so.
|
|
951
985
|
[
|
|
952
986
|
"a pointer or slice type",
|
|
953
|
-
(v) => /^(?:[*&]|\[\])(?:\[\])*[a-z][a-z0-9]*(?:\.[A-Z][A-Za-z0-9]*)
|
|
987
|
+
(v) => /^(?:(?:[*&]|\[\])(?:\[\])*[a-z][a-z0-9]*(?:\.[A-Z][A-Za-z0-9]*)?|\[\](?:\[\])*\*?[A-Z][A-Za-z0-9]*)$/.test(v) &&
|
|
988
|
+
readsAsWords(v),
|
|
954
989
|
],
|
|
955
990
|
// `!local.is_replica`, `var.db_password`, `module.db.password`.
|
|
956
991
|
["a Terraform reference", (v) => /^!?(?:local|var|module|data|each|count|path|terraform)\.[\w.[\]"-]+$/.test(v)],
|
|
@@ -1146,6 +1181,11 @@ const PASSWORD_HASHES = [
|
|
|
1146
1181
|
"LDAP",
|
|
1147
1182
|
String.raw `\{(?:SSHA\d*|SHA\d*|MD5|SMD5)\}${B64}{16,}={0,2}|\{PBKDF2(?:-SHA\d+)?\}\d+\$${C}+\$${C}{16,}|\{(?:CRYPT|ARGON2|BCRYPT)\}(?:${CRYPT_HASH})`,
|
|
1148
1183
|
],
|
|
1184
|
+
// Spring Security's DelegatingPasswordEncoder names the encoder first, `{bcrypt}$2a$10$…`, `{argon2}$argon2id$…`
|
|
1185
|
+
// (not `{noop}`, the password itself); Werkzeug's, its method and parameters, `scrypt:32768:8:1$<salt>$<hex>`,
|
|
1186
|
+
// `pbkdf2:sha256:600000$<salt>$<hex>`.
|
|
1187
|
+
["Spring Security", String.raw `\{(?:bcrypt|argon2)\}(?:${CRYPT_HASH})`],
|
|
1188
|
+
["Werkzeug", String.raw `(?:scrypt:\d+:\d+:\d+|pbkdf2:sha\d+(?::\d+)?)\$[A-Za-z0-9]{1,64}\$[0-9a-f]{40,128}`],
|
|
1149
1189
|
];
|
|
1150
1190
|
const HASH_WHOLE = new RegExp(`^(?:${PASSWORD_HASHES.map(([, whole]) => whole).join("|")})$`);
|
|
1151
1191
|
/**
|
|
@@ -1162,7 +1202,7 @@ export function isPasswordHash(value) {
|
|
|
1162
1202
|
/** Words test suites and docs use as stand-in credentials: `testpass`, `secret123`, `my-api-key`, `abc123`. */
|
|
1163
1203
|
const TEST_WORD = /^(?:hoge|fuga|piyo|hogehoge|fugafuga|piyopiyo|hogera|s3krit|s3cr3t|s3cret|secr3t|p4ss|p@ss|p4ssw0rd|passw0rd|p@ssw0rd|test(?:ing)?|secret|secrets|password|passwd|pass|pw|pwd|token|key|api|apikey|my|your|foo|bar|baz|qux|dummy|fake|sample|example|demo|user|mock|stub|value|bearer|auth|access|client|private|super|top|very|valid|invalid|wrong|bad|good|new|old|default|local|dev|one|two|abc|xyz)$/i;
|
|
1164
1204
|
/** At least one of these makes a value a stand-in; `admin2024` or `hunter22` alone could be someone's real password. */
|
|
1165
|
-
const TEST_MARKER = /^(?:hoge|fuga|piyo|hogehoge|fugafuga|piyopiyo|hogera|test(?:ing)?|dummy|fake|sample|example|demo|mock|stub|foo|bar|baz|qux|invalid|wrong|abc|xyz|secret|password|pass|token|key)$/i;
|
|
1205
|
+
const TEST_MARKER = /^(?:hoge|fuga|piyo|hogehoge|fugafuga|piyopiyo|hogera|test(?:ing)?|dummy|fake|sample|example|demo|mock|stub|foo|bar|baz|qux|invalid|wrong|abc|xyz|secret|password|pass|token|key|value)$/i;
|
|
1166
1206
|
/**
|
|
1167
1207
|
* Digits as a stand-in writes them: one or two, a count (`123`, `1234`, `6789`) or one digit repeated (`000`). A real
|
|
1168
1208
|
* password's digits are arbitrary (`Example7392`).
|
|
@@ -1188,7 +1228,7 @@ export function isTestValue(value) {
|
|
|
1188
1228
|
/(?:^|[-_.])(?:password|secret|token|key)[-_]?here$/i.test(value) ||
|
|
1189
1229
|
/(?:^|[a-z0-9])(?:Password|Secret|Token|Key)Here$/.test(value))
|
|
1190
1230
|
return true;
|
|
1191
|
-
const parts = value.split(/[-_
|
|
1231
|
+
const parts = value.split(/[-_.:,;="' ]+|(?<=[a-z])(?=[A-Z0-9])|(?<=\d)(?=[A-Za-z])/).filter(Boolean);
|
|
1192
1232
|
return (parts.length > 0 &&
|
|
1193
1233
|
parts.every((p) => TEST_WORD.test(p) || (/^\d{1,4}$/.test(p) && isCountingDigits(p)) || /^[!@#$*]+$/.test(p)) &&
|
|
1194
1234
|
parts.some((p) => TEST_MARKER.test(p)));
|
|
@@ -1202,7 +1242,8 @@ export function isTestValue(value) {
|
|
|
1202
1242
|
* letters and digits of one length (`VKXQR-8B2MP-7WJ4T`): not names.
|
|
1203
1243
|
*/
|
|
1204
1244
|
export function isIdentifierShaped(value) {
|
|
1205
|
-
|
|
1245
|
+
// (A sentinel's underscores, `__n8n_BLANK_VALUE`, wrap its parts.)
|
|
1246
|
+
const parts = value.replace(/^_+|_+$/g, "").split(/[-_.:]/);
|
|
1206
1247
|
if (parts.length < 2 ||
|
|
1207
1248
|
!/^[A-Za-z]/.test(parts[0]) ||
|
|
1208
1249
|
!parts.every((p) => /^[A-Za-z0-9]*$/.test(p)) ||
|
|
@@ -1263,9 +1304,10 @@ export function isMadePassword(value) {
|
|
|
1263
1304
|
}
|
|
1264
1305
|
/**
|
|
1265
1306
|
* Names of algorithms, encodings and formats that sentences about credentials mention: `password (UTF-8)`, `SHA-256`;
|
|
1266
|
-
* and password hashers as libraries name them: Django's `pbkdf2_sha256`, `bcrypt_sha256`, passlib's `sha512_crypt
|
|
1307
|
+
* and password hashers as libraries name them: Django's `pbkdf2_sha256`, `bcrypt_sha256`, passlib's `sha512_crypt`;
|
|
1308
|
+
* and the schemes a password is checked or sent by: `scram-sha-256`, `md5crypt`, `WPA2`, `NTLMv2`, `MSCHAPv2`.
|
|
1267
1309
|
*/
|
|
1268
|
-
export const ALGORITHM = /^(?:SHA-?(?:1|224|256|384|512)(?:\/\d{3})?|SHA3-\d{3}|AES-?(?:128|192|256)(?:-[A-Z]{2,4})?|RSA-?(?:1024|2048|3072|4096)|MD[245]|3?DES|HMAC-SHA-?(?:1|256|384|512)|PBKDF2(?:[-_](?:HMAC[-_])?SHA-?(?:1|224|256|384|512))?|(?:md5|sha1|sha256|sha512)
|
|
1310
|
+
export const ALGORITHM = /^(?:SHA-?(?:1|224|256|384|512)(?:\/\d{3})?|SHA3-\d{3}|AES-?(?:128|192|256)(?:-[A-Z]{2,4})?|RSA-?(?:1024|2048|3072|4096)|MD[245]|3?DES|HMAC-SHA-?(?:1|256|384|512)|PBKDF2(?:[-_](?:HMAC[-_])?SHA-?(?:1|224|256|384|512))?|(?:md5|sha1|sha256|sha512)_?crypt|yescrypt|SCRAM-SHA-(?:1|256)(?:-PLUS)?|WPA[23]?(?:-(?:PSK|SAE|Personal|Enterprise))?|NTLMv[12]|(?:MS-?)?CHAPv2|ECDSA|Ed(?:25519|448)|X(?:25519|448|509)|UTF-?(?:8|16|32)(?:LE|BE)?|UCS-?[24]|PKCS#?(?:1|5|7|8|11|12)|IPv[46]|ChaCha20(?:-Poly1305)?|TLSv?1\.[0-3]|SSLv[23]|ISO-8859-\d{1,2}|Windows-125\d|Latin-?\d|CP\d{3,4}|RFC-?\d{3,5}|base(?:32|58|64)(?:url)?|hex|bcrypt(?:_sha256)?|argon2(?:id|i|d)?|scrypt|\d+-(?:stellig|chiffres|caracteres|digits?|characters?))(?:-(?:encoded|decoded|encrypted))?$/i;
|
|
1269
1311
|
/** A format string's hole: `%d`, `%1$s`, `{0}`, `{{count}}`, `%{n}`. */
|
|
1270
1312
|
const FORMAT_HOLE = /%(?:\d+\$)?[-+ 0#]*\d*(?:\.\d+)?[sdifuxXc@]|\{\{?\s*\w*\s*\}?\}|%\{\w+\}/;
|
|
1271
1313
|
/** "Password" in another language, anywhere in a value. */
|
|
@@ -1350,7 +1392,9 @@ export function isBenignValue(value) {
|
|
|
1350
1392
|
/^(?:\d+:)?\d+(?:\.\d+)+(?:\.[a-z]+\d*)?(?:[-+~][\w.]*)+$/.test(v) ||
|
|
1351
1393
|
/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/.test(v) || // dates
|
|
1352
1394
|
ALGORITHM.test(v) ||
|
|
1353
|
-
|
|
1395
|
+
// Key chords, `ctrl+shift+F12`, `Cmd+K,Cmd+S`: the key after the modifiers is one character or a key's name, so a
|
|
1396
|
+
// password that starts with a modifier's word, `Super+Admin2024!`, isn't one.
|
|
1397
|
+
/^(?:(?:(?:ctrl|cmd|alt|shift|meta|option|opt|super|mod|win|control|command)\+)+(?:\S|f\d{1,2}|enter|return|esc|escape|tab|space|up|down|left|right|home|end|delete|del|backspace|pageup|pagedown|pgup|pgdn|insert|ins|plus|minus|equal|comma|period|slash|backslash|backquote|pause|break|printscreen|prtsc|capslock|numlock|scrolllock|numpad\d|(?:arrow)?(?:up|down|left|right))(?:,|$))+$/i.test(v) ||
|
|
1354
1398
|
// Key chords with `-`, a key's name after them, in a sequence: Docker's `ctrl-p,ctrl-q`, Emacs's `C-x`, an editor's
|
|
1355
1399
|
// `Down|Ctrl-N`, `Command-Left|Home|Ctrl-A`. (A key named alone, `Home`, only in a list.)
|
|
1356
1400
|
/^(?:(?:(?:ctrl|cmd|alt|shift|meta|option|opt|super|control|command|[CMS])-)+(?:[a-z0-9@[\]\\^_]|f\d{1,2}|enter|esc|escape|tab|space|up|down|left|right|home|end|delete|del|backspace|pageup|pagedown|insert)(?:[,|]|$))+$/i.test(v) ||
|
|
@@ -1449,8 +1493,8 @@ export function isCredentialReference(value) {
|
|
|
1449
1493
|
* layer's `encrypted` veto), is ciphertext, whose key isn't there.
|
|
1450
1494
|
*/
|
|
1451
1495
|
export const ENCRYPTION_WRAPPER = String.raw `ENC\[[A-Z0-9_]+,data:|ENC\(|\{cipher\}|\$\{VAULT::(?:[^:}\s]+::){0,3}|\[enc:\w+:|\bEV\[\d+:(?:[A-Za-z0-9+/=]+:)?`;
|
|
1452
|
-
// (Jenkins's `hudson.util.Secret`, `{AQAAABAAAA…}`:
|
|
1453
|
-
const ENCRYPTED_VALUE = new RegExp(String.raw `^(?:${ENCRYPTION_WRAPPER}|ENC\[|!vault\b|\$ANSIBLE_VAULT
|
|
1496
|
+
// (Not Jenkins's `hudson.util.Secret`, `{AQAAABAAAA…}`: its key sits on the same machine, policy.ts.)
|
|
1497
|
+
const ENCRYPTED_VALUE = new RegExp(String.raw `^(?:${ENCRYPTION_WRAPPER}|ENC\[|!vault\b|\$ANSIBLE_VAULT)`);
|
|
1454
1498
|
/** Default passwords and words that show up in ordinary output, too common to be anyone's secret. */
|
|
1455
1499
|
export const COMMON_WORDS = new Set([
|
|
1456
1500
|
"files",
|
|
@@ -1571,10 +1615,11 @@ export const COMMON_WORDS = new Set([
|
|
|
1571
1615
|
]);
|
|
1572
1616
|
/**
|
|
1573
1617
|
* "Password" in another language, as a form's label (i18n files, string resources): `wachtwoord`, `mot de passe
|
|
1574
|
-
* actuel`, run together or plural (`Motdepasse`, `Passwörter`, `Contraseñas`), with a
|
|
1618
|
+
* actuel`, run together or plural (`Motdepasse`, `Passwörter`, `Contraseñas`), with a word or two around it
|
|
1619
|
+
* (`Ubah kata sandi`, "change password"; `Kata sandi inisialisasi admin`) or a note in words after it
|
|
1575
1620
|
* (`Phrase de passe (facultative)`, `Contraseña (opcional)`).
|
|
1576
1621
|
*/
|
|
1577
|
-
const PASSWORD_LABEL = new RegExp(String.raw `^(?:${FOREIGN_PASSWORD_SPELLINGS.map((s) => s.replace(/ /g, "\\s*")).join("|")})(?:s|es|er|en|e)?(?:\s+\p{L}+)
|
|
1622
|
+
const PASSWORD_LABEL = new RegExp(String.raw `^(?:\p{L}+\s+){0,2}(?:${FOREIGN_PASSWORD_SPELLINGS.map((s) => s.replace(/ /g, "\\s*")).join("|")})(?:s|es|er|en|e)?(?:\s+\p{L}+){0,2}(?:\s*\([\p{L} ,.'’/-]{1,30}\))?\*?$`, "iu");
|
|
1578
1623
|
const isPasswordLabel = (value) => PASSWORD_LABEL.test(value);
|
|
1579
1624
|
/**
|
|
1580
1625
|
* Whether a value under a credential name is a literal secret worth hiding: not a type, a placeholder,
|
package/docs/holdout.svg
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 460" width="760" height="460" role="img" aria-labelledby="t d">
|
|
2
|
-
<title id="t">Held-out test set: secrets caught vs. look-alikes destroyed</title><desc id="d">redac---n guessing off: 90.
|
|
2
|
+
<title id="t">Held-out test set: secrets caught vs. look-alikes destroyed</title><desc id="d">redac---n guessing off: 90.5% of secrets caught, 0.9% of keep values lost; redac---n guessing confident: 92.6% of secrets caught, 2.9% of keep values lost; redac---n guessing eager: 92.8% of secrets caught, 3.0% of keep values lost; gitleaks: 54.6% of secrets caught, 1.5% of keep values lost; betterleaks: 67.0% of secrets caught, 1.7% of keep values lost; kingfisher: 43.1% of secrets caught, 0.5% of keep values lost; trufflehog: 38.5% of secrets caught, 0.4% of keep values lost; noseyparker: 42.1% of secrets caught, 1.5% of keep values lost; detect-secrets: 47.2% of secrets caught, 15.2% of keep values lost; credsweeper: 70.1% of secrets caught, 15.2% of keep values lost; union(all 7): 84.2% of secrets caught, 28.6% of keep values lost; union(all but detect-secrets): 83.1% of secrets caught, 16.9% of keep values lost; gitleaks+betterleaks+kingfisher: 70.6% of secrets caught, 2.2% of keep values lost</desc>
|
|
3
3
|
<style>
|
|
4
4
|
.bg { fill: #fcfcfb; } .accent { fill: #2a78d6; } .accent-line { stroke: #2a78d6; } .other { fill: #898781; }
|
|
5
5
|
.hollow { fill: none; stroke: #898781; } .chain { fill: none; stroke: #c3c2b7; stroke-width: 1.5; }
|
|
@@ -53,7 +53,7 @@ text { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; fill: #525
|
|
|
53
53
|
<line class="axis" x1="56" x2="736" y1="408" y2="408"/>
|
|
54
54
|
<text x="736" y="450" text-anchor="end">text that must survive but was redacted (lower is better, √ scale) →</text>
|
|
55
55
|
<text transform="translate(14 408) rotate(-90)">secrets caught (higher is better) →</text>
|
|
56
|
-
<polyline fill="none" class="accent-line" stroke-width="2" points="
|
|
56
|
+
<polyline fill="none" class="accent-line" stroke-width="2" points="171.4552585205683,110.80272108843525 265.6171632148547,100.36697922412208 271.4248319264723,99.41827541827541"/>
|
|
57
57
|
<circle cx="624.92" cy="74" r="6" class="accent"/><text x="635.92" y="78" class="ours">redac---n</text>
|
|
58
58
|
<circle cx="624.92" cy="94" r="5" class="other"/><text x="635.92" y="98">other scanners</text>
|
|
59
59
|
<circle cx="209.94034469409104" cy="287.26162897591473" r="5" class="other ring" stroke-width="2"><title>gitleaks: 54.6% caught, 1.5% lost</title></circle>
|
|
@@ -66,12 +66,12 @@ text { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; fill: #525
|
|
|
66
66
|
<circle cx="719.6120496050027" cy="141.47747747747746" r="5" class="other ring" stroke-width="2"><title>union(all 7): 84.2% caught, 28.6% lost</title></circle>
|
|
67
67
|
<circle cx="565.5944718414523" cy="147.16970031255744" r="5" class="other ring" stroke-width="2"><title>union(all but detect-secrets): 83.1% caught, 16.9% lost</title></circle>
|
|
68
68
|
<circle cx="239.2256589830112" cy="208.51921309064167" r="5" class="other ring" stroke-width="2"><title>gitleaks+betterleaks+kingfisher: 70.6% caught, 2.2% lost</title></circle>
|
|
69
|
-
<circle cx="
|
|
70
|
-
<circle cx="
|
|
71
|
-
<circle cx="
|
|
72
|
-
<text x="
|
|
73
|
-
<text x="
|
|
74
|
-
<text x="
|
|
69
|
+
<circle cx="171.4552585205683" cy="110.80272108843525" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing off: 90.5% caught, 0.9% lost</title></circle>
|
|
70
|
+
<circle cx="265.6171632148547" cy="100.36697922412208" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing confident: 92.6% caught, 2.9% lost</title></circle>
|
|
71
|
+
<circle cx="271.4248319264723" cy="99.41827541827541" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing eager: 92.8% caught, 3.0% lost</title></circle>
|
|
72
|
+
<text x="281.4248319264723" y="103.41827541827541" text-anchor="start" class="ours">guessing eager</text>
|
|
73
|
+
<text x="257.6171632148547" y="90.36697922412208" text-anchor="end" class="ours">guessing confident</text>
|
|
74
|
+
<text x="161.4552585205683" y="114.80272108843525" text-anchor="end" class="ours">guessing off</text>
|
|
75
75
|
<text x="709.6120496050027" y="145.47747747747746" text-anchor="end">all seven combined</text>
|
|
76
76
|
<text x="555.5944718414523" y="151.16970031255744" text-anchor="end">all but detect-secrets</text>
|
|
77
77
|
<text x="249.2256589830112" y="212.51921309064167" text-anchor="start">gitleaks + Betterleaks + Kingfisher</text>
|
package/docs/layers.md
CHANGED
|
@@ -39,7 +39,7 @@ no name, format or structure around it: the key passed to `Client("…")`, a tok
|
|
|
39
39
|
into chat. It's also wrong most often: 140 look-alikes, mostly random IDs and hashes, are destroyed by it alone,
|
|
40
40
|
which is why `guessing` is a setting.
|
|
41
41
|
|
|
42
|
-
**Formats** (`src/formats.ts`) recognizes
|
|
42
|
+
**Formats** (`src/formats.ts`) recognizes 542 vendor token formats by their prefixes and shapes (`ghp_`, `sk-ant-`,
|
|
43
43
|
`xoxb-`, `AKIA…`). On its own it catches a third of the secrets, but most of those have a name next to them too, so
|
|
44
44
|
what only it catches is smaller. It almost never fires on ordinary text: 0.09 false redactions per million
|
|
45
45
|
characters.
|