redac---n 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -10
- package/dist/calls.d.ts +8 -0
- package/dist/calls.js +60 -3
- package/dist/checksums.js +4 -3
- package/dist/commands.d.ts +5 -0
- package/dist/commands.js +93 -14
- package/dist/context.d.ts +5 -2
- package/dist/context.js +36 -9
- package/dist/encoded.js +13 -6
- package/dist/environment.d.ts +5 -3
- package/dist/environment.js +38 -14
- package/dist/formats.js +12 -0
- package/dist/index.js +10 -4
- package/dist/json.js +29 -5
- package/dist/known.d.ts +7 -7
- package/dist/known.js +33 -25
- package/dist/named.d.ts +6 -0
- package/dist/named.js +421 -125
- package/dist/names.d.ts +5 -1
- package/dist/names.js +143 -18
- package/dist/normalize.d.ts +4 -1
- package/dist/normalize.js +37 -11
- package/dist/pem.d.ts +2 -0
- package/dist/pem.js +12 -0
- package/dist/policy.js +12 -5
- package/dist/public.js +35 -13
- package/dist/secretfile.d.ts +5 -0
- package/dist/secretfile.js +49 -20
- package/dist/shell.d.ts +12 -3
- package/dist/shell.js +51 -7
- package/dist/spans.d.ts +4 -3
- package/dist/spans.js +79 -8
- package/dist/sql.d.ts +12 -8
- package/dist/sql.js +94 -16
- package/dist/stream.d.ts +2 -1
- package/dist/stream.js +15 -3
- package/dist/structure.js +210 -61
- package/dist/syntax.d.ts +5 -2
- package/dist/syntax.js +7 -4
- package/dist/transcript.js +116 -28
- package/dist/unnamed.js +128 -20
- package/dist/urls.d.ts +8 -3
- package/dist/urls.js +16 -7
- package/dist/values.d.ts +2 -1
- package/dist/values.js +70 -25
- package/docs/holdout.svg +8 -8
- package/docs/layers.md +1 -1
- package/package.json +1 -1
package/dist/names.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/** Every spelling of "password" (PASSWORD_LANGUAGES), and those of languages other than English. */
|
|
2
2
|
export declare const PASSWORD_SPELLINGS: string[];
|
|
3
3
|
export declare const FOREIGN_PASSWORD_SPELLINGS: string[];
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* The words of a name in upper case: `clientSecret` → CLIENT, SECRET; `x-api-key` → X, API, KEY. A compound some
|
|
6
|
+
* write as two words is one (`PassPhrase`, `pass_word`, `ClearTextPassword` → CLEARTEXT, PASSWORD), as it is written
|
|
7
|
+
* glued: its halves alone say something else (`PHRASE` a seed phrase, `TEXT` UI text).
|
|
8
|
+
*/
|
|
5
9
|
export declare function words(name: string): string[];
|
|
6
10
|
/**
|
|
7
11
|
* Whether a name names a session or its ID, whoever's session it is: `INSTAGRAM_SESSION_ID`, `LINKEDIN_SESSION`,
|
package/dist/names.js
CHANGED
|
@@ -48,6 +48,11 @@ const PASSWORD_LANGUAGES = [
|
|
|
48
48
|
"parole",
|
|
49
49
|
"slaptažodis",
|
|
50
50
|
"fjalëkalimi",
|
|
51
|
+
"fjalëkalim",
|
|
52
|
+
"salasõna",
|
|
53
|
+
"lykilorð",
|
|
54
|
+
"nenosiri",
|
|
55
|
+
"zaporka",
|
|
51
56
|
"kupuhipa",
|
|
52
57
|
"focal faire",
|
|
53
58
|
"mật khẩu",
|
|
@@ -133,7 +138,7 @@ const CREDENTIAL_WORDS = new Set([
|
|
|
133
138
|
"SK",
|
|
134
139
|
]);
|
|
135
140
|
/** Single words that end in a credential word: PGPASSWORD, AUTHKEY, WRITEKEY, APITOKEN. */
|
|
136
|
-
const CREDENTIAL_SUFFIX = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|TOKEN|SECRET|APIKEY|AUTH|AUTHKEY|WRITEKEY|READKEY|SECRETKEY|PRIVATEKEY|ACCESSKEY|SESSID|SESSIONID|PASS|PWD|APPKEY|MASTERKEY|SIGNINGKEY|ENCRYPTIONKEY|LICENSEKEY|DEVKEY|USERKEY)$/;
|
|
141
|
+
const CREDENTIAL_SUFFIX = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|TOKEN|SECRET|APIKEY|AUTH|AUTHKEY|WRITEKEY|READKEY|SECRETKEY|PRIVATEKEY|ACCESSKEY|SESSID|SESSIONID|PASS|PWD|PW|APPKEY|MASTERKEY|SIGNINGKEY|ENCRYPTIONKEY|LICENSEKEY|DEVKEY|USERKEY)$/;
|
|
137
142
|
/** Last words that describe a form of the word before: `client-key-data`, `SECRET_B64`, `secret_key_base`. */
|
|
138
143
|
const WRAPPER_WORDS = new Set([
|
|
139
144
|
"DATA",
|
|
@@ -151,6 +156,7 @@ const WRAPPER_WORDS = new Set([
|
|
|
151
156
|
"BYTES",
|
|
152
157
|
"PLAIN",
|
|
153
158
|
"PLAINTEXT",
|
|
159
|
+
"CLEARTEXT",
|
|
154
160
|
]);
|
|
155
161
|
/** Last words that only qualify the word before: `API_KEY_V2`, `DB_PASSWORD_PROD`. */
|
|
156
162
|
const QUALIFIER_WORDS = /^(V\d+|\d+|PROD|PRODUCTION|DEV|DEVELOPMENT|STAGING|STAGE|TEST|LIVE|OLD|NEW|PREVIOUS|PREV|CURRENT|NEXT|PRIMARY|SECONDARY|BACKUP|FALLBACKS?|ROTATED|LOCAL|REMOTE|DEFAULT|SANDBOX|QA|UAT|INT|EXT|ADMIN|ROOT|MASTER|CONFIRM|CONFIRMATION|AGAIN|REPEAT|VERIFY|VERIFICATION)$/;
|
|
@@ -297,6 +303,7 @@ const NOT_CREDENTIAL_BEFORE = lookup({
|
|
|
297
303
|
"COUNT",
|
|
298
304
|
"LEXER",
|
|
299
305
|
"PARSER",
|
|
306
|
+
"SHELL",
|
|
300
307
|
"CANCELLATION",
|
|
301
308
|
"CANCEL",
|
|
302
309
|
"INPUT",
|
|
@@ -363,7 +370,6 @@ const NOT_CREDENTIAL_BEFORE = lookup({
|
|
|
363
370
|
"ALWAYS",
|
|
364
371
|
"STRICT",
|
|
365
372
|
"NEEDS",
|
|
366
|
-
"HTTP",
|
|
367
373
|
"SASL",
|
|
368
374
|
"LDAP",
|
|
369
375
|
"OIDC",
|
|
@@ -407,6 +413,8 @@ const IDENTIFIER_WORDS = new Set([
|
|
|
407
413
|
"ACCOUNT",
|
|
408
414
|
"TENANT",
|
|
409
415
|
"ORG",
|
|
416
|
+
"ORGANIZATION",
|
|
417
|
+
"ORGANISATION",
|
|
410
418
|
"TEAM",
|
|
411
419
|
"CHANNEL",
|
|
412
420
|
"STYLE",
|
|
@@ -414,6 +422,7 @@ const IDENTIFIER_WORDS = new Set([
|
|
|
414
422
|
"UUID",
|
|
415
423
|
"GUID",
|
|
416
424
|
"REF",
|
|
425
|
+
"REFERENCE",
|
|
417
426
|
"KIND",
|
|
418
427
|
"TYPE",
|
|
419
428
|
"MODE",
|
|
@@ -476,6 +485,9 @@ const HASH_WORDS = new Set([
|
|
|
476
485
|
"COMMIT",
|
|
477
486
|
"REV",
|
|
478
487
|
"REVISION",
|
|
488
|
+
// (A release is the build's commit or version: Sentry's, Langfuse's, Bugsnag's `SENTRY_RELEASE=<sha>`.)
|
|
489
|
+
"RELEASE",
|
|
490
|
+
"VCS",
|
|
479
491
|
"FINGERPRINT",
|
|
480
492
|
"CRC",
|
|
481
493
|
"CRC32",
|
|
@@ -578,6 +590,7 @@ const ABOUT_WORDS = new Set([
|
|
|
578
590
|
"VALIDATION",
|
|
579
591
|
"VALIDATOR",
|
|
580
592
|
"POLICY",
|
|
593
|
+
"POLICIES",
|
|
581
594
|
"RULE",
|
|
582
595
|
"RULES",
|
|
583
596
|
"ICON",
|
|
@@ -603,7 +616,6 @@ const ABOUT_WORDS = new Set([
|
|
|
603
616
|
"FAILED",
|
|
604
617
|
"FAILURE",
|
|
605
618
|
"MISMATCH",
|
|
606
|
-
"MATCH",
|
|
607
619
|
"CHANGED",
|
|
608
620
|
"RESET",
|
|
609
621
|
"EMPTY",
|
|
@@ -627,13 +639,35 @@ const ABOUT_WORDS = new Set([
|
|
|
627
639
|
]);
|
|
628
640
|
/** About words that can also say what a credential belongs to. */
|
|
629
641
|
const ROUTE_WORDS = new Set(["PAGE", "SCREEN", "ROUTE", "VIEW", "EVENT", "HANDLER", "DIALOG", "MODAL", "STATUS"]);
|
|
642
|
+
/**
|
|
643
|
+
* About words that are also nouns a component is named with, its name's first half (`LABEL_STUDIO_PASSWORD`,
|
|
644
|
+
* `EVENT_STORE_PASSWORD`, `MESSAGE_BROKER_PASSWORD`, `STATUS_DB_PASSWORD`, `HELP_DESK_PASSWORD`): they make a name
|
|
645
|
+
* about its credential only next to its word, or with qualifiers between (`LABEL_NEW_PASSWORD`, `LOGIN_PAGE_PASSWORD`).
|
|
646
|
+
*/
|
|
647
|
+
const NOUN_ABOUT_WORDS = new Set([
|
|
648
|
+
...ROUTE_WORDS,
|
|
649
|
+
"LABEL",
|
|
650
|
+
"LABELS",
|
|
651
|
+
"MSG",
|
|
652
|
+
"MESSAGE",
|
|
653
|
+
"MESSAGES",
|
|
654
|
+
"TEXT",
|
|
655
|
+
"RULE",
|
|
656
|
+
"RULES",
|
|
657
|
+
"POLICY",
|
|
658
|
+
"HELP",
|
|
659
|
+
]);
|
|
630
660
|
/**
|
|
631
661
|
* Shell variables whose names end in a credential word but hold something else (`PWD`, `OLDPWD`),
|
|
632
662
|
* bare upper-case `KEY` and `AUTH` (loop keys, auth modes) and `PASS`, `XPASS` (`PASS: test name`, a test runner's
|
|
633
663
|
* verdicts; named.ts still reads `PASS=value`).
|
|
634
664
|
*/
|
|
635
665
|
const NOT_CREDENTIAL_NAMES = new Set(["PWD", "OLDPWD", "AUTH", "KEY", "PASS", "XPASS"]);
|
|
636
|
-
/**
|
|
666
|
+
/**
|
|
667
|
+
* The words of a name in upper case: `clientSecret` → CLIENT, SECRET; `x-api-key` → X, API, KEY. A compound some
|
|
668
|
+
* write as two words is one (`PassPhrase`, `pass_word`, `ClearTextPassword` → CLEARTEXT, PASSWORD), as it is written
|
|
669
|
+
* glued: its halves alone say something else (`PHRASE` a seed phrase, `TEXT` UI text).
|
|
670
|
+
*/
|
|
637
671
|
export function words(name) {
|
|
638
672
|
// (Accents dropped first, so `contraseña` is CONTRASENA and `hasło` HASLO, as CREDENTIAL_WORDS spells them.)
|
|
639
673
|
return name
|
|
@@ -643,6 +677,7 @@ export function words(name) {
|
|
|
643
677
|
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
644
678
|
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
645
679
|
.toUpperCase()
|
|
680
|
+
.replace(/(?<![\p{L}\p{N}])(?:(PASS)[^\p{L}\p{N}]+(WORD|WD|PHRASE|CODE)|(CLEAR|PLAIN)[^\p{L}\p{N}]+(TEXT))(?![\p{L}\p{N}])/gu, "$1$2$3$4")
|
|
646
681
|
.split(/[^\p{L}\p{N}]+/u)
|
|
647
682
|
.filter(Boolean);
|
|
648
683
|
}
|
|
@@ -891,17 +926,16 @@ const PASSWORD_WORDS = new Set([
|
|
|
891
926
|
]);
|
|
892
927
|
/**
|
|
893
928
|
* Whether a name's word names a password: "password" in any language, or one glued to the word before it (`PGPASSWORD`,
|
|
894
|
-
* `adminpasswd`, `DBPASSCODE`); the short `PASS`, `PWD` and Redis's `AUTH` glued only after what they belong to
|
|
895
|
-
* (`REDISPASS`, `ROOTPWD`, `masterauth`; not `bypass`, `OAuth`), as isCredentialWord reads them.
|
|
929
|
+
* `adminpasswd`, `DBPASSCODE`); the short `PASS`, `PWD`, `PW` and Redis's `AUTH` glued only after what they belong to
|
|
930
|
+
* (`REDISPASS`, `JKSPASS`, `ROOTPWD`, `rootpw`, `masterauth`; not `bypass`, `OAuth`), as isCredentialWord reads them.
|
|
896
931
|
*/
|
|
897
932
|
function isPasswordWord(word) {
|
|
898
933
|
if (PASSWORD_WORDS.has(word))
|
|
899
934
|
return true;
|
|
900
|
-
const glued = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|PASS|PWD|AUTH)$/.exec(word);
|
|
935
|
+
const glued = /(PASSWORD|PASSWD|PASSPHRASE|PASSCODE|PASS|PWD|PW|AUTH)$/.exec(word);
|
|
901
936
|
if (!glued)
|
|
902
937
|
return false;
|
|
903
|
-
|
|
904
|
-
return /^PASS(?:WORD|WD|PHRASE|CODE)$/.test(glued[1]) || PASSWORD_OWNERS.has(head) || isVendorName(head);
|
|
938
|
+
return /^PASS(?:WORD|WD|PHRASE|CODE)$/.test(glued[1]) || ownsGlued(word.slice(0, glued.index), glued[1]);
|
|
905
939
|
}
|
|
906
940
|
/**
|
|
907
941
|
* A credential's name written as a phrase of words, as one word or several: `mot_de_passe` and `palavra_passe` (a
|
|
@@ -978,7 +1012,8 @@ const PIN_SETTING = /^(?:TIMEOUT|TTL|EXPIRY|EXPIRES|EXPIRATION|SECONDS|SECS|MS|M
|
|
|
978
1012
|
* named for what it opens (`alarm_code`). namesPasswordOrPin, the name readers and the prose readers all ask this one.
|
|
979
1013
|
*/
|
|
980
1014
|
export function isPinName(name) {
|
|
981
|
-
|
|
1015
|
+
// (A numbered one is one too: `pin1`, `door_pin2`.)
|
|
1016
|
+
const w = words(name).map((x) => x.replace(/(?<=^PIN)\d+$/, ""));
|
|
982
1017
|
const at = w.findIndex((x, i) => (x === "PIN" && !NOT_CREDENTIAL_BEFORE.PIN.has(w[i - 1] ?? "")) || x === "PINCODE" || x === "PASSCODE");
|
|
983
1018
|
if (at >= 0)
|
|
984
1019
|
return !w.slice(at + 1).some((x) => PIN_SETTING.test(x) || (x !== "CODE" && isCredentialWord(x)));
|
|
@@ -1084,9 +1119,89 @@ const PASSWORD_OWNERS = new Set([
|
|
|
1084
1119
|
"BASIC",
|
|
1085
1120
|
"BEARER",
|
|
1086
1121
|
]);
|
|
1122
|
+
/**
|
|
1123
|
+
* Words `PASS` ends glued to them that are no password's owner: English words (`bypass`, `compass`, `overpass`,
|
|
1124
|
+
* `trespass`), filters (`lowpass`, `bandpass`), a renderer's or a compiler's passes (`subpass`, `renderpass`,
|
|
1125
|
+
* `multipass`, `numpass`), passes that admit a person (`daypass`, `boardingpass`) or products (`KeePass`, `LastPass`,
|
|
1126
|
+
* `gopass`, `NordPass`), and programs or directives about a password (`GIT_ASKPASS`, `getpass`, Redis' `resetpass`).
|
|
1127
|
+
*/
|
|
1128
|
+
const NOT_PASSWORD_OWNERS = new Set([
|
|
1129
|
+
"BY",
|
|
1130
|
+
"COM",
|
|
1131
|
+
"ENCOM",
|
|
1132
|
+
"OVER",
|
|
1133
|
+
"UNDER",
|
|
1134
|
+
"TRES",
|
|
1135
|
+
"SUR",
|
|
1136
|
+
"RE",
|
|
1137
|
+
"PRE",
|
|
1138
|
+
"OUT",
|
|
1139
|
+
"MIS",
|
|
1140
|
+
"LOW",
|
|
1141
|
+
"HIGH",
|
|
1142
|
+
"BAND",
|
|
1143
|
+
"ALL",
|
|
1144
|
+
"NOTCH",
|
|
1145
|
+
"SUB",
|
|
1146
|
+
"MULTI",
|
|
1147
|
+
"SINGLE",
|
|
1148
|
+
"FIRST",
|
|
1149
|
+
"SECOND",
|
|
1150
|
+
"THIRD",
|
|
1151
|
+
"FINAL",
|
|
1152
|
+
"LAST",
|
|
1153
|
+
"NEXT",
|
|
1154
|
+
"PREV",
|
|
1155
|
+
"ONE",
|
|
1156
|
+
"TWO",
|
|
1157
|
+
"DOUBLE",
|
|
1158
|
+
"NUM",
|
|
1159
|
+
"MAX",
|
|
1160
|
+
"MIN",
|
|
1161
|
+
"RENDER",
|
|
1162
|
+
"DEPTH",
|
|
1163
|
+
"SHADOW",
|
|
1164
|
+
"LIGHT",
|
|
1165
|
+
"BLUR",
|
|
1166
|
+
"DRAW",
|
|
1167
|
+
"BOARDING",
|
|
1168
|
+
"SEASON",
|
|
1169
|
+
"BATTLE",
|
|
1170
|
+
"HALL",
|
|
1171
|
+
"DAY",
|
|
1172
|
+
"BUS",
|
|
1173
|
+
"RAIL",
|
|
1174
|
+
"SKI",
|
|
1175
|
+
"FAST",
|
|
1176
|
+
"EASY",
|
|
1177
|
+
"EZ",
|
|
1178
|
+
"BACKSTAGE",
|
|
1179
|
+
"KEE",
|
|
1180
|
+
"GO",
|
|
1181
|
+
"NORD",
|
|
1182
|
+
"EN",
|
|
1183
|
+
"PROTON",
|
|
1184
|
+
"NO",
|
|
1185
|
+
"ASK",
|
|
1186
|
+
"GET",
|
|
1187
|
+
"RESET",
|
|
1188
|
+
]);
|
|
1189
|
+
/**
|
|
1190
|
+
* Whether `PASS`, `PWD`, `PW` or `AUTH` glued after `head` names a password or an auth string: after what it belongs to
|
|
1191
|
+
* (PASSWORD_OWNERS, a vendor's name); `PASS`, the commonest, after any other word that's no part of another
|
|
1192
|
+
* (NOT_PASSWORD_OWNERS): `JKSPASS`, `vpnpass`, `kafkapass`, `p12pass`. (Not after a letter or a number alone: `npass`,
|
|
1193
|
+
* `2pass`; nor `PW` after a letter, as base64 ends, `…GS+xpw==`.)
|
|
1194
|
+
*/
|
|
1195
|
+
function ownsGlued(head, suffix) {
|
|
1196
|
+
if (suffix === "PW" && head.length < 2)
|
|
1197
|
+
return false;
|
|
1198
|
+
if (PASSWORD_OWNERS.has(head) || isVendorName(head))
|
|
1199
|
+
return true;
|
|
1200
|
+
return suffix === "PASS" && /^[A-Z][A-Z0-9]+$/.test(head) && !NOT_PASSWORD_OWNERS.has(head);
|
|
1201
|
+
}
|
|
1087
1202
|
/**
|
|
1088
1203
|
* Whether one word of a name is a credential word: `PASSWORD`, `TOKEN`, `KEY`, or one glued to it, `PGPASSWORD`,
|
|
1089
|
-
* `APITOKEN`, `WRITEKEY`. The short `PASS`, `PWD` and `AUTH` count glued only after what they belong to (
|
|
1204
|
+
* `APITOKEN`, `WRITEKEY`. The short `PASS`, `PWD`, `PW` and `AUTH` count glued only after what they belong to (ownsGlued).
|
|
1090
1205
|
*/
|
|
1091
1206
|
function isCredentialWord(word) {
|
|
1092
1207
|
if (CREDENTIAL_WORDS.has(word))
|
|
@@ -1094,8 +1209,7 @@ function isCredentialWord(word) {
|
|
|
1094
1209
|
const glued = CREDENTIAL_SUFFIX.exec(word);
|
|
1095
1210
|
if (!glued)
|
|
1096
1211
|
return false;
|
|
1097
|
-
|
|
1098
|
-
return !/^(?:PASS|PWD|AUTH)$/.test(glued[1]) || PASSWORD_OWNERS.has(head) || isVendorName(head);
|
|
1212
|
+
return !/^(?:PASS|PWD|PW|AUTH)$/.test(glued[1]) || ownsGlued(word.slice(0, glued.index), glued[1]);
|
|
1099
1213
|
}
|
|
1100
1214
|
/**
|
|
1101
1215
|
* What a name says about its value:
|
|
@@ -1272,9 +1386,11 @@ function classify(name, w, core, last) {
|
|
|
1272
1386
|
return "identifier";
|
|
1273
1387
|
// An alarm's, a door's or a keypad's code is a PIN; `vapidPrivate`, `push_private`: the private half of a key pair.
|
|
1274
1388
|
// (A key's private half named as a camel-case hump or after a separator, `vapidPrivate`, `VAPID_PRIVATE`; not a word
|
|
1275
|
-
// that ends so, `isprivate`, `semiprivate`.)
|
|
1389
|
+
// that ends so, `isprivate`, `semiprivate`; nor an identifier's, `zone_private`.)
|
|
1276
1390
|
if (isPinName(name) ||
|
|
1277
|
-
(/^[a-z][a-z0-9]*(?:Private|[_-]private)$|^[A-Za-z][A-Za-z0-9]*[_-]PRIVATE$/.test(name) &&
|
|
1391
|
+
(/^[a-z][a-z0-9]*(?:Private|[_-]private)$|^[A-Za-z][A-Za-z0-9]*[_-]PRIVATE$/.test(name) &&
|
|
1392
|
+
!ABOUT_WORDS.has(w[0]) &&
|
|
1393
|
+
!IDENTIFIER_WORDS.has(w[0])))
|
|
1278
1394
|
return "credential";
|
|
1279
1395
|
if (isPublicName(name, core))
|
|
1280
1396
|
return "public";
|
|
@@ -1341,8 +1457,17 @@ function classify(name, w, core, last) {
|
|
|
1341
1457
|
(core.length >= 2 &&
|
|
1342
1458
|
/^(ACCESS|API|SECRET|CLIENT|APP|AUTH|SIGNING)$/.test(core.at(-2)) &&
|
|
1343
1459
|
/^(TOKEN|KEY)$/.test(last));
|
|
1344
|
-
|
|
1345
|
-
|
|
1460
|
+
// (A noun about word followed by another that names what the credential belongs to names a component; not a credential
|
|
1461
|
+
// phrase's first word, `LOGIN_PAGE_APP_PASSWORD`.)
|
|
1462
|
+
const about = core.some((x, i) => ABOUT_WORDS.has(x) &&
|
|
1463
|
+
!(fullPhrase && ROUTE_WORDS.has(x)) &&
|
|
1464
|
+
!(NOUN_ABOUT_WORDS.has(x) &&
|
|
1465
|
+
core
|
|
1466
|
+
.slice(i + 1, -1)
|
|
1467
|
+
.some((y) => !ABOUT_WORDS.has(y) &&
|
|
1468
|
+
!QUALIFIER_WORDS.test(y) &&
|
|
1469
|
+
!isCredentialWord(y) &&
|
|
1470
|
+
!/^(ACCESS|API|SECRET|CLIENT|APP|AUTH|SIGNING)$/.test(y)))) || /^NO(PASS|PASSWD|PASSWORD|AUTH)$/.test(last);
|
|
1346
1471
|
if (!about && isCredentialWord(last) && !(last === "PAT" && core.length > 1 && !PAT_OWNERS.has(core.at(-2))))
|
|
1347
1472
|
return "credential";
|
|
1348
1473
|
// A credential's word first, then whose it is: `PASSWORD_DB`, `TOKEN_GITHUB`, `SECRET_STRIPE`, `password_for_db`; not
|
|
@@ -1354,7 +1479,7 @@ function classify(name, w, core, last) {
|
|
|
1354
1479
|
return "credential";
|
|
1355
1480
|
if (HASH_WORDS.has(last) ||
|
|
1356
1481
|
/^(SHA|MD|BLAKE|CRC)\d*$/.test(last) ||
|
|
1357
|
-
(last
|
|
1482
|
+
(/^(?:ID|NUMBER|NUM)$/.test(last) && core.some((x) => HASH_WORDS.has(x))))
|
|
1358
1483
|
return "hash";
|
|
1359
1484
|
if (IDENTIFIER_WORDS.has(last))
|
|
1360
1485
|
return "identifier";
|
package/dist/normalize.d.ts
CHANGED
|
@@ -13,7 +13,10 @@ export interface Normalized {
|
|
|
13
13
|
}
|
|
14
14
|
/** A text with its HTML entities decoded as normalize decodes them: `<redacted>` is `<redacted>`. */
|
|
15
15
|
export declare function decodeEntities(text: string): string;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A `.pgpass` line, `host:port:database:user:password` (`*` is a wildcard in the first four). Not a line of bytes in
|
|
18
|
+
* hex pairs, as `openssl x509 -text` prints a certificate's modulus (`f2:36:d0:9e:e7:…`).
|
|
19
|
+
*/
|
|
17
20
|
export declare const PGPASS: RegExp;
|
|
18
21
|
/** Whether a text has anything to normalize, cheaply. (A pass only has work after one that ran if its own gate already fires on the original.) */
|
|
19
22
|
export declare function needsNormalizing(text: string): boolean;
|
package/dist/normalize.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. What a tool puts in front of lines: a file viewer's line numbers (` 12→`, `cat -n`), `grep -n`'s
|
|
7
7
|
* `path:12:`, a diff hunk's markers.
|
|
8
8
|
* 3. What spells a character or a string another way: shell line continuations (`--password \` then the value on
|
|
9
|
-
* the next line), HTML entities (`"`, `#`), full-width punctuation and spaces of other widths, and a
|
|
9
|
+
* the next line), HTML entities (`"`, `#`), full-width punctuation, typographic quotes and spaces of other widths, and a
|
|
10
10
|
* string literal split in pieces (`"xoxb-12ab" "34cd"`, `'sk-' + '…'`), joined.
|
|
11
11
|
*
|
|
12
12
|
* Each pass sees the lines the one before it leaves (a coloured diff's `ESC[32m+API_TOKEN=…` starts with its `+`;
|
|
@@ -93,8 +93,11 @@ const PREFIXED = /(?:^|\n)[\w./@+-]{1,200}\.\w{1,10}[:-]\d{1,7}[:-]|(?:^|\n)@@ -
|
|
|
93
93
|
*/
|
|
94
94
|
const LINE_NUMBER = /^(?:[ \t]*\d{1,7}→| +\d{1,7}\t)/gm;
|
|
95
95
|
const HAS_LINE_NUMBER = new RegExp(LINE_NUMBER.source, "m");
|
|
96
|
-
/**
|
|
97
|
-
|
|
96
|
+
/**
|
|
97
|
+
* A `.pgpass` line, `host:port:database:user:password` (`*` is a wildcard in the first four). Not a line of bytes in
|
|
98
|
+
* hex pairs, as `openssl x509 -text` prints a certificate's modulus (`f2:36:d0:9e:e7:…`).
|
|
99
|
+
*/
|
|
100
|
+
export const PGPASS = /^(?![ \t]*(?:[0-9A-Fa-f]{2}:){5})[ \t]*[^:\s#][^:\s]*:(?:\d{1,5}|\*):[^:\s]+:[^:\s]+:([^\r\n]+?)\r?$/m;
|
|
98
101
|
/** `grep -n` and `rg -n`: `path:12:` before a match, `path-13-` before a context line. */
|
|
99
102
|
const GREP_PREFIX = /^([\w./@+-]{1,200}\.[\w]{1,10})[:-](\d{1,7})[:-]/gm;
|
|
100
103
|
/**
|
|
@@ -124,7 +127,7 @@ const INVISIBLE = /[\x00\x07\x1b\u00ad\u034f\u061c\u115f\u1160\u17b4\u17b5\u180b
|
|
|
124
127
|
const NEEDS_PASS_ONE = (text) => INVISIBLE.test(text) || ESCAPED_CSI.test(text) || CARET_ESCAPE.test(text);
|
|
125
128
|
const NEEDS_PASS_TWO = (text) => HAS_LINE_NUMBER.test(text) || PREFIXED.test(text) || LINE_END_MARK_TEST.test(text);
|
|
126
129
|
const NEEDS_PASS_THREE = (text) => INLINE_TAG_TEST.test(text) ||
|
|
127
|
-
/[\u00a0\u2000-\u200a\u202f\u3000\uff1a\uff1d]|\\\r?\n/.test(text) ||
|
|
130
|
+
/[\u00a0\u2000-\u200a\u2018\u2019\u201c-\u201e\u202f\u3000\uff1a\uff1d]|\\\r?\n/.test(text) ||
|
|
128
131
|
ENTITY.test(text) ||
|
|
129
132
|
JOINT.test(text);
|
|
130
133
|
/** Whether a text has anything to normalize, cheaply. (A pass only has work after one that ran if its own gate already fires on the original.) */
|
|
@@ -405,9 +408,12 @@ function literalJoints(text) {
|
|
|
405
408
|
continue;
|
|
406
409
|
if (m[1] === "'" && /content-security-policy|-src\b/i.test(text.slice(start, lineEnd(text, m.index))))
|
|
407
410
|
continue;
|
|
408
|
-
// Across a line break with no operator: inside parentheses (Python), or a C initializer after
|
|
411
|
+
// Across a line break with no operator: inside parentheses (Python), or a C initializer after `=`, whose next
|
|
412
|
+
// piece is indented under it (a shell line's quoted value ends its line: `export PW="…"` then `"$DIR/run.sh"`).
|
|
409
413
|
if (/\n/.test(s[0]) && !/[(=][ \t]*$/.test(before) && !/[(=,][ \t]*\r?$/.test(above))
|
|
410
414
|
continue;
|
|
415
|
+
if (/\n/.test(s[0]) && !/\n[ \t]+$/.test(s[0]) && !/\([ \t]*$/.test(before))
|
|
416
|
+
continue;
|
|
411
417
|
}
|
|
412
418
|
// The next piece is more of the string, not a name: `"…"="…"` lines (a .reg file, an INI) don't join.
|
|
413
419
|
if (/^[ \t]*[=:]/.test(text.slice(next.lastIndex, next.lastIndex + 20)))
|
|
@@ -432,12 +438,13 @@ function unspell(text) {
|
|
|
432
438
|
continue;
|
|
433
439
|
}
|
|
434
440
|
// A line continuation: the backslash and newline vanish, joining the lines as a shell, a C macro or a string
|
|
435
|
-
// does (`--password \`, `TOKEN=AbCd\` then `EfGh`). Not a Windows path's last backslash (`DATA_DIR=C:\data
|
|
436
|
-
//
|
|
441
|
+
// does (`--password \`, `TOKEN=AbCd\` then `EfGh`). Not a Windows path's last backslash (`DATA_DIR=C:\data\`,
|
|
442
|
+
// `C:\Program Files\App\`, `\\server\share\`, `%ProgramData%\app\`: a word with another backslash in it), nor a
|
|
443
|
+
// Markdown hard break (`**Host:** db\` then `**Password:** …`): a line that begins with no token there.
|
|
437
444
|
if (c === 92 && (text[i + 1] === "\n" || (text[i + 1] === "\r" && text[i + 2] === "\n"))) {
|
|
438
445
|
const next = text[i + 1] === "\r" ? i + 3 : i + 2;
|
|
439
446
|
const line = text.slice(lineStart(text, i), i);
|
|
440
|
-
const windowsPath =
|
|
447
|
+
const windowsPath = /\\\S*$/.test(line);
|
|
441
448
|
const hardBreak = !/[ \t]$/.test(line) && !/^[\w"'$\s-]/.test(text[next] ?? "");
|
|
442
449
|
if (!windowsPath && !hardBreak) {
|
|
443
450
|
w.separate();
|
|
@@ -455,10 +462,20 @@ function unspell(text) {
|
|
|
455
462
|
}
|
|
456
463
|
if (c === 38) {
|
|
457
464
|
const m = ENTITY_AT.exec(text.slice(i, i + 12));
|
|
458
|
-
|
|
465
|
+
let ch = m ? entityChar(m) : undefined;
|
|
459
466
|
if (m && ch !== undefined) {
|
|
460
|
-
|
|
461
|
-
i
|
|
467
|
+
// (An entity escaped again, `&quot;` in an attribute escaped twice, is the character it stands for.)
|
|
468
|
+
let end = i + m[0].length;
|
|
469
|
+
for (let depth = 0; ch === "&" && depth < 3; depth++) {
|
|
470
|
+
const inner = ENTITY_AT.exec(`&${text.slice(end, end + 11)}`);
|
|
471
|
+
const innerChar = inner ? entityChar(inner) : undefined;
|
|
472
|
+
if (!inner || innerChar === undefined)
|
|
473
|
+
break;
|
|
474
|
+
ch = innerChar;
|
|
475
|
+
end += inner[0].length - 1;
|
|
476
|
+
}
|
|
477
|
+
w.emit(ch, i, end);
|
|
478
|
+
i = end;
|
|
462
479
|
continue;
|
|
463
480
|
}
|
|
464
481
|
}
|
|
@@ -475,6 +492,15 @@ function unspell(text) {
|
|
|
475
492
|
i++;
|
|
476
493
|
continue;
|
|
477
494
|
}
|
|
495
|
+
// Typographic quotes, as a word processor's or a chat app's autocorrect writes them in pasted JSON and config
|
|
496
|
+
// (`“password”: “…”`, German `„…“`): the straight quotes the syntax means. (Not an apostrophe inside a word, which
|
|
497
|
+
// quotes nothing: Corsican `chjave d’accessu`, `it’s`.)
|
|
498
|
+
if ((c === 0x2018 || c === 0x2019 || (c >= 0x201c && c <= 0x201e)) &&
|
|
499
|
+
!(c === 0x2019 && /\p{L}/u.test(text[i - 1] ?? "") && /\p{L}/u.test(text[i + 1] ?? ""))) {
|
|
500
|
+
w.emit(c <= 0x2019 ? "'" : '"', i, i + 1);
|
|
501
|
+
i++;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
478
504
|
w.emit(text[i], i, i + 1);
|
|
479
505
|
i++;
|
|
480
506
|
}
|
package/dist/pem.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Whether a text ends inside an armored block: its last BEGIN or END line is a BEGIN (the END is still to come). */
|
|
2
|
+
export declare function endsInBlock(text: string): boolean;
|
|
1
3
|
/** The BEGIN lines of a text, each with its label, in order. */
|
|
2
4
|
export declare function beginLines(text: string): Generator<{
|
|
3
5
|
start: number;
|
package/dist/pem.js
CHANGED
|
@@ -19,6 +19,14 @@ const ARMOR = (word) => String.raw `-{4,5} ?${word} ([A-Za-z0-9][A-Za-z0-9 ]{0,5
|
|
|
19
19
|
const END_LINE = new RegExp(ARMOR("END"), "y");
|
|
20
20
|
/** Where an END line may start. */
|
|
21
21
|
const END_START = /-{4,5} ?END /y;
|
|
22
|
+
const ARMOR_LINE = new RegExp(ARMOR("(BEGIN|END)"), "g");
|
|
23
|
+
/** Whether a text ends inside an armored block: its last BEGIN or END line is a BEGIN (the END is still to come). */
|
|
24
|
+
export function endsInBlock(text) {
|
|
25
|
+
let last;
|
|
26
|
+
for (const m of text.matchAll(ARMOR_LINE))
|
|
27
|
+
last = m[1];
|
|
28
|
+
return last === "BEGIN";
|
|
29
|
+
}
|
|
22
30
|
/** The BEGIN lines of a text, each with its label, in order. */
|
|
23
31
|
export function* beginLines(text) {
|
|
24
32
|
for (const m of text.matchAll(new RegExp(ARMOR("BEGIN"), "g")))
|
|
@@ -122,6 +130,10 @@ function pemBody(text, begin, from) {
|
|
|
122
130
|
while (j < text.length && isArmorChar(text.charCodeAt(j)))
|
|
123
131
|
j++;
|
|
124
132
|
run = j - lineFrom;
|
|
133
|
+
// (Base64 has `=` only as its padding, at the end: `password=Zq7m…` after a cut certificate, or on its BEGIN
|
|
134
|
+
// line in a log, is a setting.)
|
|
135
|
+
if (/=[^=]/.test(text.slice(lineFrom, j)))
|
|
136
|
+
break;
|
|
125
137
|
// (A blank line separates the headers from the material; once material has begun, it ends the block.)
|
|
126
138
|
if (run === 0 && (material > 0 || !breakAt(text, j)))
|
|
127
139
|
break;
|
package/dist/policy.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { clientSdkAt, isClientGoogleKey, isPublicJwt, PUBLIC_FORMAT, PUBLIC_SDK_PARAM, PUBLISHED_EXAMPLE, } from "./public.js";
|
|
12
12
|
import { isBase58Address, isBech32Address, isNatsPublicKey, isStellarPublicKey, isXrpAddress } from "./checksums.js";
|
|
13
|
-
import { isPairLabel } from "./named.js";
|
|
13
|
+
import { enclosingKey, isPairLabel } from "./named.js";
|
|
14
14
|
import { nameClass, namesPasswordOrPin } from "./names.js";
|
|
15
15
|
import { base64Binary, base64Prefix } from "./base64.js";
|
|
16
16
|
import { isDerPublicKey, isPgpPublicKey, isPublicPem } from "./publickeys.js";
|
|
@@ -60,7 +60,9 @@ function isClientSdkKey(sdk, name, before) {
|
|
|
60
60
|
const positional = !name || callee === name;
|
|
61
61
|
if (!positional)
|
|
62
62
|
return PUBLIC_SDK_PARAM.test(name.replace(/^.*[.\s]/, ""));
|
|
63
|
-
|
|
63
|
+
// (`public` as a word, not inside a name or a path: `NEXT_PUBLIC_ALGOLIA_APP_ID`, `publicIndex`, `public/`.)
|
|
64
|
+
return (!/^(?:algoliasearch|instantsearch)/.test(sdk) ||
|
|
65
|
+
/search[-_ ]?(?:only|key)|(?<![\w$./-])public(?![\w$/-])|read[-_ ]?only/i.test(before));
|
|
64
66
|
}
|
|
65
67
|
/**
|
|
66
68
|
* A `WWW-Authenticate` or `Proxy-Authenticate` header's scheme and parameters, up to inside one of the parameters the
|
|
@@ -85,12 +87,12 @@ export function keepReason(text, f, hashes = "keep") {
|
|
|
85
87
|
const left = text.slice(Math.max(0, f.start - 200), f.start);
|
|
86
88
|
// A device's published default PIN, counting or one digit repeated, that its line calls the default or an example
|
|
87
89
|
// (`The default Bluetooth PIN is 0000`, `factory PIN: 1234`). Anywhere else a PIN of `0000` is someone's PIN
|
|
88
|
-
// (`pin_value=0000`, `--password 123456`).
|
|
90
|
+
// (`pin_value=0000`, `--password 123456`). (The line's words, not a host's or a path's: `api.example.com/login`.)
|
|
89
91
|
if (/^\d{4,8}$/.test(value) &&
|
|
90
92
|
isCountingDigits(value) &&
|
|
91
93
|
f.name !== undefined &&
|
|
92
94
|
namesPasswordOrPin(f.name) &&
|
|
93
|
-
|
|
95
|
+
/(?<![\w./-])(?:default|factory|example|such as|for instance)(?![\w./-])|\be\.g\./i.test(left.slice(left.lastIndexOf("\n") + 1)))
|
|
94
96
|
return "a published default PIN";
|
|
95
97
|
// The token the span sits in, to blanks, quotes and separators, and its part after a label (`key_id:rzp_…`,
|
|
96
98
|
// `SHA256:…`; a Mapbox `pk.` token's segment, a JWT behind a diff's `+`): a layer may have cut into either.
|
|
@@ -230,6 +232,10 @@ export function keepReason(text, f, hashes = "keep") {
|
|
|
230
232
|
// (A name of key-like words: `Token for the public API:` is a sentence, not a name.)
|
|
231
233
|
if (name && nameClass(name.replace(/\s+/g, "_")) === "public" && /key|token|id\b|dsn|sid/i.test(name) && !serverLine)
|
|
232
234
|
return "public by its name";
|
|
235
|
+
// (Or with the key of the object it sits in: `mixpanel: { projectToken: … }` is `mixpanel.projectToken`.)
|
|
236
|
+
const parent = name && /key|token/i.test(name) && /^[\w$-]+$/.test(name) ? enclosingKey(text, f.start) : undefined;
|
|
237
|
+
if (parent && nameClass(`${parent}.${name}`) === "public" && !serverLine)
|
|
238
|
+
return "public by its name";
|
|
233
239
|
// A random token the unnamed layer guessed at, under a name that says ID or hash (`bridgeSessionId`, `sha`): what the
|
|
234
240
|
// unnamed layer's own name vetoes say, for a finding whose name only this stage sees (a transcript leaf's key).
|
|
235
241
|
if (isGuess(f.rule) && ["identifier", "hash"].includes(nameClass(name.replace(/\s+/g, "_"))))
|
|
@@ -262,7 +268,8 @@ export function keepReason(text, f, hashes = "keep") {
|
|
|
262
268
|
if (/^AIza/.test(value) && isClientGoogleKey(text, f.start))
|
|
263
269
|
return "client-side Google key";
|
|
264
270
|
// The search-only key of an Algolia DocSearch config (`appId`, `apiKey`, `indexName`), in every docs site;
|
|
265
|
-
// never a key named admin, write or secret
|
|
271
|
+
// never a key named admin, write or secret, nor one in code that writes to the index (an indexing script's
|
|
272
|
+
// `saveObjects`, a crawler's config), which takes the admin key.
|
|
266
273
|
if (/^[0-9a-f]{32}$/.test(value)) {
|
|
267
274
|
const near = text.slice(Math.max(0, f.start - 300), f.end + 300);
|
|
268
275
|
const key = /([\w-]+)["']?[ \t]*[:=][ \t]*["'`]?$/.exec(left.slice(-80))?.[1] ?? "";
|
package/dist/public.js
CHANGED
|
@@ -9,6 +9,7 @@ import { base64Binary } from "./base64.js";
|
|
|
9
9
|
import { naturalness } from "./charlm.js";
|
|
10
10
|
import { hmacSha256 } from "./hmac.js";
|
|
11
11
|
import { callArguments } from "./calls.js";
|
|
12
|
+
import { lastText } from "./lines.js";
|
|
12
13
|
/** Keys that are public by design: publishable, site and project keys meant for browsers. */
|
|
13
14
|
export const PUBLIC_FORMAT = /^(?:pk_(?:live|test)_|NRJS-[0-9a-f]{19}$|rzp_(?:live|test)_[A-Za-z0-9]{14}$|pk\.(?:eyJ|[A-Za-z0-9]{20})|pub[0-9a-f]{32}$|phc_|sb_publishable_|sq0idp-|sqb_[0-9a-f]{40}$)/;
|
|
14
15
|
/** Where the bracket that encloses the end of `before` opens, skipping balanced pairs, or -1. */
|
|
@@ -62,6 +63,13 @@ export function isClientGoogleKey(text, start) {
|
|
|
62
63
|
// its attributes on one line or several, in either order.)
|
|
63
64
|
if (/GMSServices\.provideAPIKey\(\s*"$|GMSPlacesClient\.provideAPIKey\(\s*"$/.test(line))
|
|
64
65
|
return true;
|
|
66
|
+
// The files Firebase's tools generate into an app: the Android plugin's `values.xml` (`<string
|
|
67
|
+
// name="google_api_key">`), and iOS's `GoogleService-Info.plist` (`<key>API_KEY</key>` beside `GOOGLE_APP_ID`).
|
|
68
|
+
if (/<string\b[^<>]*\bname="google(?:_crash_reporting)?_api_key"[^<>]*>$/.test(line))
|
|
69
|
+
return true;
|
|
70
|
+
if (/<key>API_KEY<\/key>\s*<string>$/.test(before) &&
|
|
71
|
+
/<key>(?:GOOGLE_APP_ID|GCM_SENDER_ID)<\/key>/.test(text.slice(Math.max(0, start - 3000), start + 3000)))
|
|
72
|
+
return true;
|
|
65
73
|
const tag = /<meta-data\b[^<>]*android:value="$/.exec(before)?.[0];
|
|
66
74
|
if (tag &&
|
|
67
75
|
(/android:name="com\.google\.android\.geo\.API_KEY"/.test(tag) ||
|
|
@@ -95,10 +103,11 @@ export function isClientGoogleKey(text, start) {
|
|
|
95
103
|
};
|
|
96
104
|
object = [...siblings(up), line + lineAfter, ...siblings(down)].join("\n");
|
|
97
105
|
}
|
|
98
|
-
// (The bracket must be the config's own: `firebaseConfig = {`, `initializeApp({`, `FirebaseOptions(
|
|
106
|
+
// (The bracket must be the config's own: `firebaseConfig = {`, `initializeApp({`, `FirebaseOptions(`, an Angular
|
|
107
|
+
// environment's `firebase: {`.)
|
|
99
108
|
return (/authDomain|messagingSenderId|"api_key"\s*:\s*\[/.test(object) ||
|
|
100
109
|
(open >= 0 &&
|
|
101
|
-
/(?:firebaseConfig\w*\s*[:=]\s*\{|initializeApp\(\s*\{?|FirebaseOptions\(|"api_key"\s*:\s*\[\s*\{)$/.test(before.slice(0, open + 1))));
|
|
110
|
+
/(?:firebaseConfig\w*\s*[:=]\s*\{|\bfirebase["']?\s*:\s*\{|initializeApp\(\s*\{?|FirebaseOptions\(|"api_key"\s*:\s*\[\s*\{)$/.test(before.slice(0, open + 1))));
|
|
102
111
|
}
|
|
103
112
|
/**
|
|
104
113
|
* Example keys from vendor docs and RFCs, which are published on purpose: RFC 6749's authorization code,
|
|
@@ -156,7 +165,7 @@ const STAND_IN = (() => {
|
|
|
156
165
|
// (A word starts after a non-letter or at a camel-case hump, and ends before a non-lower-case letter.)
|
|
157
166
|
return new RegExp(String.raw `(?:(?<![A-Za-z])|(?<=[a-z])(?=[A-Z]))(?:${[...new Set(cases)].join("|")}|[xX]{3,})(?![a-z])`, "g");
|
|
158
167
|
})();
|
|
159
|
-
/** What separates the parts of a placeholder
|
|
168
|
+
/** What separates the parts of a placeholder:`YOUR_API_KEY_HERE`, `example-token`, `<your-key>`, `sk-[redacted]`. */
|
|
160
169
|
const PLACEHOLDER_SEPARATORS = /[-_. :/[\]<>(){}*]+/;
|
|
161
170
|
/**
|
|
162
171
|
* A value made of stand-in words and plain ones: each part between separators is a plain one (a small number, a short
|
|
@@ -169,10 +178,8 @@ function isStandIn(token) {
|
|
|
169
178
|
if (token.search(STAND_IN) < 0)
|
|
170
179
|
return false;
|
|
171
180
|
const isWord = (p) => /^[A-Za-z]+$/.test(p) && naturalness(p.toLowerCase()) > 0.3;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.filter(Boolean)
|
|
175
|
-
.every((part) => {
|
|
181
|
+
const parts = token.split(PLACEHOLDER_SEPARATORS).filter(Boolean);
|
|
182
|
+
return parts.every((part) => {
|
|
176
183
|
const standIns = [...part.matchAll(STAND_IN)];
|
|
177
184
|
if (!standIns.length)
|
|
178
185
|
return /^\d{1,4}$/.test(part) || /^[A-Za-z]{1,4}$/.test(part) || isWord(part);
|
|
@@ -195,13 +202,19 @@ const MASKED = /^[^*\s]{0,8}\*{4,}[^*\s]{0,8}$/;
|
|
|
195
202
|
* off (`...` then 300 characters).
|
|
196
203
|
*/
|
|
197
204
|
export function isTruncated(token) {
|
|
198
|
-
const m = /^([\w+/=:.-]{0,
|
|
205
|
+
const m = /^([\w+/=:.-]{0,32}?)(?:\.\.\.|…)([\w+/=:.-]{0,40})$/.exec(token);
|
|
199
206
|
if (!m)
|
|
200
207
|
return false;
|
|
208
|
+
// (Up to 16 characters before the ellipsis, or a longer vendor prefix of short parts and up to 10 of the key:
|
|
209
|
+
// `sk-ant-api03-Zx9K…`, `github_pat_11ABCDE0Y0…`.)
|
|
210
|
+
const prefix = /^(?:[a-z][a-z0-9]{1,7}[-_]){2,4}(?=[A-Za-z0-9+/=]{0,10}$)/.exec(m[1])?.[0];
|
|
211
|
+
if (m[1].length > 16 && !prefix)
|
|
212
|
+
return false;
|
|
201
213
|
const runs = (side) => side.match(/[A-Za-z]+/g) ?? [];
|
|
202
214
|
const isWord = (run) => naturalness(run.toLowerCase()) > 0.3;
|
|
203
|
-
// (A person's word: capitalised, or five letters that read as one; a vendor's prefix is neither: `sk-proj`, `AKIA
|
|
204
|
-
|
|
215
|
+
// (A person's word: capitalised, or five letters that read as one; a vendor's prefix is neither: `sk-proj`, `AKIA`,
|
|
216
|
+
// and a long one's words are the vendor's: `github_pat_`.)
|
|
217
|
+
return (!runs(m[1].length > 16 ? m[1].slice(prefix.length) : m[1]).some((run) => /^[A-Z][a-z]{3,}$/.test(run) || (run.length >= 5 && isWord(run))) &&
|
|
205
218
|
(m[2].length <= 8 || runs(m[2]).every((run) => run.length <= 2 || isWord(run))));
|
|
206
219
|
}
|
|
207
220
|
/** A template's holes: `{{ secrets.DB }}`, `${API_KEY}`, `%(password)s`, `{db_pass}`, `<password>`. */
|
|
@@ -298,9 +311,13 @@ export function isPublicJwt(token, samplesOnly = false) {
|
|
|
298
311
|
return false;
|
|
299
312
|
// (jwt.io's sample claims say a sample when a public key signed them, RS256's and the others'; an HMAC token is a
|
|
300
313
|
// sample only by its docs key, signedWithDocsKey, as one signed with a real secret may copy the claims.)
|
|
314
|
+
// (The samples name no issuer and set no expiry: a token that does, `"iss":"https://dev-…auth0.com/"`, came from a
|
|
315
|
+
// real one, whose test user may well be John Doe.)
|
|
301
316
|
const hmac = /"alg"\s*:\s*"HS\d+"/.test(base64Binary(head) ?? "");
|
|
302
317
|
return ((!samplesOnly && /"role"\s*:\s*"anon"/.test(payload)) ||
|
|
303
|
-
(!hmac &&
|
|
318
|
+
(!hmac &&
|
|
319
|
+
(/"sub"\s*:\s*"1234567890"/.test(payload) || /"name"\s*:\s*"John Doe"/.test(payload)) &&
|
|
320
|
+
!/"(?:iss|exp)"\s*:/.test(payload)) ||
|
|
304
321
|
signedWithDocsKey(token));
|
|
305
322
|
}
|
|
306
323
|
/**
|
|
@@ -337,14 +354,17 @@ const base64url = (bytes) => btoa(String.fromCharCode(...bytes))
|
|
|
337
354
|
* Browser and mobile SDK initialisers, whose keys are public by design: calls (`Sentry.init(`, `new Pusher(`,
|
|
338
355
|
* `loadStripe(`) or config objects (`firebaseConfig = {`, `_rollbarConfig = {`). Not a vendor word alone:
|
|
339
356
|
* `IntercomClient(…)` is a server SDK, a bare `createInstance(` is any factory unless Optimizely's is imported (see
|
|
340
|
-
* clientSdkAt), and a comment naming the vendor initialises nothing.
|
|
357
|
+
* clientSdkAt), and a comment naming the vendor initialises nothing. Nor `Rollbar.init(`, which Node servers call with
|
|
358
|
+
* their server token, as `new Rollbar(` is: only the browser snippet's `_rollbarConfig` says client.
|
|
341
359
|
*/
|
|
342
|
-
const CLIENT_SDK_CALL = /(?:\b(?:AnalyticsBrowser\.load|analytics\.load|Bugsnag\.start|
|
|
360
|
+
const CLIENT_SDK_CALL = /(?:\b(?:AnalyticsBrowser\.load|analytics\.load|Bugsnag\.start|optimizely(?:Sdk)?\.createInstance|createInstance|(?:new\s+)?GrowthBook|Purchases\.configure|braintree\.\w+\.create|(?:new\s+)?AdyenCheckout|branch\.init|initializeApp|new\s+mapboxgl\.\w+|(?:new\s+)?Pusher|Sentry\.init|posthog\.init|amplitude\.(?:init|getInstance\(\)\.init)|mixpanel\.init|LDClient\.initialize|datadogRum\.init|DD_RUM\.init|heap\.load|algoliasearch|docsearch|instantsearch|Intercom|loadStripe|Stripe|Square\.payments)\s*\(|\b(?:_rollbarConfig|firebaseConfig\w*)\s*[:=]\s*\{)/g;
|
|
343
361
|
/**
|
|
344
362
|
* The client SDK initialiser whose arguments `at` is directly inside, or inside an object or list given as one of them,
|
|
345
363
|
* if the call starts within 300 characters before it: its call as written (`algoliasearch(`).
|
|
346
364
|
*/
|
|
347
365
|
export function clientSdkAt(text, at) {
|
|
366
|
+
if (!hasClientSdkCall(text))
|
|
367
|
+
return undefined;
|
|
348
368
|
const from = Math.max(0, at - 300);
|
|
349
369
|
let last;
|
|
350
370
|
for (const m of text.slice(from, at).matchAll(CLIENT_SDK_CALL))
|
|
@@ -358,5 +378,7 @@ export function clientSdkAt(text, at) {
|
|
|
358
378
|
const inside = arg.literal || at === arg.start || (/^[{[]/.test(arg.text) && holding(callArguments(text, arg.start + 1)));
|
|
359
379
|
return inside ? last[0] : undefined;
|
|
360
380
|
}
|
|
381
|
+
/** Whether a text calls a client SDK's initialiser anywhere: most don't, and need no look 300 characters back. */
|
|
382
|
+
const hasClientSdkCall = lastText((text) => new RegExp(CLIENT_SDK_CALL.source).test(text));
|
|
361
383
|
/** The parameters client SDKs take public keys under (positional arguments count too). */
|
|
362
384
|
export const PUBLIC_SDK_PARAM = /^(?:api_?key|key|client_?key|client_?token|sdk_?key|app_?id|application_?id|dsn|authorization|publishable_?key|public_?key|site_?key|access_?token|token|search_?(?:only_?)?api_?key)$/i;
|