wowdump 0.3.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 (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +21 -53
  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} +48 -48
  6. package/dist/analysis/runtime-script.js +36 -0
  7. package/dist/cli.js +255 -189
  8. package/dist/core/profile-engine.js +238 -0
  9. package/dist/frida-worker.js +54 -55
  10. package/dist/{reader-broker.js → reader/broker.js} +15 -0
  11. package/dist/{reader-client.js → reader/client.js} +1 -1
  12. package/dist/{windows-launcher.js → reader/launcher.js} +16 -4
  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 +1 -66
  17. package/dist/toolchain.js +102 -573
  18. package/package.json +9 -10
  19. package/skills/wowdump/SKILL.md +22 -15
  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 -1332
  29. package/dist/discovery.js +0 -48
  30. package/dist/dry-run.js +0 -36
  31. package/dist/error-log.js +0 -71
  32. package/dist/focused-session.js +0 -89
  33. package/dist/ghidra.js +0 -769
  34. package/dist/main.js +0 -66
  35. package/dist/observability.js +0 -41
  36. package/dist/processes.js +0 -44
  37. package/dist/session.js +0 -42
  38. package/dist/storage.js +0 -12
  39. package/dist/windows-reader.js +0 -102
  40. package/dist/wow-analysis.js +0 -1405
  41. package/skills/wowdump/commands.md +0 -44
  42. /package/dist/{adapters.js → core/build-adapters.js} +0 -0
  43. /package/dist/{types.js → core/types.js} +0 -0
package/dist/cli.js CHANGED
@@ -1,82 +1,105 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, realpathSync } from "node:fs";
3
+ import { existsSync, realpathSync, readFileSync } from "node:fs";
4
4
  import { readFile, readdir } from "node:fs/promises";
