wowdump 0.2.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +17 -111
  3. package/dist/adapters/reader.js +33 -0
  4. package/dist/analysis/disassemble.js +77 -0
  5. package/dist/{frida-runtime.js → analysis/frida-runtime.js} +3 -25
  6. package/dist/analysis/runtime-script.js +36 -0
  7. package/dist/cli.js +563 -0
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +99 -0
  10. package/dist/reader/broker.js +475 -0
  11. package/dist/reader/client.js +1 -0
  12. package/dist/reader/launcher.js +219 -0
  13. package/dist/reader/main.js +100 -0
  14. package/dist/reader/protocol.js +1 -0
  15. package/dist/reader/windows.js +242 -0
  16. package/dist/reader-main.js +2 -0
  17. package/dist/toolchain.js +123 -0
  18. package/package.json +19 -37
  19. package/skills/wowdump/SKILL.md +22 -0
  20. package/skills/wowdump/references/commands.md +63 -0
  21. package/skills/wowdump/references/disassemble.md +18 -0
  22. package/skills/wowdump/references/dynamic.md +54 -0
  23. package/skills/wowdump/references/evidence-workflow.md +41 -0
  24. package/skills/wowdump/references/profiles.md +34 -0
  25. package/skills/wowdump/references/request-schema.md +28 -0
  26. package/skills/wowdump/references/workflow.md +44 -0
  27. package/skills/wowdump/scripts/dynamic-session.js +133 -0
  28. package/dist/agent.js +0 -1335
  29. package/dist/analysis-path.js +0 -38
  30. package/dist/analysis-process-log.js +0 -146
  31. package/dist/broker-client.js +0 -411
  32. package/dist/broker-codec.js +0 -148
  33. package/dist/broker-core.js +0 -1045
  34. package/dist/broker-gateway.js +0 -447
  35. package/dist/broker-ledger.js +0 -196
  36. package/dist/broker-main.js +0 -291
  37. package/dist/broker-protocol.js +0 -119
  38. package/dist/broker-runtime.js +0 -1283
  39. package/dist/broker-server.js +0 -466
  40. package/dist/build-bundle-loader.js +0 -183
  41. package/dist/build-bundle.js +0 -11
  42. package/dist/discovery.js +0 -59
  43. package/dist/dry-run.js +0 -38
  44. package/dist/error-log.js +0 -71
  45. package/dist/focus-errors.js +0 -63
  46. package/dist/focus-service.js +0 -1855
  47. package/dist/focused-session.js +0 -1357
  48. package/dist/mcp-main.js +0 -51
  49. package/dist/mcp.js +0 -924
  50. package/dist/observability.js +0 -41
  51. package/dist/process-log-lock.js +0 -195
  52. package/dist/processes.js +0 -47
  53. package/dist/runtime-config.js +0 -399
  54. package/dist/session.js +0 -145
  55. package/dist/storage.js +0 -12
  56. package/dist/wow-analysis.js +0 -1430
  57. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  58. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  59. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  60. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  61. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
  62. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  63. /package/dist/{types.js → core/types.js} +0 -0
