redac---n 0.0.0-stage → 0.0.2

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -2
  3. package/THIRD_PARTY_NOTICES.md +59 -0
  4. package/dist/aho.d.ts +25 -0
  5. package/dist/aho.js +116 -0
  6. package/dist/base64.d.ts +10 -0
  7. package/dist/base64.js +29 -0
  8. package/dist/bip39-data.d.ts +2 -0
  9. package/dist/bip39-data.js +2 -0
  10. package/dist/cache.d.ts +14 -0
  11. package/dist/cache.js +40 -0
  12. package/dist/calls.d.ts +64 -0
  13. package/dist/calls.js +269 -0
  14. package/dist/charlm-data.d.ts +2 -0
  15. package/dist/charlm-data.js +3 -0
  16. package/dist/charlm.d.ts +7 -0
  17. package/dist/charlm.js +65 -0
  18. package/dist/checksums.d.ts +10 -0
  19. package/dist/checksums.js +98 -0
  20. package/dist/commands.d.ts +72 -0
  21. package/dist/commands.js +787 -0
  22. package/dist/context.d.ts +98 -0
  23. package/dist/context.js +469 -0
  24. package/dist/encoded.d.ts +11 -0
  25. package/dist/encoded.js +336 -0
  26. package/dist/environment.d.ts +10 -0
  27. package/dist/environment.js +62 -0
  28. package/dist/formats-data.d.ts +2 -0
  29. package/dist/formats-data.js +405 -0
  30. package/dist/formats.d.ts +5 -0
  31. package/dist/formats.js +470 -0
  32. package/dist/hmac.d.ts +3 -0
  33. package/dist/hmac.js +87 -0
  34. package/dist/index.d.ts +101 -0
  35. package/dist/index.js +488 -0
  36. package/dist/json.d.ts +82 -0
  37. package/dist/json.js +392 -0
  38. package/dist/known.d.ts +34 -0
  39. package/dist/known.js +340 -0
  40. package/dist/lines.d.ts +28 -0
  41. package/dist/lines.js +69 -0
  42. package/dist/lookup.d.ts +5 -0
  43. package/dist/lookup.js +7 -0
  44. package/dist/model-data.d.ts +3 -0
  45. package/dist/model-data.js +4 -0
  46. package/dist/model.d.ts +2 -0
  47. package/dist/model.js +19 -0
  48. package/dist/named.d.ts +20 -0
  49. package/dist/named.js +3257 -0
  50. package/dist/names.d.ts +106 -0
  51. package/dist/names.js +1517 -0
  52. package/dist/normalize.d.ts +25 -0
  53. package/dist/normalize.js +529 -0
  54. package/dist/pem.d.ts +32 -0
  55. package/dist/pem.js +269 -0
  56. package/dist/policy.d.ts +3 -0
  57. package/dist/policy.js +470 -0
  58. package/dist/public.d.ts +44 -0
  59. package/dist/public.js +362 -0
  60. package/dist/publickeys.d.ts +41 -0
  61. package/dist/publickeys.js +232 -0
  62. package/dist/random.d.ts +29 -0
  63. package/dist/random.js +165 -0
  64. package/dist/repeats.d.ts +21 -0
  65. package/dist/repeats.js +80 -0
  66. package/dist/rules.d.ts +42 -0
  67. package/dist/rules.js +58 -0
  68. package/dist/runs.d.ts +10 -0
  69. package/dist/runs.js +36 -0
  70. package/dist/secretfile.d.ts +52 -0
  71. package/dist/secretfile.js +722 -0
  72. package/dist/shell.d.ts +127 -0
  73. package/dist/shell.js +703 -0
  74. package/dist/spans.d.ts +61 -0
  75. package/dist/spans.js +287 -0
  76. package/dist/sql.d.ts +20 -0
  77. package/dist/sql.js +66 -0
  78. package/dist/stream.d.ts +46 -0
  79. package/dist/stream.js +150 -0
  80. package/dist/structure.d.ts +24 -0
  81. package/dist/structure.js +1625 -0
  82. package/dist/syntax.d.ts +156 -0
  83. package/dist/syntax.js +446 -0
  84. package/dist/transcript.d.ts +30 -0
  85. package/dist/transcript.js +713 -0
  86. package/dist/types.d.ts +81 -0
  87. package/dist/types.js +1 -0
  88. package/dist/unnamed.d.ts +59 -0
  89. package/dist/unnamed.js +939 -0
  90. package/dist/urls.d.ts +70 -0
  91. package/dist/urls.js +303 -0
  92. package/dist/values.d.ts +178 -0
  93. package/dist/values.js +1621 -0
  94. package/dist/yaml.d.ts +40 -0
  95. package/dist/yaml.js +174 -0
  96. package/docs/creddata.svg +318 -0
  97. package/docs/guessing.md +139 -0
  98. package/docs/holdout.svg +85 -0
  99. package/docs/layers.md +80 -0
  100. package/docs/models.svg +81 -0
  101. package/docs/real-text.svg +112 -0
  102. package/package.json +57 -5
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Encodings that hold a secret, decoded and judged by what they hold: base64 blobs (`base64 .env`, a Kubernetes
3
+ * Secret's data, a docker config `auth`, a Basic header built by hand), base64 wrapped over lines, and hex dumps.
4
+ * The whole encoding is the secret.
5
+ */
6
+ import { base64Prefix } from "./base64.js";
7
+ import { naturalness } from "./charlm.js";
8
+ import { lineEnd, lineStart } from "./lines.js";
9
+ import { asciiSet, longRuns } from "./runs.js";
10
+ import { keepReason } from "./policy.js";
11
+ import { hasThreeClasses } from "./values.js";
12
+ const UTF8 = new TextDecoder("utf-8");
13
+ /** The share of characters that must be printable for bytes to count as text, and of zero bytes for UTF-16. */
14
+ const TEXT_SHARE = 0.95;
15
+ /**
16
+ * The text some bytes hold, if they hold text: UTF-8 (UTF-16 when every other byte is zero, as
17
+ * `powershell -EncodedCommand`'s is, with or without a byte-order mark), 95% of it printable (ASCII, tabs and line
18
+ * breaks, and every character from U+00A0 on but U+FFFD, which marks bytes that aren't UTF-8), and at least `min`
19
+ * characters long. Binary data, keys and hashes aren't text.
20
+ */
21
+ function textOfBytes(bytes, min = 8) {
22
+ if (!bytes)
23
+ return undefined;
24
+ let b = bytes;
25
+ if ((b[0] === 0xff && b[1] === 0xfe) || (b[0] === 0xfe && b[1] === 0xff))
26
+ b = b.subarray(2);
27
+ let text;
28
+ for (const parity of [1, 0]) {
29
+ if (b.length < 16 || b[parity] !== 0)
30
+ continue;
31
+ let zeros = 0;
32
+ for (let i = parity; i < b.length; i += 2)
33
+ if (b[i] === 0)
34
+ zeros++;
35
+ if (zeros >= (b.length >> 1) * TEXT_SHARE) {
36
+ text = new TextDecoder(parity ? "utf-16le" : "utf-16be").decode(b);
37
+ break;
38
+ }
39
+ }
40
+ text ??= UTF8.decode(b);
41
+ if (text.length < min)
42
+ return undefined;
43
+ let printable = 0;
44
+ for (const ch of text) {
45
+ const c = ch.codePointAt(0);
46
+ if ((c >= 32 && c < 127) || c === 9 || c === 10 || c === 13 || (c >= 0xa0 && c !== 0xfffd))
47
+ printable++;
48
+ }
49
+ return printable >= [...text].length * TEXT_SHARE ? text : undefined;
50
+ }
51
+ /** Hex digits as bytes, or undefined for an odd count or a character that isn't a hex digit. */
52
+ function hexBytes(hex) {
53
+ if (hex.length % 2 || !/^[0-9a-f]*$/i.test(hex))
54
+ return undefined;
55
+ const out = new Uint8Array(hex.length / 2);
56
+ for (let i = 0; i < out.length; i++)
57
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
58
+ return out;
59
+ }
60
+ const BASE64_SET = asciiSet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/_-");
61
+ /** The fewest characters of a base64 value worth decoding (12 bytes). */
62
+ const MIN_BASE64_RUN = 16;
63
+ const WRAPPED_SET = asciiSet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
64
+ const HEX_SET = asciiSet("0123456789abcdefABCDEF");
65
+ /** The fewest hex digits of a run read as a hex-encoded value (12 bytes), and the most. */
66
+ const MIN_HEX_RUN = 24;
67
+ const MAX_HEX_RUN = 100000;
68
+ /**
69
+ * The base64 values in `text`, whole: a run of the alphabet (base64 or base64url) with up to two `=` of padding, bounded
70
+ * by other characters, 16 characters with its padding (`V2ludGVyMjAyNCE=`, an 11-character password's).
71
+ */
72
+ function* base64Runs(text) {
73
+ for (const [start, runEnd] of longRuns(text, BASE64_SET, MIN_BASE64_RUN - 2)) {
74
+ let end = runEnd;
75
+ while (end < runEnd + 2 && text[end] === "=")
76
+ end++;
77
+ if (end - start < MIN_BASE64_RUN || /[A-Za-z0-9+/=_-]/.test(text[end] ?? ""))
78
+ continue;
79
+ yield [start, end];
80
+ }
81
+ }
82
+ /**
83
+ * Base64 blobs whose decoded text holds a secret (`inner` decides), on one line or wrapped over several; and a line
84
+ * that is only base64 of one generated-looking token (`echo "$KEY" | base64`).
85
+ */
86
+ export function findEncoded(text, inner) {
87
+ const out = [];
88
+ const wrapped = wrappedBase64(text);
89
+ // (A line of a blob wrapped over lines is no token of its own, however random its piece of the blob reads. Runs and
90
+ // blobs both come in text order: one pass over each.)
91
+ let w = 0;
92
+ const inBlob = (start) => {
93
+ while (w < wrapped.length && wrapped[w].end <= start)
94
+ w++;
95
+ return w < wrapped.length && wrapped[w].start <= start;
96
+ };
97
+ for (const [start, end] of base64Runs(text)) {
98
+ const value = text.slice(start, end);
99
+ const decoded = textOfBytes(base64Prefix(value));
100
+ if (decoded === undefined)
101
+ continue;
102
+ if (inner(decoded))
103
+ out.push({ start, end, kind: "base64 of a secret", rule: "encoded:base64", confidence: 0.95 });
104
+ else if (/^[!-~]{8,200}\n?$/.test(decoded) &&
105
+ /[=]$|^[A-Za-z0-9+/]+$/.test(value) &&
106
+ isLineAlone(text, start, end) &&
107
+ !inBlob(start) &&
108
+ isGeneratedToken(decoded.trim())) {
109
+ out.push({ start, end, kind: "base64 of a secret", rule: "encoded:base64-token", confidence: 0.8 });
110
+ }
111
+ }
112
+ for (const run of wrapped) {
113
+ const decoded = textOfBytes(base64Prefix(run.text));
114
+ if (decoded !== undefined && inner(decoded))
115
+ out.push({
116
+ start: run.start,
117
+ end: run.end,
118
+ kind: "base64 of a secret",
119
+ rule: "encoded:base64-wrapped",
120
+ confidence: 0.95,
121
+ });
122
+ }
123
+ return out;
124
+ }
125
+ /**
126
+ * A token a generator made, or a password a person made with symbols in it; not one public by design or never a key
127
+ * (which the decision stage keeps: `pk_live_…`), a UUID, hex, an email or a URL.
128
+ */
129
+ function isGeneratedToken(token) {
130
+ if (/\s/.test(token) ||
131
+ /^[0-9a-f]+$/i.test(token) ||
132
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(token) ||
133
+ /^[\w.-]+@[\w.-]+$|^https?:/.test(token))
134
+ return false;
135
+ if (keepReason(token, { start: 0, end: token.length, kind: "random token", rule: "unnamed", confidence: 0.8 }) !==
136
+ undefined)
137
+ return false;
138
+ return naturalness(token) < 0.2 || (hasThreeClasses(token) && /[^A-Za-z0-9]/.test(token));
139
+ }
140
+ const isBase64Char = (ch) => /[A-Za-z0-9+/]/.test(ch);
141
+ /** Where the base64 (and `=` padding) at the end of `line` starts. */
142
+ function base64Start(line) {
143
+ let k = line.length;
144
+ while (k > 0 && line[k - 1] === "=" && line.length - k < 2)
145
+ k--;
146
+ while (k > 0 && isBase64Char(line[k - 1]))
147
+ k--;
148
+ return k;
149
+ }
150
+ /** Where the base64 (and `=` padding) at the start of `line` ends. */
151
+ function base64End(line) {
152
+ let k = 0;
153
+ while (k < line.length && isBase64Char(line[k]))
154
+ k++;
155
+ for (let padding = 0; k < line.length && line[k] === "=" && padding < 2; padding++)
156
+ k++;
157
+ return k;
158
+ }
159
+ /**
160
+ * Base64 wrapped over lines (`base64 .env`, `base64 -w 76`, PEM-like bodies, JSON with `\n`): full-width lines of
161
+ * 16 to 200 base64 characters, then one shorter last line or a line ending in `=`. Their characters joined, and
162
+ * the span from the first to the last. The first line may have text before its base64 (`data: `, `"d":"`) and the
163
+ * last text after it (`"}`, `")`): of two lines or more, a line's base64 at its end opens a run, and a line's
164
+ * base64 at its start, followed by a quote or a closing bracket, ends it.
165
+ */
166
+ function wrappedBase64(text) {
167
+ // (A run has a line of 16 base64 characters at least.)
168
+ if (longRuns(text, WRAPPED_SET, MIN_BASE64_RUN).next().done)
169
+ return [];
170
+ const out = [];
171
+ let run;
172
+ const close = () => {
173
+ // (One line alone is base64Runs' to judge.)
174
+ if (run && run.text.length >= 24 && run.lines > 1)
175
+ out.push(run);
176
+ run = undefined;
177
+ };
178
+ let offset = 0;
179
+ const parts = text.split(/(\\n|\r?\n)/);
180
+ for (let i = 0; i < parts.length; offset += parts[i].length, i++) {
181
+ if (i % 2)
182
+ continue;
183
+ // (No run is open, and a line this short opens none: a run's first line has 16 base64 characters.)
184
+ if (!run && parts[i].length < MIN_BASE64_RUN)
185
+ continue;
186
+ const body = parts[i].trim();
187
+ const at = offset + parts[i].indexOf(body);
188
+ // (Any other line, a blank one too, ends a run; one with base64 at its end opens the next.)
189
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(body)) {
190
+ const head = base64End(body);
191
+ if (run && head > 0 && head <= run.width && /["'`)\]}>]/.test(body[head])) {
192
+ run.text += body.slice(0, head);
193
+ run.end = at + head;
194
+ run.lines++;
195
+ close();
196
+ continue;
197
+ }
198
+ close();
199
+ const tail = base64Start(body);
200
+ const length = body.length - tail;
201
+ if (tail > 0 && length >= 16 && length <= 200 && /[\s"'`:=(\[>,]/.test(body[tail - 1]))
202
+ run = { text: body.slice(tail), start: at + tail, end: at + body.length, width: length, lines: 1 };
203
+ continue;
204
+ }
205
+ if (run && body.length <= run.width) {
206
+ run.text += body;
207
+ run.end = at + body.length;
208
+ run.lines++;
209
+ // (A shorter line, or padding, is the last. A shorter line that more base64 no wider follows may open a blob of
210
+ // its own instead, after a value printed unwrapped (`base64 -w0`, then `base64`'s 76 columns): it's read both
211
+ // ways.)
212
+ if (body.length < run.width || body.endsWith("=")) {
213
+ close();
214
+ if (!body.endsWith("=") && body.length >= MIN_BASE64_RUN && continuesAfter(parts, i, body.length))
215
+ run = { text: body, start: at, end: at + body.length, width: body.length, lines: 1 };
216
+ }
217
+ continue;
218
+ }
219
+ close();
220
+ if (body.length >= 16 && body.length <= 200) {
221
+ run = { text: body, start: at, end: at + body.length, width: body.length, lines: 1 };
222
+ if (body.endsWith("="))
223
+ close();
224
+ }
225
+ }
226
+ close();
227
+ return out;
228
+ }
229
+ /** Whether the line after `parts[i]` (text split on its line breaks, the breaks kept) is base64 no wider than `width`. */
230
+ function continuesAfter(parts, i, width) {
231
+ const next = parts[i + 2]?.trim();
232
+ return !!next && next.length <= width && /^[A-Za-z0-9+/]+={0,2}$/.test(next);
233
+ }
234
+ /** Whether [start, end) is all that's on its line, apart from a log timestamp or prompt prefix. */
235
+ function isLineAlone(text, start, end) {
236
+ const ls = lineStart(text, start, 200);
237
+ const le = lineEnd(text, end, 200);
238
+ const before = text.slice(ls, start);
239
+ const after = text.slice(end, le);
240
+ return /^\s*$/.test(after) && /^(?:\s*|\d{4}-\d\d-\d\dT[\d:.]+Z?\s+|[$>+#]\s+)$/.test(before);
241
+ }
242
+ /**
243
+ * Hex dumps (`xxd`, `xxd -p`, `hexdump -C`, `od -A x -t x1`): the bytes are decoded, and a dump whose
244
+ * contents hold a secret is redacted whole.
245
+ */
246
+ export function findHexDumps(text, inner) {
247
+ const out = [];
248
+ if (!/[0-9a-f]{4}/i.test(text))
249
+ return out;
250
+ // Offset-prefixed dumps: runs of lines `<offset>[:] <hex groups> [ascii]`.
251
+ for (const m of text.matchAll(/(?:^[0-9a-f]{6,8}:?(?:[ \t]{1,4}[0-9a-f]{2,4}){1,16}[^\n]{0,80}(?:\n|$)){1,100000}/gim)) {
252
+ const words = [];
253
+ let bytes = "";
254
+ const lines = m[0].split("\n").filter((l) => l);
255
+ // Bytes per line from the offset step (16 for xxd and `hexdump -C`), so an ASCII column that
256
+ // starts with hex letters (`cafe.txt`, `DEBUG=1.`) isn't read as more bytes.
257
+ const offsets = lines.slice(0, 2).map((l) => parseInt(l.slice(0, 8), 16));
258
+ const step = offsets.length === 2 && offsets[1] > offsets[0] && offsets[1] - offsets[0] <= 64 ? offsets[1] - offsets[0] : 16;
259
+ for (const line of lines) {
260
+ const body = line.replace(/^[0-9a-f]{6,8}:?[ \t]+/i, "");
261
+ let digits = 0;
262
+ for (const t of body.split(/[ \t]+/)) {
263
+ if (!/^(?:[0-9a-f]{2}|[0-9a-f]{4}|[0-9a-f]{8})$/i.test(t) || digits + t.length > step * 2)
264
+ break;
265
+ if (t.length === 4)
266
+ words.push(t.slice(2) + t.slice(0, 2));
267
+ bytes += t;
268
+ digits += t.length;
269
+ }
270
+ }
271
+ for (const hex of [bytes, words.join("")]) {
272
+ if (hex.length < 12)
273
+ continue;
274
+ const decoded = textOfBytes(hexBytes(hex), 0);
275
+ if (decoded !== undefined && inner(decoded)) {
276
+ out.push({
277
+ start: m.index,
278
+ end: m.index + m[0].replace(/\n$/, "").length,
279
+ kind: "hex dump of a secret",
280
+ rule: "encoded:hexdump",
281
+ confidence: 0.95,
282
+ });
283
+ break;
284
+ }
285
+ }
286
+ }
287
+ // Plain hex (`xxd -p`): runs of whole lines of hex, or one long run, that decode to text holding a secret.
288
+ const runs = [];
289
+ for (const [start, end] of longRuns(text, HEX_SET, MIN_HEX_RUN)) {
290
+ // (A hex run that a letter or digit touches is part of a longer word; one past the most is no value but a dump. A
291
+ // literal's prefix is no word: `0x…` (mysqldump's --hex-blob, SQL Server), `\x…` (psql's bytea).)
292
+ const prefixed = /(?:^|[^0-9a-z])0x$|\\x$/i.test(text.slice(Math.max(0, start - 3), start));
293
+ if (end - start <= MAX_HEX_RUN &&
294
+ (prefixed || !/[0-9a-z]/i.test(text[start - 1] ?? "")) &&
295
+ !/[0-9a-z]/i.test(text[end] ?? ""))
296
+ runs.push({ index: start, text: text.slice(start, end) });
297
+ }
298
+ // (Lines are read together, a dump's last line as short as it is: a secret read line by line leaves its tail on the
299
+ // next line. A list of hashes, one a line, decodes to no text.)
300
+ let runStart = -1;
301
+ let runEnd = -1;
302
+ let lines = 0;
303
+ const flush = () => {
304
+ if (lines > 1)
305
+ runs.push({ index: runStart, text: text.slice(runStart, runEnd) });
306
+ runStart = -1;
307
+ lines = 0;
308
+ };
309
+ for (const m of text.matchAll(/^(?:[0-9a-f]{2}){1,100}\r?$/gim)) {
310
+ if (runStart >= 0 && m.index === runEnd + 1) {
311
+ runEnd = m.index + m[0].length;
312
+ lines++;
313
+ }
314
+ else {
315
+ flush();
316
+ if (m[0].trimEnd().length < 16)
317
+ continue;
318
+ runStart = m.index;
319
+ runEnd = m.index + m[0].length;
320
+ lines = 1;
321
+ }
322
+ }
323
+ flush();
324
+ for (const m of runs) {
325
+ const decoded = textOfBytes(hexBytes(m.text.replace(/\s+/g, "")));
326
+ if (decoded !== undefined && inner(decoded))
327
+ out.push({
328
+ start: m.index,
329
+ end: m.index + m.text.replace(/\s+$/, "").length,
330
+ kind: "hex of a secret",
331
+ rule: "encoded:hex",
332
+ confidence: 0.95,
333
+ });
334
+ }
335
+ return out;
336
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The secrets in an environment, as known values: variables whose name has a credential word (`*_API_KEY`, `*_TOKEN`,
3
+ * `BW_SESSION`, read widely), a vendor's token under any name, and URL passwords inside any variable. Only values a
4
+ * Redactor accepts as known values (the rule `addKnown` applies) are returned; a short or plain-word value is left out.
5
+ * `new Redactor({ known: fromEnvironment(process.env) })`.
6
+ */
7
+ export declare function fromEnvironment(env: Record<string, string | undefined>): {
8
+ value: string;
9
+ label: string;
10
+ }[];
@@ -0,0 +1,62 @@
1
+ import { findFormats } from "./formats.js";
2
+ import { isReplaceable } from "./known.js";
3
+ import { hasCredentialWord, isCredentialName, isSettingName, namesPasswordOrPin } from "./names.js";
4
+ import { isLiteralPassword } from "./structure.js";
5
+ import { urls } from "./urls.js";
6
+ import { naturalness } from "./charlm.js";
7
+ import { hasThreeClasses, isInert, isRandomToken } from "./values.js";
8
+ /**
9
+ * The secrets in an environment, as known values: variables whose name has a credential word (`*_API_KEY`, `*_TOKEN`,
10
+ * `BW_SESSION`, read widely), a vendor's token under any name, and URL passwords inside any variable. Only values a
11
+ * Redactor accepts as known values (the rule `addKnown` applies) are returned; a short or plain-word value is left out.
12
+ * `new Redactor({ known: fromEnvironment(process.env) })`.
13
+ */
14
+ export function fromEnvironment(env) {
15
+ const out = [];
16
+ for (const [name, value] of Object.entries(env)) {
17
+ if (!value || value.length < 6 || value.length > 16_384)
18
+ continue;
19
+ // (Read widely: the caller's own environment, where a credential missed goes to a provider. A credential word anywhere
20
+ // in the name or a session's at its end, `BW_SESSION`, `TF_TOKEN_app_terraform_io`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN`,
21
+ // unless the name ends as a setting's or a generator didn't make the value: `TOKEN_ENDPOINT`, `DESKTOP_SESSION=ubuntu-wayland`.
22
+ // A password's name takes any value, `MATCH_PASSWORD`; a webhook's URL is its secret whole.)
23
+ const widened = !isSettingName(name) &&
24
+ ((namesPasswordOrPin(name) && !isInert(value)) ||
25
+ ((hasCredentialWord(name) || /(?:^|_)(?:SESSION|PEPPER)$/i.test(name)) && isGenerated(value)));
26
+ const secretName = isCredentialName(name) || /WEBHOOK/i.test(name) || widened;
27
+ if (secretName && !isInert(value))
28
+ out.push({ value, label: `$${name}` });
29
+ // (A vendor's token under any name: `CI_DEPLOY=ghp_…`, a Slack webhook URL.)
30
+ else
31
+ for (const f of findFormats(value))
32
+ if (!out.some((o) => o.value === value.slice(f.start, f.end)))
33
+ out.push({ value: value.slice(f.start, f.end), label: `${f.kind} in $${name}` });
34
+ // (A literal password, as the URL readers take one: not `${DB_PASSWORD}`, `<password>`, `%PASSWORD%`.)
35
+ for (const url of value.includes("://") ? urls(value) : [])
36
+ if (url.password && isLiteralPassword(url.password.value))
37
+ out.push({ value: decodeSafe(url.password.value), label: `password in $${name}` });
38
+ }
39
+ // (Only what a Redactor takes as a known value, so the caller isn't told a `JWT_SECRET=devsecret` is protected when it isn't.)
40
+ return out.filter((o) => isReplaceable(o.value.trim()));
41
+ }
42
+ /**
43
+ * A value a generator made: a random token (`Xk9mQ2pLrTvB7nWz`), or one with symbols in it, of all three other classes
44
+ * and no word (`Pq7!zR2@xW9#kL4m`); not a URL or a path.
45
+ */
46
+ function isGenerated(value) {
47
+ return (isRandomToken(value, 12) ||
48
+ (value.length >= 12 &&
49
+ /^[!-~]+$/.test(value) &&
50
+ hasThreeClasses(value) &&
51
+ /[^A-Za-z0-9]/.test(value) &&
52
+ !/:\/\/|^[/~.$]/.test(value) &&
53
+ naturalness(value) < 0.3));
54
+ }
55
+ function decodeSafe(v) {
56
+ try {
57
+ return decodeURIComponent(v);
58
+ }
59
+ catch {
60
+ return v;
61
+ }
62
+ }
@@ -0,0 +1,2 @@
1
+ /** [name, regex source, flags, keywords (any must appear, lower case), minimum entropy, 1 if anchored on a vendor word]. */
2
+ export declare const IMPORTED_FORMATS: [string, string, string, string[], number, number][];