5
- import { basename, extname, isAbsolute, join, resolve } from "node:path";
6
- import { pathToFileURL } from "node:url";
5
+ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { Command } from "commander";
8
- import { WindowsBrokerManager } from "./windows-launcher.js";
9
- import { bootstrapToolchain, resolveToolchain, resolveWowdumpHome } from "./toolchain.js";
10
- import { runGhidraStaticAnalysis as runCanonicalGhidraStaticAnalysis, buildGhidraHeadlessCommand } from "./ghidra.js";
11
- async function runStaticAnalysis(options) {
12
- // ghidra.ts owns the canonical evidence/profile schema. Keep the small
13
- // toolchain resolver as the CLI-facing bootstrap layer, then translate the
14
- // command options into the canonical runner's vocabulary.
15
- const ghidraValue = options.ghidra;
16
- const ghidraResolved = ghidraValue ? resolve(ghidraValue) : undefined;
17
- const ghidraIsFile = ghidraResolved ? existsSync(ghidraResolved) && !existsSync(join(ghidraResolved, "support")) : false;
18
- const canonicalOptions = {
19
- exe: options.executable,
20
- buildKey: options.buildKey,
21
- output: options.evidenceFile ?? join(options.outputDirectory ?? resolveWowdumpHome(options.env ?? process.env), "profiles", `${options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-")}.json`),
22
- projectDir: options.projectDirectory,
23
- projectName: options.projectName,
24
- ghidraHome: ghidraResolved && !ghidraIsFile ? ghidraResolved : undefined,
25
- analyzeHeadless: ghidraResolved && ghidraIsFile ? ghidraResolved : undefined,
26
- javaPath: options.java,
27
- scriptPath: options.scriptDirectory,
28
- scriptName: options.exporterScript,
29
- timeoutMs: (options.timeoutSeconds ?? 1800) * 1000,
30
- env: options.env,
31
- home: options.home,
32
- commandRunner: options.runner
33
- ? async (file, args, commandOptions) => {
34
- const value = await options.runner({ file, args: [...args], cwd: commandOptions.cwd, env: commandOptions.env });
35
- return { code: value.exitCode, stdout: value.stdout, stderr: value.stderr };
8
+ import { WindowsBrokerManager } from "./reader/launcher.js";
9
+ import { initializeWowdumpHome, bootstrapToolchain, resolveToolchain, resolveWowdumpHome } from "./toolchain.js";
10
+ import { runDisassembly } from "./analysis/disassemble.js";
11
+ import { RUNTIME_EXPORT_SCRIPT } from "./analysis/runtime-script.js";
12
+ import { ProfileEngine } from "./core/profile-engine.js";
13
+ import { ReaderProfileAdapter } from "./adapters/reader.js";
14
+ import { enumerateWindowsProcesses } from "./reader/windows.js";
15
+ export function parseBuildInfo(content, file) {
16
+ const lines = content.split(/\r?\n/).filter(line => line.trim());
17
+ const headers = lines[0]?.split("|") ?? [];
18
+ const versionIndex = headers.indexOf("Version!STRING:0");
19
+ const activeIndex = headers.indexOf("Active!DEC:1");
20
+ const productIndex = headers.indexOf("Product!STRING:0");
21
+ if (versionIndex < 0 || activeIndex < 0 || productIndex < 0)
22
+ return null;
23
+ const row = lines.slice(1).map(line => line.split("|")).find(values => values[activeIndex] === "1" && values[productIndex] === "wow");
24
+ const version = row?.[versionIndex]?.trim();
25
+ const product = row?.[productIndex]?.trim();
26
+ if (!version || !product)
27
+ return null;
28
+ return { fileVersion: version, product, buildKey: `retail@${version}`, buildInfoFile: file };
29
+ }
30
+ async function discoverWowTargets(selectedPid, elevatedModules) {
31
+ if (process.platform !== "win32")
32
+ return [];
33
+ const targets = [];
34
+ const rows = enumerateWindowsProcesses()
35
+ .filter(item => /^Wow\.exe$/i.test(item.name))
36
+ .filter(item => selectedPid === undefined || item.pid === selectedPid);
37
+ for (const item of rows) {
38
+ const pid = item.pid;
39
+ const name = item.name;
40
+ if (!Number.isSafeInteger(pid) || pid < 1 || !/^Wow\.exe$/i.test(name) || (selectedPid !== undefined && pid !== selectedPid))
41
+ continue;
42
+ let path = item.path;
43
+ let moduleBase = null;
44
+ let moduleSize = null;
45
+ let diagnostic;
46
+ if ((!path || !moduleBase) && elevatedModules) {
47
+ try {
48
+ const elevated = elevatedModules(pid);
49
+ const response = await elevated;
50
+ const modules = response && typeof response === "object" && Array.isArray(response.modules)
51
+ ? response.modules
52
+ : [];
53
+ const main = modules.find(module => /^Wow\.exe$/i.test(String(module.name ?? ""))) ?? modules[0];
54
+ if (!path && typeof main?.path === "string" && main.path)
55
+ path = main.path;
56
+ if (!moduleBase && typeof main?.base === "string")
57
+ moduleBase = main.base;
58
+ if (moduleSize === null && Number.isFinite(Number(main?.size)))
59
+ moduleSize = Number(main?.size);
60
+ }
61
+ catch (error) {
62
+ diagnostic = diagnostic ?? (error instanceof Error ? error.message : String(error));
36
63
  }
37
- : undefined
38
- };
39
- if (options.dryRun) {
40
- const resolved = resolveToolchain(options);
41
- if (!resolved.ghidra || !resolved.java) {
42
- throw new CliError("TOOLCHAIN_INCOMPLETE", `Ghidra toolchain is incomplete: missing ${resolved.missing.join(", ")}`, resolved);
43
64
  }
44
- const projectDir = options.projectDirectory ?? join(resolved.home, "cache", "ghidra", options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-"));
45
- const command = buildGhidraHeadlessCommand({ analyzeHeadless: resolved.ghidra.executable, javaPath: resolved.java.executable }, {
46
- exe: options.executable,
47
- buildKey: options.buildKey,
48
- projectDir,
49
- projectName: options.projectName,
50
- scriptPath: options.scriptDirectory,
51
- scriptName: options.exporterScript,
52
- output: options.evidenceFile
53
- });
54
- return {
55
- ok: true,
56
- command: "analyze.static",
57
- dryRun: true,
58
- buildKey: options.buildKey,
59
- executable: resolve(options.executable),
60
- outputFile: options.evidenceFile ?? join(projectDir, `${options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-")}.json`),
61
- commandLine: { file: command.executable, args: command.args, cwd: command.cwd },
62
- toolchain: resolved,
63
- profile: { schema: "wowdump.profile.v1", buildKey: options.buildKey, confidence: "candidate" }
64
- };
65
+ let buildInfoFile = null;
66
+ let fileVersion = null;
67
+ let product = null;
68
+ if (path) {
69
+ const candidates = [join(dirname(path), ".build.info"), join(dirname(dirname(path)), ".build.info")];
70
+ for (const candidate of candidates) {
71
+ try {
72
+ const text = await readFile(candidate, "utf8");
73
+ const parsed = parseBuildInfo(text, candidate);
74
+ if (parsed) {
75
+ fileVersion = parsed.fileVersion;
76
+ product = parsed.product;
77
+ buildInfoFile = parsed.buildInfoFile;
78
+ break;
79
+ }
80
+ }
81
+ catch { /* try the next parent directory */ }
82
+ }
83
+ }
84
+ targets.push({ pid, name, path, moduleBase, moduleSize, fileVersion, product, buildKey: fileVersion ? `retail@${fileVersion}` : null, ...(buildInfoFile ? { buildInfoFile } : {}), ...(diagnostic ? { diagnostics: { frida: diagnostic } } : {}) });
85
+ }
86
+ return targets;
87
+ }
88
+ function packageVersion() {
89
+ const override = process.env.WOWDUMP_VERSION?.trim();
90
+ if (override)
91
+ return override;
92
+ try {
93
+ const packageFile = fileURLToPath(new URL("../package.json", import.meta.url));
94
+ const packageJson = JSON.parse(readFileSync(packageFile, "utf8"));
95
+ return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
96
+ }
97
+ catch {
98
+ return "0.0.0";
65
99
  }
66
- const result = await runCanonicalGhidraStaticAnalysis(canonicalOptions);
67
- return {
68
- ok: true,
69
- command: "analyze.static",
70
- dryRun: false,
71
- buildKey: result.buildKey,
72
- executable: result.executable.path,
73
- executableSha256: result.executable.sha256,
74
- outputFile: options.evidenceFile ?? join(options.outputDirectory ?? resolveWowdumpHome(options.env ?? process.env), "profiles", `${options.buildKey.replace(/[^A-Za-z0-9_.-]+/g, "-")}.json`),
75
- commandLine: { file: result.analysis.command[0] ?? "", args: result.analysis.command.slice(1), cwd: options.projectDirectory ?? resolveWowdumpHome(options.env ?? process.env) },
76
- toolchain: resolveToolchain(options),
77
- profile: result.profile,
78
- evidence: result
79
- };
100
+ }
101
+ function installedDistDirectory() {
102
+ return fileURLToPath(new URL(".", import.meta.url));
80
103
  }
81
104
  class CliError extends Error {
82
105
  code;
@@ -133,17 +156,17 @@ async function defaultSidecar(file, args, input) {
133
156
  child.stdin.end();
134
157
  });
135
158
  }
136
- function brokerCommandLine(env, cwd) {
159
+ function brokerCommandLine(env, packageDist) {
137
160
  const configured = env.WOWDUMP_READER_COMMAND?.trim();
138
161
  if (configured)
139
162
  return { file: configured, args: [] };
140
- const candidate = resolve(cwd, "dist", "reader-main.js");
163
+ const candidate = join(packageDist, "reader-main.js");
141
164
  return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
142
165
  }
143
- async function defaultBroker(invocation, env, cwd, sidecar) {
144
- const command = brokerCommandLine(env, cwd);
166
+ async function defaultBroker(invocation, env, packageDist, sidecar) {
167
+ const command = brokerCommandLine(env, packageDist);
145
168
  if (!command) {
146
- throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: resolve(cwd, "dist", "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
169
+ throw new CliError("BROKER_NOT_CONFIGURED", "reader broker entry was not found", { expected: join(packageDist, "reader-main.js"), environment: "WOWDUMP_READER_COMMAND" });
147
170
  }
148
171
  const result = await (sidecar ?? defaultSidecar)(command.file, command.args, `${JSON.stringify(invocation)}\n`);
149
172
  if (result.exitCode !== 0)
@@ -158,12 +181,28 @@ async function defaultBroker(invocation, env, cwd, sidecar) {
158
181
  throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
159
182
  }
160
183
  }
184
+ async function runFridaWorker(worker, input, timeoutMs, broker) {
185
+ if (process.platform !== "win32") {
186
+ throw new CliError("PLATFORM_UNSUPPORTED", "elevated Frida broker is currently supported on Windows only");
187
+ }
188
+ const raw = await broker({ command: "frida", payload: { worker, input, timeoutMs } });
189
+ const response = raw && typeof raw === "object" ? raw : {};
190
+ if (response.ok === false) {
191
+ const error = response.error && typeof response.error === "object" ? response.error : {};
192
+ throw new CliError("BROKER_FAILED", String(error.message ?? "elevated Frida worker failed"), { response });
193
+ }
194
+ return {
195
+ exitCode: Number.isSafeInteger(Number(response.exitCode)) ? Number(response.exitCode) : 1,
196
+ stdout: typeof response.stdout === "string" ? response.stdout : "",
197
+ stderr: typeof response.stderr === "string" ? response.stderr : ""
198
+ };
199
+ }
161
200
  const windowsBrokerManagers = new Map();
162
- function persistentWindowsBroker(home, cwd) {
201
+ function persistentWindowsBroker(home, packageDist) {
163
202
  const key = resolve(home).toLowerCase();
164
203
  let manager = windowsBrokerManagers.get(key);
165
204
  if (!manager) {
166
- manager = new WindowsBrokerManager({ home, readerEntry: resolve(cwd, "dist", "reader-main.js") });
205
+ manager = new WindowsBrokerManager({ home, readerEntry: join(packageDist, "reader-main.js") });
167
206
  windowsBrokerManagers.set(key, manager);
168
207
  }
169
208
  return manager;
@@ -212,7 +251,7 @@ function requireRuntimeConfirmation(options) {
212
251
  throw new CliError("CONFIRMATION_REQUIRED", "runtime export requires --confirm", {
213
252
  pid: options.pid,
214
253
  buildKey: options.build,
215
- operation: "runtime-export",
254
+ operation: "runtime",
216
255
  kind: options.kind,
217
256
  maxHooks: options.maxHooks,
218
257
  durationMs: options.durationMs,
@@ -220,53 +259,46 @@ function requireRuntimeConfirmation(options) {
220
259
  cleanupDeadlineMs: options.cleanupDeadlineMs
221
260
  });
222
261
  }
223
- function nested(record, ...path) {
224
- let value = record;
225
- for (const key of path) {
226
- if (!value || typeof value !== "object" || Array.isArray(value))
227
- return undefined;
228
- value = value[key];
229
- }
230
- return value;
231
- }
232
- function compareEvidence(staticProfile, runtimeExport) {
233
- const definitions = [
234
- { name: "buildKey", left: staticProfile.buildKey, right: runtimeExport.buildKey },
235
- {
236
- name: "executableSha256",
237
- left: staticProfile.executableSha256 ?? nested(staticProfile, "executable", "sha256"),
238
- right: runtimeExport.executableSha256 ?? nested(runtimeExport, "executable", "sha256")
239
- },
240
- { name: "moduleBase", left: staticProfile.moduleBase ?? nested(staticProfile, "module", "base"), right: runtimeExport.moduleBase ?? nested(runtimeExport, "module", "base") },
241
- { name: "sectionBounds", left: staticProfile.sectionBounds ?? staticProfile.sections, right: runtimeExport.sectionBounds ?? runtimeExport.sections },
242
- { name: "entryBytes", left: staticProfile.entryBytes, right: runtimeExport.entryBytes },
243
- { name: "matchCount", left: staticProfile.matchCount, right: runtimeExport.matchCount }
244
- ];
245
- return definitions.map(item => ({
246
- name: item.name,
247
- static: item.left ?? null,
248
- runtime: item.right ?? null,
249
- status: item.left === undefined || item.right === undefined
250
- ? "missing"
251
- : JSON.stringify(item.left) === JSON.stringify(item.right) ? "match" : "mismatch"
252
- }));
253
- }
254
262
  export function createWowdumpCli(dependencies = {}) {
255
263
  const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
256
264
  const env = dependencies.env ?? process.env;
257
265
  const cwd = resolve(dependencies.cwd ?? process.cwd());
266
+ const packageDist = installedDistDirectory();
258
267
  const home = resolveWowdumpHome(env);
259
268
  const broker = dependencies.broker ?? (process.platform === "win32" && !dependencies.sidecar
260
- ? (invocation => persistentWindowsBroker(home, cwd).request(invocation))
261
- : (invocation => defaultBroker(invocation, env, cwd, dependencies.sidecar)));
262
- const staticAnalysis = dependencies.staticAnalysis ?? runStaticAnalysis;
263
- const sidecar = dependencies.sidecar ?? defaultSidecar;
269
+ ? (invocation => persistentWindowsBroker(home, packageDist).request(invocation))
270
+ : (invocation => defaultBroker(invocation, env, packageDist, dependencies.sidecar)));
271
+ const baseSidecar = dependencies.sidecar ?? defaultSidecar;
272
+ const sidecar = async (file, args, input) => {
273
+ const worker = args.length === 1 && /\.m?js$/i.test(args[0] ?? "") ? args[0] : undefined;
274
+ if (worker && input !== undefined)
275
+ return runFridaWorker(worker, input, 120_000, broker);
276
+ return baseSidecar(file, args, input);
277
+ };
278
+ const profileEngine = new ProfileEngine();
279
+ const profileAdapter = new ReaderProfileAdapter(broker);
264
280
  const program = new Command()
265
281
  .name("wowdump")
266
282
  .description("Build-aware WoW native memory analysis CLI")
267
- .version("0.3.0")
283
+ .version(packageVersion())
268
284
  .showHelpAfterError()
269
285
  .configureOutput({ writeOut: value => io.stdout.write(value), writeErr: value => io.stderr.write(value) });
286
+ program.command("targets")
287
+ .description("Discover running WoW targets, executable paths, and build versions")
288
+ .option("--pid <pid>", "select one process ID")
289
+ .action(async (options) => {
290
+ const selectedPid = options.pid ? positiveInteger(options.pid, "pid") : undefined;
291
+ const targets = await discoverWowTargets(selectedPid, async (pid) => broker({ command: "modules", payload: { pid } }));
292
+ if (selectedPid !== undefined && targets.length === 0)
293
+ throw new CliError("TARGET_NOT_FOUND", `Wow.exe process ${selectedPid} was not found`);
294
+ writeJson(io, {
295
+ ok: targets.length > 0,
296
+ command: "targets",
297
+ selected: targets.length === 1 ? targets[0] : null,
298
+ requiresSelection: selectedPid === undefined && targets.length > 1,
299
+ targets
300
+ });
301
+ });
270
302
  program.command("target")
271
303
  .description("Show target, reader broker, profile, monitor, and Frida state")
272
304
  .option("--pid <pid>", "target process ID")
@@ -305,7 +337,8 @@ export function createWowdumpCli(dependencies = {}) {
305
337
  .option("--size <bytes>", "bounded byte count")
306
338
  .option("--request <json>", "complete read request as JSON")
307
339
  .action(async (options) => {
308
- const payload = options.request ? jsonRecord(options.request, "request") : {
340
+ const requested = options.request ? jsonRecord(options.request, "request") : undefined;
341
+ const payload = requested ?? {
309
342
  pid: positiveInteger(options.pid, "pid"),
310
343
  ...(options.build ? { buildKey: options.build } : {}),
311
344
  ...(options.profile ? { profileId: options.profile } : {}),
@@ -313,6 +346,20 @@ export function createWowdumpCli(dependencies = {}) {
313
346
  ...(options.address ? { address: options.address } : {}),
314
347
  ...(options.size ? { size: positiveInteger(options.size, "size") } : {})
315
348
  };
349
+ const pid = positiveInteger(String(payload.pid ?? options.pid), "pid");
350
+ const profileId = typeof payload.profileId === "string" ? payload.profileId : (typeof options.profile === "string" ? options.profile : undefined);
351
+ if (profileId && payload.address === undefined) {
352
+ const profileFilePath = profileFile(join(home, "profiles"), profileId);
353
+ const profile = jsonRecord(await readFile(profileFilePath, "utf8"), profileFilePath);
354
+ const result = await profileEngine.read({
355
+ pid,
356
+ buildKey: typeof payload.buildKey === "string" ? payload.buildKey : undefined,
357
+ profile,
358
+ fields: Array.isArray(payload.fields) ? payload.fields.map(String) : undefined
359
+ }, profileAdapter);
360
+ writeJson(io, { ...result, profileFile: profileFilePath });
361
+ return;
362
+ }
316
363
  writeJson(io, await broker({ command: "read", payload }));
317
364
  });
318
365
  const watch = memory.command("watch").description("Manage broker-owned memory monitors");
@@ -327,17 +374,21 @@ export function createWowdumpCli(dependencies = {}) {
327
374
  .option("--interval <ms>", "sample interval", "250")
328
375
  .option("--max-samples <count>", "sample limit", "1000")
329
376
  .option("--all-samples", "emit unchanged samples")
330
- .action(async (options) => writeJson(io, await broker({ command: "watch.start", payload: {
331
- pid: positiveInteger(options.pid, "pid"),
332
- ...(options.build ? { buildKey: options.build } : {}),
333
- ...(options.profile ? { profileId: options.profile } : {}),
334
- ...(options.field ? { fields: options.field } : {}),
335
- ...(options.address ? { address: options.address } : {}),
336
- ...(options.size ? { size: positiveInteger(options.size, "size") } : {}),
337
- intervalMs: positiveInteger(options.interval, "interval"),
338
- maxSamples: positiveInteger(options.maxSamples, "max-samples"),
339
- changeOnly: options.allSamples !== true
340
- } })));
377
+ .action(async (options) => {
378
+ if (options.profile)
379
+ throw new CliError("PROFILE_WATCH_UNSUPPORTED", "profile watch requires a fixed address; use repeated memory read calls for dynamic pointer fields");
380
+ writeJson(io, await broker({ command: "watch.start", payload: {
381
+ pid: positiveInteger(options.pid, "pid"),
382
+ ...(options.build ? { buildKey: options.build } : {}),
383
+ ...(options.profile ? { profileId: options.profile } : {}),
384
+ ...(options.field ? { fields: options.field } : {}),
385
+ ...(options.address ? { address: options.address } : {}),
386
+ ...(options.size ? { size: positiveInteger(options.size, "size") } : {}),
387
+ intervalMs: positiveInteger(options.interval, "interval"),
388
+ maxSamples: positiveInteger(options.maxSamples, "max-samples"),
389
+ changeOnly: options.allSamples !== true
390
+ } }));
391
+ });
341
392
  watch.command("poll")
342
393
  .description("Poll monitor events")
343
394
  .requiredOption("--id <watchId>", "monitor ID")
@@ -350,46 +401,33 @@ export function createWowdumpCli(dependencies = {}) {
350
401
  .description("Stop a monitor and release its resources")
351
402
  .requiredOption("--id <watchId>", "monitor ID")
352
403
  .action(async (options) => writeJson(io, await broker({ command: "watch.stop", payload: { watchId: options.id } })));
353
- const analyze = program.command("analyze").description("Run separate static, runtime-export, and verification stages");
354
- analyze.command("static")
355
- .description("Analyze a disk executable with Ghidra Headless")
404
+ const analyze = program.command("analyze").description("Run Frida evidence export and runtime-guided disassembly");
405
+ analyze.command("disassemble")
406
+ .description("Decode Frida text evidence and produce a Reader profile")
356
407
  .requiredOption("--exe <path>", "path to Wow.exe")
357
408
  .requiredOption("--build <buildKey>", "build key")
358
- .option("--build-key <buildKey>", "alias for --build")
359
- .option("--java <path>", "Java executable or JDK root")
360
- .option("--ghidra <path>", "analyzeHeadless executable or Ghidra root")
361
- .option("--output <directory>", "profile output directory")
362
- .option("--project-directory <directory>", "Ghidra project directory")
363
- .option("--project-name <name>", "Ghidra project name")
364
- .option("--script-directory <directory>", "Ghidra script directory")
365
- .option("--exporter-script <name>", "Ghidra post-script name")
366
- .option("--evidence <file>", "Ghidra exporter JSON output")
367
- .option("--timeout <seconds>", "analysis timeout", "1800")
368
- .option("--dry-run", "print the exact command without starting Ghidra")
369
- .action(async (options) => writeJson(io, await staticAnalysis({
370
- executable: options.exe,
371
- buildKey: options.build ?? options.buildKey,
372
- home,
373
- env,
374
- java: options.java,
375
- ghidra: options.ghidra,
376
- outputDirectory: options.output,
377
- projectDirectory: options.projectDirectory,
378
- projectName: options.projectName,
379
- scriptDirectory: options.scriptDirectory,
380
- exporterScript: options.exporterScript,
381
- evidenceFile: options.evidence,
382
- timeoutSeconds: positiveInteger(options.timeout, "timeout"),
409
+ .requiredOption("--runtime-export <file>", "Frida runtime text JSON")
410
+ .option("--ida-evidence <file>", "JSON produced by an IDA Pro MCP analysis")
411
+ .option("--output <file>", "profile output JSON")
412
+ .option("--max-instructions <count>", "maximum decoded instructions", "256")
413
+ .option("--dry-run", "validate inputs without writing a profile")
414
+ .action(async (options) => writeJson(io, await runDisassembly({
415
+ exe: options.exe,
416
+ buildKey: options.build,
417
+ runtimeExport: options.runtimeExport,
418
+ ...(options.idaEvidence ? { idaEvidence: options.idaEvidence } : {}),
419
+ ...(options.output ? { output: options.output } : {}),
420
+ maxInstructions: positiveInteger(options.maxInstructions, "max-instructions"),
383
421
  dryRun: options.dryRun === true
384
422
  })));
385
- analyze.command("runtime-export")
386
- .description("Export runtime evidence through the isolated Frida sidecar")
423
+ analyze.command("runtime")
424
+ .description("Read runtime evidence through the elevated Frida broker")
387
425
  .requiredOption("--pid <pid>", "target process ID")
388
426
  .requiredOption("--build <buildKey>", "build key")
389
427
  .option("--build-key <buildKey>", "alias for --build")
390
428
  .option("--kind <kind>", "text or providers", "providers")
391
429
  .option("--worker <path>", "Frida worker entry")
392
- .option("--static-profile <path>", "static profile containing candidate RVAs")
430
+ .option("--profile <path>", "existing reader profile containing candidate RVAs")
393
431
  .option("--max-hooks <count>", "maximum hooks", "1")
394
432
  .option("--duration-ms <ms>", "maximum duration", "5000")
395
433
  .option("--max-events <count>", "maximum events", "100")
@@ -409,49 +447,75 @@ export function createWowdumpCli(dependencies = {}) {
409
447
  if (!["text", "providers"].includes(normalized.kind))
410
448
  throw new CliError("ARGUMENT_INVALID", "kind must be text or providers");
411
449
  requireRuntimeConfirmation(normalized);
412
- const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(cwd, "dist", "frida-worker.js"));
450
+ const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
413
451
  if (!existsSync(worker))
414
452
  throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
453
+ const profile = options.profile
454
+ ? jsonRecord(await readFile(resolve(options.profile), "utf8"), options.profile)
455
+ : undefined;
415
456
  const request = {
416
- command: "runtime-export",
417
- ...selected(normalized, ["pid", "build", "kind", "maxHooks", "durationMs", "maxEvents", "cleanupDeadlineMs"]),
418
- ...(options.staticProfile ? { staticProfile: resolve(options.staticProfile) } : {})
457
+ command: "dynamic-script",
458
+ pid: normalized.pid,
459
+ build: normalized.build,
460
+ source: RUNTIME_EXPORT_SCRIPT,
461
+ exportName: "collect",
462
+ args: {
463
+ kind: normalized.kind,
464
+ maxHooks: normalized.maxHooks,
465
+ durationMs: normalized.durationMs,
466
+ maxEvents: normalized.maxEvents,
467
+ cleanupDeadlineMs: normalized.cleanupDeadlineMs,
468
+ ...(profile ? { profile } : {})
469
+ },
470
+ callArgs: [],
471
+ durationMs: normalized.durationMs
419
472
  };
420
473
  const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
421
474
  if (result.exitCode !== 0)
422
475
  throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
423
476
  const line = result.stdout.split(/\r?\n/).find(value => value.trim());
424
- writeJson(io, line ? JSON.parse(line) : { ok: true, command: "analyze.runtime-export", stdout: result.stdout });
477
+ const value = line ? jsonRecord(line, "runtime result") : { ok: true, stdout: result.stdout };
478
+ writeJson(io, { ...value, command: "analyze.runtime" });
425
479
  });
426
- analyze.command("verify")
427
- .description("Compare static and runtime evidence without implicit promotion")
428
- .requiredOption("--static-profile <path>", "static profile JSON")
429
- .requiredOption("--runtime-export <path>", "runtime export JSON")
430
- .option("--output <path>", "optional verification report path")
480
+ analyze.command("dynamic")
481
+ .description("Run a caller-supplied GumJS script through Frida")
482
+ .requiredOption("--pid <pid>", "target process ID")
483
+ .requiredOption("--build <buildKey>", "build key")
484
+ .requiredOption("--script <path>", "GumJS script file")
485
+ .option("--export <name>", "rpc.exports function to call")
486
+ .option("--args <json>", "JSON object exposed as __WOWDUMP_INPUT__", "{}")
487
+ .option("--call-args <json>", "JSON array passed to the exported function", "[]")
488
+ .option("--duration-ms <ms>", "maximum script duration", "5000")
489
+ .option("--confirm", "confirm the exact Frida operation")
431
490
  .action(async (options) => {
432
- const staticFile = resolve(options.staticProfile);
433
- const runtimeFile = resolve(options.runtimeExport);
434
- const staticProfile = jsonRecord(await readFile(staticFile, "utf8"), staticFile);
435
- const runtimeExport = jsonRecord(await readFile(runtimeFile, "utf8"), runtimeFile);
436
- const checks = compareEvidence(staticProfile, runtimeExport);
437
- const report = {
438
- ok: checks.every(check => check.status !== "mismatch"),
439
- command: "analyze.verify",
440
- staticProfile: staticFile,
441
- runtimeExport: runtimeFile,
442
- checks,
443
- confidence: staticProfile.confidence ?? "unverified",
444
- promoted: false
491
+ const normalized = {
492
+ pid: positiveInteger(options.pid, "pid"),
493
+ build: options.build,
494
+ script: resolve(options.script),
495
+ ...(options.export ? { exportName: options.export } : {}),
496
+ args: jsonRecord(options.args, "args"),
497
+ callArgs: JSON.parse(options.callArgs),
498
+ durationMs: positiveInteger(options.durationMs, "duration-ms"),
499
+ confirm: options.confirm === true
445
500
  };
446
- if (options.output) {
447
- const { writeFile } = await import("node:fs/promises");
448
- await writeFile(resolve(options.output), `${JSON.stringify(report, null, 2)}\n`, "utf8");
449
- }
450
- writeJson(io, report);
501
+ if (!Array.isArray(normalized.callArgs))
502
+ throw new CliError("ARGUMENT_INVALID", "call-args must be a JSON array");
503
+ requireRuntimeConfirmation(normalized);
504
+ if (!existsSync(normalized.script))
505
+ throw new CliError("FRIDA_SCRIPT_NOT_FOUND", `Frida script was not found: ${normalized.script}`);
506
+ const worker = resolve(env.WOWDUMP_FRIDA_WORKER ?? join(packageDist, "frida-worker.js"));
507
+ if (!existsSync(worker))
508
+ throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
509
+ const request = { command: "dynamic-script", ...selected(normalized, ["pid", "build", "script", "exportName", "args", "callArgs", "durationMs"]) };
510
+ const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
511
+ if (result.exitCode !== 0)
512
+ throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
513
+ const line = result.stdout.split(/\r?\n/).find(value => value.trim());
514
+ writeJson(io, line ? JSON.parse(line) : { ok: true, command: "analyze.dynamic", stdout: result.stdout });
451
515
  });
452
516
  program.command("init")
453
517
  .description("Initialize WOWDUMP_HOME without overwriting existing files")
454
- .option("--skip-toolchain-download", "only detect existing Java/Ghidra installations")
518
+ .option("--skip-toolchain-download", "kept for compatibility; no external toolchain is installed")
455
519
  .action(async (options) => {
456
520
  const installMissing = options.skipToolchainDownload !== true && env.WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD !== "1";
457
521
  const result = await bootstrapToolchain({ home, env, installMissing });
@@ -493,5 +557,7 @@ if (process.argv[1]) {
493
557
  invokedFile = pathToFileURL(invokedPath).href;
494
558
  }
495
559
  }
496
- if (invokedFile === import.meta.url)
560
+ if (invokedFile === import.meta.url) {
561
+ await initializeWowdumpHome();
497
562
  process.exitCode = await runWowdumpCli();
563
+ }