redac---n 0.0.4 → 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.
Files changed (48) hide show
  1. package/README.md +11 -10
  2. package/dist/calls.d.ts +8 -0
  3. package/dist/calls.js +60 -3
  4. package/dist/checksums.js +4 -3
  5. package/dist/commands.d.ts +5 -0
  6. package/dist/commands.js +93 -14
  7. package/dist/context.d.ts +5 -2
  8. package/dist/context.js +36 -9
  9. package/dist/encoded.js +13 -6
  10. package/dist/environment.d.ts +5 -3
  11. package/dist/environment.js +38 -14
  12. package/dist/formats.js +12 -0
  13. package/dist/index.js +10 -4
  14. package/dist/json.js +29 -5
  15. package/dist/known.d.ts +7 -7
  16. package/dist/known.js +33 -25
  17. package/dist/named.d.ts +6 -0
  18. package/dist/named.js +421 -125
  19. package/dist/names.d.ts +5 -1
  20. package/dist/names.js +143 -18
  21. package/dist/normalize.d.ts +4 -1
  22. package/dist/normalize.js +37 -11
  23. package/dist/pem.d.ts +2 -0
  24. package/dist/pem.js +12 -0
  25. package/dist/policy.js +12 -5
  26. package/dist/public.js +35 -13
  27. package/dist/secretfile.d.ts +5 -0
  28. package/dist/secretfile.js +49 -20
  29. package/dist/shell.d.ts +12 -3
  30. package/dist/shell.js +51 -7
  31. package/dist/spans.d.ts +4 -3
  32. package/dist/spans.js +79 -8
  33. package/dist/sql.d.ts +12 -8
  34. package/dist/sql.js +94 -16
  35. package/dist/stream.d.ts +2 -1
  36. package/dist/stream.js +15 -3
  37. package/dist/structure.js +210 -61
  38. package/dist/syntax.d.ts +5 -2
  39. package/dist/syntax.js +7 -4
  40. package/dist/transcript.js +116 -28
  41. package/dist/unnamed.js +128 -20
  42. package/dist/urls.d.ts +8 -3
  43. package/dist/urls.js +16 -7
  44. package/dist/values.d.ts +2 -1
  45. package/dist/values.js +70 -25
  46. package/docs/holdout.svg +8 -8
  47. package/docs/layers.md +1 -1
  48. 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 ((c === "'" || c === '"') &&
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
- /** A parameter's start and name: after `?`, `&` (or HTML's `&amp;`) or `#`, a name with PHP's `[]` allowed. */
228
- export const PARAM_START = String.raw `(?:&amp;|[?&#])`;
233
+ /**
234
+ * A parameter's start and name: after `?`, `&` (or HTML's `&amp;`, 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 `(?:&amp;|\\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
- const SPEC = /%[-+ #0]{0,3}(?:\d{1,3}|\*)?(?:\.(?:\d{1,3}|\*))?[sdvxfiu]|%\(\w+\)[sd]|\{\d*\}|\{:\w*\}/g;
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
- const reads = (name) => name.length <= 8 || naturalness(name.replace(/[_.-]+/g, " ")) > 0;
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})\}|@([\w.-]{1,80})@)$/.exec(v);
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/:+=.@*${}-]+$/.test(v));
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
- [...v.matchAll(SPEC)].every((m) => (!/[A-Za-z0-9]/.test(v[m.index - 1] ?? "") || !/[A-Za-z0-9]/.test(v[m.index + m[0].length] ?? "")) &&
418
- !(m[0][0] === "%" && /[A-Za-z]/.test(v[m.index + m[0].length] ?? ""))));
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
- if (!/^(\.{1,2}\/|~\/|\/|\$\{?[A-Za-z_]\w*\}?\/|[A-Za-z]:\\)[\w.\-/+@${}\\ ]+$/.test(value) ||
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
- 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, "");
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`: no password starts so.
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]*)?$/.test(v) && readsAsWords(v),
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(/[-_.: ]+|(?<=[a-z])(?=[A-Z0-9])|(?<=\d)(?=[A-Za-z])/).filter(Boolean);
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
- const parts = value.split(/[-_.:]/);
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)_crypt|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;
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
- /^(?:(?:ctrl|cmd|alt|shift|meta|option|opt|super|mod|win|control|command)\+)+\S{1,12}$/i.test(v) || // key chords
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…}`: a version byte and the IV's length, then the IV and the ciphertext.)
1453
- const ENCRYPTED_VALUE = new RegExp(String.raw `^(?:${ENCRYPTION_WRAPPER}|ENC\[|!vault\b|\$ANSIBLE_VAULT|\{?AQAAABAAAA[A-Za-z0-9+/]{30,}={0,2}\}?$)`);
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 note in words after it
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}+)?(?:\s*\([\p{L} ,.'’/-]{1,30}\))?\*?$`, "iu");
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.9% of secrets caught, 1.1% of keep values lost; redac---n guessing confident: 93.0% of secrets caught, 3.0% of keep values lost; redac---n guessing eager: 93.2% of secrets caught, 3.2% 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>
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="183.64059086206942,108.90531347674198 272.5676772508076,98.46957161242875 278.1937486206931,97.52086780658203"/>
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="183.64059086206942" cy="108.90531347674198" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing off: 90.9% caught, 1.1% lost</title></circle>
70
- <circle cx="272.5676772508076" cy="98.46957161242875" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing confident: 93.0% caught, 3.0% lost</title></circle>
71
- <circle cx="278.1937486206931" cy="97.52086780658203" r="6" class="accent ring" stroke-width="2"><title>redac---n guessing eager: 93.2% caught, 3.2% lost</title></circle>
72
- <text x="288.1937486206931" y="101.52086780658203" text-anchor="start" class="ours">guessing eager</text>
73
- <text x="264.5676772508076" y="88.46957161242875" text-anchor="end" class="ours">guessing confident</text>
74
- <text x="173.64059086206942" y="112.90531347674198" text-anchor="end" class="ours">guessing off</text>
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 536 vendor token formats by their prefixes and shapes (`ghp_`, `sk-ant-`,
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "redac---n",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Redact secrets from text and agent transcripts before they reach an untrusted model provider",
5
5
  "keywords": [
6
6
  "secrets",