@@ -1,38 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- function worktreeFormalRoot(start) {
5
- const normalized = resolve(start).replaceAll("\\", "/");
6
- const marker = "/.worktrees/";
7
- const index = normalized.toLowerCase().lastIndexOf(marker);
8
- return index >= 0 ? join(normalized.slice(0, index), "analyze", "vm") : undefined;
9
- }
10
- function existingAnalysisRoot(start) {
11
- let current = resolve(start);
12
- while (true) {
13
- const candidate = join(current, "analyze", "vm");
14
- if (existsSync(candidate))
15
- return candidate;
16
- const parent = dirname(current);
17
- if (parent === current)
18
- return undefined;
19
- current = parent;
20
- }
21
- }
22
- /** Resolve the one formal evidence root even when the process runs in a Comet worktree. */
23
- export function resolveAnalysisDir(configured = process.env.WOW_ANALYZE_DIR, cwd = process.cwd(), moduleUrl = import.meta.url) {
24
- if (configured)
25
- return resolve(cwd, configured);
26
- const anchors = [resolve(cwd), dirname(fileURLToPath(moduleUrl))];
27
- for (const anchor of anchors) {
28
- const formalRoot = worktreeFormalRoot(anchor);
29
- if (formalRoot)
30
- return formalRoot;
31
- }
32
- for (const anchor of anchors) {
33
- const formalRoot = existingAnalysisRoot(anchor);
34
- if (formalRoot)
35
- return formalRoot;
36
- }
37
- return join(anchors[0], "analyze", "vm");
38
- }
@@ -1,146 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdir, open, readFile, truncate } from "node:fs/promises";
3
- import { join, resolve } from "node:path";
4
- import { acquireProcessLogLock } from "./process-log-lock.js";
5
- export class AnalysisProcessLog {
6
- fileName;
7
- tail = Promise.resolve();
8
- root;
9
- constructor(root, fileName = "wow-analysis-process-68974.jsonl") {
10
- this.fileName = fileName;
11
- this.root = resolve(root);
12
- }
13
- append(input) {
14
- const operation = this.tail.then(() => this.appendOwned(input));
15
- this.tail = operation.catch(() => undefined);
16
- return operation;
17
- }
18
- async flush() { await this.tail; }
19
- async appendOwned(input) {
20
- await mkdir(this.root, { recursive: true });
21
- const release = await acquireProcessLogLock(join(this.root, ".wow-analysis-process.lock"));
22
- try {
23
- const file = join(this.root, this.fileName);
24
- const seq = await nextSequence(file);
25
- const event = {
26
- seq,
27
- timestamp: new Date().toISOString(),
28
- buildKey: input.buildKey ?? "retail@12.0.7.68974",
29
- phase: input.phase ?? "verify",
30
- action: input.action,
31
- tool: input.tool ?? "frida-mcp",
32
- target: input.target ?? { name: "Wow.exe", va: null, rva: null, imageBase: "0x140000000", moduleBase: null, runtimeAddress: null },
33
- command: input.command ?? null,
34
- inputs: input.inputs ?? [],
35
- inputHashes: input.inputHashes ?? {},
36
- result: input.result ?? {},
37
- status: input.status,
38
- durationMs: input.durationMs ?? 0,
39
- artifacts: input.artifacts ?? [],
40
- error: input.error ?? null,
41
- nextAction: input.nextAction ?? null
42
- };
43
- const handle = await open(file, "a", 0o600);
44
- try {
45
- await handle.write(`${JSON.stringify(event)}\n`, null, "utf8");
46
- await handle.sync();
47
- }
48
- finally {
49
- await handle.close();
50
- }
51
- }
52
- finally {
53
- await release();
54
- }
55
- }
56
- }
57
- async function nextSequence(file) {
58
- try {
59
- await recoverIncompleteTail(file);
60
- const text = await readFile(file, "utf8");
61
- let highest = 0;
62
- for (const line of text.split(/\r?\n/)) {
63
- if (!line)
64
- continue;
65
- const value = JSON.parse(line);
66
- if (Number.isSafeInteger(value.seq))
67
- highest = Math.max(highest, Number(value.seq));
68
- }
69
- return highest + 1;
70
- }
71
- catch (error) {
72
- if (error.code === "ENOENT")
73
- return 1;
74
- throw error;
75
- }
76
- }
77
- async function recoverIncompleteTail(file) {
78
- const content = await readFile(file).catch(error => {
79
- if (error.code === "ENOENT")
80
- return Buffer.alloc(0);
81
- throw error;
82
- });
83
- if (content.length === 0 || content.at(-1) === 0x0a)
84
- return;
85
- const offset = content.lastIndexOf(0x0a) + 1;
86
- const tail = content.subarray(offset);
87
- if (!isIncompleteJsonTail(tail))
88
- return;
89
- const sha256 = createHash("sha256").update(tail).digest("hex");
90
- const quarantine = `${file}.tail-${sha256}.quarantine.json`;
91
- const evidence = Buffer.from(`${JSON.stringify({ schemaVersion: 1, kind: "analysis-process-incomplete-tail", sourcePath: resolve(file), byteOffset: offset, bytes: tail.length, sha256, encoding: "base64", data: tail.toString("base64") })}\n`, "utf8");
92
- try {
93
- const handle = await open(quarantine, "wx", 0o600);
94
- try {
95
- await handle.write(evidence);
96
- await handle.sync();
97
- }
98
- finally {
99
- await handle.close();
100
- }
101
- }
102
- catch (error) {
103
- if (error.code !== "EEXIST")
104
- throw error;
105
- const prior = await readFile(quarantine);
106
- if (!prior.equals(evidence))
107
- throw new Error(`process log quarantine conflict: ${quarantine}`);
108
- }
109
- await truncate(file, offset);
110
- const handle = await open(file, "r+");
111
- try {
112
- await handle.sync();
113
- }
114
- finally {
115
- await handle.close();
116
- }
117
- }
118
- function isIncompleteJsonTail(tail) {
119
- const text = tail.toString("utf8").trimStart();
120
- if (!text.startsWith("{"))
121
- return false;
122
- const stack = [];
123
- let inString = false;
124
- let escaped = false;
125
- for (const character of text) {
126
- if (inString) {
127
- if (escaped)
128
- escaped = false;
129
- else if (character === "\\")
130
- escaped = true;
131
- else if (character === '"')
132
- inString = false;
133
- continue;
134
- }
135
- if (character === '"')
136
- inString = true;
137
- else if (character === "{" || character === "[")
138
- stack.push(character);
139
- else if (character === "}" || character === "]") {
140
- const expected = character === "}" ? "{" : "[";
141
- if (stack.pop() !== expected)
142
- return false;
143
- }
144
- }
145
- return inString || escaped || stack.length > 0;
146
- }
@@ -1,411 +0,0 @@
1
- import { connect } from "node:net";
2
- import { spawn } from "node:child_process";
3
- import { randomUUID } from "node:crypto";
4
- import { isAbsolute } from "node:path";
5
- import { join } from "node:path";
6
- import { homedir, tmpdir } from "node:os";
7
- import { open, readFile, stat, unlink } from "node:fs/promises";
8
- import { BROKER_DEFAULT_TIMEOUT_MS, BROKER_PROTOCOL_VERSION, FrameDecoder, BrokerProtocolError, canonicalArtifactRoot, encodeFrame, pipeNameForSid, sidHash } from "./broker-protocol.js";
9
- export class BrokerBootstrapCoordinator {
10
- platform;
11
- constructor(platform) {
12
- this.platform = platform;
13
- }
14
- async start(options) {
15
- if (!isAbsolute(options.executable) || !isAbsolute(options.workdir))
16
- throw new BrokerProtocolError("BROKER_EXECUTABLE_INVALID", "Broker executable and workdir must be absolute configured paths");
17
- const hash = sidHash(options.sid);
18
- const pipeName = options.pipeName ?? pipeNameForSid(options.sid);
19
- const bootstrapNonce = options.nonce?.() ?? randomUUID();
20
- const artifactRoot = canonicalArtifactRoot(options.artifactRoot ?? process.env.WOWDUMP_RUNTIME_DIR ?? defaultRuntimeRoot());
21
- const timeoutMs = options.timeoutMs ?? 10_000;
22
- const deadline = Date.now() + Math.max(0, timeoutMs);
23
- const beforeDeadline = (operation, task) => runBootstrapBeforeDeadline(deadline, operation, task);
24
- const mutex = await beforeDeadline("acquire bootstrap mutex", () => this.platform.acquireBootstrapMutex(`wowdump-frida-bootstrap-${hash}`));
25
- if (!mutex) {
26
- let ready;
27
- try {
28
- ready = await beforeDeadline("wait for existing Broker readiness", () => this.platform.waitReady(pipeName, remainingBootstrapMs(deadline)));
29
- }
30
- catch (error) {
31
- throw new BrokerProtocolError("BOOTSTRAP_TIMEOUT", error instanceof Error ? error.message : "Broker ready timeout", "Inspect the active Broker bootstrap and retry.");
32
- }
33
- this.validateReady(ready, { hash, pipeName, artifactRoot });
34
- return ready;
35
- }
36
- try {
37
- const input = { pipeName, sidHash: hash, bootstrapNonce, artifactRoot, executable: options.executable, workdir: options.workdir, argv: options.argv ?? [] };
38
- let elevation;
39
- try {
40
- if (await beforeDeadline("check process elevation", () => this.platform.isElevated()))
41
- await beforeDeadline("launch Broker directly", () => this.platform.launchDirect(input));
42
- else {
43
- elevation = normalizeElevationResult(await beforeDeadline("launch elevated Broker", () => this.platform.launchElevated(input)));
44
- if (elevation.status === "cancelled")
45
- throw elevationError("UAC_CANCELLED", "Broker elevation was cancelled", elevation, "Start the Broker again and approve UAC.");
46
- if (elevation.status === "failed")
47
- throw elevationError("UAC_FAILED", "Broker elevation failed", elevation, "Inspect Windows elevation logs and retry.");
48
- }
49
- }
50
- catch (error) {
51
- if (error instanceof BrokerProtocolError)
52
- throw error;
53
- throw new BrokerProtocolError("BOOTSTRAP_SPAWN_FAILED", error instanceof Error ? error.message : String(error), "Verify Broker executable configuration and retry.");
54
- }
55
- let ready;
56
- try {
57
- ready = await beforeDeadline("wait for Broker readiness", () => this.platform.waitReady(pipeName, remainingBootstrapMs(deadline)));
58
- }
59
- catch (error) {
60
- const detail = elevation ? ` (${elevationDiagnostic(elevation)})` : "";
61
- throw new BrokerProtocolError("BOOTSTRAP_TIMEOUT", `${error instanceof Error ? error.message : "Broker ready timeout"}${detail}`, "Inspect Broker startup logs and retry.");
62
- }
63
- this.validateReady(ready, { hash, pipeName, bootstrapNonce, artifactRoot });
64
- return ready;
65
- }
66
- finally {
67
- const release = Promise.resolve().then(() => mutex.release());
68
- try {
69
- await beforeDeadline("release bootstrap mutex", () => release);
70
- }
71
- catch { /* release is best-effort and must not mask the bootstrap result */ }
72
- }
73
- }
74
- validateReady(ready, expected) {
75
- let readyArtifactRoot = "";
76
- try {
77
- readyArtifactRoot = canonicalArtifactRoot(ready.artifactRoot);
78
- }
79
- catch { /* normalize all identity failures below */ }
80
- if (ready.protocolVersion !== BROKER_PROTOCOL_VERSION || ready.sidHash !== expected.hash || ready.pipeName !== expected.pipeName ||
81
- (expected.bootstrapNonce !== undefined && ready.bootstrapNonce !== expected.bootstrapNonce) || !ready.instanceId || !ready.elevated ||
82
- readyArtifactRoot !== expected.artifactRoot) {
83
- throw new BrokerProtocolError("READY_IDENTITY_MISMATCH", "Broker ready identity did not match bootstrap inputs", "Terminate the untrusted child and restart Broker.");
84
- }
85
- }
86
- }
87
- function remainingBootstrapMs(deadline) { return Math.max(0, deadline - Date.now()); }
88
- function runBootstrapBeforeDeadline(deadline, operation, task) {
89
- const remaining = remainingBootstrapMs(deadline);
90
- if (remaining <= 0)
91
- return Promise.reject(bootstrapTimeout(operation));
92
- return new Promise((resolve, reject) => {
93
- let settled = false;
94
- const timer = setTimeout(() => {
95
- if (settled)
96
- return;
97
- settled = true;
98
- reject(bootstrapTimeout(operation));
99
- }, remaining);
100
- Promise.resolve().then(task).then(value => { if (!settled) {
101
- settled = true;
102
- clearTimeout(timer);
103
- resolve(value);
104
- } }, error => { if (!settled) {
105
- settled = true;
106
- clearTimeout(timer);
107
- reject(error);
108
- } });
109
- });
110
- }
111
- function bootstrapTimeout(operation) {
112
- return new BrokerProtocolError("BOOTSTRAP_TIMEOUT", `Broker bootstrap deadline expired during ${operation}`, "Inspect Broker startup logs and retry.");
113
- }
114
- export class WindowsBrokerBootstrapPlatform {
115
- options;
116
- constructor(options = {}) {
117
- this.options = options;
118
- }
119
- async acquireBootstrapMutex(name) {
120
- const path = join(this.options.directory ?? tmpdir(), `${name}.lock`);
121
- const pipeName = bootstrapPipeName(name);
122
- for (let attempt = 0; attempt < 2; attempt += 1) {
123
- try {
124
- const handle = await open(path, "wx");
125
- try {
126
- await handle.writeFile(JSON.stringify({ pid: process.pid, pipeName, acquiredAt: new Date().toISOString() }), "utf8");
127
- }
128
- catch (error) {
129
- await handle.close().catch(() => undefined);
130
- await unlink(path).catch(() => undefined);
131
- throw error;
132
- }
133
- return { release: async () => { await handle.close().catch(() => undefined); await unlink(path).catch(() => undefined); } };
134
- }
135
- catch (error) {
136
- if (error?.code !== "EEXIST" || attempt !== 0)
137
- return null;
138
- if (!await staleBootstrapLock(path, pipeName, this.options))
139
- return null;
140
- await unlink(path).catch(() => undefined);
141
- }
142
- }
143
- return null;
144
- }
145
- async isElevated() { return isWindowsProcessElevated(); }
146
- async launchDirect(input) {
147
- const child = spawn(input.executable, brokerChildArguments(input), { cwd: input.workdir, detached: true, windowsHide: true, stdio: "ignore", env: brokerLaunchEnvironment({ WOW_BROKER_SID_HASH: input.sidHash, WOW_BROKER_ARTIFACT_ROOT: input.artifactRoot }) });
148
- child.unref();
149
- }
150
- async launchElevated(input) {
151
- const args = brokerChildArguments(input);
152
- const environment = brokerLaunchEnvironment({ WOW_BROKER_LAUNCH_FILE: input.executable, WOW_BROKER_LAUNCH_ARGS: serializeWindowsArgumentList(args), WOW_BROKER_LAUNCH_CWD: input.workdir, WOW_BROKER_SID_HASH: input.sidHash, WOW_BROKER_ARTIFACT_ROOT: input.artifactRoot });
153
- const result = await runProcess("powershell.exe", brokerElevatedPowerShellArguments(), environment);
154
- return brokerElevationResultFromProcess(result);
155
- }
156
- async waitReady(pipeName, timeoutMs) {
157
- const deadline = Date.now() + timeoutMs;
158
- while (Date.now() < deadline) {
159
- const transport = new NamedPipeClientTransport(pipeName);
160
- try {
161
- const client = new BrokerClient(transport, `bootstrap-${process.pid}`);
162
- const result = await client.request("broker_status", { timeoutMs: Math.min(500, Math.max(1, deadline - Date.now())) });
163
- if (result && typeof result === "object" && !Array.isArray(result))
164
- return result;
165
- }
166
- catch {
167
- await delay(25);
168
- }
169
- finally {
170
- await transport.close();
171
- }
172
- }
173
- throw new Error("Broker ready deadline expired");
174
- }
175
- }
176
- export function brokerChildArguments(input) {
177
- return [
178
- ...input.argv,
179
- "--pipe", input.pipeName,
180
- "--bootstrap-nonce", input.bootstrapNonce,
181
- "--sid-hash", input.sidHash,
182
- "--artifact-root", input.artifactRoot
183
- ];
184
- }
185
- export function brokerElevatedPowerShellArguments() {
186
- const command = [
187
- "try {",
188
- "$startInfo = [System.Diagnostics.ProcessStartInfo]::new();",
189
- "$startInfo.FileName = $env:WOW_BROKER_LAUNCH_FILE;",
190
- "$startInfo.Arguments = $env:WOW_BROKER_LAUNCH_ARGS;",
191
- "$startInfo.WorkingDirectory = $env:WOW_BROKER_LAUNCH_CWD;",
192
- "$startInfo.UseShellExecute = $true;",
193
- "$startInfo.Verb = 'runas';",
194
- "$startInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden;",
195
- "$child = [System.Diagnostics.Process]::Start($startInfo); [Console]::Error.WriteLine('__WOW_BROKER_ELEVATION_STARTED__ pid=' + $child.Id); exit 0",
196
- "} catch {",
197
- "$root = $_.Exception; $cursor = $root; $native = $null;",
198
- "while ($null -ne $cursor) { if ($null -ne $cursor.PSObject.Properties['NativeErrorCode']) { $native = $cursor.NativeErrorCode; break }; $cursor = $cursor.InnerException };",
199
- "$nativeText = if ($null -eq $native) { 'null' } else { [string][int]$native };",
200
- "$hresultText = if ($null -eq $root) { 'null' } else { [string][int]$root.HResult };",
201
- "$typeText = if ($null -eq $root) { 'unknown' } else { $root.GetType().FullName -replace '[^A-Za-z0-9_.+]', '_' };",
202
- "[Console]::Error.WriteLine('__WOW_BROKER_ELEVATION_ERROR__ nativeErrorCode=' + $nativeText + ' hresult=' + $hresultText + ' type=' + $typeText); exit 1",
203
- "}"
204
- ].join(" ");
205
- return ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", command];
206
- }
207
- const ELEVATION_ERROR_MARKER = /__WOW_BROKER_ELEVATION_ERROR__\s+nativeErrorCode=(null|-?\d+)\s+hresult=(?:null|-?\d+)\s+type=[A-Za-z0-9_.+]+/;
208
- const ELEVATION_STARTED_MARKER = /__WOW_BROKER_ELEVATION_STARTED__\s+pid=(\d+)/;
209
- export function brokerElevationResultFromProcess(result) {
210
- const marker = ELEVATION_ERROR_MARKER.exec(result.stderr);
211
- const started = ELEVATION_STARTED_MARKER.exec(result.stderr);
212
- const nativeErrorCode = marker && marker[1] !== "null" ? Number.parseInt(marker[1], 10) : null;
213
- return {
214
- status: result.code === 0 ? "started" : nativeErrorCode === 1223 ? "cancelled" : "failed",
215
- exitCode: result.code,
216
- stderr: sanitizeDiagnostic(result.stderr),
217
- cause: result.cause ? sanitizeDiagnostic(result.cause) : null,
218
- nativeErrorCode,
219
- childPid: started ? Number.parseInt(started[1], 10) : null
220
- };
221
- }
222
- /** Serialize argv as one Windows command line for ProcessStartInfo.Arguments. */
223
- export function serializeWindowsArgumentList(args) {
224
- return args.map(quoteWindowsArgument).join(" ");
225
- }
226
- function quoteWindowsArgument(value) {
227
- if (value.length === 0)
228
- return '""';
229
- let result = '"';
230
- let slashes = 0;
231
- for (const character of value) {
232
- if (character === "\\") {
233
- slashes += 1;
234
- continue;
235
- }
236
- if (character === '"') {
237
- result += "\\".repeat(slashes * 2 + 1) + '"';
238
- slashes = 0;
239
- continue;
240
- }
241
- result += "\\".repeat(slashes) + character;
242
- slashes = 0;
243
- }
244
- result += "\\".repeat(slashes * 2) + '"';
245
- return result;
246
- }
247
- const BROKER_ENV_ALLOWLIST = ["SystemRoot", "WINDIR", "ComSpec", "PATH", "PATHEXT", "TEMP", "TMP", "USERPROFILE", "LOCALAPPDATA", "APPDATA", "ProgramData", "WOWDUMP_RUNTIME_DIR", "WOWDUMP_PROFILE_DIR", "WOWDUMP_GAME_ROOTS", "WOWDUMP_BROKER_IDLE_MS", "WOW_ANALYZE_DIR"];
248
- export function brokerLaunchEnvironment(required, source = process.env) {
249
- const environment = {};
250
- for (const key of BROKER_ENV_ALLOWLIST)
251
- if (source[key] !== undefined)
252
- environment[key] = source[key];
253
- for (const [key, value] of Object.entries(required)) {
254
- if (!key.startsWith("WOW_BROKER_") || key === "WOW_BROKER_ELEVATED" || value === undefined)
255
- continue;
256
- environment[key] = value;
257
- }
258
- return environment;
259
- }
260
- function bootstrapPipeName(lockName) {
261
- const match = /^wowdump-frida-bootstrap-([a-f0-9]{24})$/i.exec(lockName);
262
- return match ? `\\\\.\\pipe\\wowdump-frida-${match[1].toLowerCase()}` : undefined;
263
- }
264
- async function staleBootstrapLock(lockPath, pipeName, options) {
265
- let metadata;
266
- try {
267
- const raw = await readFile(lockPath, "utf8");
268
- const parsed = JSON.parse(raw);
269
- if (parsed && typeof parsed === "object")
270
- metadata = parsed;
271
- }
272
- catch { /* an empty/corrupt lock is handled by the age and pipe checks below */ }
273
- const pid = typeof metadata?.pid === "number" && Number.isInteger(metadata.pid) ? metadata.pid : undefined;
274
- if (pid !== undefined && (options.processProbe ?? processAlive)(pid))
275
- return false;
276
- if (pipeName && await (options.pipeProbe ?? pipeReachable)(pipeName))
277
- return false;
278
- try {
279
- const ageMs = Date.now() - (await stat(lockPath)).mtimeMs;
280
- return ageMs >= (options.staleAfterMs ?? 2_000);
281
- }
282
- catch {
283
- return false;
284
- }
285
- }
286
- function processAlive(pid) {
287
- if (pid <= 0 || pid === process.pid)
288
- return pid === process.pid;
289
- try {
290
- process.kill(pid, 0);
291
- return true;
292
- }
293
- catch (error) {
294
- return error?.code === "EPERM";
295
- }
296
- }
297
- function pipeReachable(pipeName) {
298
- return new Promise(resolve => {
299
- const socket = connect(pipeName);
300
- const finish = (value) => { socket.destroy(); resolve(value); };
301
- const timer = setTimeout(() => finish(false), 100);
302
- socket.once("connect", () => { clearTimeout(timer); finish(true); });
303
- socket.once("error", () => { clearTimeout(timer); finish(false); });
304
- });
305
- }
306
- export async function isWindowsProcessElevated(options = {}) {
307
- if ((options.platform ?? process.platform) !== "win32")
308
- return false;
309
- const result = await (options.queryGroups ?? (() => runProcess("whoami.exe", ["/groups"], process.env)))();
310
- return result.code === 0 && /\bS-1-16-12288\b/.test(result.stdout);
311
- }
312
- async function runProcess(file, args, env) {
313
- return new Promise(resolve => {
314
- const child = spawn(file, args, { env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
315
- let stdout = "";
316
- let stderr = "";
317
- child.stdout.setEncoding("utf8");
318
- child.stderr.setEncoding("utf8");
319
- child.stdout.on("data", chunk => { stdout += chunk; });
320
- child.stderr.on("data", chunk => { stderr += chunk; });
321
- child.on("error", error => resolve({ code: null, stdout, stderr, cause: sanitizeDiagnostic(error.message) || error.name }));
322
- child.on("exit", code => resolve({ code, stdout, stderr, cause: null }));
323
- });
324
- }
325
- function normalizeElevationResult(result) {
326
- return typeof result === "string" ? { status: result, exitCode: null, stderr: "", cause: null, nativeErrorCode: null, childPid: null } : result;
327
- }
328
- function elevationDiagnostic(result) {
329
- const fields = [`status=${result.status}`, `exitCode=${result.exitCode === null ? "null" : result.exitCode}`];
330
- if (result.childPid !== undefined && result.childPid !== null)
331
- fields.push(`childPid=${result.childPid}`);
332
- if (result.nativeErrorCode !== null)
333
- fields.push(`nativeErrorCode=${result.nativeErrorCode}`);
334
- if (result.stderr)
335
- fields.push(`stderr=${JSON.stringify(sanitizeDiagnostic(result.stderr, 512))}`);
336
- if (result.cause)
337
- fields.push(`cause=${JSON.stringify(sanitizeDiagnostic(result.cause, 512))}`);
338
- return fields.join(", ");
339
- }
340
- function elevationError(code, summary, result, nextAction) {
341
- const details = [`exitCode=${result.exitCode === null ? "null" : result.exitCode}`];
342
- if (result.nativeErrorCode !== null)
343
- details.push(`nativeErrorCode=${result.nativeErrorCode}`);
344
- if (result.stderr)
345
- details.push(`stderr=${JSON.stringify(sanitizeDiagnostic(result.stderr))}`);
346
- if (result.cause)
347
- details.push(`cause=${JSON.stringify(sanitizeDiagnostic(result.cause))}`);
348
- const error = new BrokerProtocolError(code, `${summary} (${details.join(", ")})`, nextAction);
349
- error.exitCode = result.exitCode;
350
- error.stderr = sanitizeDiagnostic(result.stderr);
351
- error.cause = result.cause ? sanitizeDiagnostic(result.cause) : null;
352
- error.nativeErrorCode = result.nativeErrorCode;
353
- return error;
354
- }
355
- function sanitizeDiagnostic(value, maxLength = 2_048) {
356
- const normalized = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").replace(/\s+/g, " ").trim();
357
- const suffix = "...[truncated]";
358
- return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - suffix.length))}${suffix}`;
359
- }
360
- function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
361
- function defaultRuntimeRoot() { return join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "wowdump"); }
362
- export class NamedPipeClientTransport {
363
- pipeName;
364
- constructor(pipeName) {
365
- this.pipeName = pipeName;
366
- }
367
- request(frame, timeoutMs) {
368
- return new Promise((resolve, reject) => {
369
- const socket = connect(this.pipeName);
370
- const chunks = [];
371
- const decoder = new FrameDecoder();
372
- const timer = setTimeout(() => { socket.destroy(); reject(new BrokerProtocolError("REQUEST_TIMEOUT", "Broker request timed out", "Query request_status before retrying mutating work.")); }, timeoutMs);
373
- socket.on("connect", () => socket.write(frame));
374
- socket.on("data", chunk => { chunks.push(chunk); try {
375
- if (decoder.push(chunk).length) {
376
- clearTimeout(timer);
377
- socket.end();
378
- resolve(Buffer.concat(chunks));
379
- }
380
- }
381
- catch (error) {
382
- clearTimeout(timer);
383
- socket.destroy();
384
- reject(error);
385
- } });
386
- socket.on("error", error => { clearTimeout(timer); reject(error); });
387
- });
388
- }
389
- async close() { }
390
- }
391
- export class BrokerClient {
392
- transport;
393
- clientId;
394
- constructor(transport, clientId = randomUUID()) {
395
- this.transport = transport;
396
- this.clientId = clientId;
397
- }
398
- async request(operation, options = {}) {
399
- const request = { schemaVersion: 1, protocolVersion: BROKER_PROTOCOL_VERSION, requestId: options.requestId ?? randomUUID(), clientId: this.clientId, operation, pid: options.pid ?? null, buildKey: options.buildKey ?? null, payload: options.payload ?? {} };
400
- const raw = await this.transport.request(encodeFrame(request), options.timeoutMs ?? BROKER_DEFAULT_TIMEOUT_MS);
401
- const decoder = new FrameDecoder();
402
- const values = decoder.push(raw);
403
- if (values.length !== 1)
404
- throw new BrokerProtocolError("FRAME_INVALID", "expected one Broker response frame");
405
- const response = values[0];
406
- if (response.ok === false)
407
- throw new BrokerProtocolError(response.error.code, response.error.message, response.error.nextAction);
408
- return response.result;
409
- }
410
- close() { return this.transport.close(); }
411
- }