rechrome 1.40.0 → 1.41.0
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/fill-secret.ts +2 -1
- package/package.json +5 -2
- package/rechrome.js +14 -7
- package/rechrome.ts +14 -7
- package/redact.js +236 -0
- package/redact.ts +242 -0
- package/scoped-upload.js +4 -3
- package/scoped-upload.ts +6 -3
- package/secret-rules/gitleaks.LICENSE +21 -0
- package/secret-rules/gitleaks.toml +3209 -0
package/fill-secret.ts
CHANGED
|
@@ -21,7 +21,8 @@ export const SECRET_MASK = "***";
|
|
|
21
21
|
export const SECRET_MASK_TTL_MS = 15 * 60_000;
|
|
22
22
|
/** Shorter values are never masked: masking "1" everywhere would turn every 1 in every output
|
|
23
23
|
* into ***, corrupting ids and refs for the daemon's lifetime. A TOTP code (6 digits) still is.
|
|
24
|
-
*
|
|
24
|
+
* A short value is kept out of fill-secret's own reply by dropping the echoed code that holds it,
|
|
25
|
+
* and out of its error message by an exact replace; anything the page itself shows is not masked. */
|
|
25
26
|
export const MIN_MASK_LENGTH = 6;
|
|
26
27
|
export const maskable = (value: string | undefined | null): value is string => !!value && value.length >= MIN_MASK_LENGTH;
|
|
27
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rechrome",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/snomiao/rechrome.git"
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"listeners.js",
|
|
24
24
|
"fill-secret.ts",
|
|
25
25
|
"fill-secret.js",
|
|
26
|
+
"redact.ts",
|
|
27
|
+
"redact.js",
|
|
28
|
+
"secret-rules",
|
|
26
29
|
"extension",
|
|
27
30
|
"rechrome.js",
|
|
28
31
|
"rechrome.ts",
|
|
@@ -50,7 +53,7 @@
|
|
|
50
53
|
"vendor-cli": "bash scripts/vendor-cli.sh",
|
|
51
54
|
"prepare": "bash scripts/vendor-cli.sh --prepare",
|
|
52
55
|
"check-pack": "bash scripts/check-pack.sh",
|
|
53
|
-
"prepublishOnly": "bash scripts/vendor-cli.sh && sed 's/\\.ts/\\.js/g' rechrome.ts > rechrome.js && sed 's/\\.ts/\\.js/g' serve.ts > serve.js && bun build extension-token.ts --target=bun --outfile=extension-token.js && bun build daemon-manager.ts --target=bun --outfile=daemon-manager.js && bun build listeners.ts --target=bun --outfile=listeners.js && bun build fill-secret.ts --target=bun --outfile=fill-secret.js && bun build scoped-upload.ts --target=bun --outfile=scoped-upload.js && bun build secret.ts --target=bun --external node-datachannel --outfile=secret.js && bun build shares.ts --target=bun --external node-datachannel --outfile=shares.js && bun build e2e.ts --target=bun --external node-datachannel --outfile=e2e.js && bun build webrtc.ts --target=bun --external node-datachannel --outfile=webrtc.js && bun build signal-relay.ts --target=bun --external node-datachannel --outfile=signal-relay.js && bash scripts/check-pack.sh"
|
|
56
|
+
"prepublishOnly": "bash scripts/vendor-cli.sh && sed 's/\\.ts/\\.js/g' rechrome.ts > rechrome.js && sed 's/\\.ts/\\.js/g' serve.ts > serve.js && bun build extension-token.ts --target=bun --outfile=extension-token.js && bun build daemon-manager.ts --target=bun --outfile=daemon-manager.js && bun build listeners.ts --target=bun --outfile=listeners.js && bun build fill-secret.ts --target=bun --outfile=fill-secret.js && bun build scoped-upload.ts --target=bun --outfile=scoped-upload.js && bun build secret.ts --target=bun --external node-datachannel --outfile=secret.js && bun build shares.ts --target=bun --external node-datachannel --outfile=shares.js && bun build e2e.ts --target=bun --external node-datachannel --outfile=e2e.js && bun build webrtc.ts --target=bun --external node-datachannel --outfile=webrtc.js && bun build signal-relay.ts --target=bun --external node-datachannel --outfile=signal-relay.js && bun build redact.ts --target=bun --outfile=redact.js && bash scripts/check-pack.sh"
|
|
54
57
|
},
|
|
55
58
|
"devDependencies": {
|
|
56
59
|
"@types/bun": "latest",
|
package/rechrome.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readListeners, writeListeners as writeListenersFile, listenerAddress, i
|
|
|
5
5
|
import { file } from "bun";
|
|
6
6
|
import yargs from "yargs";
|
|
7
7
|
import { readExtensionTokenFromProfile } from "./extension-token.js";
|
|
8
|
+
import { redact } from "./redact.js";
|
|
8
9
|
import { FILL_SECRET_USAGE, SECRET_MASK, parseFillSecretArgs, fillSecretWireArgs, readSecretSource, totpCode, totpWaitMs } from "./fill-secret.js";
|
|
9
10
|
import { createHash, randomBytes } from "crypto";
|
|
10
11
|
import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync, unlinkSync, readFileSync, readdirSync, renameSync, readlinkSync, rmdirSync, constants as fsConstants } from "fs";
|
|
@@ -642,7 +643,7 @@ export async function getOrCreateUrl(persist = true): Promise<string> {
|
|
|
642
643
|
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
643
644
|
const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
644
645
|
const content = [...lines, newLine, ""].join("\n");
|
|
645
|
-
await
|
|
646
|
+
await writeAtomic(globalEnvFile, content, 0o600); // holds the management key
|
|
646
647
|
}
|
|
647
648
|
process.env[ENV_KEY] = url;
|
|
648
649
|
return url;
|
|
@@ -1779,7 +1780,7 @@ async function printProfileUri(selector?: string, listener?: string, opts: { loc
|
|
|
1779
1780
|
if (!picked.startsWith(SETUP)) return picked;
|
|
1780
1781
|
const dir = picked.slice(SETUP.length);
|
|
1781
1782
|
console.error(`[rech] ${dir} isn't set up yet; running rech setup --profile ${JSON.stringify(dir)} first.`);
|
|
1782
|
-
await setup({ profile: dir });
|
|
1783
|
+
await setup({ profile: dir, skipShare: true });
|
|
1783
1784
|
const after = await readTokenRegistry();
|
|
1784
1785
|
const key = unregisteredChromeProfiles(after, { [dir]: {} }).length ? undefined : canonicalProfileKeys(after).find(k => after[k].profileDir === dir);
|
|
1785
1786
|
if (process.exitCode || !key) throw new Error(`Setup didn't register ${dir}; nothing shared. Retry with: rech setup --profile ${JSON.stringify(dir)}`);
|
|
@@ -2188,7 +2189,13 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
2188
2189
|
}
|
|
2189
2190
|
const served = await callServe(url, args, overrideEnv, identity, false, upload, secret);
|
|
2190
2191
|
// The daemon already masks the value; this is a second line of defence for an older daemon.
|
|
2191
|
-
|
|
2192
|
+
// Then any secret-shaped string (API keys, tokens) in page text, unless RECH_REDACT=0.
|
|
2193
|
+
const scrub = (s: string) => {
|
|
2194
|
+
if (!s) return s;
|
|
2195
|
+
if (secret) s = s.replaceAll(secret, SECRET_MASK);
|
|
2196
|
+
if (process.env.RECH_REDACT === "0") return s;
|
|
2197
|
+
try { return redact(s).text; } catch { return s; } // missing rules must not swallow output
|
|
2198
|
+
};
|
|
2192
2199
|
const { files, existingSession } = served;
|
|
2193
2200
|
const status = served.status, stdout = scrub(served.stdout), stderr = scrub(served.stderr);
|
|
2194
2201
|
if (!opts.bridgeRetried && isBridgeTimeout(args, { status, stderr })) {
|
|
@@ -2730,7 +2737,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
2730
2737
|
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
2731
2738
|
const lines = envRaw.trimEnd().split("\n");
|
|
2732
2739
|
const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
2733
|
-
await
|
|
2740
|
+
await writeAtomic(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"), 0o600);
|
|
2734
2741
|
|
|
2735
2742
|
const home = HOME;
|
|
2736
2743
|
const bunBin = Bun.which("bun") ?? process.execPath;
|
|
@@ -3427,7 +3434,6 @@ async function saveGlobalUrl(url: string): Promise<string> {
|
|
|
3427
3434
|
return writeEnvUrl(GLOBAL_ENV_FILE, url);
|
|
3428
3435
|
}
|
|
3429
3436
|
|
|
3430
|
-
/** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
|
|
3431
3437
|
/** Write a file whole or not at all: a temp file beside it, then rename (atomic on one filesystem). */
|
|
3432
3438
|
export async function writeAtomic(path: string, data: string, mode?: number): Promise<void> {
|
|
3433
3439
|
// A symlinked .env.local stays a link: write the file it points to.
|
|
@@ -3464,6 +3470,7 @@ export async function ensureProjectDir(dir: string): Promise<void> {
|
|
|
3464
3470
|
if (!existsSync(readme)) await writeAtomic(readme, PROJECT_DIR_README);
|
|
3465
3471
|
}
|
|
3466
3472
|
|
|
3473
|
+
/** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
|
|
3467
3474
|
async function writeEnvUrl(envPath: string, url: string): Promise<string> {
|
|
3468
3475
|
const lines = (await file(envPath).text().catch(() => "")).split("\n").filter(l => l.trim() && !l.startsWith(`${ENV_KEY}=`));
|
|
3469
3476
|
await writeAtomic(envPath, [...lines, envAssignment(ENV_KEY, url), ""].join("\n"), 0o600);
|
|
@@ -3974,7 +3981,7 @@ async function setup(opts: SetupOptions = {}): Promise<void> {
|
|
|
3974
3981
|
guide?.close();
|
|
3975
3982
|
console.log(` Saved token for ${extraDisplay}`);
|
|
3976
3983
|
}
|
|
3977
|
-
if (bridgeVerified && isTTY && !opts.listen) await setupShareStep(ask, profileEmail);
|
|
3984
|
+
if (bridgeVerified && isTTY && !opts.listen && !opts.skipShare) await setupShareStep(ask, profileEmail);
|
|
3978
3985
|
rl?.close();
|
|
3979
3986
|
envWatcher?.close();
|
|
3980
3987
|
if (bridgeVerified)
|
|
@@ -4423,7 +4430,7 @@ async function status(): Promise<void> {
|
|
|
4423
4430
|
}
|
|
4424
4431
|
|
|
4425
4432
|
|
|
4426
|
-
export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean };
|
|
4433
|
+
export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean; /** Called from `rech share`, which shares the profile itself. */ skipShare?: boolean };
|
|
4427
4434
|
export type RechHandlers = {
|
|
4428
4435
|
serve(): Promise<void> | void;
|
|
4429
4436
|
status(): Promise<void>;
|
package/rechrome.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { readListeners, writeListeners as writeListenersFile, listenerAddress, i
|
|
|
5
5
|
import { file } from "bun";
|
|
6
6
|
import yargs from "yargs";
|
|
7
7
|
import { readExtensionTokenFromProfile } from "./extension-token.ts";
|
|
8
|
+
import { redact } from "./redact.ts";
|
|
8
9
|
import { FILL_SECRET_USAGE, SECRET_MASK, parseFillSecretArgs, fillSecretWireArgs, readSecretSource, totpCode, totpWaitMs } from "./fill-secret.ts";
|
|
9
10
|
import { createHash, randomBytes } from "crypto";
|
|
10
11
|
import { mkdirSync, appendFileSync, existsSync, realpathSync, accessSync, cpSync, unlinkSync, readFileSync, readdirSync, renameSync, readlinkSync, rmdirSync, constants as fsConstants } from "fs";
|
|
@@ -642,7 +643,7 @@ export async function getOrCreateUrl(persist = true): Promise<string> {
|
|
|
642
643
|
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
643
644
|
const lines = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
644
645
|
const content = [...lines, newLine, ""].join("\n");
|
|
645
|
-
await
|
|
646
|
+
await writeAtomic(globalEnvFile, content, 0o600); // holds the management key
|
|
646
647
|
}
|
|
647
648
|
process.env[ENV_KEY] = url;
|
|
648
649
|
return url;
|
|
@@ -1779,7 +1780,7 @@ async function printProfileUri(selector?: string, listener?: string, opts: { loc
|
|
|
1779
1780
|
if (!picked.startsWith(SETUP)) return picked;
|
|
1780
1781
|
const dir = picked.slice(SETUP.length);
|
|
1781
1782
|
console.error(`[rech] ${dir} isn't set up yet; running rech setup --profile ${JSON.stringify(dir)} first.`);
|
|
1782
|
-
await setup({ profile: dir });
|
|
1783
|
+
await setup({ profile: dir, skipShare: true });
|
|
1783
1784
|
const after = await readTokenRegistry();
|
|
1784
1785
|
const key = unregisteredChromeProfiles(after, { [dir]: {} }).length ? undefined : canonicalProfileKeys(after).find(k => after[k].profileDir === dir);
|
|
1785
1786
|
if (process.exitCode || !key) throw new Error(`Setup didn't register ${dir}; nothing shared. Retry with: rech setup --profile ${JSON.stringify(dir)}`);
|
|
@@ -2188,7 +2189,13 @@ async function run(url: string, args: string[], overrideEnv?: Record<string, str
|
|
|
2188
2189
|
}
|
|
2189
2190
|
const served = await callServe(url, args, overrideEnv, identity, false, upload, secret);
|
|
2190
2191
|
// The daemon already masks the value; this is a second line of defence for an older daemon.
|
|
2191
|
-
|
|
2192
|
+
// Then any secret-shaped string (API keys, tokens) in page text, unless RECH_REDACT=0.
|
|
2193
|
+
const scrub = (s: string) => {
|
|
2194
|
+
if (!s) return s;
|
|
2195
|
+
if (secret) s = s.replaceAll(secret, SECRET_MASK);
|
|
2196
|
+
if (process.env.RECH_REDACT === "0") return s;
|
|
2197
|
+
try { return redact(s).text; } catch { return s; } // missing rules must not swallow output
|
|
2198
|
+
};
|
|
2192
2199
|
const { files, existingSession } = served;
|
|
2193
2200
|
const status = served.status, stdout = scrub(served.stdout), stderr = scrub(served.stderr);
|
|
2194
2201
|
if (!opts.bridgeRetried && isBridgeTimeout(args, { status, stderr })) {
|
|
@@ -2730,7 +2737,7 @@ export async function daemonInstall(serveUrl: string): Promise<void> {
|
|
|
2730
2737
|
const envRaw = await file(globalEnvFile).text().catch(() => "");
|
|
2731
2738
|
const lines = envRaw.trimEnd().split("\n");
|
|
2732
2739
|
const filtered = lines.filter(l => !l.startsWith(`${ENV_KEY}=`));
|
|
2733
|
-
await
|
|
2740
|
+
await writeAtomic(globalEnvFile, [...filtered, envAssignment(ENV_KEY, keepProfileParam(serveUrl, lines.find(l => l.startsWith(`${ENV_KEY}=`))?.slice(ENV_KEY.length + 1))), ""].join("\n"), 0o600);
|
|
2734
2741
|
|
|
2735
2742
|
const home = HOME;
|
|
2736
2743
|
const bunBin = Bun.which("bun") ?? process.execPath;
|
|
@@ -3427,7 +3434,6 @@ async function saveGlobalUrl(url: string): Promise<string> {
|
|
|
3427
3434
|
return writeEnvUrl(GLOBAL_ENV_FILE, url);
|
|
3428
3435
|
}
|
|
3429
3436
|
|
|
3430
|
-
/** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
|
|
3431
3437
|
/** Write a file whole or not at all: a temp file beside it, then rename (atomic on one filesystem). */
|
|
3432
3438
|
export async function writeAtomic(path: string, data: string, mode?: number): Promise<void> {
|
|
3433
3439
|
// A symlinked .env.local stays a link: write the file it points to.
|
|
@@ -3464,6 +3470,7 @@ export async function ensureProjectDir(dir: string): Promise<void> {
|
|
|
3464
3470
|
if (!existsSync(readme)) await writeAtomic(readme, PROJECT_DIR_README);
|
|
3465
3471
|
}
|
|
3466
3472
|
|
|
3473
|
+
/** Replace RECHROME_URL in an env file, keeping its other lines; owner-only, since the URL holds a key. */
|
|
3467
3474
|
async function writeEnvUrl(envPath: string, url: string): Promise<string> {
|
|
3468
3475
|
const lines = (await file(envPath).text().catch(() => "")).split("\n").filter(l => l.trim() && !l.startsWith(`${ENV_KEY}=`));
|
|
3469
3476
|
await writeAtomic(envPath, [...lines, envAssignment(ENV_KEY, url), ""].join("\n"), 0o600);
|
|
@@ -3974,7 +3981,7 @@ async function setup(opts: SetupOptions = {}): Promise<void> {
|
|
|
3974
3981
|
guide?.close();
|
|
3975
3982
|
console.log(` Saved token for ${extraDisplay}`);
|
|
3976
3983
|
}
|
|
3977
|
-
if (bridgeVerified && isTTY && !opts.listen) await setupShareStep(ask, profileEmail);
|
|
3984
|
+
if (bridgeVerified && isTTY && !opts.listen && !opts.skipShare) await setupShareStep(ask, profileEmail);
|
|
3978
3985
|
rl?.close();
|
|
3979
3986
|
envWatcher?.close();
|
|
3980
3987
|
if (bridgeVerified)
|
|
@@ -4423,7 +4430,7 @@ async function status(): Promise<void> {
|
|
|
4423
4430
|
}
|
|
4424
4431
|
|
|
4425
4432
|
|
|
4426
|
-
export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean };
|
|
4433
|
+
export type SetupOptions = { profile?: string; token?: string; listen?: string; prefix?: string; port?: number; yes?: boolean; /** Called from `rech share`, which shares the profile itself. */ skipShare?: boolean };
|
|
4427
4434
|
export type RechHandlers = {
|
|
4428
4435
|
serve(): Promise<void> | void;
|
|
4429
4436
|
status(): Promise<void>;
|
package/redact.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// redact.ts
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
function goToJs(src) {
|
|
5
|
+
let flags = "";
|
|
6
|
+
for (;; ) {
|
|
7
|
+
const m = /^\(\?([is]+)\)/.exec(src);
|
|
8
|
+
if (!m)
|
|
9
|
+
break;
|
|
10
|
+
flags += m[1];
|
|
11
|
+
src = src.slice(m[0].length);
|
|
12
|
+
}
|
|
13
|
+
src = src.replaceAll("(?P<", "(?<").replaceAll("\\z", "$").replaceAll("[[:alnum:]]", "[A-Za-z0-9]").replaceAll("[:alnum:]", "A-Za-z0-9").replaceAll("[:alpha:]", "A-Za-z").replaceAll("[:digit:]", "0-9").replaceAll("[:space:]", "\\s");
|
|
14
|
+
let out = "";
|
|
15
|
+
const closers = [];
|
|
16
|
+
let extra = 0;
|
|
17
|
+
for (let i = 0;i < src.length; i++) {
|
|
18
|
+
const c = src[i];
|
|
19
|
+
if (c === "\\") {
|
|
20
|
+
out += c + (src[i + 1] ?? "");
|
|
21
|
+
i++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (c === "[") {
|
|
25
|
+
let j = i + 1;
|
|
26
|
+
if (src[j] === "^")
|
|
27
|
+
j++;
|
|
28
|
+
if (src[j] === "]")
|
|
29
|
+
j++;
|
|
30
|
+
while (j < src.length && src[j] !== "]") {
|
|
31
|
+
if (src[j] === "\\")
|
|
32
|
+
j++;
|
|
33
|
+
j++;
|
|
34
|
+
}
|
|
35
|
+
out += src.slice(i, j + 1);
|
|
36
|
+
i = j;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (src.startsWith("(?i)", i)) {
|
|
40
|
+
out += "(?i:";
|
|
41
|
+
extra++;
|
|
42
|
+
i += 3;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (c === "(") {
|
|
46
|
+
closers.push(extra);
|
|
47
|
+
extra = 0;
|
|
48
|
+
out += c;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === ")") {
|
|
52
|
+
out += ")".repeat(extra);
|
|
53
|
+
extra = closers.pop() ?? 0;
|
|
54
|
+
out += c;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (c === "|" && extra) {
|
|
58
|
+
out += ")".repeat(extra) + "|" + "(?i:".repeat(extra);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
out += c;
|
|
62
|
+
}
|
|
63
|
+
out += ")".repeat(extra);
|
|
64
|
+
return { source: out, flags: [...new Set(flags)].join("") };
|
|
65
|
+
}
|
|
66
|
+
function loadGitleaksRules(toml) {
|
|
67
|
+
const c = Bun.TOML.parse(toml);
|
|
68
|
+
const compile = (r) => {
|
|
69
|
+
const j = goToJs(r);
|
|
70
|
+
return new RegExp(j.source, j.flags);
|
|
71
|
+
};
|
|
72
|
+
const gAllow = (c.allowlist?.regexes ?? []).map(compile);
|
|
73
|
+
const gStop = (c.allowlist?.stopwords ?? []).map((s) => s.toLowerCase());
|
|
74
|
+
const rules = [];
|
|
75
|
+
for (const r of c.rules) {
|
|
76
|
+
if (!r.regex)
|
|
77
|
+
continue;
|
|
78
|
+
const allow = [...gAllow];
|
|
79
|
+
const allowMatch = [];
|
|
80
|
+
const allowLine = [];
|
|
81
|
+
const stop = [...gStop];
|
|
82
|
+
for (const a of [...r.allowlists ?? [], ...r.allowlist ? [r.allowlist] : []]) {
|
|
83
|
+
const into = a.regexTarget === "match" ? allowMatch : a.regexTarget === "line" ? allowLine : allow;
|
|
84
|
+
for (const x of a.regexes ?? [])
|
|
85
|
+
into.push(compile(x));
|
|
86
|
+
for (const s of a.stopwords ?? [])
|
|
87
|
+
stop.push(String(s).toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
const j = goToJs(r.regex);
|
|
90
|
+
rules.push({
|
|
91
|
+
id: r.id,
|
|
92
|
+
re: new RegExp(j.source, j.flags + "gd"),
|
|
93
|
+
keywords: (r.keywords ?? []).map((k) => k.toLowerCase()),
|
|
94
|
+
...r.entropy ? { entropy: r.entropy } : {},
|
|
95
|
+
...r.secretGroup ? { group: r.secretGroup } : {},
|
|
96
|
+
allow,
|
|
97
|
+
...allowMatch.length ? { allowMatch } : {},
|
|
98
|
+
...allowLine.length ? { allowLine } : {},
|
|
99
|
+
stop
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return rules;
|
|
103
|
+
}
|
|
104
|
+
var OWN_RULES = [
|
|
105
|
+
{ id: "url-secret-param", re: /[?&](?:access_token|token|key|api_key|apikey|secret|client_secret|password|passwd|pwd|sig|signature)=([^&\s#<>"'|]{6,})/gid, group: 1, keywords: ["="], allow: [/^(?:x+|\*+|\.\.\.|your[-_]?\w*|xxx\w*)$/i], stop: [] },
|
|
106
|
+
{ id: "sk-key", re: /\b(sk-(?:proj-|ant-|or-v1-|svcacct-)?[A-Za-z0-9_-]{20,})/gd, group: 1, keywords: ["sk-"], entropy: 3.5, allow: [], stop: [] },
|
|
107
|
+
{ id: "otp-code", re: /(?:code|\u30B3\u30FC\u30C9|otp|pin|passcode|\u8A8D\u8A3C|\u78BA\u8A8D\u756A\u53F7|\u30EF\u30F3\u30BF\u30A4\u30E0)[^\d\n]{0,20}(?<![\d.])(\d{6})(?![\d.])/gid, group: 1, keywords: [], allow: [], stop: [] }
|
|
108
|
+
];
|
|
109
|
+
var cached;
|
|
110
|
+
function secretRules() {
|
|
111
|
+
cached ??= [...loadGitleaksRules(readFileSync(new URL("./secret-rules/gitleaks.toml", import.meta.url), "utf8")), ...OWN_RULES];
|
|
112
|
+
return cached;
|
|
113
|
+
}
|
|
114
|
+
function shannon(s) {
|
|
115
|
+
const f = new Map;
|
|
116
|
+
for (const ch of s)
|
|
117
|
+
f.set(ch, (f.get(ch) ?? 0) + 1);
|
|
118
|
+
let h = 0;
|
|
119
|
+
for (const n of f.values()) {
|
|
120
|
+
const p = n / s.length;
|
|
121
|
+
h -= p * Math.log2(p);
|
|
122
|
+
}
|
|
123
|
+
return h;
|
|
124
|
+
}
|
|
125
|
+
function findKnown(text, known) {
|
|
126
|
+
const forms = new Set;
|
|
127
|
+
for (const v of known) {
|
|
128
|
+
if (v.length < 6)
|
|
129
|
+
continue;
|
|
130
|
+
forms.add(v);
|
|
131
|
+
forms.add(JSON.stringify(v).slice(1, -1));
|
|
132
|
+
}
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const f of [...forms].sort((a, b) => b.length - a.length)) {
|
|
135
|
+
for (let i = text.indexOf(f);i >= 0; i = text.indexOf(f, i + f.length))
|
|
136
|
+
out.push({ id: "known-secret", start: i, end: i + f.length });
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
var OPAQUE_RUN = /[A-Za-z0-9+/=_-]{4096,}/g;
|
|
141
|
+
function findSecrets(text, opts = {}) {
|
|
142
|
+
if (text.length >= 4096 && OPAQUE_RUN.test(text)) {
|
|
143
|
+
OPAQUE_RUN.lastIndex = 0;
|
|
144
|
+
const out = opts.known?.length ? findKnown(text, opts.known) : [];
|
|
145
|
+
let at = 0;
|
|
146
|
+
for (const m of [...text.matchAll(OPAQUE_RUN), { index: text.length, 0: "" }]) {
|
|
147
|
+
const piece = text.slice(at, m.index);
|
|
148
|
+
if (piece)
|
|
149
|
+
for (const h of findSecrets(piece, { ...opts, known: [] }))
|
|
150
|
+
out.push({ ...h, start: h.start + at, end: h.end + at });
|
|
151
|
+
at = m.index + m[0].length;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
OPAQUE_RUN.lastIndex = 0;
|
|
156
|
+
const rules = opts.rules ?? secretRules();
|
|
157
|
+
const lower = text.toLowerCase();
|
|
158
|
+
const out = opts.known?.length ? findKnown(text, opts.known) : [];
|
|
159
|
+
for (const r of rules) {
|
|
160
|
+
if (r.keywords.length && !r.keywords.some((k) => lower.includes(k)))
|
|
161
|
+
continue;
|
|
162
|
+
for (const m of text.matchAll(r.re)) {
|
|
163
|
+
const gi = r.group ?? m.findIndex((g, i) => i > 0 && !!g);
|
|
164
|
+
const secret = gi > 0 ? m[gi] : m[0];
|
|
165
|
+
if (!secret)
|
|
166
|
+
continue;
|
|
167
|
+
if (r.entropy && shannon(secret) < r.entropy)
|
|
168
|
+
continue;
|
|
169
|
+
if (r.allow.some((a) => a.test(secret)))
|
|
170
|
+
continue;
|
|
171
|
+
if (r.allowMatch?.some((a) => a.test(m[0])))
|
|
172
|
+
continue;
|
|
173
|
+
if (r.allowLine) {
|
|
174
|
+
const ls = text.lastIndexOf(`
|
|
175
|
+
`, m.index) + 1;
|
|
176
|
+
const le = text.indexOf(`
|
|
177
|
+
`, m.index + m[0].length);
|
|
178
|
+
if (r.allowLine.some((a) => a.test(text.slice(ls, le < 0 ? undefined : le))))
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const sl = secret.toLowerCase();
|
|
182
|
+
if (r.stop.some((s) => sl.includes(s)))
|
|
183
|
+
continue;
|
|
184
|
+
const span = r.group ? m.indices?.[r.group] : undefined;
|
|
185
|
+
if (span)
|
|
186
|
+
out.push({ id: r.id, start: span[0], end: span[1] });
|
|
187
|
+
else {
|
|
188
|
+
let a = m.index;
|
|
189
|
+
let b = m.index + m[0].length;
|
|
190
|
+
const edge = /[\s'"`;=:,]/;
|
|
191
|
+
while (a < b && edge.test(text[a]))
|
|
192
|
+
a++;
|
|
193
|
+
for (;; ) {
|
|
194
|
+
if (b > a && edge.test(text[b - 1]))
|
|
195
|
+
b--;
|
|
196
|
+
else if (b - 2 >= a && /\\[nr]/.test(text.slice(b - 2, b)))
|
|
197
|
+
b -= 2;
|
|
198
|
+
else
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
out.push({ id: r.id, start: a, end: b });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function redact(text, opts = {}) {
|
|
208
|
+
const hits = findSecrets(text, opts).sort((a, b) => a.start - b.start || b.end - a.end);
|
|
209
|
+
if (!hits.length)
|
|
210
|
+
return { text, kinds: [] };
|
|
211
|
+
const spans = [];
|
|
212
|
+
for (const h of hits) {
|
|
213
|
+
const last = spans[spans.length - 1];
|
|
214
|
+
if (last && h.start < last.end) {
|
|
215
|
+
if (h.end - h.start > last.end - last.start)
|
|
216
|
+
last.id = h.id;
|
|
217
|
+
last.end = Math.max(last.end, h.end);
|
|
218
|
+
} else
|
|
219
|
+
spans.push({ ...h });
|
|
220
|
+
}
|
|
221
|
+
let out = "";
|
|
222
|
+
let at = 0;
|
|
223
|
+
for (const s of spans) {
|
|
224
|
+
out += text.slice(at, s.start) + `[redacted:${s.id}]`;
|
|
225
|
+
at = s.end;
|
|
226
|
+
}
|
|
227
|
+
return { text: out + text.slice(at), kinds: [...new Set(spans.map((s) => s.id))] };
|
|
228
|
+
}
|
|
229
|
+
export {
|
|
230
|
+
OWN_RULES,
|
|
231
|
+
findSecrets,
|
|
232
|
+
goToJs,
|
|
233
|
+
loadGitleaksRules,
|
|
234
|
+
redact,
|
|
235
|
+
secretRules
|
|
236
|
+
};
|