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.
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/THIRD_PARTY_NOTICES.md +59 -0
- package/dist/aho.d.ts +25 -0
- package/dist/aho.js +116 -0
- package/dist/base64.d.ts +10 -0
- package/dist/base64.js +29 -0
- package/dist/bip39-data.d.ts +2 -0
- package/dist/bip39-data.js +2 -0
- package/dist/cache.d.ts +14 -0
- package/dist/cache.js +40 -0
- package/dist/calls.d.ts +64 -0
- package/dist/calls.js +269 -0
- package/dist/charlm-data.d.ts +2 -0
- package/dist/charlm-data.js +3 -0
- package/dist/charlm.d.ts +7 -0
- package/dist/charlm.js +65 -0
- package/dist/checksums.d.ts +10 -0
- package/dist/checksums.js +98 -0
- package/dist/commands.d.ts +72 -0
- package/dist/commands.js +787 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.js +469 -0
- package/dist/encoded.d.ts +11 -0
- package/dist/encoded.js +336 -0
- package/dist/environment.d.ts +10 -0
- package/dist/environment.js +62 -0
- package/dist/formats-data.d.ts +2 -0
- package/dist/formats-data.js +405 -0
- package/dist/formats.d.ts +5 -0
- package/dist/formats.js +470 -0
- package/dist/hmac.d.ts +3 -0
- package/dist/hmac.js +87 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +488 -0
- package/dist/json.d.ts +82 -0
- package/dist/json.js +392 -0
- package/dist/known.d.ts +34 -0
- package/dist/known.js +340 -0
- package/dist/lines.d.ts +28 -0
- package/dist/lines.js +69 -0
- package/dist/lookup.d.ts +5 -0
- package/dist/lookup.js +7 -0
- package/dist/model-data.d.ts +3 -0
- package/dist/model-data.js +4 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +19 -0
- package/dist/named.d.ts +20 -0
- package/dist/named.js +3257 -0
- package/dist/names.d.ts +106 -0
- package/dist/names.js +1517 -0
- package/dist/normalize.d.ts +25 -0
- package/dist/normalize.js +529 -0
- package/dist/pem.d.ts +32 -0
- package/dist/pem.js +269 -0
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +470 -0
- package/dist/public.d.ts +44 -0
- package/dist/public.js +362 -0
- package/dist/publickeys.d.ts +41 -0
- package/dist/publickeys.js +232 -0
- package/dist/random.d.ts +29 -0
- package/dist/random.js +165 -0
- package/dist/repeats.d.ts +21 -0
- package/dist/repeats.js +80 -0
- package/dist/rules.d.ts +42 -0
- package/dist/rules.js +58 -0
- package/dist/runs.d.ts +10 -0
- package/dist/runs.js +36 -0
- package/dist/secretfile.d.ts +52 -0
- package/dist/secretfile.js +722 -0
- package/dist/shell.d.ts +127 -0
- package/dist/shell.js +703 -0
- package/dist/spans.d.ts +61 -0
- package/dist/spans.js +287 -0
- package/dist/sql.d.ts +20 -0
- package/dist/sql.js +66 -0
- package/dist/stream.d.ts +46 -0
- package/dist/stream.js +150 -0
- package/dist/structure.d.ts +24 -0
- package/dist/structure.js +1625 -0
- package/dist/syntax.d.ts +156 -0
- package/dist/syntax.js +446 -0
- package/dist/transcript.d.ts +30 -0
- package/dist/transcript.js +713 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +1 -0
- package/dist/unnamed.d.ts +59 -0
- package/dist/unnamed.js +939 -0
- package/dist/urls.d.ts +70 -0
- package/dist/urls.js +303 -0
- package/dist/values.d.ts +178 -0
- package/dist/values.js +1621 -0
- package/dist/yaml.d.ts +40 -0
- package/dist/yaml.js +174 -0
- package/docs/creddata.svg +318 -0
- package/docs/guessing.md +139 -0
- package/docs/holdout.svg +85 -0
- package/docs/layers.md +80 -0
- package/docs/models.svg +81 -0
- package/docs/real-text.svg +112 -0
- package/package.json +57 -5
package/dist/values.js
ADDED
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests on a value that sits under a credential name: is it a literal secret, or something that only
|
|
3
|
+
* stands in for one (a reference, a template, a placeholder, a mask, code)? Agents read config and
|
|
4
|
+
* code full of `password: ${DB_PASSWORD}` and `api_key = os.environ["KEY"]`; hiding those costs the
|
|
5
|
+
* agent the structure it needs and protects nothing.
|
|
6
|
+
*/
|
|
7
|
+
import { naturalness } from "./charlm.js";
|
|
8
|
+
import { FOREIGN_PASSWORD_SPELLINGS, isCredentialName } from "./names.js";
|
|
9
|
+
import { base64Binary } from "./base64.js";
|
|
10
|
+
import { isBech32Address } from "./checksums.js";
|
|
11
|
+
import { isPlaceholder, isTruncated } from "./public.js";
|
|
12
|
+
import { isDerCertificate, isDerPublicKey, isDerSignature, isRsaPublicKey } from "./publickeys.js";
|
|
13
|
+
import { isPasswordlessUrl } from "./urls.js";
|
|
14
|
+
import { MAX_VALUE } from "./syntax.js";
|
|
15
|
+
import { decodeEntities } from "./normalize.js";
|
|
16
|
+
/**
|
|
17
|
+
* An identifier, and a member of one as the languages write it: `.`, `?.` (TypeScript, C#, Kotlin), `->` (PHP, C), `::`
|
|
18
|
+
* (Rust, C++, Ruby). One source for every shape below that reads names, so none forgets a language's operator.
|
|
19
|
+
*/
|
|
20
|
+
const IDENT = String.raw `[A-Za-z_$][\w$]*`;
|
|
21
|
+
const MEMBER = String.raw `(?:\??\.|->|::)${IDENT}`;
|
|
22
|
+
/** An identifier path: `config.db.password`, `cfg?.pw`, `$this->password`, `Settings::SECRET`. */
|
|
23
|
+
export const IDENT_PATH = String.raw `${IDENT}(?:${MEMBER})*`;
|
|
24
|
+
/** A path of two links or more, whole. */
|
|
25
|
+
const MEMBER_PATH = new RegExp(String.raw `^${IDENT}(?:${MEMBER})+$`);
|
|
26
|
+
/**
|
|
27
|
+
* A reference as code writes it: a path, called with names (`decrypt(m_pwd)`), and members of what the call returns
|
|
28
|
+
* (`getPassword().trim()`; reached only through the call, so a path splits one way).
|
|
29
|
+
*/
|
|
30
|
+
const CREDENTIAL_REFERENCE = new RegExp(String.raw `^${IDENT_PATH}(?:\((?:\$?[\w.]+(?:\s*,\s*\$?[\w.]+)*)?\)(?:(?:\??\.|->)${IDENT}(?:\(\))?)*)?$`);
|
|
31
|
+
/** Strips quotes, a trailing comma or semicolon, and an inline comment from a raw value. */
|
|
32
|
+
export function cleanValue(raw) {
|
|
33
|
+
const value = raw.trim().replace(/[,;]$/, "").trim();
|
|
34
|
+
const quoted = value.match(/^(["'`])(.*?)\1(?:\s+#.*)?$/s);
|
|
35
|
+
if (quoted)
|
|
36
|
+
return quoted[2].replace(/\\(["'\\])/g, "$1");
|
|
37
|
+
return value.replace(/(?<!\s)\s+#.*$/, "").trim();
|
|
38
|
+
}
|
|
39
|
+
/** What joins the parts of a wordy value: `acme-prod-uploads-2024`, `refs/pull/482/merge`. */
|
|
40
|
+
const WORD_SEPARATORS = /[-_./=+:,@ !?]+/;
|
|
41
|
+
/**
|
|
42
|
+
* Words, short numbers and short IDs joined by separators: names like `acme-prod-uploads-2024`,
|
|
43
|
+
* `refs/pull/482/merge` or `--max-old-space-size=4096`, not keys.
|
|
44
|
+
*/
|
|
45
|
+
export function isWordy(value) {
|
|
46
|
+
// (Real words or numbers in it, and most of its longer parts words: `F--Cuj=_yu-/`, `yAhcb,anc/NW?` are fragments.)
|
|
47
|
+
const parts = value.split(WORD_SEPARATORS).filter(Boolean);
|
|
48
|
+
const real = (p) => /^\d+$/.test(p) || /^(?=[a-f]*\d)[0-9a-f]{6,12}$/i.test(p) || (p.length >= 3 && naturalness(p) > 0.3);
|
|
49
|
+
const long = parts.filter((p) => p.length >= 3);
|
|
50
|
+
if (!parts.some(real) || long.filter(real).length * 3 < long.length * 2)
|
|
51
|
+
return false;
|
|
52
|
+
return value
|
|
53
|
+
.split(WORD_SEPARATORS)
|
|
54
|
+
.every((part) => part === "" ||
|
|
55
|
+
(/^\p{L}+\d{0,4}$/u.test(part) &&
|
|
56
|
+
!isRandomCase(part) &&
|
|
57
|
+
!(part.length >= 3 && /[A-Z]/.test(part.slice(1)) && /[a-z]/.test(part) && !WORD_CASE.test(part))) ||
|
|
58
|
+
/^[0-9a-f]{1,12}$/i.test(part));
|
|
59
|
+
}
|
|
60
|
+
/** The longest segment `isNameLike` takes as a name for its length alone. */
|
|
61
|
+
const MAX_SHORT_NAME = 4;
|
|
62
|
+
/** A short name that is one word, lower, upper or capitalised, then a digit or two: `db`, `API`, `Key`, `s3`, not `Xk9p`. */
|
|
63
|
+
const PLAIN_SHORT_NAME = /^(?:[a-z]+|[A-Z]+|[A-Z][a-z]+)\d{0,2}$/;
|
|
64
|
+
/**
|
|
65
|
+
* Identifier segments that read as names (a word's case, or short): `md5`, `htmlspecialchars`, `jmat.gId`, not
|
|
66
|
+
* `IWNwpPK.QC.BXh`, nor humps a generator's case flips made (`hqArruGhkoRtvpQplEac`).
|
|
67
|
+
*/
|
|
68
|
+
export function isNameLike(path) {
|
|
69
|
+
const parts = path.split(/[^A-Za-z0-9_]+/).filter(Boolean);
|
|
70
|
+
return (parts.length > 0 &&
|
|
71
|
+
parts.every((p) => p.length <= MAX_SHORT_NAME ||
|
|
72
|
+
(!isRandomCase(p) &&
|
|
73
|
+
(WORD_CASE.test(p.replace(/_/g, "")) ||
|
|
74
|
+
/^[a-z_][a-z0-9_]*$|^[A-Z][A-Z0-9_]*$/.test(p) ||
|
|
75
|
+
(/^[A-Z]{0,2}[a-z]+(?:[A-Z][a-z]+)*\d{0,4}$/.test(p) && naturalness(p) > -0.5)))));
|
|
76
|
+
}
|
|
77
|
+
/** A word in brackets that names something: a name, and a plain one when short (`<db password>`, `[API]`; not `[Xk9p]`). */
|
|
78
|
+
function isBracketedWord(word) {
|
|
79
|
+
return isNameLike(word) && (word.length > MAX_SHORT_NAME || PLAIN_SHORT_NAME.test(word));
|
|
80
|
+
}
|
|
81
|
+
/** The path of a reference to a secret: every part a name or a number (`secret/data/app`, `projects/123/secrets/db`). */
|
|
82
|
+
function isReferencePath(path) {
|
|
83
|
+
return path
|
|
84
|
+
.split(/[^A-Za-z0-9_]+/)
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.every((p) => /^\d+$/.test(p) || isNameLike(p));
|
|
87
|
+
}
|
|
88
|
+
/** The cases a word or an identifier comes in: `bucket`, `URL`, `Bucket`, `myBucketName`, `GitHub`, `iPhone`, `OAuth2`. */
|
|
89
|
+
const WORD_CASE = /^(?:[a-z]+|[A-Z]+|[A-Z]{0,2}[a-z]+(?:[A-Z][a-z]{2,})*)\d{0,4}$/;
|
|
90
|
+
/** Letters whose case flips the way a generator's do, not a word's or an identifier's: `hqArruGHKoRtvpQplEAc`. */
|
|
91
|
+
function isRandomCase(part) {
|
|
92
|
+
return (part.length >= 12 &&
|
|
93
|
+
/[a-z]/.test(part) &&
|
|
94
|
+
(part.slice(1).match(/[A-Z]/g)?.length ?? 0) >= 4 &&
|
|
95
|
+
naturalness(part) < 0.5);
|
|
96
|
+
}
|
|
97
|
+
const PLACEHOLDER_WORDS = new Set([
|
|
98
|
+
"your",
|
|
99
|
+
"my",
|
|
100
|
+
"the",
|
|
101
|
+
"a",
|
|
102
|
+
"an",
|
|
103
|
+
"example",
|
|
104
|
+
"sample",
|
|
105
|
+
"dummy",
|
|
106
|
+
"fake",
|
|
107
|
+
"test",
|
|
108
|
+
"placeholder",
|
|
109
|
+
"replace",
|
|
110
|
+
"replaceme",
|
|
111
|
+
"change",
|
|
112
|
+
"changeme",
|
|
113
|
+
"insert",
|
|
114
|
+
"enter",
|
|
115
|
+
"put",
|
|
116
|
+
"here",
|
|
117
|
+
"this",
|
|
118
|
+
"with",
|
|
119
|
+
"me",
|
|
120
|
+
"make",
|
|
121
|
+
"something",
|
|
122
|
+
"up",
|
|
123
|
+
"whatever",
|
|
124
|
+
"anything",
|
|
125
|
+
"key",
|
|
126
|
+
"api",
|
|
127
|
+
"secret",
|
|
128
|
+
"token",
|
|
129
|
+
"password",
|
|
130
|
+
"pass",
|
|
131
|
+
"value",
|
|
132
|
+
"goes",
|
|
133
|
+
"redacted",
|
|
134
|
+
"hidden",
|
|
135
|
+
"removed",
|
|
136
|
+
"omitted",
|
|
137
|
+
"masked",
|
|
138
|
+
"xxx",
|
|
139
|
+
"todo",
|
|
140
|
+
"fixme",
|
|
141
|
+
"tbd",
|
|
142
|
+
"none",
|
|
143
|
+
"null",
|
|
144
|
+
"empty",
|
|
145
|
+
"default",
|
|
146
|
+
"foo",
|
|
147
|
+
"bar",
|
|
148
|
+
"baz",
|
|
149
|
+
"qux",
|
|
150
|
+
"some",
|
|
151
|
+
"random",
|
|
152
|
+
"string",
|
|
153
|
+
"s3cr3t",
|
|
154
|
+
"secure",
|
|
155
|
+
"strong",
|
|
156
|
+
"real",
|
|
157
|
+
"actual",
|
|
158
|
+
"to",
|
|
159
|
+
"be",
|
|
160
|
+
"set",
|
|
161
|
+
"fill",
|
|
162
|
+
"in",
|
|
163
|
+
"id",
|
|
164
|
+
"access",
|
|
165
|
+
"private",
|
|
166
|
+
"client",
|
|
167
|
+
"auth",
|
|
168
|
+
"user",
|
|
169
|
+
"username",
|
|
170
|
+
"name",
|
|
171
|
+
"db",
|
|
172
|
+
"database",
|
|
173
|
+
"admin",
|
|
174
|
+
"app",
|
|
175
|
+
"service",
|
|
176
|
+
"account",
|
|
177
|
+
"personal",
|
|
178
|
+
"bot",
|
|
179
|
+
"webhook",
|
|
180
|
+
"url",
|
|
181
|
+
"credentials",
|
|
182
|
+
"credential",
|
|
183
|
+
"passphrase",
|
|
184
|
+
"pwd",
|
|
185
|
+
"pw",
|
|
186
|
+
"new",
|
|
187
|
+
"old",
|
|
188
|
+
"generated",
|
|
189
|
+
"own",
|
|
190
|
+
"provided",
|
|
191
|
+
]);
|
|
192
|
+
const PLACEHOLDER_MARKERS = new Set([
|
|
193
|
+
"your",
|
|
194
|
+
"my",
|
|
195
|
+
"example",
|
|
196
|
+
"sample",
|
|
197
|
+
"dummy",
|
|
198
|
+
"fake",
|
|
199
|
+
"placeholder",
|
|
200
|
+
"replace",
|
|
201
|
+
"replaceme",
|
|
202
|
+
"changeme",
|
|
203
|
+
"insert",
|
|
204
|
+
"enter",
|
|
205
|
+
"here",
|
|
206
|
+
"xxx",
|
|
207
|
+
"todo",
|
|
208
|
+
"fixme",
|
|
209
|
+
"tbd",
|
|
210
|
+
"redacted",
|
|
211
|
+
"hidden",
|
|
212
|
+
"removed",
|
|
213
|
+
"omitted",
|
|
214
|
+
"masked",
|
|
215
|
+
"goes",
|
|
216
|
+
"fill",
|
|
217
|
+
]);
|
|
218
|
+
/**
|
|
219
|
+
* A PHP variable's name, without its `$`: it reads as words, its capitals few (`loginPass`) or each of its parts in a
|
|
220
|
+
* word's case (`API_Password`); `XkPqWm` is random.
|
|
221
|
+
*/
|
|
222
|
+
function isPhpVariableName(name) {
|
|
223
|
+
return (naturalness(name) > 0 &&
|
|
224
|
+
((name.slice(1).match(/[A-Z]/g)?.length ?? 0) <= 2 ||
|
|
225
|
+
name
|
|
226
|
+
.split("_")
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.every((part) => WORD_CASE.test(part))));
|
|
229
|
+
}
|
|
230
|
+
/** A name as a template or a tool writes one: an identifier path whose parts read as names (`db.password`, `DB_PASS`). */
|
|
231
|
+
const isRefName = (name) => /^[A-Za-z_][\w.:-]*$/.test(name) && isNameLike(name);
|
|
232
|
+
/**
|
|
233
|
+
* Shell command substitution, whole or read up to an inner quote: `$(cat f)`, `` `cmd` ``, `$((pass+1))`, `$$(cat f)`
|
|
234
|
+
* (Make), `$(kubectl get secret … -o jsonpath="`; Azure Pipelines' and Make's `$(SqlAdminPassword)` variables. Arithmetic holds names, numbers and operators; a command line
|
|
235
|
+
* starts with a command's name (a word, or a path to one) and then a blank or the end.
|
|
236
|
+
*/
|
|
237
|
+
function isCommandSubstitution(v) {
|
|
238
|
+
const arithmetic = /^\$\$?\(\(([^()]*)\)\)$/.exec(v);
|
|
239
|
+
if (arithmetic)
|
|
240
|
+
return /^[\w\s+*/%$-]+$/.test(arithmetic[1]) && /[A-Za-z_]/.test(arithmetic[1]);
|
|
241
|
+
const body = /^\$\$?\(([\s\S]*)\)$/.exec(v)?.[1] ??
|
|
242
|
+
/^`([^`]{0,400})`$/.exec(v)?.[1] ??
|
|
243
|
+
/^\$\$?\(([A-Za-z][\w.-]{1,30}(?: [^\n]*)?)$/.exec(v)?.[1];
|
|
244
|
+
const command = body === undefined
|
|
245
|
+
? undefined
|
|
246
|
+
: /^\s*([A-Za-z_][\w.-]*|(?:\.{0,2}\/)?[\w.-]+(?:\/[\w.-]+)+)(?:\s|$)/.exec(body)?.[1];
|
|
247
|
+
return command !== undefined && command.split("/").every((p) => p === "" || p === "." || p === ".." || isNameLike(p));
|
|
248
|
+
}
|
|
249
|
+
/** 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;
|
|
251
|
+
/**
|
|
252
|
+
* Shapes of values that can't be credentials, by name: constants, paths, references to a value held elsewhere
|
|
253
|
+
* (variables, templates, secret managers), masks and placeholders, encrypted values, public identifiers.
|
|
254
|
+
* Each shape is whole (closers included, the value nothing but the shape), and the names inside it read as names:
|
|
255
|
+
* a generated password can start with `<`, `{{`, `$(` or `@`, and contain `%d` or `${`. test/values.test.ts runs
|
|
256
|
+
* every shape against generated passwords and keys.
|
|
257
|
+
*/
|
|
258
|
+
const INERT_SHAPES = [
|
|
259
|
+
[
|
|
260
|
+
"constant",
|
|
261
|
+
(v) => v === "" ||
|
|
262
|
+
/^(true|false|null|none|nil|undefined|yes|no|on|off|inherit|required|optional|string|str|int|number|bool|boolean|object|any|secret|password|token)$/i.test(v),
|
|
263
|
+
],
|
|
264
|
+
["path", isPath],
|
|
265
|
+
// `${NAME}`, `${NAME:-default}`, Spring's `${NAME:default}`, `%{NAME}`, `@{NAME}`, `$NAME`, `%NAME%`; PowerShell's
|
|
266
|
+
// environment, `$env:NAME`, `${env:NAME}`, `$($env:NAME)`. (A default is a value of its own, read by named.ts.)
|
|
267
|
+
[
|
|
268
|
+
"variable",
|
|
269
|
+
(v) => {
|
|
270
|
+
const m = /^[$%@&]\{([\w.-]{1,80})(?:(?::?[-?=+]|:)[^}]{0,80})?\}$/.exec(v);
|
|
271
|
+
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;
|
|
274
|
+
// (Lower case with digits closing it reads as a password: `$summer2024`, not `$db_pass2`.)
|
|
275
|
+
return ((!!m && isRefName(m[1])) ||
|
|
276
|
+
(!!ps && isRefName(ps[1] ?? ps[2] ?? ps[3])) ||
|
|
277
|
+
(/^\$(?:[A-Z_][A-Z0-9_]{0,80}|[a-z_][a-z0-9_]{0,80}|\d)$/.test(v) &&
|
|
278
|
+
reads(v.slice(1)) &&
|
|
279
|
+
!/^\$[a-z]+\d{2,}$/.test(v)) ||
|
|
280
|
+
(/^%(?:[A-Z_][A-Z0-9_]{0,80}|[a-z_][a-z0-9_.-]{0,80}|[A-Z][a-z]+(?:[A-Z][a-z]+)*)%$/.test(v) &&
|
|
281
|
+
reads(v.slice(1, -1))));
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
// `$loginPass`, `$API_Password`, `{$pwd}`, `$this->passwd`, `{$cfg['db']['pass']}` (mixed case only without digits:
|
|
285
|
+
// `$Secr3t` is a password); run together, as PHP interpolates them: `SIGNATURE=$API_Signature$nvpStr_`.
|
|
286
|
+
[
|
|
287
|
+
"PHP variable",
|
|
288
|
+
(v) => (/^(?:\$[A-Za-z_][A-Za-z_]{0,80}){1,4}$/.test(v) && v.split("$").slice(1).every(isPhpVariableName)) ||
|
|
289
|
+
/^\{\$\w+(?:->\w+|\[[^\]]{1,40}\])*\}$|^\$\w+(?:->\w+)+$/.test(v),
|
|
290
|
+
],
|
|
291
|
+
// `$(cat f)`, `$((pass+1))`, `$$(cat f)` (Make), `` `cmd` ``; or opening with a command's name, read up to an inner
|
|
292
|
+
// quote: `$(kubectl get secret … -o jsonpath="`.
|
|
293
|
+
["command substitution", isCommandSubstitution],
|
|
294
|
+
// `{{ 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 }}`.)
|
|
296
|
+
[
|
|
297
|
+
"template",
|
|
298
|
+
(v) => {
|
|
299
|
+
const m = /^(?:\$?\{\{-?(.{0,200}?)-?\}\}|<%=?([^%]{0,200})%>|#\{([^}]{0,200})\}|@([\w.-]{1,80})@)$/.exec(v);
|
|
300
|
+
if (!m)
|
|
301
|
+
return false;
|
|
302
|
+
const inner = (m[1] ?? m[2] ?? m[3] ?? m[4]).trim();
|
|
303
|
+
return /^[A-Za-z_@$.]/.test(inner) && isNameLike(inner.split(/[^\w.]/)[0] || "x");
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
// `<db password>`, `[TOKEN]`, `{api_key}`: a name in brackets.
|
|
307
|
+
[
|
|
308
|
+
"bracketed name",
|
|
309
|
+
(v) => {
|
|
310
|
+
const m = /^(?:<([\w .-]{1,60})>|\[([\w .-]{1,60})\]|\{([\w.-]{1,60})\})$/.exec(v);
|
|
311
|
+
return (!!m &&
|
|
312
|
+
(m[1] ?? m[2] ?? m[3])
|
|
313
|
+
.split(/[ .-]+/)
|
|
314
|
+
.filter(Boolean)
|
|
315
|
+
.every(isBracketedWord));
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
// A reference to a secret held elsewhere, as the tools write it: `op://vault/item/field`, `sm://…`, `ref+vault://…`
|
|
319
|
+
// (the scheme's `//`, then a path of names); `vault:secret/data/app#key`, `ssm:/prod/db`, `env:DB_PASS` (a path, or a
|
|
320
|
+
// variable's name in capitals); `arn:aws:secretsmanager:region:account:secret:name`; TeamCity's `credentialsJSON:<uuid>`.
|
|
321
|
+
// (Not a word after a scheme's colon: `env:hunter2`, `op:Passw0rd` are passwords.)
|
|
322
|
+
[
|
|
323
|
+
"secret manager",
|
|
324
|
+
(v) => {
|
|
325
|
+
const m = /^(?:(op|sm|gcpsm|awssm|azkv|doppler|infisical|bws|ref\+\w+):\/\/|(vault|keyring|secretsmanager|ssm|env|file):)([\w.@+${}/#:?=&-]{1,200})$/.exec(v);
|
|
326
|
+
if (m)
|
|
327
|
+
return (isReferencePath(m[3]) &&
|
|
328
|
+
(m[1] !== undefined || m[3].includes("/") || (m[2] === "env" && /^[A-Z_][A-Z0-9_]*$/.test(m[3]))));
|
|
329
|
+
return (/^credentialsJSON:[0-9a-f-]{36}$/.test(v) ||
|
|
330
|
+
/^arn:aws[\w-]*:(?:secretsmanager|ssm|kms):[a-z0-9-]*:(?:\d{12})?:[\w/:+=.@*${}-]+$/.test(v));
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
// `****`, `xxxxxxxx`, `••••`, `[REDACTED]`, `<hidden>`, `ab****yz`; AWX's `$encrypted$`;
|
|
334
|
+
// `xxxx`, `xxx-xxx`, `xxxx1234`, `sk-ant-xxxxx`, `ghp_xxxx` (but `Xxxq7Lm2x` is a password); an elision, `(...)`, `[…]`.
|
|
335
|
+
[
|
|
336
|
+
"mask",
|
|
337
|
+
(v) => (/^(.)\1{2,}$/u.test(v) && /[*x•·#?X0-]/.test(v[0])) ||
|
|
338
|
+
/^(?:\.{3}|…|\(\.{3}\)|\(…\)|\[\.{3}\]|\[…\]|\{\.{3}\}|\{…\}|<\.{3}>|<…>)$/.test(v) ||
|
|
339
|
+
/^\[?(redacted|hidden|masked|filtered|secret|removed|omitted|censored|sanitized|obfuscated|scrubbed|protected|not set|unset|not[ _-]?set|empty)\]?(?:\s+\([^()]{0,20}\))?$/i.test(v) ||
|
|
340
|
+
/^\*+[^*\s]{0,6}$/.test(v) ||
|
|
341
|
+
/^[^*\s]{0,6}\*{4,}[^*\s]{0,6}$/.test(v) ||
|
|
342
|
+
v === "$encrypted$" ||
|
|
343
|
+
/^x{3,}(?:[-_.\d]*$|[-_.]x)/i.test(v) ||
|
|
344
|
+
/^[a-z]{1,10}(?:[-_][a-z]{1,10}){0,2}[-_](?:x{4,}|X{4,}|\*{4,}|\.{3,})$/.test(v),
|
|
345
|
+
],
|
|
346
|
+
// An interpolation cut where the reader stopped, at a quote inside it: Ruby `#{ENV['`, `#{node['db'`, JS `${cfg[`.
|
|
347
|
+
[
|
|
348
|
+
"cut interpolation",
|
|
349
|
+
(v) => {
|
|
350
|
+
const m = new RegExp(String.raw `^[#$]\{@{0,2}(${IDENT_PATH})([([](?:["'][\w.-]*["']?)?)?$`).exec(v);
|
|
351
|
+
return !!m && (!!m[2] || m[1].includes(".")) && isNameLike(m[1]);
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
// `<?php echo $password ?>`, `<p></p>`, `<dbname>_maint_ps` (a tag that reads as a word, then a name).
|
|
355
|
+
[
|
|
356
|
+
"markup",
|
|
357
|
+
(v) => /^<\?(?:php|=)[\s\S]*\?>$/.test(v) ||
|
|
358
|
+
/^<([a-z][\w-]*)\b[^<>]*>[\s\S]*<\/\1>$/.test(v) ||
|
|
359
|
+
(/^<[a-z][a-z0-9_-]{1,30}>[\w.-]{0,40}$/.test(v) &&
|
|
360
|
+
naturalness(v.slice(1, v.indexOf(">")).replace(/[_-]+/g, " ")) > 0),
|
|
361
|
+
],
|
|
362
|
+
// Other tools' references: YAML tags (`!Ref DbPassword`, `!secret x`, `!ENV ${X}`), a YAML alias (`*default_password`),
|
|
363
|
+
// ARM (`[parameters('adminPassword')]`), Grafana (`$__env{X}`), Hiera (`%{hiera('x')}`, `%{::x}`), Nutanix Calm
|
|
364
|
+
// (`@@{x}@@`), Home Assistant's translations (`[%key:common::config_flow::data::password%]`).
|
|
365
|
+
[
|
|
366
|
+
"tool reference",
|
|
367
|
+
(v) => /^!(?:Ref|GetAtt|Sub|ImportValue|FindInMap|Select|Join|Base64|Cidr|GetAZs|If|Split|secret|env_var|ENV|env|vault|include|input|lambda|Var)\b(?:[ \t]+\S[^\n]{0,200})?$/.test(v) ||
|
|
368
|
+
(/^\*[A-Za-z_][\w-]{0,60}$/.test(v) &&
|
|
369
|
+
naturalness(v.slice(1).replace(/[_-]+/g, " ")) > 0.5 &&
|
|
370
|
+
isNameLike(v.slice(1))) ||
|
|
371
|
+
/^\[(?:parameters|variables|reference|concat|format|listKeys|listSecrets|resourceId)\(.*\)\]$/.test(v) ||
|
|
372
|
+
/^\$__(?:env|file|vault)\{[^}\n]{1,200}\}$/.test(v) ||
|
|
373
|
+
/^%\{(?:::)?[A-Za-z_][\w:]*(?:\([^}\n]{0,200}\))?\}$/.test(v) ||
|
|
374
|
+
/^@@\{[^}\n]{1,100}\}@@$/.test(v) ||
|
|
375
|
+
/^\[%key:[\w:]{1,200}%\]$/.test(v),
|
|
376
|
+
],
|
|
377
|
+
// A placeholder after a prefix: `sk.<ask-in-#atlas-dev>`, `ghp_<your token>`; in words: `<from vault: "Android upload keystore">`,
|
|
378
|
+
// `<your-key>`; names in brackets, a path of them too: `<SecretsManager:prod/db>` (not `<Xk9p>`); marked: `change-me`,
|
|
379
|
+
// `your_token`, `KEY_HERE`, `sk-...`, `abc…`.
|
|
380
|
+
[
|
|
381
|
+
"placeholder",
|
|
382
|
+
(v) => /^[\w.-]{0,12}<[a-z][a-z0-9_ #.:/-]{2,40}>$/.test(v) ||
|
|
383
|
+
(/^<[A-Za-z][^<>\n]{0,80}>$/.test(v) &&
|
|
384
|
+
/\s/.test(v) &&
|
|
385
|
+
naturalness(v
|
|
386
|
+
.slice(1, -1)
|
|
387
|
+
.replace(/[^A-Za-z]+/g, " ")
|
|
388
|
+
.trim()) > 0.5) ||
|
|
389
|
+
(/^<[A-Za-z][\w .:/-]{0,60}>$/.test(v) &&
|
|
390
|
+
v
|
|
391
|
+
.slice(1, -1)
|
|
392
|
+
.split(/[ .:/-]+/)
|
|
393
|
+
.filter(Boolean)
|
|
394
|
+
.every(isBracketedWord)) ||
|
|
395
|
+
isTruncated(v) ||
|
|
396
|
+
/change[-_ ]?me|^your[-_ ]|_here$|-here$/i.test(v),
|
|
397
|
+
],
|
|
398
|
+
// A template whose literal text is too short or plain to be a secret: `${PREFIX}_db`, `#{user}-key` (formats.ts).
|
|
399
|
+
["template text", (v) => /^[$#]\{/.test(v) && isPlaceholder(v)],
|
|
400
|
+
// `%s/%s.gpg`, `{}:{}`, `%(password)s`, `{0}`. Each specifier stands apart (a boundary or a non-alphanumeric
|
|
401
|
+
// character on one side: `k3%dq7pz` is random; and no letter right after a `%` one: `%silver2024` is a word),
|
|
402
|
+
// and what's left reads as words (`%s/%s.gpg`, not `%v^IqngjRymtGin`).
|
|
403
|
+
[
|
|
404
|
+
"printf template",
|
|
405
|
+
(v) => {
|
|
406
|
+
if (!new RegExp(SPEC.source).test(v))
|
|
407
|
+
return false;
|
|
408
|
+
const residue = v.replace(/\\[nrt]/g, " ").replace(SPEC, " ");
|
|
409
|
+
return (((residue.match(/[A-Za-z]/g)?.length ?? 0) <= 4 ||
|
|
410
|
+
naturalness(residue.replace(/[^A-Za-z]+/g, " ").trim()) > 0) &&
|
|
411
|
+
residue.split(/[^A-Za-z]+/).every((w) => w.length <= 3 || (naturalness(w) > 0.6 && isNameLike(w))) &&
|
|
412
|
+
v
|
|
413
|
+
.replace(SPEC, "")
|
|
414
|
+
.replace(/\\[nrt]/g, "")
|
|
415
|
+
.replace(/[\w.\-/: ]/g, "").length <= 1 &&
|
|
416
|
+
!(/[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] ?? ""))));
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
["option", (v) => /^--[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/.test(v)], // `--password-stdin`: a flag named, not a value
|
|
422
|
+
// Symfony's environment references, resolved when the configuration loads: `env(RESEND_API_KEY)`, `%env(resolve:DB_URL)%`.
|
|
423
|
+
["environment reference", (v) => /^%?env\((?:[a-z]+:)*[A-Za-z_][\w.]*\)%?$/.test(v)],
|
|
424
|
+
["heredoc marker", (v) => /^<<[-~]?(["']?)[A-Za-z_]\w*\1$/.test(v)], // `<<EOT`: the value is on the lines below
|
|
425
|
+
[
|
|
426
|
+
"escaped variable",
|
|
427
|
+
(v) => /^\$\$(?:[A-Z_][A-Z0-9_]*|[a-z_][a-z0-9_]*|\{\w+\})$/.test(v) && isNameLike(v.replace(/[${}]/g, "")),
|
|
428
|
+
], // docker compose's `$$VAR`
|
|
429
|
+
// `%Y-%m-%dT%H`, `%d/%m/%Y %H:%M:%S`.
|
|
430
|
+
[
|
|
431
|
+
"strftime",
|
|
432
|
+
(v) => (v.match(/%[aAbBcdDeFgGhHIjklmMnpPrRsStTuUVwWxXyYzZ]/g)?.length ?? 0) >= 2 &&
|
|
433
|
+
/^(?:%[aAbBcdDeFgGhHIjklmMnpPrRsStTuUVwWxXyYzZ]|[-:T _/.,])+$/.test(v),
|
|
434
|
+
],
|
|
435
|
+
// A value in an encryption wrapper (ENCRYPTION_WRAPPER), Ansible's `!vault |`, Splunk's `$7$…` (and older `$1$…`: one
|
|
436
|
+
// base64 field, where a crypt hash has a salt and a hash).
|
|
437
|
+
// (Pulumi's passphrase-encrypted values and its stack's salt: `v1:<salt>:v1:<nonce>:<ciphertext>`, `v1:<nonce>:<ciphertext>`.)
|
|
438
|
+
[
|
|
439
|
+
"encrypted",
|
|
440
|
+
(v) => /^\$[17]\$[A-Za-z0-9+/]{16,}={0,2}$/.test(v) ||
|
|
441
|
+
ENCRYPTED_VALUE.test(v) ||
|
|
442
|
+
/^v1:[A-Za-z0-9+/=]{8,}:(?:v1:[A-Za-z0-9+/=]{8,}:)?[A-Za-z0-9+/=]{8,}$/.test(v),
|
|
443
|
+
],
|
|
444
|
+
// What a secret listing shows instead of the value: `16 bytes`, a timestamp, an object repr (`Secret{name=db}`),
|
|
445
|
+
// a relative time (`about 1 month ago`).
|
|
446
|
+
[
|
|
447
|
+
"listing metadata",
|
|
448
|
+
(v) => /^\d+ bytes?$/i.test(v) ||
|
|
449
|
+
/^\d{4}-\d\d-\d\d(?:[T ][\d:.]+(?:Z|[+-]\d\d:?\d\d)?)?$/.test(v) ||
|
|
450
|
+
(/^[A-Za-z_]\w{2,}\{(?:\w+=[\w, :'".-]*)?\}$/.test(v) &&
|
|
451
|
+
naturalness(v.slice(0, v.indexOf("{"))) > 0 &&
|
|
452
|
+
isNameLike(v.slice(0, v.indexOf("{")))) ||
|
|
453
|
+
/^[A-Z][A-Za-z]{2,40}\{[0-9a-f]{6,8}(?:[ }]|$)/.test(v) ||
|
|
454
|
+
/^(?:about |less than |over |almost )?(?:a|an|\d+) (?:second|minute|hour|day|week|month|year)s? ago$/i.test(v),
|
|
455
|
+
],
|
|
456
|
+
// Public identifiers: blockchain addresses and content IDs (`0x…`, bc1…, Qm…/bafy…, npub1…), Twilio SIDs, Windows
|
|
457
|
+
// SIDs and GUIDs in braces (`{a75eafe9-…}`), Firebase app IDs (`1:1234…:web:3f2a…`), IAM statement IDs, email Message-IDs, hex numbers up to 64 bits (`0x3f2a…` in ZooKeeper, addresses in dumps).
|
|
458
|
+
[
|
|
459
|
+
"public identifier",
|
|
460
|
+
(v) => /^0x[0-9a-fA-F]{40}$/.test(v) ||
|
|
461
|
+
(/^(?:bc1|tb1)[02-9ac-hj-np-z]{25,90}$/.test(v) && isBech32Address(v)) ||
|
|
462
|
+
/^Qm[1-9A-HJ-NP-Za-km-z]{44}$/.test(v) ||
|
|
463
|
+
/^bafy[a-z2-7]{50,}$/.test(v) ||
|
|
464
|
+
/^npub1[a-z0-9]{58}$/.test(v) ||
|
|
465
|
+
/^(?:AC|SM|MM|PN|CA|MG|AP|CF|RE|TR|US|IS|PK|WS|WA|WK|EV|QU|XE|NO|IP|CR|BU|CL|FW|KS|NP|PA|PH|PV|RT|SC|SD|SL|TO|TP|VA|VE|ZS)[0-9a-f]{32}$/.test(v) ||
|
|
466
|
+
/^S-1-\d+(?:-\d+){1,14}$/.test(v) ||
|
|
467
|
+
/^\{[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\}$/.test(v) ||
|
|
468
|
+
/^\d:\d{6,15}:(?:web|android|ios):[0-9a-f]{16,40}$/.test(v) ||
|
|
469
|
+
/^Stmt\d*$/.test(v) ||
|
|
470
|
+
/^<[\w.$=+%-]{1,200}@[\w.-]{1,200}>$/.test(v) ||
|
|
471
|
+
/^0x[0-9a-fA-F]{1,16}$/.test(v),
|
|
472
|
+
],
|
|
473
|
+
// DER public keys, certificates and signatures in base64, by their structure (publickeys.ts): SPKI (`MIIBIjAN…`,
|
|
474
|
+
// `MCowBQYDK2Vw…`), PKCS #1 (`MIIBCgKC…`), X.509 (`MIIDdzCCAl+gAwIBAgI…`), ECDSA signatures (`MEUCI…`); an SSH public
|
|
475
|
+
// key line, an `age1…` recipient.
|
|
476
|
+
[
|
|
477
|
+
"public key",
|
|
478
|
+
(v) => isDerPublicKey(v) ||
|
|
479
|
+
isRsaPublicKey(v) ||
|
|
480
|
+
isDerCertificate(v) ||
|
|
481
|
+
isDerSignature(v) ||
|
|
482
|
+
isSshPublicKey(v) ||
|
|
483
|
+
/^age1[0-9a-z]{58}$/.test(v),
|
|
484
|
+
],
|
|
485
|
+
// Hardhat/Anvil's first account, BIP-39's `abandon … about` test vectors.
|
|
486
|
+
[
|
|
487
|
+
"published test key",
|
|
488
|
+
(v) => /^(?:0x)?ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80$/i.test(v) ||
|
|
489
|
+
/^(?:test ){11}junk$|^(?:abandon ){11}about$|^(?:abandon ){23}art$/.test(v),
|
|
490
|
+
],
|
|
491
|
+
// Every word a placeholder word, at least one an unmistakable marker: `your-api-key`, `example_token`.
|
|
492
|
+
[
|
|
493
|
+
"placeholder words",
|
|
494
|
+
(v) => {
|
|
495
|
+
const words = v
|
|
496
|
+
.toLowerCase()
|
|
497
|
+
.split(/[-_. ]+/)
|
|
498
|
+
.filter(Boolean);
|
|
499
|
+
return (words.length > 0 &&
|
|
500
|
+
words.every((w) => PLACEHOLDER_WORDS.has(w)) &&
|
|
501
|
+
words.some((w) => PLACEHOLDER_MARKERS.has(w)));
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
];
|
|
505
|
+
/** An OpenSSH public key line: the key type, a space, base64 that opens with that type (length-prefixed), maybe cut at `...`, and a comment. */
|
|
506
|
+
const SSH_KEY_LINE = /^(ssh-(?:rsa|dss|ed25519|ed448)|ecdsa-sha2-nistp\d{3}|sk-(?:ssh-ed25519|ecdsa-sha2-nistp256)@openssh\.com) ([A-Za-z0-9+/]{8,}={0,2})(?:\.{3}|…)?(?: .*)?$/s;
|
|
507
|
+
function isSshPublicKey(v) {
|
|
508
|
+
const m = SSH_KEY_LINE.exec(v);
|
|
509
|
+
if (!m)
|
|
510
|
+
return false;
|
|
511
|
+
// (Whole groups of four characters only; a truncated key is read as far as it goes.)
|
|
512
|
+
const opening = base64Binary(m[2].slice(0, m[2].length - (m[2].length % 4)));
|
|
513
|
+
const expected = `\0\0\0${String.fromCharCode(m[1].length)}${m[1]}`;
|
|
514
|
+
return opening !== undefined && expected.startsWith(opening.slice(0, expected.length));
|
|
515
|
+
}
|
|
516
|
+
/** Which inert shape a value has (INERT_SHAPES), if any. (Written in markup's escapes, a shape is that shape: `<redacted>`.) */
|
|
517
|
+
export function inertShape(value) {
|
|
518
|
+
// (Read through HTML's entities and a URL's percent-encoding too: `<token>`, `?apikey=%7B%7Bapi_key%7D%7D`.)
|
|
519
|
+
const decoded = value.includes("&") ? decodeEntities(value) : percentDecoded(value);
|
|
520
|
+
return INERT_SHAPES.find(([, test]) => test(value) || (decoded !== value && test(decoded)))?.[0];
|
|
521
|
+
}
|
|
522
|
+
/** `value` with its percent-encoded bytes decoded, or as it is when it has none or they aren't UTF-8. */
|
|
523
|
+
function percentDecoded(value) {
|
|
524
|
+
if (!/%[0-9A-Fa-f]{2}/.test(value))
|
|
525
|
+
return value;
|
|
526
|
+
try {
|
|
527
|
+
return decodeURIComponent(value);
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/** A value that can't be a credential: empty, a boolean, a path, a placeholder or a reference to one held elsewhere. */
|
|
534
|
+
export function isInert(value) {
|
|
535
|
+
return inertShape(value) !== undefined;
|
|
536
|
+
}
|
|
537
|
+
/** A file path: path-shaped, with no segment that a generator made (`/NKR7uXt0UfYN9/`, `/eB7/ZDoDhDrnTAZWfbkgNMM`). */
|
|
538
|
+
export function isPath(path) {
|
|
539
|
+
// (A variable in a template's syntax is one too: Puppet's and RPM's `%{KEY}`, read as `${KEY}`.)
|
|
540
|
+
const value = path.replace(/%\{([A-Za-z_]\w*)\}/g, "$${$1}");
|
|
541
|
+
// (Relative, a directory's name then a file's with its extension, in lower case: `vpn/auth.txt`, `certs/dev-key.pem`.
|
|
542
|
+
// Names switch between letters and digits three times at most, `x509v3`; a generator's `a1b2c3d4` more.)
|
|
543
|
+
if (/^[a-z0-9_-]+(?:\/[a-z0-9_.-]+)*\/[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.[a-z0-9]{1,8}$/.test(value))
|
|
544
|
+
return value.split(/[/._-]/).every((seg) => (seg.match(/[a-z](?=\d)|\d(?=[a-z])/g)?.length ?? 0) <= 3);
|
|
545
|
+
// (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) ||
|
|
548
|
+
!/[/\\]/.test(value.slice(1)) ||
|
|
549
|
+
/[{}]/.test(value.replace(/\$?\{[A-Za-z_]\w*\}/g, "")))
|
|
550
|
+
return false;
|
|
551
|
+
return !value.split(/[/\\]/).some(isGeneratedSegment);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* A path segment a generator made, not a person: one that mixes cases and digits and is long, isn't a name, or has a
|
|
555
|
+
* symbol no name has (`Tv$-ycX.K9+`); mixed-case letters that read as no word, lower-cased (`ZDoDhDrnTAZWfbkgNMM`:
|
|
556
|
+
* the character model reads each camel-case hump on its own, and random humps often score as words); or a `+`
|
|
557
|
+
* inside mixed-case letters, base64's (`GbeaKB+qGmA+X`; mkcert's `localhost+2-key.pem` is one case, `c++` ends with it).
|
|
558
|
+
*/
|
|
559
|
+
export function isGeneratedSegment(seg) {
|
|
560
|
+
const mixed = /[a-z]/.test(seg) && /[A-Z]/.test(seg);
|
|
561
|
+
if (mixed && /\d/.test(seg) && (seg.length >= 16 || /[$+@ ]/.test(seg) || (seg.length >= 5 && !isNameLike(seg))))
|
|
562
|
+
return true;
|
|
563
|
+
if (mixed && /[A-Za-z0-9]\+[A-Za-z0-9]/.test(seg))
|
|
564
|
+
return true;
|
|
565
|
+
return mixed && seg.length >= 6 && !isNameLike(seg) && naturalness(seg.toLowerCase()) < 0.5;
|
|
566
|
+
}
|
|
567
|
+
/** Abbreviations code names things with, which read as no word to the character model. */
|
|
568
|
+
const CODE_WORDS = new Set([
|
|
569
|
+
"ctx",
|
|
570
|
+
"pwd",
|
|
571
|
+
"pw",
|
|
572
|
+
"usr",
|
|
573
|
+
"cfg",
|
|
574
|
+
"env",
|
|
575
|
+
"db",
|
|
576
|
+
"tmp",
|
|
577
|
+
"vm",
|
|
578
|
+
"msg",
|
|
579
|
+
"req",
|
|
580
|
+
"res",
|
|
581
|
+
"err",
|
|
582
|
+
"args",
|
|
583
|
+
"kwargs",
|
|
584
|
+
"opts",
|
|
585
|
+
"obj",
|
|
586
|
+
"src",
|
|
587
|
+
"dst",
|
|
588
|
+
"idx",
|
|
589
|
+
"ptr",
|
|
590
|
+
"buf",
|
|
591
|
+
"str",
|
|
592
|
+
"num",
|
|
593
|
+
"cls",
|
|
594
|
+
"fn",
|
|
595
|
+
"cb",
|
|
596
|
+
"el",
|
|
597
|
+
"js",
|
|
598
|
+
"os",
|
|
599
|
+
"io",
|
|
600
|
+
"id",
|
|
601
|
+
"ui",
|
|
602
|
+
"api",
|
|
603
|
+
"url",
|
|
604
|
+
"uri",
|
|
605
|
+
"jwt",
|
|
606
|
+
"otp",
|
|
607
|
+
"pin",
|
|
608
|
+
"pass",
|
|
609
|
+
"key",
|
|
610
|
+
"md",
|
|
611
|
+
"sha",
|
|
612
|
+
"hex",
|
|
613
|
+
"enc",
|
|
614
|
+
"dec",
|
|
615
|
+
"b",
|
|
616
|
+
"rsa",
|
|
617
|
+
"aes",
|
|
618
|
+
"ecdsa",
|
|
619
|
+
"ec",
|
|
620
|
+
"hmac",
|
|
621
|
+
"jwk",
|
|
622
|
+
"tls",
|
|
623
|
+
"ssl",
|
|
624
|
+
"pem",
|
|
625
|
+
"der",
|
|
626
|
+
"gcm",
|
|
627
|
+
"cbc",
|
|
628
|
+
"ssh",
|
|
629
|
+
]);
|
|
630
|
+
/** An identifier path with a word in it (`config.password`, `ctx.pw`): code is written in words; `KY4_.XT`, `zpu.KcqEeQfd` are not. */
|
|
631
|
+
function hasWord(path) {
|
|
632
|
+
return path
|
|
633
|
+
.split(/[^A-Za-z]+|(?<=[a-z])(?=[A-Z])/)
|
|
634
|
+
.some((p) => CODE_WORDS.has(p.toLowerCase()) || (p.length >= 3 && naturalness(p.toLowerCase()) > 0.5));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Most of a value's letters are in words: its runs of letters (camel-case humps apart) of three or more read as
|
|
638
|
+
* words or as code's abbreviations, and hold two thirds of its letters. `env(DATABASE_URL)`, `CORP\svc_deploy`,
|
|
639
|
+
* `logger.warning(` do; `JujvAIngWfOqw`, `)AM{?NfoSuFiMi.s` don't.
|
|
640
|
+
*/
|
|
641
|
+
export function readsAsWords(value) {
|
|
642
|
+
const letters = value.match(/[A-Za-z]/g)?.length ?? 0;
|
|
643
|
+
return letters > 0 && wordLetters(value, 3) * 3 >= letters * 2;
|
|
644
|
+
}
|
|
645
|
+
/** How many of a value's letters are in words: runs of letters (camel-case humps apart) of `shortest` or more that read as words, or code's abbreviations. */
|
|
646
|
+
function wordLetters(value, shortest) {
|
|
647
|
+
const runs = value.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+/g) ?? [];
|
|
648
|
+
return runs
|
|
649
|
+
.filter((r) => CODE_WORDS.has(r.toLowerCase()) || (r.length >= shortest && naturalness(r.toLowerCase()) > 0.3))
|
|
650
|
+
.reduce((n, r) => n + r.length, 0);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* A value made of words, run together or joined by `_` or `-` (`required`, `not-set`, `sameorigin`, `Bearer`), as
|
|
654
|
+
* opposed to random letters, which have no digit by chance (`XGxAdcVIefCUMAVsDhkYhErsQwPz`,
|
|
655
|
+
* `kXbQzMwRpLtY-vNcDhJsGfWqZr_mKdTy`): every part of three letters or more reads as a word.
|
|
656
|
+
*/
|
|
657
|
+
export function isWordsValue(value) {
|
|
658
|
+
return (/^[a-z]+(?:[_-][a-z]+)*$/i.test(value) &&
|
|
659
|
+
readsAsWords(value) &&
|
|
660
|
+
value.split(/[_-]/).every((p) => p.length < 3 || !isRandomCase(p)));
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* A token a generator made, by its characters alone: 16 or more of a token's alphabet, with letters, random as the
|
|
664
|
+
* character model reads it lower-cased, and not half words. Hex with digits is one whatever the model reads in its
|
|
665
|
+
* letters (`cafe`, `dead`). Fewer than one random token in two hundred fails this at 16 characters (hex with one
|
|
666
|
+
* letter reads as a number), one in ten thousand at 32; words run together (`reset-password-confirm`, `ProjectSettingsPage`), with numbers in them
|
|
667
|
+
* (`VerifyEmailPage2024.tsx`, `sso-saml-setup-v2`), and a date with a letter in it (`2024-01-15T10-30-00`) pass.
|
|
668
|
+
*/
|
|
669
|
+
export function isRandomToken(v, min = 16) {
|
|
670
|
+
if (v.length < min || !/^[A-Za-z0-9_~=+/.-]+$/.test(v) || !/[A-Za-z][^A-Za-z]*[A-Za-z]/.test(v))
|
|
671
|
+
return false;
|
|
672
|
+
if (/^(?=.*\d)(?:[0-9a-f]+|[0-9A-F]+)$/.test(v))
|
|
673
|
+
return true;
|
|
674
|
+
return naturalness(v.toLowerCase()) < 0.3 && wordLetters(v, 4) * 2 < v.length;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* A name as people write identifiers: named the way code names things (isNameLike), not mixed-case letters a
|
|
678
|
+
* generator made (isGeneratedSegment), and words to the character model read lower-cased (mixed case alone inflates
|
|
679
|
+
* its score): `changePasswd`, `lPass`, `login-form`; not `AssvKlk`, `cpPygBl`.
|
|
680
|
+
*/
|
|
681
|
+
function readsAsIdentifier(name) {
|
|
682
|
+
return isNameLike(name) && !isGeneratedSegment(name) && naturalness(name.toLowerCase()) > 0.5;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* A credential's name written as an identifier and ending in its credential word (`set_guest_wifi_password`,
|
|
686
|
+
* `DB_PASSWORD`, `githubToken`): a value that names a credential, not one. (Not kebab-case, which passwords are written
|
|
687
|
+
* in too, `test-pass-123`; nor a passphrase in title case, `FluffyBunnyPassword`; nor one that only holds the word,
|
|
688
|
+
* `verde_dev_pw`, `admin_pass1`.)
|
|
689
|
+
*/
|
|
690
|
+
export function namesCredential(value) {
|
|
691
|
+
return (/^(?:[a-z][a-z0-9]*(?:_[a-z0-9]+)+|[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+|[a-z]+(?:[A-Z][a-z0-9]+)+)$/.test(value) &&
|
|
692
|
+
/(?:password|passwd|passphrase|secret|token|key)s?$/i.test(value) &&
|
|
693
|
+
isCredentialName(value));
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* A version range as package managers write one (`^4.0.0`, `~1.2`, `>=1.2.0 <2`, `1.x || 2.x`), after a protocol
|
|
697
|
+
* (`npm:`, `workspace:`, `patch:`). Each version has an operator or a dot: a bare number is no range.
|
|
698
|
+
*/
|
|
699
|
+
const VERSION = String.raw `(?:(?:[~^]|[<>]=?|=)v?\d+(?:\.(?:\d+|[xX*])){0,2}|v?\d+(?:\.(?:\d+|[xX*])){1,2})(?:-[\w.]+)?`;
|
|
700
|
+
const SEMVER_RANGE = new RegExp(String.raw `^(?:(?:npm|workspace|patch|link|portal|file):)?(?:\*|${VERSION}(?:[ \t]*(?:\|\||-|[ \t])[ \t]*${VERSION})*)$`);
|
|
701
|
+
/** Brackets that close in order: `f(a[0], {b})`, not `Ssaw(53n9DYc!(l7)`. (Quotes aren't tracked.) */
|
|
702
|
+
export function isBalanced(s) {
|
|
703
|
+
const open = [];
|
|
704
|
+
for (const c of s) {
|
|
705
|
+
if (c === "(" || c === "[" || c === "{")
|
|
706
|
+
open.push(c);
|
|
707
|
+
else if (c === ")" || c === "]" || c === "}")
|
|
708
|
+
if (open.pop() !== { ")": "(", "]": "[", "}": "{" }[c])
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
return open.length === 0;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Variables and the punctuation between them, nothing literal: `$RANDOM***$RANDOM`, `%RANDOM%@%RANDOM%`, `${*}`.
|
|
715
|
+
* (A bare variable in capitals, after no letter, digit or `$`: `Pa$$WORD`, `P@$SWORD1` keep letters of their own.)
|
|
716
|
+
*/
|
|
717
|
+
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, "");
|
|
719
|
+
// (One variable at least: `!@#$%^&*()` is a password of symbols.)
|
|
720
|
+
return rest !== v && !/[\p{L}\p{N}]/u.test(rest);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Types the languages build in, or their standard libraries and secret wrappers give: Python's and pydantic's (`str`,
|
|
724
|
+
* `Optional`, `Dict`, `SecretStr`), TypeScript's and the platform's (`string`, `Record`, `Uint8Array`, `CryptoKey`), Go's
|
|
725
|
+
* (`byte`, `rune`), Rust's and its crates' (`u8`, `Vec`, `Option`, `SecretString`), Kotlin's, Java's and C#'s (`String`,
|
|
726
|
+
* `CharArray`, `SecureString`).
|
|
727
|
+
*/
|
|
728
|
+
const BUILT_IN_TYPES = new Set([
|
|
729
|
+
"str bytes bytearray int float bool None NoneType Any object dict list tuple set frozenset Optional Union List Dict",
|
|
730
|
+
"Tuple Set FrozenSet Sequence Mapping MutableMapping Iterable Iterator Callable Literal Annotated Path PathLike",
|
|
731
|
+
"SecretStr SecretBytes",
|
|
732
|
+
"string number boolean bigint symbol null undefined void unknown any never Array ReadonlyArray Record Partial",
|
|
733
|
+
"Readonly Promise Map WeakMap Buffer ArrayBuffer ArrayBufferLike SharedArrayBuffer DataView Uint8Array",
|
|
734
|
+
"Uint8ClampedArray Int8Array Uint16Array Int16Array Uint32Array Int32Array Float32Array Float64Array BigInt64Array",
|
|
735
|
+
"BigUint64Array CryptoKey CryptoKeyPair KeyObject KeyLike",
|
|
736
|
+
"byte rune error uint uint8 uint16 uint32 uint64 int8 int16 int32 int64 float32 float64",
|
|
737
|
+
"u8 u16 u32 u64 u128 usize i8 i16 i32 i64 i128 isize f32 f64 char String Vec Option Result Box Rc Arc Cow HashMap",
|
|
738
|
+
"BTreeMap SecretString SecretBox Zeroizing",
|
|
739
|
+
"Int Long Boolean Double Integer CharArray ByteArray SecureString Nullable",
|
|
740
|
+
]
|
|
741
|
+
.join(" ")
|
|
742
|
+
.split(" "));
|
|
743
|
+
/**
|
|
744
|
+
* A class whose name ends in a word only types end in: `PasswordField`, `EncryptedStr`, `SecretModel`. (Title-case words
|
|
745
|
+
* ending `String`, `Secret`, `Type` or `Model` are a passphrase too, `LittleSecret`: see isTypeExpression's `classes`.)
|
|
746
|
+
*/
|
|
747
|
+
const TYPE_CLASS = /^[A-Z]\w*(?:Str|String|Bytes|Field|Type|Secret|Model)$/;
|
|
748
|
+
/** A type's token: a lifetime (`'a`), a string literal's type, a name (maybe qualified), a number, punctuation. */
|
|
749
|
+
const TYPE_TOKEN = /[ \t]*(?:('[a-z_]\w*)(?![\w'])|("[^"\n]{0,40}"|'[^'\n]{0,40}')|([A-Za-z_]\w*(?:(?:\.|::)[A-Za-z_]\w*)*)|(\d{1,6})|(\.\.\.|=>|\[\]|[[\]<>(),|&?!*;:]))/y;
|
|
750
|
+
/**
|
|
751
|
+
* A type as a declaration writes one, in Python, TypeScript, Go, Rust, Kotlin, Java or C#: a name, maybe qualified, with
|
|
752
|
+
* its arguments (`Optional[str]`, `Dict[str, Any]`, `Map<String, String>`, `Vec<u8>`) and marks (`string[]`, `String?`,
|
|
753
|
+
* `[]byte`, `*string`, `char*`, `&'a str`, `String!`), in a union (`str | None`, `string | null`; not an intersection,
|
|
754
|
+
* `Path&Ember&winter` is a passphrase), a tuple, a function's type (`() => string`) or a type query (`typeof DEFAULT`).
|
|
755
|
+
* One of its names at least is a type the language builds in, and every other reads as a name:
|
|
756
|
+
* `Optional[PasswordData]` is a type, `Summer|Winter`, `Hunter2!` and `Optional[Xk9mQ2vL]` aren't. With `classes`, a
|
|
757
|
+
* class named as types are (TYPE_CLASS) counts as built in: `"any"` where code is written; `"marked"` in a literal
|
|
758
|
+
* context, where a name alone is a type only by a type's own ending, `SecretStr`, `PasswordField` (`LittleSecret` is a
|
|
759
|
+
* passphrase there, and `Uint8Array` alone may be a password), but `Optional[ClientSecret]` is a type.
|
|
760
|
+
*/
|
|
761
|
+
export function isTypeExpression(value, classes = "none") {
|
|
762
|
+
const v = value.trim();
|
|
763
|
+
if (v.length > 200)
|
|
764
|
+
return false;
|
|
765
|
+
const tokens = [];
|
|
766
|
+
for (TYPE_TOKEN.lastIndex = 0; TYPE_TOKEN.lastIndex < v.length;) {
|
|
767
|
+
const m = TYPE_TOKEN.exec(v);
|
|
768
|
+
if (!m)
|
|
769
|
+
return false;
|
|
770
|
+
tokens.push(m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5]);
|
|
771
|
+
}
|
|
772
|
+
// A recursive descent over the tokens from `i`, collecting the names it reads. (Nesting deeper than 8 isn't read.)
|
|
773
|
+
let i = 0;
|
|
774
|
+
const names = [];
|
|
775
|
+
// A lifetime or a type query, which only types have.
|
|
776
|
+
let typeOnly = false;
|
|
777
|
+
const eat = (t) => {
|
|
778
|
+
if (tokens[i] !== t)
|
|
779
|
+
return false;
|
|
780
|
+
i++;
|
|
781
|
+
return true;
|
|
782
|
+
};
|
|
783
|
+
const isName = (t) => /^[A-Za-z_]/.test(t ?? "");
|
|
784
|
+
// Types joined by `|`, a union.
|
|
785
|
+
const union = (depth) => {
|
|
786
|
+
if (depth > 8)
|
|
787
|
+
return false;
|
|
788
|
+
eat("|");
|
|
789
|
+
do
|
|
790
|
+
if (!withMarks(depth))
|
|
791
|
+
return false;
|
|
792
|
+
while (eat("|"));
|
|
793
|
+
return true;
|
|
794
|
+
};
|
|
795
|
+
// A type and the marks after it: an array's `[]`, a nullable's `?`, GraphQL's non-null `!`, C's pointer `*`.
|
|
796
|
+
const withMarks = (depth) => {
|
|
797
|
+
if (!prefixed(depth))
|
|
798
|
+
return false;
|
|
799
|
+
while (eat("[]") || eat("?") || eat("!") || eat("*"))
|
|
800
|
+
;
|
|
801
|
+
return true;
|
|
802
|
+
};
|
|
803
|
+
// A type and the marks before it: a pointer or a reference (`*T`, `&'a mut T`), Go's slice and array (`[]T`,
|
|
804
|
+
// `[32]T`), a keyword (`dyn Error`, `readonly string[]`, `typeof DEFAULT`), Go's map (`map[string]T`).
|
|
805
|
+
const prefixed = (depth) => {
|
|
806
|
+
if (eat("*") || eat("[]"))
|
|
807
|
+
return prefixed(depth + 1);
|
|
808
|
+
if (eat("&")) {
|
|
809
|
+
if (/^'\w/.test(tokens[i] ?? "") && !tokens[i].endsWith("'")) {
|
|
810
|
+
typeOnly = true;
|
|
811
|
+
i++;
|
|
812
|
+
}
|
|
813
|
+
eat("mut");
|
|
814
|
+
return prefixed(depth + 1);
|
|
815
|
+
}
|
|
816
|
+
if (tokens[i] === "[" && /^\d/.test(tokens[i + 1] ?? "") && tokens[i + 2] === "]") {
|
|
817
|
+
i += 3;
|
|
818
|
+
return prefixed(depth + 1);
|
|
819
|
+
}
|
|
820
|
+
if ((tokens[i] === "typeof" || tokens[i] === "keyof") && isName(tokens[i + 1])) {
|
|
821
|
+
typeOnly = true;
|
|
822
|
+
i++;
|
|
823
|
+
return atom(depth + 1);
|
|
824
|
+
}
|
|
825
|
+
if (/^(?:dyn|impl|mut|const|readonly|chan)$/.test(tokens[i] ?? "") && i + 1 < tokens.length) {
|
|
826
|
+
i++;
|
|
827
|
+
return prefixed(depth + 1);
|
|
828
|
+
}
|
|
829
|
+
if (tokens[i] === "map" && tokens[i + 1] === "[") {
|
|
830
|
+
i += 2;
|
|
831
|
+
return union(depth + 1) && eat("]") && prefixed(depth + 1);
|
|
832
|
+
}
|
|
833
|
+
return atom(depth + 1);
|
|
834
|
+
};
|
|
835
|
+
// Types up to the bracket that closes them, separated by commas (Python's `...` among them; Rust's array, `[u8; 32]`).
|
|
836
|
+
const list = (depth, close) => {
|
|
837
|
+
if (eat(close))
|
|
838
|
+
return true;
|
|
839
|
+
do
|
|
840
|
+
if (!eat("...") && !union(depth))
|
|
841
|
+
return false;
|
|
842
|
+
while (eat(","));
|
|
843
|
+
if (eat(";") && !/^\d/.test(tokens[i++] ?? ""))
|
|
844
|
+
return false;
|
|
845
|
+
return eat(close);
|
|
846
|
+
};
|
|
847
|
+
// A function type's parameters, to the `)`: `a: string, b?: number`.
|
|
848
|
+
const parameters = (depth) => {
|
|
849
|
+
if (eat(")"))
|
|
850
|
+
return true;
|
|
851
|
+
do {
|
|
852
|
+
if (!isName(tokens[i]))
|
|
853
|
+
return false;
|
|
854
|
+
i++;
|
|
855
|
+
eat("?");
|
|
856
|
+
if (!eat(":") || !union(depth))
|
|
857
|
+
return false;
|
|
858
|
+
} while (eat(","));
|
|
859
|
+
return eat(")");
|
|
860
|
+
};
|
|
861
|
+
// A name with its arguments, a type in parentheses, a function's type, a tuple, a string literal's type.
|
|
862
|
+
const atom = (depth) => {
|
|
863
|
+
const t = tokens[i++];
|
|
864
|
+
if (t === "(") {
|
|
865
|
+
const from = i;
|
|
866
|
+
if (parameters(depth) && eat("=>"))
|
|
867
|
+
return union(depth);
|
|
868
|
+
i = from;
|
|
869
|
+
return union(depth) && eat(")");
|
|
870
|
+
}
|
|
871
|
+
if (t === "[")
|
|
872
|
+
return list(depth, "]");
|
|
873
|
+
if (/^(["']).+\1$/.test(t ?? ""))
|
|
874
|
+
return true;
|
|
875
|
+
if (!isName(t))
|
|
876
|
+
return false;
|
|
877
|
+
names.push(t);
|
|
878
|
+
if (eat("<"))
|
|
879
|
+
return list(depth, ">");
|
|
880
|
+
if (eat("["))
|
|
881
|
+
return list(depth, "]");
|
|
882
|
+
return true;
|
|
883
|
+
};
|
|
884
|
+
if (!union(0) || i !== tokens.length)
|
|
885
|
+
return false;
|
|
886
|
+
if (classes === "marked" && tokens.length === 1 && !/(?:Str|Bytes|Field)$/.test(tokens[0]))
|
|
887
|
+
return false;
|
|
888
|
+
const last = (n) => n.split(/\.|::/).pop();
|
|
889
|
+
// (Built in by its name, maybe from a module in lower case: `typing.Optional`, `std::string`; `Path.Ember.list` is a
|
|
890
|
+
// passphrase.)
|
|
891
|
+
const typeName = (n) => (BUILT_IN_TYPES.has(last(n)) && /^(?:[a-z_]\w*(?:\.|::))*\w+$/.test(n)) ||
|
|
892
|
+
(classes !== "none" && TYPE_CLASS.test(n) && readsAsWords(n));
|
|
893
|
+
return ((typeOnly || names.some(typeName)) &&
|
|
894
|
+
names.every((n) => typeName(n) || (isNameLike(n) && (n.length <= 2 || readsAsWords(last(n))))));
|
|
895
|
+
}
|
|
896
|
+
const ENV_LOOKUP = /process\.env|os\.environ|getenv|ENV\[|System\.getenv|Environment\.GetEnvironmentVariable|env::var|std::env|Deno\.env|import\.meta\.env|^(config|settings|secrets|params|args|options|opts|self|this|props|ctx|context|req|request|env)\./;
|
|
897
|
+
/**
|
|
898
|
+
* Code rather than a literal value: an environment lookup, a call, member access, a variable.
|
|
899
|
+
* Quoted strings are literals, unless built from code. Each shape below is code in any context; in a literal context
|
|
900
|
+
* (`NAME=value` in env, `key: value` in YAML, a CLI flag) only REFERENCES are code besides; elsewhere CODE_ONLY too,
|
|
901
|
+
* where an unquoted lower-case word without digits is taken as a variable (`password = userPassword`; with digits,
|
|
902
|
+
* `PASSWORD=hunter2` in a .env, it's a literal).
|
|
903
|
+
*/
|
|
904
|
+
export function isCode(raw, literal = false) {
|
|
905
|
+
const v = raw.trim().replace(/[;,]$/, "").trim();
|
|
906
|
+
// A quoted string built from variables alone: `"${prefix}${key}"`, `"$RANDOM***$RANDOM"`. (One spliced into code,
|
|
907
|
+
// `"…'".$password."'…"`, is the decision stage's to read, by the quotes around it: policy.ts codeShape.)
|
|
908
|
+
if (/^["'`]/.test(v))
|
|
909
|
+
return /^"[^"]*"$/.test(v) && isExpansionsOnly(v.slice(1, -1));
|
|
910
|
+
if (CODE_SHAPES.some(([, test]) => test(v, literal)))
|
|
911
|
+
return true;
|
|
912
|
+
return (literal ? REFERENCES : CODE_ONLY).some(([, test]) => test(v));
|
|
913
|
+
}
|
|
914
|
+
/** Links joined by `||`, `&&`, `??`, `or` or `and`; a link an identifier path, `options[:password]`, `self.get_password()`. */
|
|
915
|
+
const LINK = String.raw `@?${IDENT}(?:(?:\??\.|->|::)[\w$]+|\[:?[\w'"]+\]|\(\))*`;
|
|
916
|
+
const ALTERNATIVES = new RegExp(String.raw `^${LINK}(?:\s*(?:\|\||&&|\?\?)\s*${LINK}|\s+(?:or|and)\s+${LINK})+\)?$`);
|
|
917
|
+
/** An operand: an identifier path or a call (its arguments without brackets or quotes), a string, a number. (No run crosses a bracket or a quote, so operands split one way.) */
|
|
918
|
+
const OPERAND = String.raw `(?:!?${LINK}(?:\([^()"'\n]{0,200}\))?|"[^"\n]{0,200}"|'[^'\n]{0,200}'|-?\d{1,20}(?:\.\d{1,20})?)`;
|
|
919
|
+
/** Operands joined by operators, a ternary's among them. */
|
|
920
|
+
const OPERATION = new RegExp(String.raw `^\(?${OPERAND}(?:\s(?:\|\||&&|\?\?|!=|==|->|\?|:|or|and)\s${OPERAND})+\)?$`);
|
|
921
|
+
const PY_CONDITIONAL = new RegExp(String.raw `^${OPERAND}\s+if\s+\S[^\n]{0,200}?\s+else\s+${OPERAND}$`);
|
|
922
|
+
/**
|
|
923
|
+
* A path subscripted by a key in quotes or a symbol, then more members or subscripts: `parts['password']`,
|
|
924
|
+
* `$row["pwd"]`, `cfg["db"]["pw"]`, `params[:password]`. (Not by a number or a bare word: a password may hold
|
|
925
|
+
* brackets, `Summer[2024]`.)
|
|
926
|
+
*/
|
|
927
|
+
const KEY_INDEX = String.raw `\[(?:"[\w.-]{1,80}"|'[\w.-]{1,80}'|:[A-Za-z_]\w{0,80})\]`;
|
|
928
|
+
const SUBSCRIPT = new RegExp(String.raw `^${IDENT_PATH}${KEY_INDEX}(?:${MEMBER}|${KEY_INDEX})*,?$`);
|
|
929
|
+
/** A keyword and what it takes: a constructor's call, an awaited call or member, a module, a function's parameters and body, a lambda. */
|
|
930
|
+
const KEYWORD_EXPRESSION = new RegExp(String.raw `^(?:new[ \t]+${IDENT_PATH}\(.*\)|await[ \t]+${IDENT_PATH}(?:\(.*\)|${MEMBER})|(?:require|import)\([ \t]*(["'\x60])[^"'\x60\n]+\1[ \t]*\)|(?:function\b[ \t]*[\w$]*|fn)[ \t]*\([^()]*\)[ \t]*(?:\{.*\}|->.+)|lambda\b[^:\n]{0,80}:[ \t]*\S.*)$`);
|
|
931
|
+
/** Code in any context. */
|
|
932
|
+
const CODE_SHAPES = [
|
|
933
|
+
// `generate_password_hash(self.password.data)`, `getenv(KEY, default)`, `getSecret(names[0])`; or a variable quoted,
|
|
934
|
+
// `password('%passwd%')`, `md5("$pw")`.
|
|
935
|
+
[
|
|
936
|
+
"a call whose arguments are names",
|
|
937
|
+
(v) => {
|
|
938
|
+
const m = /^([A-Za-z_][\w.]*)\((.*)\)$/.exec(v);
|
|
939
|
+
return (!!m &&
|
|
940
|
+
/[a-z_]$/.test(m[1]) &&
|
|
941
|
+
hasWord(m[1]) &&
|
|
942
|
+
(!m[2].trim() ||
|
|
943
|
+
m[2]
|
|
944
|
+
.split(",")
|
|
945
|
+
.map((a) => a.trim())
|
|
946
|
+
.every((a) => /^[A-Za-z_][\w.]*(?:\(\)|\[\w{1,20}\])?$/.test(a) ||
|
|
947
|
+
(/^(["'`]).+\1$/.test(a) && isInert(a.slice(1, -1))))));
|
|
948
|
+
},
|
|
949
|
+
],
|
|
950
|
+
// Go's pointer to a qualified type or a slice of one, `*corev1.Secret`, `[]byte`: no password starts so.
|
|
951
|
+
[
|
|
952
|
+
"a pointer or slice type",
|
|
953
|
+
(v) => /^(?:[*&]|\[\])(?:\[\])*[a-z][a-z0-9]*(?:\.[A-Z][A-Za-z0-9]*)?$/.test(v) && readsAsWords(v),
|
|
954
|
+
],
|
|
955
|
+
// `!local.is_replica`, `var.db_password`, `module.db.password`.
|
|
956
|
+
["a Terraform reference", (v) => /^!?(?:local|var|module|data|each|count|path|terraform)\.[\w.[\]"-]+$/.test(v)],
|
|
957
|
+
[
|
|
958
|
+
"a C cast",
|
|
959
|
+
(v) => /^\((?:const\s+)?(?:unsigned\s+)?(?:char|wchar_t|void|int|uint8_t|u_char|BYTE|LPCSTR|LPSTR|LPWSTR|LPCWSTR|string|String|byte|NSString)\s*\*{0,2}\)?$/.test(v),
|
|
960
|
+
],
|
|
961
|
+
// `Get-Content .\\secrets.txt`, `ConvertTo-SecureString $pw -AsPlainText`: the cmdlet, before its arguments (alone,
|
|
962
|
+
// only where code is written: `DB_PASSWORD=Copy-Paste` is a password), or opening a subexpression, which says code
|
|
963
|
+
// anywhere: `-Password (ConvertTo-SecureString`, as a flag's value is cut at its blank.
|
|
964
|
+
[
|
|
965
|
+
"a PowerShell cmdlet",
|
|
966
|
+
(v, literal) => {
|
|
967
|
+
const m = /^(\$?\()?((?:Get|Set|New|Remove|ConvertTo|ConvertFrom|Invoke|Start|Stop|Write|Read|Test|Add|Import|Export|Out|Select|Where|ForEach|Format|Join|Split|Resolve|Enter|Exit|Measure|Sort|Group|Update|Register|Enable|Disable|Install|Restart|Wait|Clear|Copy|Move|Rename|Compare|Find|Save|Send|Receive|Connect|Protect|Unprotect)-[A-Z][A-Za-z]+)(?:$|[ \t]+[-$.\\"'A-Za-z])/.exec(v);
|
|
968
|
+
return !!m && (!literal || !!m[1] || /[ \t]/.test(v)) && readsAsWords(m[2]);
|
|
969
|
+
},
|
|
970
|
+
],
|
|
971
|
+
// `user != null ? user.password : ""`, `cfg.pw == nil`, `$this -> password` (`or` and `and` only where code is written).
|
|
972
|
+
[
|
|
973
|
+
"an operator between operands",
|
|
974
|
+
(v, literal) => OPERATION.test(v) && (!literal || /\s(?:\|\||&&|\?\?|!=|==|->|\?)\s/.test(v)),
|
|
975
|
+
],
|
|
976
|
+
// `$pw | base64`, `${TOKEN} | jq -r .access_token`.
|
|
977
|
+
["a shell variable piped", (v) => /^\$\{?[A-Za-z_]\w*\}?[ \t]*\|[ \t]*[a-z][\w.-]*(?:[ \t].*)?$/.test(v)],
|
|
978
|
+
// `PasswordField`, `SecretManager2`: a camel-case class name, unquoted in code.
|
|
979
|
+
[
|
|
980
|
+
"a class name",
|
|
981
|
+
(v, literal) => !literal &&
|
|
982
|
+
/^[A-Z][A-Za-z0-9]{2,40}$/.test(v) &&
|
|
983
|
+
/[a-z][A-Z]/.test(v) &&
|
|
984
|
+
naturalness(v) > (/\d/.test(v) ? 0.5 : 1.2),
|
|
985
|
+
],
|
|
986
|
+
["an environment lookup", (v) => ENV_LOOKUP.test(v)],
|
|
987
|
+
// `new Buffer(pw, "base64")`, `await vault.read(path)`, `require("crypto")`, `function () { … }`, `lambda: os.environ["PW"]`:
|
|
988
|
+
// the keyword and the code it takes, whole (`fn(Harbor)2291`, `new moon rising` aren't).
|
|
989
|
+
["a keyword expression", (v) => isBalanced(v) && KEYWORD_EXPRESSION.test(v)],
|
|
990
|
+
// `(await vault.read("db")).data.password`: code in parentheses, and a member of what it gives.
|
|
991
|
+
[
|
|
992
|
+
"a member of an expression",
|
|
993
|
+
(v, literal) => {
|
|
994
|
+
const m = /^\((.+)\)((?:\??\.[A-Za-z_$][\w$]*)+)$/.exec(v);
|
|
995
|
+
return !!m && isBalanced(m[1]) && isNameLike(m[2]) && isCode(m[1], literal);
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
// `function password() {`, `async function* gen(a, b)`, a shell function's `() {`: a function's head, its body below.
|
|
999
|
+
[
|
|
1000
|
+
"a function's head",
|
|
1001
|
+
(v) => /^(?:async[ \t]+)?function\b[ \t]*\*?[ \t]*[\w$]*[ \t]*\([\w$, \t]*\)[ \t]*\{?$|^\(\)[ \t]*\{$/.test(v),
|
|
1002
|
+
],
|
|
1003
|
+
// `%RANDOM%@%RANDOM%`: variables and the punctuation between them.
|
|
1004
|
+
["expansions only", isExpansionsOnly],
|
|
1005
|
+
// `quote(pw) if pw else None`: code on either side (a call, a member, a literal), so `rain if sun else snow` isn't.
|
|
1006
|
+
["a Python conditional", (v) => PY_CONDITIONAL.test(v) && /[.([\]"']|\b(?:None|True|False)\b/.test(v)],
|
|
1007
|
+
// `password: SecretStr`, `db_password: Optional[str]`, `password: string | null;`, `key: &'a [u8],`, `PasswordField`;
|
|
1008
|
+
// and where code is written `ClientSecret`, `SecretString`, `PasswordType`. (Alone elsewhere, words in title case
|
|
1009
|
+
// ending so are a passphrase: `DB_PASSWORD=LittleSecret`, `The password is: HarborModel`.)
|
|
1010
|
+
["a type", (v, literal) => isTypeExpression(v, literal ? "marked" : "any")],
|
|
1011
|
+
// `@options.password`.
|
|
1012
|
+
["a Ruby member", (v) => /^@[A-Za-z_]\w*(?:\.\w+)+$/.test(v) && isNameLike(v.slice(1))],
|
|
1013
|
+
// `@user_password`, unquoted in code.
|
|
1014
|
+
["a Ruby instance variable", (v, literal) => !literal && /^@{1,2}[a-z]\w*_\w+\)?$|^@{1,2}[a-z_]\w*\)$/.test(v)],
|
|
1015
|
+
// `password||options[:password]||self.get_password`, `dest.password or self.password`: every link an identifier path
|
|
1016
|
+
// (and with Python's `or` and `and`, one of them a member path, so a passphrase of words isn't code).
|
|
1017
|
+
[
|
|
1018
|
+
"an alternatives chain",
|
|
1019
|
+
(v) => ALTERNATIVES.test(v) &&
|
|
1020
|
+
(!/\s(?:or|and)\s/.test(v) || /\.\w/.test(v)) &&
|
|
1021
|
+
v
|
|
1022
|
+
.replace(/\)$/, "")
|
|
1023
|
+
.split(/\|\||&&|\?\?|\s(?:or|and)\s/)
|
|
1024
|
+
.every((p) => isNameLike(p.trim())) &&
|
|
1025
|
+
hasWord(v),
|
|
1026
|
+
],
|
|
1027
|
+
// `t.authorizePassword=t.preAuthorizeImplicit=…` (two links make it code anywhere), its targets names
|
|
1028
|
+
// (`xo.n0B46v=AqN=` is base64).
|
|
1029
|
+
[
|
|
1030
|
+
"a chained assignment",
|
|
1031
|
+
(v, literal) => new RegExp(String.raw `^${IDENT}(?:${MEMBER})+=(?!=)`).test(v) &&
|
|
1032
|
+
(!literal || new RegExp(String.raw `^${IDENT}(?:${MEMBER})+=${IDENT_PATH}=`).test(v)) &&
|
|
1033
|
+
v
|
|
1034
|
+
.split("=")
|
|
1035
|
+
.slice(0, literal ? 2 : 1)
|
|
1036
|
+
.every(isNameLike) &&
|
|
1037
|
+
hasWord(v.split("=")[0]),
|
|
1038
|
+
],
|
|
1039
|
+
];
|
|
1040
|
+
/** An argument of a lookup call: a name or a quoted name. (Commas between them: run together, two names are one.) */
|
|
1041
|
+
const LOOKUP_ARG = String.raw `[ \t]*(?:"[\w.:/-]{1,80}"|'[\w.:/-]{1,80}'|[A-Za-z_][\w.]{0,80})[ \t]*`;
|
|
1042
|
+
/** References, code in a literal context too: `$(cat /run/secrets/db)`, `${{ secrets.X }}`, `credentials.value`, `credentials.get('password')`. */
|
|
1043
|
+
const REFERENCES = [
|
|
1044
|
+
["a command substitution", (v) => isCommandSubstitution(v)],
|
|
1045
|
+
["a workflow expression", (v) => /^\$\{\{.*\}\}$/.test(v)],
|
|
1046
|
+
[
|
|
1047
|
+
"a member path",
|
|
1048
|
+
(v) => MEMBER_PATH.test(v) &&
|
|
1049
|
+
!/\d/.test(v) &&
|
|
1050
|
+
isNameLike(v) &&
|
|
1051
|
+
/^(?:self|this|cls|config|settings|options|opts|params|args|kwargs|request|req|ctx|context|credentials|creds|env|os|process|app|data|user|secrets|auth|form|cfg|conf)\./.test(v),
|
|
1052
|
+
],
|
|
1053
|
+
[
|
|
1054
|
+
"a member call",
|
|
1055
|
+
(v) => new RegExp(String.raw `^${IDENT}(?:${MEMBER})+\(.*\),?$`).test(v) &&
|
|
1056
|
+
isBalanced(v) &&
|
|
1057
|
+
hasWord(v.slice(0, v.indexOf("("))),
|
|
1058
|
+
],
|
|
1059
|
+
// `password: data["password"]`, `pwd: $row['pwd']`, `params[:password]`.
|
|
1060
|
+
["a subscript by key", (v) => SUBSCRIPT.test(v)],
|
|
1061
|
+
// `EnvVar("SNOWFLAKE_PASSWORD")`, `Secret(name)`: a capitalised call of names (`Summer(2024)` is a password).
|
|
1062
|
+
[
|
|
1063
|
+
"a lookup call",
|
|
1064
|
+
(v) => new RegExp(String.raw `^[A-Z][A-Za-z]{2,40}\(${LOOKUP_ARG}(?:,${LOOKUP_ARG})*,?\),?$`).test(v) &&
|
|
1065
|
+
hasWord(v.slice(0, v.indexOf("("))),
|
|
1066
|
+
],
|
|
1067
|
+
];
|
|
1068
|
+
/** Code only where code is written, not in a literal context. */
|
|
1069
|
+
const CODE_ONLY = [
|
|
1070
|
+
// `oauth2.Token`. (A pointer or a slice is code anywhere.)
|
|
1071
|
+
["a qualified type", (v) => /^[a-z][a-z0-9]*\.[A-Z][A-Za-z0-9]*$/.test(v) && readsAsWords(v)],
|
|
1072
|
+
["a Ruby symbol", (v) => /^:[A-Za-z_]\w*$/.test(v) && isNameLike(v.slice(1))],
|
|
1073
|
+
// `password: !prompt.echo,`: a member negated.
|
|
1074
|
+
[
|
|
1075
|
+
"a negated member",
|
|
1076
|
+
(v) => /^!{1,2}[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(v) && readsAsWords(v.replace(/^!+/, "")),
|
|
1077
|
+
],
|
|
1078
|
+
["a call or a subscript", (v) => new RegExp(String.raw `^${IDENT_PATH}[ \t]*[([]`).test(v)],
|
|
1079
|
+
// `config.password`, `mqttConfig?.password`, `template.users.user2.data.password`.
|
|
1080
|
+
[
|
|
1081
|
+
"member access",
|
|
1082
|
+
(v) => MEMBER_PATH.test(v) && (!/\d/.test(v) || /\?\.|(?:\.[A-Za-z_$][\w$]*){2,}/.test(v)) && isNameLike(v),
|
|
1083
|
+
],
|
|
1084
|
+
// `password: str):`, `token: str) -> None:`.
|
|
1085
|
+
["the end of a Python parameter list", (v) => /\)\s*(?:->\s*[\w.[\], |]+)?:$/.test(v)],
|
|
1086
|
+
// Member access, calls, generics, operators, statement ends.
|
|
1087
|
+
["code's punctuation", (v) => /->|::|[()[\]{}<>;]|\s[-+*/|&?:]{1,2}\s|^\$\w/.test(v)],
|
|
1088
|
+
["a variable", (v) => /^[a-z_$][A-Za-z_$]{0,39}$/.test(v) && /[a-z]/.test(v) && (/[A-Z_]/.test(v) || v.length <= 12)],
|
|
1089
|
+
];
|
|
1090
|
+
/**
|
|
1091
|
+
* Digits in groups, six or more of them, as a PIN or a phone number is written: `4821-7730`, `0171 555 2368`. Under a
|
|
1092
|
+
* password's or a PIN's name, that is the password; elsewhere an ID. (`12/12`, `8-64`, `1.2.3` are counts, lengths and
|
|
1093
|
+
* versions under any name, and an ISO date is the date a listing shows: `password: 2024-05-01`, when it was set.)
|
|
1094
|
+
*/
|
|
1095
|
+
export function isGroupedDigits(value) {
|
|
1096
|
+
return (/^\d{2,8}(?:[-./ ]\d{1,8}){1,4}$/.test(value) &&
|
|
1097
|
+
value.replace(/\D/g, "").length >= 6 &&
|
|
1098
|
+
!/^(?:19|20)\d\d-\d\d-\d\d$/.test(value));
|
|
1099
|
+
}
|
|
1100
|
+
/** The crypt alphabet (`./0-9A-Za-z`), and base64's. */
|
|
1101
|
+
const C = String.raw `[./A-Za-z0-9]`;
|
|
1102
|
+
const B64 = String.raw `[A-Za-z0-9+/]`;
|
|
1103
|
+
/** Modular crypt hashes, `$<id>$…`, each with its salt's and its digest's lengths. */
|
|
1104
|
+
const CRYPT = [
|
|
1105
|
+
["bcrypt", String.raw `\$2[abxy]?\$\d\d\$${C}{53}`],
|
|
1106
|
+
["MD5-crypt, Apache's", String.raw `\$(?:1|apr1)\$${C}{1,8}\$${C}{22}`],
|
|
1107
|
+
[
|
|
1108
|
+
"SHA-crypt",
|
|
1109
|
+
String.raw `\$5\$(?:rounds=\d{1,9}\$)?${C}{1,16}\$${C}{43}|\$6\$(?:rounds=\d{1,9}\$)?${C}{1,16}\$${C}{86}`,
|
|
1110
|
+
],
|
|
1111
|
+
["yescrypt, GOST yescrypt", String.raw `\$g?y\$${C}{3,}\$${C}{1,86}\$${C}{43}`],
|
|
1112
|
+
["scrypt (libxcrypt)", String.raw `\$7\$${C}{11,}\$${C}{43}`],
|
|
1113
|
+
["NT hash", String.raw `\$3\$\$[0-9a-f]{32}`],
|
|
1114
|
+
["argon2", String.raw `\$argon2(?:id|i|d)\$(?:v=\d+\$)?m=\d+,t=\d+,p=\d+\$${B64}{11,}\$${B64}{16,}`],
|
|
1115
|
+
// (passlib writes its own base64 with `.` for `+`; other libraries write the standard one.)
|
|
1116
|
+
[
|
|
1117
|
+
"passlib scrypt and pbkdf2",
|
|
1118
|
+
String.raw `\$scrypt\$ln=\d+,r=\d+,p=\d+\$[./A-Za-z0-9+]{1,64}\$[./A-Za-z0-9+]{16,}={0,2}|\$pbkdf2(?:-sha\d+)?\$\d+\$[./A-Za-z0-9+]{1,64}\$[./A-Za-z0-9+]{27,}={0,2}`,
|
|
1119
|
+
],
|
|
1120
|
+
["SHA1-crypt, Sun MD5", String.raw `\$sha1\$\d+\$${C}{1,64}\$${C}{28}|\$md5(?:,rounds=\d+)?\$${C}{1,8}\$\$?${C}{22}`],
|
|
1121
|
+
[
|
|
1122
|
+
"balloon",
|
|
1123
|
+
String.raw `\$BALLOON(?:-SHA256)?\$v=\d+\$[a-z]=\d+(?:,[a-z]=\d+){0,3}\$${B64}{11,}={0,2}\$${B64}{16,}={0,2}`,
|
|
1124
|
+
],
|
|
1125
|
+
];
|
|
1126
|
+
const CRYPT_HASH = CRYPT.map(([, whole]) => whole).join("|");
|
|
1127
|
+
/**
|
|
1128
|
+
* Password hash formats, each by its whole shape as its software writes it: identifiers, parameters, and a salt and a digest of the lengths it makes (LDAP's
|
|
1129
|
+
* scheme in capitals). A password that only starts like one (`$1$Summer$2024`, `md5$Xk9#mP2vQ!&z`, `{SSHA}Xk9#mP2vQ!&z`)
|
|
1130
|
+
* isn't one.
|
|
1131
|
+
*/
|
|
1132
|
+
const PASSWORD_HASHES = [
|
|
1133
|
+
["MySQL", String.raw `\*[0-9A-F]{40}`],
|
|
1134
|
+
["Cisco type 8 and 9", String.raw `\$[89]\$${C}{14}\$${C}{43}`],
|
|
1135
|
+
["Postgres MD5", String.raw `md5[0-9a-f]{32}`],
|
|
1136
|
+
["Postgres SCRAM", String.raw `SCRAM-SHA-256\$\d+:${B64}+={0,2}\$${B64}+={0,2}:${B64}+={0,2}`],
|
|
1137
|
+
["crypt(3)", CRYPT_HASH],
|
|
1138
|
+
// `pbkdf2_sha256$870000$<salt>$<base64>`, `argon2$argon2id$v=19$m=…$<salt>$<hash>`, `sha1$<salt>$<40 hex>`,
|
|
1139
|
+
// `md5$<salt>$<32 hex>`, `bcrypt_sha256$$2b$…`, `scrypt$16384$<salt>$8$1$<base64>`, `crypt$$<DES crypt>`.
|
|
1140
|
+
[
|
|
1141
|
+
"Django",
|
|
1142
|
+
String.raw `pbkdf2_sha\d+\$\d+\$\w{1,64}\$${B64}{27,88}={0,2}|argon2\$argon2(?:id|i|d)\$v=\d+\$m=\d+,t=\d+,p=\d+\$${B64}{11,}\$${B64}{16,}|sha1\$\w{1,64}\$[0-9a-f]{40}|md5\$\w{1,64}\$[0-9a-f]{32}|bcrypt(?:_sha256)?\$\$2[aby]\$\d\d\$${C}{53}|scrypt\$\d+\$\w{1,64}\$\d+\$\d+\$${B64}{43,88}={0,2}|crypt\$\$${C}{13}`,
|
|
1143
|
+
],
|
|
1144
|
+
// `{SSHA}<base64>`, `{PBKDF2-SHA256}<iterations>$<salt>$<hash>`, `{CRYPT}$6$…`, `{BCRYPT}$2b$…`.
|
|
1145
|
+
[
|
|
1146
|
+
"LDAP",
|
|
1147
|
+
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
|
+
],
|
|
1149
|
+
];
|
|
1150
|
+
const HASH_WHOLE = new RegExp(`^(?:${PASSWORD_HASHES.map(([, whole]) => whole).join("|")})$`);
|
|
1151
|
+
/**
|
|
1152
|
+
* Password hashes in text, as a global search: each format's whole shape, every `$` possibly doubled (Compose escapes
|
|
1153
|
+
* it: `$$2y$$10$$…`), with no hash character on either side (a full stop after it ends a sentence, unless more hash follows).
|
|
1154
|
+
*/
|
|
1155
|
+
export const PASSWORD_HASH_IN_TEXT = new RegExp(String.raw `(?<![\w$./+])(?:${PASSWORD_HASHES.map(([, whole]) => whole)
|
|
1156
|
+
.join("|")
|
|
1157
|
+
.replaceAll(String.raw `\$`, String.raw `\$\$?`)})(?![\w$/+=]|\.[\w$./+=])`, "g");
|
|
1158
|
+
/** Password hashes: bcrypt, argon2, scrypt, crypt(3), Django, LDAP. Sensitive, but not a credential anyone can use. */
|
|
1159
|
+
export function isPasswordHash(value) {
|
|
1160
|
+
return HASH_WHOLE.test(value);
|
|
1161
|
+
}
|
|
1162
|
+
/** Words test suites and docs use as stand-in credentials: `testpass`, `secret123`, `my-api-key`, `abc123`. */
|
|
1163
|
+
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
|
+
/** 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;
|
|
1166
|
+
/**
|
|
1167
|
+
* Digits as a stand-in writes them: one or two, a count (`123`, `1234`, `6789`) or one digit repeated (`000`). A real
|
|
1168
|
+
* password's digits are arbitrary (`Example7392`).
|
|
1169
|
+
*/
|
|
1170
|
+
export function isCountingDigits(digits) {
|
|
1171
|
+
if (digits.length <= 2)
|
|
1172
|
+
return true;
|
|
1173
|
+
const step = Number(digits[1]) - Number(digits[0]);
|
|
1174
|
+
return ((step === 0 || step === 1) && [...digits].every((d, i) => i === 0 || Number(d) - Number(digits[i - 1]) === step));
|
|
1175
|
+
}
|
|
1176
|
+
/** A stand-in credential from a test or a doc: only test words and a few digits as a stand-in writes them. */
|
|
1177
|
+
export function isTestValue(value) {
|
|
1178
|
+
// (The famous four words joined as a fixture spells one identifier, `correct-horse-battery-staple`. Spaced, as the
|
|
1179
|
+
// comic writes them, they are as often someone's passphrase in its style: hidden.)
|
|
1180
|
+
if (/^correct[-_.]horse[-_.]battery[-_.]staple$/.test(value))
|
|
1181
|
+
return true;
|
|
1182
|
+
if (/^(?:open ?sesame|hunter2|swordfish|letmein|trustno1|p@ssw0rd|passw0rd|tr0ub4dor&3)$/i.test(value))
|
|
1183
|
+
return true;
|
|
1184
|
+
// `mypassword`, `testpass1`, `thePwd`, `yourpasswordhere`, `StrongPasswordHere`, `secret-key-here`: a placeholder
|
|
1185
|
+
// phrase run together.
|
|
1186
|
+
if ((/^(?:my|your|the|some|test|dummy|fake|example|sample|default|secret|super|strong|new|good|real|actual)[-_]?(?:password|passwd|pass|pwd|pw|secret|token|key|apikey|api[-_]?key)(?:\d{1,4}|[-_]?here)?$/i.test(value) &&
|
|
1187
|
+
isCountingDigits(/\d*$/.exec(value)[0])) ||
|
|
1188
|
+
/(?:^|[-_.])(?:password|secret|token|key)[-_]?here$/i.test(value) ||
|
|
1189
|
+
/(?:^|[a-z0-9])(?:Password|Secret|Token|Key)Here$/.test(value))
|
|
1190
|
+
return true;
|
|
1191
|
+
const parts = value.split(/[-_.: ]+|(?<=[a-z])(?=[A-Z0-9])|(?<=\d)(?=[A-Za-z])/).filter(Boolean);
|
|
1192
|
+
return (parts.length > 0 &&
|
|
1193
|
+
parts.every((p) => TEST_WORD.test(p) || (/^\d{1,4}$/.test(p) && isCountingDigits(p)) || /^[!@#$*]+$/.test(p)) &&
|
|
1194
|
+
parts.some((p) => TEST_MARKER.test(p)));
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Names built from short parts: `BOB_sect163r2`, `KAS-ECC-CDH_P-256_C14`, `ED448-1-PUBLIC-Raw`, `ALICE_cf_c2pnb368w1`,
|
|
1198
|
+
* `us-east-1a`, `login_and_password_removed`. The first part starts with a letter, and every part reads as a name
|
|
1199
|
+
* part: short, a number, lower-case hex, a word, an acronym, letters then digits (`amd64`, `SECP521R1`), or a word then
|
|
1200
|
+
* a code (`brainpoolP224r1`). Beside a word (`ALICE`, `PUBLIC`), any part of one case with digits is a code
|
|
1201
|
+
* (`c2pnb368w1`). A random part mixes cases with digits (`JWPa`, `Xk9mP2`), and a product key's groups are random
|
|
1202
|
+
* letters and digits of one length (`VKXQR-8B2MP-7WJ4T`): not names.
|
|
1203
|
+
*/
|
|
1204
|
+
export function isIdentifierShaped(value) {
|
|
1205
|
+
const parts = value.split(/[-_.:]/);
|
|
1206
|
+
if (parts.length < 2 ||
|
|
1207
|
+
!/^[A-Za-z]/.test(parts[0]) ||
|
|
1208
|
+
!parts.every((p) => /^[A-Za-z0-9]*$/.test(p)) ||
|
|
1209
|
+
isKeyGroups(parts))
|
|
1210
|
+
return false;
|
|
1211
|
+
const word = parts.some((p) => p.length >= 3 && /^[A-Za-z]+$/.test(p) && naturalness(p.toLowerCase()) > 0.3);
|
|
1212
|
+
const named = (p) => p.length <= 3 ||
|
|
1213
|
+
/^[0-9a-f]+$/.test(p) ||
|
|
1214
|
+
/^[A-Z]{2,5}$/.test(p) ||
|
|
1215
|
+
/^(?:[a-z]+|[A-Z]+)\d+[A-Za-z]?\d*$/.test(p) ||
|
|
1216
|
+
naturalness(p) > 0 ||
|
|
1217
|
+
(word && /^(?:[a-z][a-z0-9]*|[A-Z][A-Z0-9]*)$/.test(p));
|
|
1218
|
+
const wordThenCode = (p) => /^[a-z]{4,}[A-Z0-9][A-Za-z0-9]{0,7}$/.test(p) && naturalness(/^[a-z]+/.exec(p)[0]) > 0.3;
|
|
1219
|
+
const random = (p) => (/[a-z]/.test(p) && /[A-Z]/.test(p) && /\d/.test(p)) ||
|
|
1220
|
+
(/[a-z]/.test(p) && /[A-Z].*[A-Z]/.test(p.slice(1)) && p.length > 6);
|
|
1221
|
+
return parts.every((p) => (p.length <= 12 && named(p) && !random(p)) || (p.length <= 16 && wordThenCode(p)));
|
|
1222
|
+
}
|
|
1223
|
+
/** A product or licence key's groups: three or more of one length (four or more), random letters and digits, no word among them. */
|
|
1224
|
+
function isKeyGroups(parts) {
|
|
1225
|
+
if (parts.length < 3 || parts[0].length < 4 || !parts.every((p) => p.length === parts[0].length))
|
|
1226
|
+
return false;
|
|
1227
|
+
return (parts.filter((p) => /\d/.test(p) && /[A-Za-z]/.test(p)).length >= 2 &&
|
|
1228
|
+
!parts.some((p) => /^[A-Za-z]+$/.test(p) && naturalness(p) > 0.5));
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* A Kubernetes pod's name: its workload's name, then the generated suffixes of a ReplicaSet (6–10 characters)
|
|
1232
|
+
* and of the pod (5): `ledger-api-58d4f7c9b-rbjl3`, `search-i12seefem-wq3g4`. (A word in the workload's name: its first
|
|
1233
|
+
* part may be none, `k8s-agent-…`.)
|
|
1234
|
+
*/
|
|
1235
|
+
export function isPodName(value) {
|
|
1236
|
+
return (/^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*-[a-z0-9]{6,10}-[a-z0-9]{5}$/.test(value) &&
|
|
1237
|
+
value
|
|
1238
|
+
.split("-")
|
|
1239
|
+
.slice(0, -2)
|
|
1240
|
+
.some((part) => naturalness(part) > 0));
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* A lower-case letter, a capital and a digit: the classes a password rule asks for, in a password made of words
|
|
1244
|
+
* (`Maple-Orbit-42`, `Harvest2025`) or by a generator (`Xk9mP2vQ7`).
|
|
1245
|
+
*/
|
|
1246
|
+
export function hasThreeClasses(value) {
|
|
1247
|
+
return /[a-z]/.test(value) && /[A-Z]/.test(value) && /\d/.test(value);
|
|
1248
|
+
}
|
|
1249
|
+
/** How many of the four character classes a value has: lower case, upper case, digits, symbols. */
|
|
1250
|
+
function characterClasses(value) {
|
|
1251
|
+
return [/[a-z]/, /[A-Z]/, /\d/, /[^A-Za-z0-9\s]/].filter((r) => r.test(value)).length;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* A password made to a password rule's demands: eight characters or more of three classes, a symbol among them (not
|
|
1255
|
+
* only a name's `.` `_` `-`), and neither code nor words: `CANYpfso8s%`, `Harbor2291!`.
|
|
1256
|
+
*/
|
|
1257
|
+
export function isMadePassword(value) {
|
|
1258
|
+
return (value.length >= 8 &&
|
|
1259
|
+
characterClasses(value) >= 3 &&
|
|
1260
|
+
/[^A-Za-z0-9._-]/.test(value) &&
|
|
1261
|
+
!isCode(value, true) &&
|
|
1262
|
+
naturalness(value) < 1);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* 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`.
|
|
1267
|
+
*/
|
|
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;
|
|
1269
|
+
/** A format string's hole: `%d`, `%1$s`, `{0}`, `{{count}}`, `%{n}`. */
|
|
1270
|
+
const FORMAT_HOLE = /%(?:\d+\$)?[-+ 0#]*\d*(?:\.\d+)?[sdifuxXc@]|\{\{?\s*\w*\s*\}?\}|%\{\w+\}/;
|
|
1271
|
+
/** "Password" in another language, anywhere in a value. */
|
|
1272
|
+
const PASSWORD_WORD = new RegExp(FOREIGN_PASSWORD_SPELLINGS.join("|"), "iu");
|
|
1273
|
+
/** A number with the word that counts in CJK: `6位`, `8文字`, `3回`, `8자`, `2번`. */
|
|
1274
|
+
export const CJK_COUNT = /\d+(?:位|文字|字符?|个|個|次|回|桁|件|名|人|歩|步|秒|分|日|天|月|年|時間|小时|자리?|번|개|회|글자)/u;
|
|
1275
|
+
/**
|
|
1276
|
+
* A password in a script other than Latin: letters with digits or ASCII symbols glued among them (`Солнце2024!`,
|
|
1277
|
+
* `密码2024!`, `Зима1240Солнце#Луна`, `ネコ*ラーメン*`), and nothing a UI string has: no blank, no full-width bracket, no CJK
|
|
1278
|
+
* punctuation, no format hole, no number counting what follows (`请输入6位密码`, `8文字以上`), and not just a word with
|
|
1279
|
+
* a required field's star (`パスワード*`).
|
|
1280
|
+
*/
|
|
1281
|
+
function isForeignPassword(v) {
|
|
1282
|
+
// (The word "password" itself, with no digit, is a label's: `密码*必填`, `パスワード*`.)
|
|
1283
|
+
if (PASSWORD_WORD.test(v) && !/\d/.test(v))
|
|
1284
|
+
return false;
|
|
1285
|
+
return (!FORMAT_HOLE.test(v) &&
|
|
1286
|
+
!/[\s()【】「」『』〔〕[]{}、。,!?:;〜]/u.test(v) &&
|
|
1287
|
+
/[\d!@#$%^&*+=?_.~-]/.test(v.replace(/^\*+|\*+$/g, "")) &&
|
|
1288
|
+
!CJK_COUNT.test(v));
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Text in a script other than Latin: a label or a sentence (`アクセストークン`, `тот,`, `Введите пароль`, `パスワード*`,
|
|
1292
|
+
* `密码错误,还剩%d次机会`, `は変更して下さい。`), unless it is a password in that script. Mostly in that script, or
|
|
1293
|
+
* mixed with Latin words and no digit (`パスワード/Password`, `【tomcat-users.xmlで設定したパスワード】`).
|
|
1294
|
+
*/
|
|
1295
|
+
function isForeignText(v) {
|
|
1296
|
+
const foreign = v.match(/(?!\p{Script=Latin})\p{L}/gu)?.length ?? 0;
|
|
1297
|
+
// (Mixed with Latin: no digit, and each long Latin run a word, not random letters among the foreign ones.)
|
|
1298
|
+
const latinWords = !/\d/.test(v) && (v.match(/\p{Script=Latin}{5,}/gu) ?? []).every((run) => naturalness(run) > 0.3);
|
|
1299
|
+
return foreign > 0 && (foreign * 2 >= (v.match(/\p{L}/gu)?.length ?? 0) || latinWords) && !isForeignPassword(v);
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* A quantity written with its unit: a ratio (`3.52x`), a price (`~$280`), a test count and its share (`40/42 —
|
|
1303
|
+
* 95.2%`). Benign, except under a password's name, where `1337x` and `$4821` are what was chosen. (A PIN's value is
|
|
1304
|
+
* digits: named.ts proposes no other.)
|
|
1305
|
+
*/
|
|
1306
|
+
export const QUANTITY = /^\d+(?:\.\d+)?x$|^~?[$€£¥]\d[\d,.]*[kKmM]?$|^\d+\/\d+[ \t]*[—–-][ \t]*\d+(?:\.\d+)?%$/;
|
|
1307
|
+
/** HTML's elements, opened with an attribute or closed: `<input name="password" …>`, `any</span>`, a comment's end. */
|
|
1308
|
+
const HTML_ELEMENT = "a|b|i|p|br|hr|em|li|ol|ul|td|th|tr|h[1-6]|div|span|img|form|input|button|label|select|option|textarea|table|thead|tbody|script|style|link|meta|strong|small|code|pre|iframe|section|header|footer|nav|main|article";
|
|
1309
|
+
const HTML_MARKUP = new RegExp(String.raw `^<(?:${HTML_ELEMENT})(?:\s[^<>]*=[^<>]*)?\/?>|<\/(?:${HTML_ELEMENT})>|\s-->$`, "i");
|
|
1310
|
+
/**
|
|
1311
|
+
* Bytes percent-encoded, none a letter or a digit, fewer than a key has: a test's `%07%7F`, a URL's `%E2%9C%93` (✓).
|
|
1312
|
+
*/
|
|
1313
|
+
function isEscapedSymbols(v) {
|
|
1314
|
+
const m = /^(?:%[0-9A-Fa-f]{2}){1,12}$/.exec(v);
|
|
1315
|
+
return (!!m && v.match(/%[0-9A-Fa-f]{2}/g).every((e) => !/[0-9A-Za-z]/.test(String.fromCharCode(parseInt(e.slice(1), 16)))));
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* A package's path, a host and then words: Go's `github.com/hashicorp/go-secure-stdlib/password`. (No piece is long
|
|
1319
|
+
* and unreadable, as a webhook's token in `hooks.example.com/<token>` is; at least half read as words.)
|
|
1320
|
+
*/
|
|
1321
|
+
function isPackagePath(v) {
|
|
1322
|
+
const m = /^(?:[a-z0-9-]+\.)+[a-z]{2,}((?:\/[a-z0-9._~-]+)+)$/.exec(v);
|
|
1323
|
+
if (!m)
|
|
1324
|
+
return false;
|
|
1325
|
+
const pieces = m[1].split(/[/._~-]/).filter(Boolean);
|
|
1326
|
+
const words = pieces.filter((w) => naturalness(w) > 0.5).length;
|
|
1327
|
+
return words * 2 >= pieces.length && pieces.every((w) => w.length <= 12 || naturalness(w) > 0.5);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Values that are never a secret, whatever names them and whichever rule read them: counts, ratios and
|
|
1331
|
+
* percentages (`12/12`, `98.5%`), ranges (`8-64`), versions (`v2.4.1`), dates and times, algorithm and
|
|
1332
|
+
* encoding names, key chords (`ctrl+shift+F12`, `ctrl-p,ctrl-q`), type annotations (`str|None`), cloud regions, the
|
|
1333
|
+
* words of a password rule (`min.`, `at least`), and code or markup that a reader can take for a value: regular
|
|
1334
|
+
* expressions, package and class paths, IAM actions (`iam:ChangePassword`), XAML bindings, HTML elements, tickets
|
|
1335
|
+
* (`[GH-784]`), hash schemes (`md5$`), short escaped or percent-encoded bytes, hex numbers, constants and the brackets
|
|
1336
|
+
* after them (`None):`). Every name- and structure-based finding passes this
|
|
1337
|
+
* gate (policy.ts keepReason), so a new rule can't bring back an over-redaction another rule already ruled out.
|
|
1338
|
+
* Values from secret files and formats don't: there the file or the vendor format says what it is.
|
|
1339
|
+
*/
|
|
1340
|
+
export function isBenignValue(value) {
|
|
1341
|
+
const v = value.trim().replace(/[.,;:!?)]+$/, "");
|
|
1342
|
+
return (/^\d+(?:[/.:]\d+)+%?$|^\d+(?:\.\d+)?%$/.test(v) || // 12/12, 98.5%, 10:30, 1.2.3.4
|
|
1343
|
+
/^\d{1,4}-\d{1,4}$/.test(v) || // 8-64
|
|
1344
|
+
/^\d{1,7}[-:]$/.test(value.trim()) || // a listing's line number, `1800-` in grep's context lines
|
|
1345
|
+
/^\(?(?:e\.g|i\.e|etc|cf|viz)\.?$/i.test(v) || // an abbreviation, `(e.g`
|
|
1346
|
+
QUANTITY.test(v) ||
|
|
1347
|
+
// v2.4.1, 1.0.0-rc1, and PEP 440's pre-, post- and dev-releases: 4.12.0b2, 1.1.0rc3, 0.1.dev3, 1.0.post1.
|
|
1348
|
+
/^v?\d+(?:\.\d+){1,3}(?:[-.]?(?:a|b|c|rc|alpha|beta|pre|preview|dev|post)\d*)*(?:-(?:alpha|beta|rc|dev|pre)[.\w]*)?$/i.test(v) ||
|
|
1349
|
+
// Debian and RPM versions, a repack's suffix among them: 3.1.6+~cs8.7.18-1, 1:2.3-4.el9, 1:1.2.11.dfsg-2ubuntu9.2.
|
|
1350
|
+
/^(?:\d+:)?\d+(?:\.\d+)+(?:\.[a-z]+\d*)?(?:[-+~][\w.]*)+$/.test(v) ||
|
|
1351
|
+
/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/.test(v) || // dates
|
|
1352
|
+
ALGORITHM.test(v) ||
|
|
1353
|
+
/^(?:(?:ctrl|cmd|alt|shift|meta|option|opt|super|mod|win|control|command)\+)+\S{1,12}$/i.test(v) || // key chords
|
|
1354
|
+
// 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
|
+
// `Down|Ctrl-N`, `Command-Left|Home|Ctrl-A`. (A key named alone, `Home`, only in a list.)
|
|
1356
|
+
/^(?:(?:(?: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) ||
|
|
1357
|
+
(/\|/.test(v) &&
|
|
1358
|
+
/^(?:(?:(?:ctrl|cmd|alt|shift|meta|option|opt|super|control|command)-)*(?:f\d{1,2}|enter|esc|escape|tab|space|up|down|left|right|home|end|delete|del|backspace|pageup|pagedown|insert|[a-z0-9])(?:\||$))+$/i.test(v)) ||
|
|
1359
|
+
/^\[[A-Z][A-Z0-9]{1,9}-\d{1,6}\]$/.test(v) || // a ticket, `[GH-784]`, `[PROJ-1234]`
|
|
1360
|
+
// A constant and the brackets that close its call or literal, read past the value: `password=None):`, `True))[:-2]`;
|
|
1361
|
+
// or compared, a ternary's branches after it: `password2==null?password:password2`.
|
|
1362
|
+
/^(?:None|True|False|null|nil|undefined)(?:[)\]}][)\]},;:[\]\d-]*|\?[\w$.]+:[\w$.]+[)\]},;]*)$/.test(value.trim()) ||
|
|
1363
|
+
// A regular expression: a group's construct or a character range, `(?:%s)`, `[a-zA-Z_][a-zA-Z0-9_-]*`.
|
|
1364
|
+
/\(\?(?:[:=!]|<[=!])|\[\^?(?:a-z|A-Z|0-9|a-f|A-F|\\[dwsDWS])/.test(v) ||
|
|
1365
|
+
/^\{[!-]?[A-Za-z_$][\w$.]*\}\/?>$/.test(value.trim()) || // a JSX attribute's expression with its tag's end, `key={idx}>`
|
|
1366
|
+
isPackagePath(v) ||
|
|
1367
|
+
// An AWS IAM action: a service, then a verb and what it acts on, `iam:ChangePassword`, `ec2:GetPasswordData`.
|
|
1368
|
+
/^[a-z][a-z0-9-]{1,40}:(?:Get|Put|List|Create|Delete|Describe|Update|Set|Change|Reset|Admin|Associate|Disassociate|Attach|Detach|Enable|Disable|Start|Stop|Tag|Untag|Add|Remove|Modify|Invoke|Retrieve|Send|Receive|Register|Deregister|Import|Export|Run|Batch|Decrypt|Encrypt|Generate|Assume|Pass|Rotate|Restore|Copy|Upload|Download|Respond|Confirm|Initiate|Verify|Validate|Accept|Reject|Cancel|Publish|Subscribe|Unsubscribe|Search|Query|Scan|Execute)[A-Z][A-Za-z]*$/.test(v) ||
|
|
1369
|
+
// A class by its module's path, `allauth.account.forms.SetPasswordForm`.
|
|
1370
|
+
/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)+\.[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]*)*$/.test(v) ||
|
|
1371
|
+
// XAML's markup extensions, `{Binding LightBrush, Mode=OneWay}`, `{StaticResource Brush}`.
|
|
1372
|
+
/^\{(?:Binding|MultiBinding|StaticResource|DynamicResource|ThemeResource|TemplateBinding|RelativeSource|x:Static|x:Bind|x:Null|x:Type)\b[^{}]*\}$/.test(value.trim()) ||
|
|
1373
|
+
/^-?0x[0-9a-f]{1,16}$/i.test(v) || // a number in hex, `-0x3f879678`
|
|
1374
|
+
isEscapedSymbols(v) ||
|
|
1375
|
+
// Markup: an HTML element, `<input name="password" type="password">`, or text closing one or a comment, `any</span> -->`.
|
|
1376
|
+
HTML_MARKUP.test(value.trim()) ||
|
|
1377
|
+
/^\$\{\([^()]*\)[^{}]*\}$/.test(v) || // a shell's expansion with flags, zsh's `${(j:-:)words}`
|
|
1378
|
+
/^(?:md5|sha1|sha224|sha256|sha384|sha512|bcrypt|bcrypt_sha256|crypt|scrypt|argon2(?:id|i|d)?|pbkdf2_sha\d+|unsalted_\w+)\$+$/.test(v) || // a hash's scheme, `password__startswith="md5$"`
|
|
1379
|
+
// A type with its punctuation, `str|None`, `Optional[str]`, `string | null;`, or one before its default, read up to
|
|
1380
|
+
// the `=`: `password: str=''`, `token: str | None=None`. (A name alone may be a password in a literal context,
|
|
1381
|
+
// `password: Uint8Array`: isCode reads one where code is written.)
|
|
1382
|
+
(/[^\w\s]/.test(value.trim().replace(/[;,]$/, "")) && isTypeExpression(value.trim().replace(/[ \t]*[=;,]$/, ""))) ||
|
|
1383
|
+
// A parameter's type in API docs, with its note: `string (required)`, `boolean (optional)`, `SecureString`.
|
|
1384
|
+
/^(?:string|str|integer|int|number|boolean|bool|object|array|list|map|dict|bytes|float|double|long|any|secret|securestring)(?:\s*\((?:required|optional|sensitive|secret|read-only|write-only|default[^()]{0,30})\)?)?$/i.test(v) ||
|
|
1385
|
+
/^(?:us|eu|ap|sa|ca|me|af|il|cn|mx)-(?:gov-)?(?:north|south|east|west|central|northeast|southeast|northwest|southwest)-\d$|^(?:us|europe|asia|australia|northamerica|southamerica|me|africa)-[a-z]+\d$/.test(v) || // regions
|
|
1386
|
+
// A version range, with a package manager's protocol: `npm:^4.0.0`, `workspace:*`, `>=1.2.0 <2`, `~1.4 || ^2`.
|
|
1387
|
+
SEMVER_RANGE.test(v) ||
|
|
1388
|
+
// A length in a password policy: `min 8`, `minimum: 12 characters`, `between 8 and 64`, `max`.
|
|
1389
|
+
/^(?:min|max|minimum|maximum|at least|between|up to)(?:\.?[:\s]*\d{1,4}(?:\s*(?:-|and|to)\s*\d{1,4})?(?:\s*(?:chars?|characters|symbols|letters|digits)\.?)?)?$/i.test(v) ||
|
|
1390
|
+
isForeignText(v) ||
|
|
1391
|
+
// SQL and ORM column types, with constraints: `varchar(255)`, `character varying(255) NOT NULL`, `TEXT`.
|
|
1392
|
+
/^(?:(?:var)?char(?:acter)?(?:\s+varying)?(?:\(\d+\))?|n?varchar\((?:\d+|max)\)|varchar2\(\d+(?:\s+(?:byte|char))?\)|text|tinytext|mediumtext|longtext|bytea|blob|binary\(\d+\)|varbinary\(\d+\)|string(?:\(\d+\))?)(?:\s+(?:not\s+null|null|unique|default\s+\S+|collate\s+\S+|primary\s+key|character\s+set\s+\S+))*[,;]?$/i.test(value.trim()) ||
|
|
1393
|
+
// A validation rule list: `required|string|min:8|confirmed`, `required|alpha_num|between:8,64`.
|
|
1394
|
+
(/^[a-z_]+(?::[^|]*)?(?:\|[a-z_]+(?::[^|]*)?)+$/.test(v) &&
|
|
1395
|
+
/(?:^|\|)(?:required|nullable|string|min|max|between|size|confirmed|regex|alpha\w*|email|unique|sometimes|present|filled|same|different)(?::|\||$)/.test(v)) ||
|
|
1396
|
+
/^(?:\\x[0-9a-f]{2}|\\u[0-9a-f]{4}|\\u\{[0-9a-f]{1,6}\}|\\[0-7]{3}){1,3}$/i.test(v) || // `"\x41"`: one to three escaped characters
|
|
1397
|
+
// Bytes escaped, fewer than a key has: an object identifier's encoding, `"\x2B\x06\x01\x05\x05\x07\x03\x01"`.
|
|
1398
|
+
/^(?:\\x[0-9a-f]{2}){4,12}$/i.test(v) ||
|
|
1399
|
+
(/^\*+[\p{L} _-]{1,24}\*+$/u.test(v) &&
|
|
1400
|
+
v
|
|
1401
|
+
.replace(/^\*+|\*+$/g, "")
|
|
1402
|
+
.split(/[ _-]+/)
|
|
1403
|
+
.filter(Boolean)
|
|
1404
|
+
.every((w) => isNameLike(w))) || // `*secret*`, a mask
|
|
1405
|
+
// A label from a form or a settings file: words ending in a colon, or words naming the credential
|
|
1406
|
+
// they label, capitalized as a label is (`Email password (app-specific password recommended)`).
|
|
1407
|
+
// (At most eight words, and the credential word the label's head: a seed phrase holding `secret` isn't one, nor a
|
|
1408
|
+
// passphrase in lower case, `purple monkey dishwasher secret`.)
|
|
1409
|
+
(/^(?:[?>*•-]\s*)?[\p{L}'’-]+(?:\s+[\p{L}'’()-]+){1,7}$/u.test(value.trim().replace(/[::]$/, "")) &&
|
|
1410
|
+
(/[::]$/.test(value.trim()) ||
|
|
1411
|
+
(/^(?:[?>*•-]\s*)?\p{Lu}/u.test(value.trim()) &&
|
|
1412
|
+
/\b(?:password|passphrase|token|secret|api key|pin)(?:\s+(?:setup|settings?|field|prompt|reset|hint|policy|label|placeholder|change|recovery|manager|strength|confirmation|required|missing|expired|invalid|incorrect|page|screen|dialog|form|input))?\s*(?:\([^)]*\))?[::]?$/i.test(value.trim())))) ||
|
|
1413
|
+
// A value built at run time: `Lea5_#{SecureRandom.base58(24)}`, `0x#{key.unpack('H*')[0]}`.
|
|
1414
|
+
(/[#$]\{\s*[A-Za-z_@$][\w@$]*(?:\.[A-Za-z_]\w*|\([^(){}]*\)|\[[^\]{}]*\])+\s*\}/.test(v) &&
|
|
1415
|
+
v.replace(/[#$]\{[^}]*\}/g, "").length < 8) ||
|
|
1416
|
+
(/^\/[A-Za-z][A-Za-z_-]{2,40}$/.test(v) && readsAsIdentifier(v.slice(1))) || // a route: `/changePasswd`
|
|
1417
|
+
// A regular expression, as Python's raw string or read out of its quotes with an escape class quantified among its
|
|
1418
|
+
// anchors and brackets: `r'-?\d+\.?[0-9A-F]*'`, `^\S{8,}$`, `\w{32}\b`.
|
|
1419
|
+
((/^r["'].*["']$/.test(value.trim()) || /\\[dswDSW](?:[*+?]|\{\d+,?\d*\})/.test(v)) &&
|
|
1420
|
+
(value.match(/\\[dswDSWbB]|[[\]()*+?{}|^$]/g)?.length ?? 0) >= 4) ||
|
|
1421
|
+
(/^[#.][A-Za-z][A-Za-z_-]{2,40}$/.test(v) && readsAsIdentifier(v.slice(1))) // a selector: `#lPass`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
// (Lower or camel case, as names are written; `Password1`, `Secret24` are how passwords are.)
|
|
1425
|
+
const CREDENTIAL_PART = /^(?:[a-z]{1,3}_|[A-Z]|\w*_)?(?:pass(?:word|wd|w|phrase)?|pwd|pw|secret|token|credentials?|creds)s?\d{0,2}$/;
|
|
1426
|
+
/**
|
|
1427
|
+
* Unquoted code that refers to a credential by name, maybe ending its statement: `account.password`, `m_password;`,
|
|
1428
|
+
* `md5($password);`, `secrets.db` (policy.ts, for unquoted values only). (Not a credential's member, `passwords.pop`,
|
|
1429
|
+
* `txtPassword.Text`: under an env line's name that may be the password.)
|
|
1430
|
+
*/
|
|
1431
|
+
export function isCredentialReference(value) {
|
|
1432
|
+
const v = value.replace(/;\s*(?:\/\/.*)?$/, "").replace(/,$/, "");
|
|
1433
|
+
if (!CREDENTIAL_REFERENCE.test(v) || !isNameLike(v))
|
|
1434
|
+
return false;
|
|
1435
|
+
// A bare word needs a sign it's code: a sigil, a member, a call, a statement end, an underscore prefix (`m_password`).
|
|
1436
|
+
if (!/[$.(]|->|::/.test(v) && !/;/.test(value) && !/^[a-z]{1,3}_/.test(v))
|
|
1437
|
+
return false;
|
|
1438
|
+
// The credential is what it names last (`account.password`, `get_password()`, `decrypt(m_pwd)`), or what a container
|
|
1439
|
+
// of credentials holds (`credentials.value`, `secrets.db`): `token.Magic144` is a password with a dot in it.
|
|
1440
|
+
const parts = v.split(/[^\w]+/).filter(Boolean);
|
|
1441
|
+
const credential = (p) => CREDENTIAL_PART.test(p) || /[a-z](?:Password|Passwd|Pwd|Secret|Token)s?$/.test(p);
|
|
1442
|
+
return (credential(parts.at(-1)) ||
|
|
1443
|
+
(/^\$?(?:credentials?|creds|secrets?|vault)$/i.test(parts[0]) && /^[a-z_][a-z0-9_]*$/.test(parts.at(-1))));
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* What an encryption tool writes before its ciphertext: SOPS `ENC[AES256_GCM,data:`, Jasypt `ENC(`, Spring Cloud
|
|
1447
|
+
* Config `{cipher}`, Vault's `${VAULT::…::`, IIS configuration encryption `[enc:IISCngProvider:`, DigitalOcean App
|
|
1448
|
+
* Platform's `EV[1:<nonce>:`. A value that starts with one (ENCRYPTED_VALUE), or a token right after one (the random
|
|
1449
|
+
* layer's `encrypted` veto), is ciphertext, whose key isn't there.
|
|
1450
|
+
*/
|
|
1451
|
+
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}\}?$)`);
|
|
1454
|
+
/** Default passwords and words that show up in ordinary output, too common to be anyone's secret. */
|
|
1455
|
+
export const COMMON_WORDS = new Set([
|
|
1456
|
+
"files",
|
|
1457
|
+
"systemd",
|
|
1458
|
+
"sss",
|
|
1459
|
+
"compat",
|
|
1460
|
+
"nis",
|
|
1461
|
+
"ldap",
|
|
1462
|
+
"winbind",
|
|
1463
|
+
"changeit",
|
|
1464
|
+
"requisite",
|
|
1465
|
+
"required",
|
|
1466
|
+
"sufficient",
|
|
1467
|
+
"fail",
|
|
1468
|
+
"failed",
|
|
1469
|
+
"failure",
|
|
1470
|
+
"success",
|
|
1471
|
+
"ok",
|
|
1472
|
+
"okay",
|
|
1473
|
+
"error",
|
|
1474
|
+
"pass",
|
|
1475
|
+
"passed",
|
|
1476
|
+
"skip",
|
|
1477
|
+
"skipped",
|
|
1478
|
+
"pending",
|
|
1479
|
+
"unknown",
|
|
1480
|
+
"ignore",
|
|
1481
|
+
"valid",
|
|
1482
|
+
"invalid",
|
|
1483
|
+
"match",
|
|
1484
|
+
"nomatch",
|
|
1485
|
+
"enabled",
|
|
1486
|
+
"disabled",
|
|
1487
|
+
"optional",
|
|
1488
|
+
"plain",
|
|
1489
|
+
"plaintext",
|
|
1490
|
+
"md5",
|
|
1491
|
+
"sha1",
|
|
1492
|
+
"sha256",
|
|
1493
|
+
"scram",
|
|
1494
|
+
"blank",
|
|
1495
|
+
"empty",
|
|
1496
|
+
"unset",
|
|
1497
|
+
"trust",
|
|
1498
|
+
"reject",
|
|
1499
|
+
"peer",
|
|
1500
|
+
"ident",
|
|
1501
|
+
"basic",
|
|
1502
|
+
"digest",
|
|
1503
|
+
"bearer",
|
|
1504
|
+
"none",
|
|
1505
|
+
"auto",
|
|
1506
|
+
"manual",
|
|
1507
|
+
"prompt",
|
|
1508
|
+
"keyring",
|
|
1509
|
+
"file",
|
|
1510
|
+
"path",
|
|
1511
|
+
"stdin",
|
|
1512
|
+
"env",
|
|
1513
|
+
"vault",
|
|
1514
|
+
"password",
|
|
1515
|
+
"passwords",
|
|
1516
|
+
"passwd",
|
|
1517
|
+
"credential",
|
|
1518
|
+
"credentials",
|
|
1519
|
+
"creds",
|
|
1520
|
+
"token",
|
|
1521
|
+
"tokens",
|
|
1522
|
+
"apikey",
|
|
1523
|
+
"passphrase",
|
|
1524
|
+
"postgres",
|
|
1525
|
+
"postgresql",
|
|
1526
|
+
"database",
|
|
1527
|
+
"changeme",
|
|
1528
|
+
"secret",
|
|
1529
|
+
"secrets",
|
|
1530
|
+
"admin",
|
|
1531
|
+
"administrator",
|
|
1532
|
+
"example",
|
|
1533
|
+
"mariadb",
|
|
1534
|
+
"mongodb",
|
|
1535
|
+
"default",
|
|
1536
|
+
"development",
|
|
1537
|
+
"production",
|
|
1538
|
+
"staging",
|
|
1539
|
+
"localhost",
|
|
1540
|
+
"testing",
|
|
1541
|
+
"letmein",
|
|
1542
|
+
"welcome",
|
|
1543
|
+
"superuser",
|
|
1544
|
+
"sqlserver",
|
|
1545
|
+
"rabbitmq",
|
|
1546
|
+
"minioadmin",
|
|
1547
|
+
"keycloak",
|
|
1548
|
+
"elastic",
|
|
1549
|
+
"grafana",
|
|
1550
|
+
"jenkins",
|
|
1551
|
+
"vagrant",
|
|
1552
|
+
"username",
|
|
1553
|
+
"anonymous",
|
|
1554
|
+
"readonly",
|
|
1555
|
+
"readwrite",
|
|
1556
|
+
"internal",
|
|
1557
|
+
"external",
|
|
1558
|
+
"visible",
|
|
1559
|
+
"show",
|
|
1560
|
+
"hide",
|
|
1561
|
+
"toggle",
|
|
1562
|
+
"text",
|
|
1563
|
+
"sandbox",
|
|
1564
|
+
"guest",
|
|
1565
|
+
"root",
|
|
1566
|
+
"test",
|
|
1567
|
+
"user",
|
|
1568
|
+
"demo",
|
|
1569
|
+
"public",
|
|
1570
|
+
"private",
|
|
1571
|
+
]);
|
|
1572
|
+
/**
|
|
1573
|
+
* "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
|
|
1575
|
+
* (`Phrase de passe (facultative)`, `Contraseña (opcional)`).
|
|
1576
|
+
*/
|
|
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");
|
|
1578
|
+
const isPasswordLabel = (value) => PASSWORD_LABEL.test(value);
|
|
1579
|
+
/**
|
|
1580
|
+
* Whether a value under a credential name is a literal secret worth hiding: not a type, a placeholder,
|
|
1581
|
+
* a reference, code, a lone word or a number. (A password hash, and text in another script, are the decision stage's
|
|
1582
|
+
* to keep: policy.ts keepReason, isBenignValue.)
|
|
1583
|
+
*/
|
|
1584
|
+
export function looksSecret(raw, literal = false) {
|
|
1585
|
+
const v = cleanValue(raw);
|
|
1586
|
+
if (v.length < 4 || v.length > MAX_VALUE || isInert(v) || isCode(raw, literal) || isTestValue(v))
|
|
1587
|
+
return false;
|
|
1588
|
+
if (isPasswordLabel(v))
|
|
1589
|
+
return false;
|
|
1590
|
+
// A list, a call, an object: closed, its items quoted strings, numbers, names or pairs.
|
|
1591
|
+
const ITEM = String.raw `(?:"[^"]*"|'[^']*'|-?\d[\d.]*|[A-Za-z_$][\w$.]*(?:\s*[:=]\s*(?:"[^"]*"|'[^']*'|-?\d[\d.]*|[A-Za-z_$][\w$.]*))?)`;
|
|
1592
|
+
if (/^[[({]/.test(raw.trim()) &&
|
|
1593
|
+
isBalanced(raw.trim()) &&
|
|
1594
|
+
new RegExp(String.raw `^[\[({]\s*(?:${ITEM}\s*(?:,\s*${ITEM}\s*)*,?)?\s*[\])}][,;]?$`).test(raw.trim()))
|
|
1595
|
+
return false;
|
|
1596
|
+
if (/^[[({<](?:[A-Z][A-Z0-9_]{2,}|[a-z][a-z_ -]{2,})[\])}>]?$/.test(v))
|
|
1597
|
+
return false; // `[REPMGR_PASSWORD`, `<your token`: a placeholder cut at its bracket
|
|
1598
|
+
if (v.length >= 8 && isPlaceholder(v))
|
|
1599
|
+
return false;
|
|
1600
|
+
if (/^\d{1,5}$/.test(v))
|
|
1601
|
+
return false; // ports, counts, lengths
|
|
1602
|
+
if (/^[\w.+-]+@(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,24}|[A-Z0-9-]+(?:\.[A-Z0-9-]+)*\.[A-Z]{2,4})$/.test(v))
|
|
1603
|
+
return false; // emails (a domain in one case)
|
|
1604
|
+
// Endpoints of other schemes without credentials (`postgres://db.internal/app`) are settings; an http(s) URL goes on,
|
|
1605
|
+
// as a path segment of it may be a generated token (webhooks; see isNamedSecret).
|
|
1606
|
+
if (isPasswordlessUrl(v) && !/^https?:/i.test(v))
|
|
1607
|
+
return false;
|
|
1608
|
+
if (/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/.test(v))
|
|
1609
|
+
return false; // CONSTANT_NAMES
|
|
1610
|
+
if (/^`[A-Z][A-Z0-9_]*`(?:\s*[–-]\s*`[A-Z][A-Z0-9_]*`)?$/.test(v))
|
|
1611
|
+
return false; // `KEY_F1`–`KEY_F10` in a table
|
|
1612
|
+
// (With the sentence's mark after it: `Passwort: root!`.)
|
|
1613
|
+
if (/^\p{L}{1,12}[!?.]?$/u.test(v) && COMMON_WORDS.has(v.toLowerCase().replace(/[!?.]$/, "")))
|
|
1614
|
+
return false;
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Analytics, consent, load-balancer and preference cookies: `_ga=GA1.1.35…`, `_gid`, `_fbp`, `AWSALB`, `__cf_bm`,
|
|
1619
|
+
* PostHog's `ph_…_posthog`, a locale or a theme. Not a session.
|
|
1620
|
+
*/
|
|
1621
|
+
export const ANALYTICS_COOKIE = /^(?:AWSALB(?:CORS)?|AWSALBTG(?:CORS)?|ai_user|ai_session|ph_\w+_posthog|_ga\w*|_gid|_gat\w*|_fbp|_fbc|_gcl\w*|_hj\w*|ajs_\w+|amplitude\w*|AMP_\w*|mp_\w+|_clck|_clsk|_uetsid|_uetvid|edgebucket|loid|cookieconsent\w*|consent\w*|OptanonConsent|OptanonAlertBoxClosed|intercom-id-\w+|intercom-device-id-\w+|hubspotutk|__hstc|__hssc|__hssrc|__stripe_mid|__stripe_sid|_pk_\w+(?:\.\w+)*|__cf_bm|_cfuvid|cf_clearance|lang|locale|theme|tz|timezone)$/i;
|