rechrome 1.47.0 → 1.48.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/README.md +4 -0
- package/e2e.js +2 -2
- package/e2e.ts +2 -2
- package/extension-token.js +37 -1
- package/extension-token.ts +37 -1
- package/fill-secret.js +542 -40
- package/fill-secret.ts +293 -33
- package/listeners.js +9 -5
- package/listeners.ts +10 -4
- package/package.json +9 -2
- package/rechrome.js +96 -21
- package/rechrome.ts +96 -21
- package/record.js +281 -0
- package/record.ts +251 -0
- package/rtc-channel.js +303 -0
- package/rtc-channel.ts +130 -0
- package/secret.js +15 -10
- package/secret.ts +2 -70
- package/serve.js +137 -13
- package/serve.ts +137 -13
- package/shares.js +49 -12
- package/shares.ts +39 -6
- package/token.js +88 -0
- package/token.ts +74 -0
- package/webrtc.js +92 -85
- package/webrtc.ts +6 -128
package/fill-secret.js
CHANGED
|
@@ -2,9 +2,239 @@
|
|
|
2
2
|
// fill-secret.ts
|
|
3
3
|
import { createHmac } from "crypto";
|
|
4
4
|
import { connect } from "net";
|
|
5
|
-
import { readdirSync, readFileSync } from "fs";
|
|
5
|
+
import { readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { join } from "path";
|
|
8
|
+
|
|
9
|
+
// redact.ts
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
function goToJs(src) {
|
|
12
|
+
let flags = "";
|
|
13
|
+
for (;; ) {
|
|
14
|
+
const m = /^\(\?([is]+)\)/.exec(src);
|
|
15
|
+
if (!m)
|
|
16
|
+
break;
|
|
17
|
+
flags += m[1];
|
|
18
|
+
src = src.slice(m[0].length);
|
|
19
|
+
}
|
|
20
|
+
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");
|
|
21
|
+
let out = "";
|
|
22
|
+
const closers = [];
|
|
23
|
+
let extra = 0;
|
|
24
|
+
for (let i = 0;i < src.length; i++) {
|
|
25
|
+
const c = src[i];
|
|
26
|
+
if (c === "\\") {
|
|
27
|
+
out += c + (src[i + 1] ?? "");
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (c === "[") {
|
|
32
|
+
let j = i + 1;
|
|
33
|
+
if (src[j] === "^")
|
|
34
|
+
j++;
|
|
35
|
+
if (src[j] === "]")
|
|
36
|
+
j++;
|
|
37
|
+
while (j < src.length && src[j] !== "]") {
|
|
38
|
+
if (src[j] === "\\")
|
|
39
|
+
j++;
|
|
40
|
+
j++;
|
|
41
|
+
}
|
|
42
|
+
out += src.slice(i, j + 1);
|
|
43
|
+
i = j;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (src.startsWith("(?i)", i)) {
|
|
47
|
+
out += "(?i:";
|
|
48
|
+
extra++;
|
|
49
|
+
i += 3;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (c === "(") {
|
|
53
|
+
closers.push(extra);
|
|
54
|
+
extra = 0;
|
|
55
|
+
out += c;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (c === ")") {
|
|
59
|
+
out += ")".repeat(extra);
|
|
60
|
+
extra = closers.pop() ?? 0;
|
|
61
|
+
out += c;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === "|" && extra) {
|
|
65
|
+
out += ")".repeat(extra) + "|" + "(?i:".repeat(extra);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
out += c;
|
|
69
|
+
}
|
|
70
|
+
out += ")".repeat(extra);
|
|
71
|
+
return { source: out, flags: [...new Set(flags)].join("") };
|
|
72
|
+
}
|
|
73
|
+
function loadGitleaksRules(toml) {
|
|
74
|
+
const c = Bun.TOML.parse(toml);
|
|
75
|
+
const compile = (r) => {
|
|
76
|
+
const j = goToJs(r);
|
|
77
|
+
return new RegExp(j.source, j.flags);
|
|
78
|
+
};
|
|
79
|
+
const gAllow = (c.allowlist?.regexes ?? []).map(compile);
|
|
80
|
+
const gStop = (c.allowlist?.stopwords ?? []).map((s) => s.toLowerCase());
|
|
81
|
+
const rules = [];
|
|
82
|
+
for (const r of c.rules) {
|
|
83
|
+
if (!r.regex)
|
|
84
|
+
continue;
|
|
85
|
+
const allow = [...gAllow];
|
|
86
|
+
const allowMatch = [];
|
|
87
|
+
const allowLine = [];
|
|
88
|
+
const stop = [...gStop];
|
|
89
|
+
for (const a of [...r.allowlists ?? [], ...r.allowlist ? [r.allowlist] : []]) {
|
|
90
|
+
const into = a.regexTarget === "match" ? allowMatch : a.regexTarget === "line" ? allowLine : allow;
|
|
91
|
+
for (const x of a.regexes ?? [])
|
|
92
|
+
into.push(compile(x));
|
|
93
|
+
for (const s of a.stopwords ?? [])
|
|
94
|
+
stop.push(String(s).toLowerCase());
|
|
95
|
+
}
|
|
96
|
+
const j = goToJs(r.regex);
|
|
97
|
+
rules.push({
|
|
98
|
+
id: r.id,
|
|
99
|
+
re: new RegExp(j.source, j.flags + "gd"),
|
|
100
|
+
keywords: (r.keywords ?? []).map((k) => k.toLowerCase()),
|
|
101
|
+
...r.entropy ? { entropy: r.entropy } : {},
|
|
102
|
+
...r.secretGroup ? { group: r.secretGroup } : {},
|
|
103
|
+
allow,
|
|
104
|
+
...allowMatch.length ? { allowMatch } : {},
|
|
105
|
+
...allowLine.length ? { allowLine } : {},
|
|
106
|
+
stop
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return rules;
|
|
110
|
+
}
|
|
111
|
+
var OWN_RULES = [
|
|
112
|
+
{ 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: [] },
|
|
113
|
+
{ 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: [] },
|
|
114
|
+
{ 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: [] }
|
|
115
|
+
];
|
|
116
|
+
var cached;
|
|
117
|
+
function secretRules() {
|
|
118
|
+
cached ??= [...loadGitleaksRules(readFileSync(new URL("./secret-rules/gitleaks.toml", import.meta.url), "utf8")), ...OWN_RULES];
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
function shannon(s) {
|
|
122
|
+
const f = new Map;
|
|
123
|
+
for (const ch of s)
|
|
124
|
+
f.set(ch, (f.get(ch) ?? 0) + 1);
|
|
125
|
+
let h = 0;
|
|
126
|
+
for (const n of f.values()) {
|
|
127
|
+
const p = n / s.length;
|
|
128
|
+
h -= p * Math.log2(p);
|
|
129
|
+
}
|
|
130
|
+
return h;
|
|
131
|
+
}
|
|
132
|
+
function findKnown(text, known) {
|
|
133
|
+
const forms = new Set;
|
|
134
|
+
for (const v of known) {
|
|
135
|
+
if (v.length < 6)
|
|
136
|
+
continue;
|
|
137
|
+
forms.add(v);
|
|
138
|
+
forms.add(JSON.stringify(v).slice(1, -1));
|
|
139
|
+
}
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const f of [...forms].sort((a, b) => b.length - a.length)) {
|
|
142
|
+
for (let i = text.indexOf(f);i >= 0; i = text.indexOf(f, i + f.length))
|
|
143
|
+
out.push({ id: "known-secret", start: i, end: i + f.length });
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
var OPAQUE_RUN = /[A-Za-z0-9+/=_-]{4096,}/g;
|
|
148
|
+
function findSecrets(text, opts = {}) {
|
|
149
|
+
if (text.length >= 4096 && OPAQUE_RUN.test(text)) {
|
|
150
|
+
OPAQUE_RUN.lastIndex = 0;
|
|
151
|
+
const out = opts.known?.length ? findKnown(text, opts.known) : [];
|
|
152
|
+
let at = 0;
|
|
153
|
+
for (const m of [...text.matchAll(OPAQUE_RUN), { index: text.length, 0: "" }]) {
|
|
154
|
+
const piece = text.slice(at, m.index);
|
|
155
|
+
if (piece)
|
|
156
|
+
for (const h of findSecrets(piece, { ...opts, known: [] }))
|
|
157
|
+
out.push({ ...h, start: h.start + at, end: h.end + at });
|
|
158
|
+
at = m.index + m[0].length;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
OPAQUE_RUN.lastIndex = 0;
|
|
163
|
+
const rules = opts.rules ?? secretRules();
|
|
164
|
+
const lower = text.toLowerCase();
|
|
165
|
+
const out = opts.known?.length ? findKnown(text, opts.known) : [];
|
|
166
|
+
for (const r of rules) {
|
|
167
|
+
if (r.keywords.length && !r.keywords.some((k) => lower.includes(k)))
|
|
168
|
+
continue;
|
|
169
|
+
for (const m of text.matchAll(r.re)) {
|
|
170
|
+
const gi = r.group ?? m.findIndex((g, i) => i > 0 && !!g);
|
|
171
|
+
const secret = gi > 0 ? m[gi] : m[0];
|
|
172
|
+
if (!secret)
|
|
173
|
+
continue;
|
|
174
|
+
if (r.entropy && shannon(secret) < r.entropy)
|
|
175
|
+
continue;
|
|
176
|
+
if (r.allow.some((a) => a.test(secret)))
|
|
177
|
+
continue;
|
|
178
|
+
if (r.allowMatch?.some((a) => a.test(m[0])))
|
|
179
|
+
continue;
|
|
180
|
+
if (r.allowLine) {
|
|
181
|
+
const ls = text.lastIndexOf(`
|
|
182
|
+
`, m.index) + 1;
|
|
183
|
+
const le = text.indexOf(`
|
|
184
|
+
`, m.index + m[0].length);
|
|
185
|
+
if (r.allowLine.some((a) => a.test(text.slice(ls, le < 0 ? undefined : le))))
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const sl = secret.toLowerCase();
|
|
189
|
+
if (r.stop.some((s) => sl.includes(s)))
|
|
190
|
+
continue;
|
|
191
|
+
const span = r.group ? m.indices?.[r.group] : undefined;
|
|
192
|
+
if (span)
|
|
193
|
+
out.push({ id: r.id, start: span[0], end: span[1] });
|
|
194
|
+
else {
|
|
195
|
+
let a = m.index;
|
|
196
|
+
let b = m.index + m[0].length;
|
|
197
|
+
const edge = /[\s'"`;=:,]/;
|
|
198
|
+
while (a < b && edge.test(text[a]))
|
|
199
|
+
a++;
|
|
200
|
+
for (;; ) {
|
|
201
|
+
if (b > a && edge.test(text[b - 1]))
|
|
202
|
+
b--;
|
|
203
|
+
else if (b - 2 >= a && /\\[nr]/.test(text.slice(b - 2, b)))
|
|
204
|
+
b -= 2;
|
|
205
|
+
else
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
out.push({ id: r.id, start: a, end: b });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function redact(text, opts = {}) {
|
|
215
|
+
const hits = findSecrets(text, opts).sort((a, b) => a.start - b.start || b.end - a.end);
|
|
216
|
+
if (!hits.length)
|
|
217
|
+
return { text, kinds: [] };
|
|
218
|
+
const spans = [];
|
|
219
|
+
for (const h of hits) {
|
|
220
|
+
const last = spans[spans.length - 1];
|
|
221
|
+
if (last && h.start < last.end) {
|
|
222
|
+
if (h.end - h.start > last.end - last.start)
|
|
223
|
+
last.id = h.id;
|
|
224
|
+
last.end = Math.max(last.end, h.end);
|
|
225
|
+
} else
|
|
226
|
+
spans.push({ ...h });
|
|
227
|
+
}
|
|
228
|
+
let out = "";
|
|
229
|
+
let at = 0;
|
|
230
|
+
for (const s of spans) {
|
|
231
|
+
out += text.slice(at, s.start) + `[redacted:${s.id}]`;
|
|
232
|
+
at = s.end;
|
|
233
|
+
}
|
|
234
|
+
return { text: out + text.slice(at), kinds: [...new Set(spans.map((s) => s.id))] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// fill-secret.ts
|
|
8
238
|
var SECRET_MASK = "***";
|
|
9
239
|
var SECRET_MASK_TTL_MS = 15 * 60000;
|
|
10
240
|
var MIN_MASK_LENGTH = 6;
|
|
@@ -266,7 +496,7 @@ function daemonSessionSockets(session, env = process.env) {
|
|
|
266
496
|
if (!/^[0-9a-f]{16}$/.test(h))
|
|
267
497
|
continue;
|
|
268
498
|
try {
|
|
269
|
-
const cfg = JSON.parse(
|
|
499
|
+
const cfg = JSON.parse(readFileSync2(join(base, h, `${session}.session`), "utf8"));
|
|
270
500
|
if ((cfg?.name ?? session) === session && typeof cfg?.socketPath === "string" && cfg.socketPath)
|
|
271
501
|
out.push(cfg.socketPath);
|
|
272
502
|
} catch {}
|
|
@@ -317,7 +547,7 @@ async function runCodeOnSession(opts) {
|
|
|
317
547
|
for (const path of candidates) {
|
|
318
548
|
let reply;
|
|
319
549
|
try {
|
|
320
|
-
reply = await socketRequest(path, "run", { args: { _: ["run-code", opts.code] }, cwd: opts.cwd, ...opts.json ? { json: true } : {} });
|
|
550
|
+
reply = await socketRequest(path, "run", { args: { _: ["run-code", opts.code] }, cwd: opts.cwd, ...opts.json ? { json: true } : {} }, opts.timeoutMs);
|
|
321
551
|
} catch (e) {
|
|
322
552
|
errors.push(e instanceof Error ? e.message : String(e));
|
|
323
553
|
continue;
|
|
@@ -329,6 +559,22 @@ async function runCodeOnSession(opts) {
|
|
|
329
559
|
}
|
|
330
560
|
throw new Error(`could not reach this session's browser (${errors.join("; ")}); run \`rech open <url>\` first`);
|
|
331
561
|
}
|
|
562
|
+
async function runCliOnSession(opts, cliArgs) {
|
|
563
|
+
const candidates = [...new Set([...sessionSocketCandidates(opts.socketRoot, opts.session), ...daemonSessionSockets(opts.session)])];
|
|
564
|
+
for (const path of candidates) {
|
|
565
|
+
let reply;
|
|
566
|
+
try {
|
|
567
|
+
reply = await socketRequest(path, "run", { args: { _: cliArgs }, cwd: opts.cwd });
|
|
568
|
+
} catch {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (reply.error)
|
|
572
|
+
throw new Error(reply.error);
|
|
573
|
+
const r = reply.result;
|
|
574
|
+
return { isError: !!r?.isError, text: typeof r === "string" ? r : typeof r?.text === "string" ? r.text : "" };
|
|
575
|
+
}
|
|
576
|
+
throw new Error("no browser is open in this session");
|
|
577
|
+
}
|
|
332
578
|
function fillSecretOnSession(opts) {
|
|
333
579
|
return runCodeOnSession({ ...opts, code: buildFillCode(opts.ref, opts.value, opts.allowDomains, opts.submit) });
|
|
334
580
|
}
|
|
@@ -434,13 +680,27 @@ async function readSecretOnSession(opts) {
|
|
|
434
680
|
throw new Error("read-secret: the page returned no value");
|
|
435
681
|
return out;
|
|
436
682
|
}
|
|
437
|
-
var READ_STORAGE_USAGE = `Usage: rech read-storage <local|session> <key> [json-path] --allow-domain <glob> | <command>
|
|
683
|
+
var READ_STORAGE_USAGE = `Usage: rech read-storage <local|session> <key> [json-path] --allow-domain <glob> [--goto <url>] | <command>
|
|
438
684
|
Writes the storage value, raw and without a newline, to stdout, which must be a pipe into the
|
|
439
685
|
program that stores it (never a terminal or a file). Never logged; masked in later outputs.
|
|
440
686
|
<local|session> localStorage or sessionStorage
|
|
441
687
|
<key> the storage key, e.g. localConfig_v2
|
|
442
688
|
[json-path] optional: a path into the JSON value, e.g. teams.T0123.token or teams[T0123].token
|
|
443
|
-
--allow-domain GLOB required: the page's host must match, e.g. app.slack.com
|
|
689
|
+
--allow-domain GLOB required: the page's host must match, e.g. app.slack.com
|
|
690
|
+
--goto URL first navigate the attached tab to URL (host must match --allow-domain),
|
|
691
|
+
so the value is read from it, e.g. --goto https://app.slack.com/client/T0`;
|
|
692
|
+
function checkGotoUrl(gotoUrl, allowDomains) {
|
|
693
|
+
let u;
|
|
694
|
+
try {
|
|
695
|
+
u = new URL(gotoUrl);
|
|
696
|
+
} catch {
|
|
697
|
+
throw new Error("--goto must be an http(s) URL");
|
|
698
|
+
}
|
|
699
|
+
if (!/^https?:$/.test(u.protocol))
|
|
700
|
+
throw new Error("--goto must be an http(s) URL");
|
|
701
|
+
if (!allowDomains.some((d) => domainGlobToRegExp(d).test(u.hostname.toLowerCase())))
|
|
702
|
+
throw new Error("--goto URL host must match --allow-domain");
|
|
703
|
+
}
|
|
444
704
|
function parseStoragePath(path) {
|
|
445
705
|
return path.match(/[^.[\]]+/g) ?? [];
|
|
446
706
|
}
|
|
@@ -450,6 +710,7 @@ function parseReadStorageArgs(args) {
|
|
|
450
710
|
throw new Error("Expected read-storage");
|
|
451
711
|
const pos = [];
|
|
452
712
|
const allowDomains = [];
|
|
713
|
+
let gotoUrl;
|
|
453
714
|
for (let i = 0;i < rest.length; i++) {
|
|
454
715
|
const a = rest[i];
|
|
455
716
|
if (a === "--allow-domain" || a.startsWith("--allow-domain=")) {
|
|
@@ -460,6 +721,10 @@ function parseReadStorageArgs(args) {
|
|
|
460
721
|
domainGlobToRegExp(d.trim());
|
|
461
722
|
allowDomains.push(d.trim());
|
|
462
723
|
}
|
|
724
|
+
} else if (a === "--goto" || a.startsWith("--goto=")) {
|
|
725
|
+
gotoUrl = a === "--goto" ? rest[++i] : a.slice("--goto=".length);
|
|
726
|
+
if (!gotoUrl)
|
|
727
|
+
throw new Error("--goto got an empty URL");
|
|
463
728
|
} else if (a.startsWith("-"))
|
|
464
729
|
throw new Error(`read-storage: unknown option ${a}`);
|
|
465
730
|
else
|
|
@@ -474,10 +739,12 @@ function parseReadStorageArgs(args) {
|
|
|
474
739
|
throw new Error("read-storage: a storage key is required");
|
|
475
740
|
if (!allowDomains.length)
|
|
476
741
|
throw new Error("read-storage needs --allow-domain: name the site the value must come from");
|
|
477
|
-
|
|
742
|
+
if (gotoUrl)
|
|
743
|
+
checkGotoUrl(gotoUrl, allowDomains);
|
|
744
|
+
return { area, key, path: rawPath ? parseStoragePath(rawPath) : [], allowDomains, gotoUrl };
|
|
478
745
|
}
|
|
479
746
|
function readStorageWireArgs(req) {
|
|
480
|
-
return ["read-storage", req.area, req.key, ...req.path.length ? [req.path.join(".")] : [], ...req.allowDomains.map((d) => `--allow-domain=${d}`)];
|
|
747
|
+
return ["read-storage", req.area, req.key, ...req.path.length ? [req.path.join(".")] : [], ...req.allowDomains.map((d) => `--allow-domain=${d}`), ...req.gotoUrl ? [`--goto=${req.gotoUrl}`] : []];
|
|
481
748
|
}
|
|
482
749
|
function parseReadStorageWire(args) {
|
|
483
750
|
const [cmd, area, key, ...rest] = args;
|
|
@@ -485,11 +752,14 @@ function parseReadStorageWire(args) {
|
|
|
485
752
|
throw new Error("read-storage needs <local|session> <key>");
|
|
486
753
|
const allowDomains = [];
|
|
487
754
|
let path = [];
|
|
755
|
+
let gotoUrl;
|
|
488
756
|
for (const a of rest) {
|
|
489
757
|
if (a.startsWith("--allow-domain=")) {
|
|
490
758
|
const d = a.slice("--allow-domain=".length);
|
|
491
759
|
domainGlobToRegExp(d);
|
|
492
760
|
allowDomains.push(d);
|
|
761
|
+
} else if (a.startsWith("--goto=")) {
|
|
762
|
+
gotoUrl = a.slice("--goto=".length);
|
|
493
763
|
} else if (!a.startsWith("-") && !path.length)
|
|
494
764
|
path = parseStoragePath(a);
|
|
495
765
|
else
|
|
@@ -497,31 +767,42 @@ function parseReadStorageWire(args) {
|
|
|
497
767
|
}
|
|
498
768
|
if (!allowDomains.length)
|
|
499
769
|
throw new Error("read-storage needs --allow-domain");
|
|
500
|
-
|
|
770
|
+
if (gotoUrl !== undefined)
|
|
771
|
+
checkGotoUrl(gotoUrl, allowDomains);
|
|
772
|
+
return { area, key, path, allowDomains, gotoUrl };
|
|
501
773
|
}
|
|
502
774
|
function buildReadStorageCode(area, key, path, allowDomains) {
|
|
503
775
|
const patterns = allowDomains.map((d) => domainGlobToRegExp(d).source);
|
|
504
776
|
const store = area === "local" ? "localStorage" : "sessionStorage";
|
|
505
777
|
return `async page => {
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
|
|
778
|
+
const allowed = ${JSON.stringify(patterns)};
|
|
779
|
+
const hostOf = (u) => { const m = /^[a-z][a-z0-9+.-]*:\\/\\/([^/?#]+)/i.exec(String(u || "")); return m ? m[1].replace(/^[^@]*@/, "").replace(/:\\d+$/, "").toLowerCase() : ""; };
|
|
780
|
+
const ok = (h) => h && allowed.some(p => new RegExp(p, "i").test(h));
|
|
781
|
+
// The caller has foregrounded the --allow-domain tab; read from whichever of this tab's frames
|
|
782
|
+
// is on the allowed host (the top frame can be a blank wrapper over the bridge).
|
|
783
|
+
const frame = page.frames().find(f => ok(hostOf(f.url())));
|
|
784
|
+
const host = frame ? hostOf(frame.url()) : "";
|
|
785
|
+
if (!frame)
|
|
786
|
+
throw new Error("read-storage refused: the foreground tab is not on an --allow-domain host (host " + JSON.stringify(hostOf(page.url())) + ")");
|
|
787
|
+
// Re-check the host inside the same evaluate that reads, so a navigation between the frame
|
|
788
|
+
// pick and the read can't hand over another origin's storage.
|
|
789
|
+
const out = await frame.evaluate(({ key, path, allowed }) => {
|
|
790
|
+
const m = /^[a-z][a-z0-9+.-]*:\\/\\/([^/?#]+)/i.exec(String(document.URL || ""));
|
|
791
|
+
const h = m ? m[1].replace(/^[^@]*@/, "").replace(/:\\d+$/, "").toLowerCase() : "";
|
|
792
|
+
if (!h || !allowed.some(p => new RegExp(p, "i").test(h))) return { moved: true };
|
|
509
793
|
const raw = window.${store}.getItem(key);
|
|
510
|
-
if (raw === null) return {
|
|
511
|
-
if (!path.length) return {
|
|
794
|
+
if (raw === null) return { missing: true };
|
|
795
|
+
if (!path.length) return { value: raw };
|
|
512
796
|
let v;
|
|
513
|
-
try { v = JSON.parse(raw); } catch { return {
|
|
797
|
+
try { v = JSON.parse(raw); } catch { return { notjson: true }; }
|
|
514
798
|
for (const seg of path) {
|
|
515
|
-
if (v == null || typeof v !== "object") return {
|
|
799
|
+
if (v == null || typeof v !== "object") return { nopath: true };
|
|
516
800
|
v = v[seg];
|
|
517
801
|
}
|
|
518
|
-
if (v === undefined) return {
|
|
519
|
-
return {
|
|
520
|
-
}, { key: ${JSON.stringify(key)}, path: ${JSON.stringify(path)} });
|
|
521
|
-
|
|
522
|
-
const allowed = ${JSON.stringify(patterns)};
|
|
523
|
-
if (!allowed.some(p => new RegExp(p, "i").test(host)))
|
|
524
|
-
throw new Error("read-storage refused: the page host " + JSON.stringify(host) + " is not allowed by --allow-domain");
|
|
802
|
+
if (v === undefined) return { nopath: true };
|
|
803
|
+
return { value: typeof v === "string" ? v : JSON.stringify(v) };
|
|
804
|
+
}, { key: ${JSON.stringify(key)}, path: ${JSON.stringify(path)}, allowed });
|
|
805
|
+
if (out.moved) throw new Error("read-storage refused: the foreground tab is not on an --allow-domain host (it navigated away)");
|
|
525
806
|
if (out.missing) throw new Error("read-storage: no ${store} key " + JSON.stringify(${JSON.stringify(key)}) + " on " + host);
|
|
526
807
|
if (out.notjson) throw new Error("read-storage: the value is not JSON, so the path can't apply");
|
|
527
808
|
if (out.nopath) throw new Error("read-storage: the json-path is not present in the value");
|
|
@@ -529,27 +810,41 @@ function buildReadStorageCode(area, key, path, allowDomains) {
|
|
|
529
810
|
}`;
|
|
530
811
|
}
|
|
531
812
|
async function readStorageOnSession(opts) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
`)[0] : "read-storage: could not read the value"));
|
|
536
|
-
let raw;
|
|
537
|
-
try {
|
|
538
|
-
raw = JSON.parse(text).result;
|
|
539
|
-
} catch {
|
|
540
|
-
raw = text.match(/### Result\n([^\n]*)/)?.[1];
|
|
813
|
+
if (opts.gotoUrl) {
|
|
814
|
+
await runCliOnSession(opts, ["goto", opts.gotoUrl]).catch(() => {});
|
|
815
|
+
await new Promise((r) => setTimeout(r, 2500));
|
|
541
816
|
}
|
|
542
|
-
let
|
|
543
|
-
for (let
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
817
|
+
let last = "";
|
|
818
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
819
|
+
const { isError, text } = await runCodeOnSession({ socketRoot: opts.socketRoot, session: opts.session, cwd: opts.cwd, code: buildReadStorageCode(opts.area, opts.key, opts.path, opts.allowDomains), json: true });
|
|
820
|
+
if (!isError) {
|
|
821
|
+
let raw;
|
|
822
|
+
try {
|
|
823
|
+
raw = JSON.parse(text).result;
|
|
824
|
+
} catch {
|
|
825
|
+
raw = text.match(/### Result\n([^\n]*)/)?.[1];
|
|
826
|
+
}
|
|
827
|
+
let o = raw;
|
|
828
|
+
for (let i = 0;i < 2 && typeof o === "string"; i++) {
|
|
829
|
+
try {
|
|
830
|
+
o = JSON.parse(o);
|
|
831
|
+
} catch {
|
|
832
|
+
o = undefined;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (o && typeof o.host === "string" && typeof o.value === "string")
|
|
836
|
+
return o;
|
|
837
|
+
throw new Error("read-storage: the page returned no value");
|
|
548
838
|
}
|
|
839
|
+
last = text.match(/read-storage(?: refused)?: [^\n]*/)?.[0] ?? (/no browser is open|could not reach/.test(text) ? text.trim().split(`
|
|
840
|
+
`)[0] : "read-storage: could not read the value");
|
|
841
|
+
if (!/foreground tab is not on/.test(last))
|
|
842
|
+
break;
|
|
843
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
549
844
|
}
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
|
|
845
|
+
if (/foreground tab is not on/.test(last))
|
|
846
|
+
last += ". Put the site on the active tab first: rech goto <url on that host>, then rerun read-storage.";
|
|
847
|
+
throw new Error(last || "read-storage: could not read the value");
|
|
553
848
|
}
|
|
554
849
|
var READ_COOKIE_USAGE = `Usage: rech read-cookie <name> <domain> --allow-domain <domain> | <command>
|
|
555
850
|
Writes the cookie's value, raw and without a newline, to stdout, which must be a pipe into the
|
|
@@ -656,6 +951,194 @@ async function readCookieOnSession(opts) {
|
|
|
656
951
|
throw new Error("read-cookie: the browser returned no value");
|
|
657
952
|
return out;
|
|
658
953
|
}
|
|
954
|
+
var REDACTED_HEADERS = [
|
|
955
|
+
"set-cookie",
|
|
956
|
+
"cookie",
|
|
957
|
+
"authorization",
|
|
958
|
+
"proxy-authorization",
|
|
959
|
+
"www-authenticate",
|
|
960
|
+
"proxy-authenticate",
|
|
961
|
+
"x-api-key",
|
|
962
|
+
"x-csrf-token",
|
|
963
|
+
"x-xsrf-token"
|
|
964
|
+
];
|
|
965
|
+
var REDACTED_HEADER_PATTERN = "[-_](?:token|secret|api[-_]?key)$";
|
|
966
|
+
var URL_HEADERS = ["location", "content-location"];
|
|
967
|
+
var HEADER_REDACTION = "<redacted>";
|
|
968
|
+
var READ_HEADERS_DEFAULT_SECONDS = 5;
|
|
969
|
+
var READ_HEADERS_MAX_SECONDS = 50;
|
|
970
|
+
var READ_HEADERS_USAGE = `Usage: rech read-headers --allow-domain <host> [--allow-domain <host>]... [--seconds N] [--goto <url>]
|
|
971
|
+
Captures the status code and response headers (NO bodies) of every request on an --allow-domain
|
|
972
|
+
host over a capture window, and writes them as a JSON evidence file in the output dir (an array of
|
|
973
|
+
{url, status, headers}). Credential header values (cookie, set-cookie, authorization,
|
|
974
|
+
*-authenticate, x-api-key, *-token, *-secret, ...) are redacted, as are URL query values,
|
|
975
|
+
fragments and userinfo; then the file goes through the secret scanner.
|
|
976
|
+
--allow-domain GLOB required: capture only responses whose host matches, e.g. app.example.com
|
|
977
|
+
--seconds N capture window in seconds (default ${READ_HEADERS_DEFAULT_SECONDS}, max ${READ_HEADERS_MAX_SECONDS})
|
|
978
|
+
--goto URL first navigate the attached tab to URL (host must match --allow-domain),
|
|
979
|
+
then capture, so a page load's requests are captured (within the window)`;
|
|
980
|
+
function hostFromUrl(u) {
|
|
981
|
+
const m = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]+)/i.exec(String(u || ""));
|
|
982
|
+
return m ? m[1].replace(/^[^@]*@/, "").replace(/:\d+$/, "").toLowerCase() : "";
|
|
983
|
+
}
|
|
984
|
+
function redactResponse(e, names, pattern, urlHeaders, mask) {
|
|
985
|
+
const maskUrl = (u) => {
|
|
986
|
+
u = u.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, `$1${mask}@`);
|
|
987
|
+
const hash = u.indexOf("#");
|
|
988
|
+
const frag = hash >= 0 ? `#${mask}` : "";
|
|
989
|
+
if (hash >= 0)
|
|
990
|
+
u = u.slice(0, hash);
|
|
991
|
+
const q = u.indexOf("?");
|
|
992
|
+
if (q < 0)
|
|
993
|
+
return u + frag;
|
|
994
|
+
const query = u.slice(q + 1).split("&").map((p) => p.includes("=") ? `${p.slice(0, p.indexOf("="))}=${mask}` : p ? mask : p).join("&");
|
|
995
|
+
return `${u.slice(0, q)}?${query}${frag}`;
|
|
996
|
+
};
|
|
997
|
+
const credential = new RegExp(pattern, "i");
|
|
998
|
+
const raw = e.headers && typeof e.headers === "object" ? e.headers : {};
|
|
999
|
+
const headers = {};
|
|
1000
|
+
for (const k of Object.keys(raw)) {
|
|
1001
|
+
const n = k.toLowerCase();
|
|
1002
|
+
headers[k] = names.includes(n) || credential.test(n) ? mask : urlHeaders.includes(n) ? maskUrl(String(raw[k])) : String(raw[k]);
|
|
1003
|
+
}
|
|
1004
|
+
const status = Number(e.status);
|
|
1005
|
+
return { url: maskUrl(String(e.url ?? "")), status: Number.isFinite(status) ? status : 0, headers };
|
|
1006
|
+
}
|
|
1007
|
+
function readHeadersEvidence(captured, mask) {
|
|
1008
|
+
return redact(mask(JSON.stringify(captured.map(redactCaptured), null, 2))).text + `
|
|
1009
|
+
`;
|
|
1010
|
+
}
|
|
1011
|
+
function redactCaptured(e) {
|
|
1012
|
+
return redactResponse(e, REDACTED_HEADERS, REDACTED_HEADER_PATTERN, URL_HEADERS, HEADER_REDACTION);
|
|
1013
|
+
}
|
|
1014
|
+
function redactHeaders(headers) {
|
|
1015
|
+
return redactCaptured({ headers }).headers;
|
|
1016
|
+
}
|
|
1017
|
+
function validateSeconds(seconds) {
|
|
1018
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > READ_HEADERS_MAX_SECONDS)
|
|
1019
|
+
throw new Error(`read-headers: --seconds must be an integer between 1 and ${READ_HEADERS_MAX_SECONDS}`);
|
|
1020
|
+
return seconds;
|
|
1021
|
+
}
|
|
1022
|
+
function parseReadHeadersArgs(args) {
|
|
1023
|
+
const [cmd, ...rest] = args;
|
|
1024
|
+
if (cmd !== "read-headers")
|
|
1025
|
+
throw new Error("Expected read-headers");
|
|
1026
|
+
const allowDomains = [];
|
|
1027
|
+
let seconds = READ_HEADERS_DEFAULT_SECONDS;
|
|
1028
|
+
let gotoUrl;
|
|
1029
|
+
for (let i = 0;i < rest.length; i++) {
|
|
1030
|
+
const a = rest[i];
|
|
1031
|
+
if (a === "--allow-domain" || a.startsWith("--allow-domain=")) {
|
|
1032
|
+
const v = a === "--allow-domain" ? rest[++i] : a.slice("--allow-domain=".length);
|
|
1033
|
+
if (!v)
|
|
1034
|
+
throw new Error("--allow-domain got an empty host");
|
|
1035
|
+
for (const d of v.split(",")) {
|
|
1036
|
+
domainGlobToRegExp(d.trim());
|
|
1037
|
+
allowDomains.push(d.trim());
|
|
1038
|
+
}
|
|
1039
|
+
} else if (a === "--seconds" || a.startsWith("--seconds=")) {
|
|
1040
|
+
const v = a === "--seconds" ? rest[++i] : a.slice("--seconds=".length);
|
|
1041
|
+
if (!v)
|
|
1042
|
+
throw new Error("--seconds needs a value");
|
|
1043
|
+
seconds = validateSeconds(Number(v));
|
|
1044
|
+
} else if (a === "--goto" || a.startsWith("--goto=")) {
|
|
1045
|
+
gotoUrl = a === "--goto" ? rest[++i] : a.slice("--goto=".length);
|
|
1046
|
+
if (!gotoUrl)
|
|
1047
|
+
throw new Error("--goto got an empty URL");
|
|
1048
|
+
} else if (a.startsWith("-"))
|
|
1049
|
+
throw new Error(`read-headers: unknown option ${a}`);
|
|
1050
|
+
else
|
|
1051
|
+
throw new Error(`read-headers: unexpected argument ${a}`);
|
|
1052
|
+
}
|
|
1053
|
+
if (!allowDomains.length)
|
|
1054
|
+
throw new Error("read-headers needs --allow-domain: name the host to capture responses from");
|
|
1055
|
+
if (gotoUrl)
|
|
1056
|
+
checkGotoUrl(gotoUrl, allowDomains);
|
|
1057
|
+
return { allowDomains, seconds, gotoUrl };
|
|
1058
|
+
}
|
|
1059
|
+
function readHeadersWireArgs(req) {
|
|
1060
|
+
return ["read-headers", ...req.allowDomains.map((d) => `--allow-domain=${d}`), `--seconds=${req.seconds}`, ...req.gotoUrl ? [`--goto=${req.gotoUrl}`] : []];
|
|
1061
|
+
}
|
|
1062
|
+
function parseReadHeadersWire(args) {
|
|
1063
|
+
const [cmd, ...rest] = args;
|
|
1064
|
+
if (cmd !== "read-headers")
|
|
1065
|
+
throw new Error("read-headers expected");
|
|
1066
|
+
const allowDomains = [];
|
|
1067
|
+
let seconds = READ_HEADERS_DEFAULT_SECONDS;
|
|
1068
|
+
let gotoUrl;
|
|
1069
|
+
for (const a of rest) {
|
|
1070
|
+
if (a.startsWith("--allow-domain=")) {
|
|
1071
|
+
const d = a.slice("--allow-domain=".length);
|
|
1072
|
+
domainGlobToRegExp(d);
|
|
1073
|
+
allowDomains.push(d);
|
|
1074
|
+
} else if (a.startsWith("--seconds=")) {
|
|
1075
|
+
seconds = validateSeconds(Number(a.slice("--seconds=".length)));
|
|
1076
|
+
} else if (a.startsWith("--goto=")) {
|
|
1077
|
+
gotoUrl = a.slice("--goto=".length);
|
|
1078
|
+
} else
|
|
1079
|
+
throw new Error(`read-headers: unexpected argument ${a.startsWith("-") ? a : "(positional)"}`);
|
|
1080
|
+
}
|
|
1081
|
+
if (!allowDomains.length)
|
|
1082
|
+
throw new Error("read-headers needs --allow-domain");
|
|
1083
|
+
if (gotoUrl)
|
|
1084
|
+
checkGotoUrl(gotoUrl, allowDomains);
|
|
1085
|
+
return { allowDomains, seconds, gotoUrl };
|
|
1086
|
+
}
|
|
1087
|
+
function buildReadHeadersCode(allowDomains, seconds, gotoUrl) {
|
|
1088
|
+
const patterns = allowDomains.map((d) => domainGlobToRegExp(d).source);
|
|
1089
|
+
return `async page => {
|
|
1090
|
+
const allowed = ${JSON.stringify(patterns)};
|
|
1091
|
+
const redact = ${redactResponse.toString()};
|
|
1092
|
+
const hostOf = (u) => { const m = /^[a-z][a-z0-9+.-]*:\\/\\/([^/?#]+)/i.exec(String(u || "")); return m ? m[1].replace(/^[^@]*@/, "").replace(/:\\d+$/, "").toLowerCase() : ""; };
|
|
1093
|
+
const ok = (h) => h && allowed.some(p => new RegExp(p, "i").test(h));
|
|
1094
|
+
const captured = [];
|
|
1095
|
+
const onResponse = async (response) => {
|
|
1096
|
+
try {
|
|
1097
|
+
const url = String(response.url());
|
|
1098
|
+
const host = hostOf(url);
|
|
1099
|
+
if (!ok(host)) return;
|
|
1100
|
+
let headers = {};
|
|
1101
|
+
try { headers = await response.allHeaders(); } catch { try { headers = response.headers(); } catch { headers = {}; } }
|
|
1102
|
+
captured.push(redact({ url, status: response.status(), headers }, ${JSON.stringify(REDACTED_HEADERS)}, ${JSON.stringify(REDACTED_HEADER_PATTERN)}, ${JSON.stringify(URL_HEADERS)}, ${JSON.stringify(HEADER_REDACTION)}));
|
|
1103
|
+
} catch {}
|
|
1104
|
+
};
|
|
1105
|
+
page.on("response", onResponse);
|
|
1106
|
+
// The window includes the --goto: the whole capture fits in the one request.
|
|
1107
|
+
const endAt = Date.now() + ${seconds} * 1000;
|
|
1108
|
+
try {
|
|
1109
|
+
${gotoUrl ? `try { await page.goto(${JSON.stringify(gotoUrl)}, { waitUntil: "load", timeout: ${seconds * 1000} }); } catch {}` : ""}
|
|
1110
|
+
await page.waitForTimeout(Math.max(0, endAt - Date.now()));
|
|
1111
|
+
} finally {
|
|
1112
|
+
try { page.off("response", onResponse); } catch {}
|
|
1113
|
+
}
|
|
1114
|
+
return JSON.stringify({ host: ${JSON.stringify(allowDomains[0] ?? "")}, count: captured.length, captured });
|
|
1115
|
+
}`;
|
|
1116
|
+
}
|
|
1117
|
+
async function readHeadersOnSession(opts) {
|
|
1118
|
+
const timeoutMs = opts.seconds * 1000 + 15000;
|
|
1119
|
+
const { isError, text } = await runCodeOnSession({ socketRoot: opts.socketRoot, session: opts.session, cwd: opts.cwd, code: buildReadHeadersCode(opts.allowDomains, opts.seconds, opts.gotoUrl), json: true, timeoutMs });
|
|
1120
|
+
if (isError)
|
|
1121
|
+
throw new Error(text.match(/read-headers(?: refused)?: [^\n]*/)?.[0] ?? (/no browser is open|could not reach/.test(text) ? text.trim().split(`
|
|
1122
|
+
`)[0] : "read-headers: could not capture responses"));
|
|
1123
|
+
let raw;
|
|
1124
|
+
try {
|
|
1125
|
+
raw = JSON.parse(text).result;
|
|
1126
|
+
} catch {
|
|
1127
|
+
raw = text.match(/### Result\n([^\n]*)/)?.[1];
|
|
1128
|
+
}
|
|
1129
|
+
let out = raw;
|
|
1130
|
+
for (let i = 0;i < 2 && typeof out === "string"; i++) {
|
|
1131
|
+
try {
|
|
1132
|
+
out = JSON.parse(out);
|
|
1133
|
+
} catch {
|
|
1134
|
+
out = undefined;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (!out || !Array.isArray(out.captured))
|
|
1138
|
+
throw new Error("read-headers: the browser returned no capture");
|
|
1139
|
+
const captured = out.captured.map((e) => redactCaptured(e && typeof e === "object" ? e : {}));
|
|
1140
|
+
return { host: typeof out.host === "string" ? out.host : opts.allowDomains[0] ?? "", captured };
|
|
1141
|
+
}
|
|
659
1142
|
var PASSWORD_FIELDS = 'input[type=password], input[autocomplete~="current-password" i], input[autocomplete~="new-password" i], input[name*="pass" i], input[id*="pass" i]';
|
|
660
1143
|
var PASSWORD_VALUES_CODE = `async page => {
|
|
661
1144
|
const values = [];
|
|
@@ -704,24 +1187,34 @@ function maskPasswordLines(text) {
|
|
|
704
1187
|
}
|
|
705
1188
|
export {
|
|
706
1189
|
FILL_SECRET_USAGE,
|
|
1190
|
+
HEADER_REDACTION,
|
|
707
1191
|
MIN_MASK_LENGTH,
|
|
708
1192
|
READ_COOKIE_USAGE,
|
|
1193
|
+
READ_HEADERS_DEFAULT_SECONDS,
|
|
1194
|
+
READ_HEADERS_MAX_SECONDS,
|
|
1195
|
+
READ_HEADERS_USAGE,
|
|
709
1196
|
READ_SECRET_USAGE,
|
|
710
1197
|
READ_STORAGE_USAGE,
|
|
1198
|
+
REDACTED_HEADERS,
|
|
1199
|
+
REDACTED_HEADER_PATTERN,
|
|
711
1200
|
SECRET_MASK,
|
|
712
1201
|
SECRET_MASK_TTL_MS,
|
|
713
1202
|
SecretMasker,
|
|
1203
|
+
URL_HEADERS,
|
|
714
1204
|
base32Decode,
|
|
715
1205
|
buildFillCode,
|
|
716
1206
|
buildReadCode,
|
|
717
1207
|
buildReadCookieCode,
|
|
1208
|
+
buildReadHeadersCode,
|
|
718
1209
|
buildReadStorageCode,
|
|
1210
|
+
checkGotoUrl,
|
|
719
1211
|
daemonSessionSockets,
|
|
720
1212
|
domainGlobToRegExp,
|
|
721
1213
|
fillSecretOnSession,
|
|
722
1214
|
fillSecretWireArgs,
|
|
723
1215
|
hasSnapshot,
|
|
724
1216
|
hostAllowed,
|
|
1217
|
+
hostFromUrl,
|
|
725
1218
|
maskPasswordLines,
|
|
726
1219
|
maskable,
|
|
727
1220
|
parseDotenv,
|
|
@@ -729,6 +1222,8 @@ export {
|
|
|
729
1222
|
parseFillSecretWire,
|
|
730
1223
|
parseReadCookieArgs,
|
|
731
1224
|
parseReadCookieWire,
|
|
1225
|
+
parseReadHeadersArgs,
|
|
1226
|
+
parseReadHeadersWire,
|
|
732
1227
|
parseReadSecretArgs,
|
|
733
1228
|
parseReadSecretWire,
|
|
734
1229
|
parseReadStorageArgs,
|
|
@@ -738,11 +1233,18 @@ export {
|
|
|
738
1233
|
passwordValuesOnSession,
|
|
739
1234
|
readCookieOnSession,
|
|
740
1235
|
readCookieWireArgs,
|
|
1236
|
+
readHeadersEvidence,
|
|
1237
|
+
readHeadersOnSession,
|
|
1238
|
+
readHeadersWireArgs,
|
|
741
1239
|
readSecretOnSession,
|
|
742
1240
|
readSecretSource,
|
|
743
1241
|
readSecretWireArgs,
|
|
744
1242
|
readStorageOnSession,
|
|
745
1243
|
readStorageWireArgs,
|
|
1244
|
+
redactCaptured,
|
|
1245
|
+
redactHeaders,
|
|
1246
|
+
redactResponse,
|
|
1247
|
+
runCliOnSession,
|
|
746
1248
|
runCodeOnSession,
|
|
747
1249
|
sessionSocketCandidates,
|
|
748
1250
|
socketRequest,
|