wowdump 0.2.1 → 0.3.1
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/LICENSE +21 -21
- package/README.md +56 -118
- package/dist/agent.js +5 -8
- package/dist/cli.js +497 -0
- package/dist/discovery.js +1 -12
- package/dist/dry-run.js +0 -2
- package/dist/focused-session.js +61 -1329
- package/dist/frida-runtime.js +51 -73
- package/dist/frida-worker.js +100 -0
- package/dist/ghidra.js +769 -0
- package/dist/main.js +66 -0
- package/dist/processes.js +2 -5
- package/dist/reader-broker.js +460 -0
- package/dist/reader-client.js +1 -0
- package/dist/reader-main.js +67 -0
- package/dist/session.js +17 -120
- package/dist/toolchain.js +594 -0
- package/dist/windows-launcher.js +207 -0
- package/dist/windows-reader.js +102 -0
- package/dist/wow-analysis.js +211 -236
- package/package.json +18 -35
- package/skills/wowdump/SKILL.md +15 -0
- package/skills/wowdump/commands.md +44 -0
- package/dist/analysis-path.js +0 -38
- package/dist/analysis-process-log.js +0 -146
- package/dist/broker-client.js +0 -411
- package/dist/broker-codec.js +0 -148
- package/dist/broker-core.js +0 -1045
- package/dist/broker-gateway.js +0 -447
- package/dist/broker-ledger.js +0 -196
- package/dist/broker-main.js +0 -291
- package/dist/broker-protocol.js +0 -119
- package/dist/broker-runtime.js +0 -1283
- package/dist/broker-server.js +0 -466
- package/dist/build-bundle-loader.js +0 -183
- package/dist/build-bundle.js +0 -11
- package/dist/focus-errors.js +0 -63
- package/dist/focus-service.js +0 -1855
- package/dist/mcp-main.js +0 -51
- package/dist/mcp.js +0 -924
- package/dist/process-log-lock.js +0 -195
- package/dist/runtime-config.js +0 -399
- package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
- package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
- package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
- package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
- package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
package/dist/cli.js
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
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 };
|
|
36
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
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
|
+
};
|
|
80
|
+
}
|
|
81
|
+
class CliError extends Error {
|
|
82
|
+
code;
|
|
83
|
+
details;
|
|
84
|
+
constructor(code, message, details) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "CliError";
|
|
87
|
+
this.code = code;
|
|
88
|
+
this.details = details;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function positiveInteger(value, field) {
|
|
92
|
+
const result = Number(value);
|
|
93
|
+
if (!Number.isSafeInteger(result) || result <= 0)
|
|
94
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be a positive integer`);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
function nonNegativeInteger(value, field) {
|
|
98
|
+
const result = Number(value);
|
|
99
|
+
if (!Number.isSafeInteger(result) || result < 0)
|
|
100
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be a non-negative integer`);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function jsonRecord(value, field) {
|
|
104
|
+
let decoded;
|
|
105
|
+
try {
|
|
106
|
+
decoded = JSON.parse(value);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be valid JSON`);
|
|
110
|
+
}
|
|
111
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
112
|
+
throw new CliError("ARGUMENT_INVALID", `${field} must be a JSON object`);
|
|
113
|
+
}
|
|
114
|
+
return decoded;
|
|
115
|
+
}
|
|
116
|
+
function writeJson(io, value) {
|
|
117
|
+
io.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
118
|
+
}
|
|
119
|
+
async function defaultSidecar(file, args, input) {
|
|
120
|
+
return new Promise(resolveResult => {
|
|
121
|
+
const child = spawn(file, args, { windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
122
|
+
let stdout = "";
|
|
123
|
+
let stderr = "";
|
|
124
|
+
child.stdout.setEncoding("utf8");
|
|
125
|
+
child.stderr.setEncoding("utf8");
|
|
126
|
+
child.stdout.on("data", value => { stdout += value; });
|
|
127
|
+
child.stderr.on("data", value => { stderr += value; });
|
|
128
|
+
child.once("error", error => resolveResult({ exitCode: 1, stdout, stderr: `${stderr}${error.message}` }));
|
|
129
|
+
child.once("exit", code => resolveResult({ exitCode: code ?? 1, stdout, stderr }));
|
|
130
|
+
if (input !== undefined)
|
|
131
|
+
child.stdin.end(input);
|
|
132
|
+
else
|
|
133
|
+
child.stdin.end();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function brokerCommandLine(env, cwd) {
|
|
137
|
+
const configured = env.WOWDUMP_READER_COMMAND?.trim();
|
|
138
|
+
if (configured)
|
|
139
|
+
return { file: configured, args: [] };
|
|
140
|
+
const candidate = resolve(cwd, "dist", "reader-main.js");
|
|
141
|
+
return existsSync(candidate) ? { file: process.execPath, args: [candidate, "--request-stdio"] } : null;
|
|
142
|
+
}
|
|
143
|
+
async function defaultBroker(invocation, env, cwd, sidecar) {
|
|
144
|
+
const command = brokerCommandLine(env, cwd);
|
|
145
|
+
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" });
|
|
147
|
+
}
|
|
148
|
+
const result = await (sidecar ?? defaultSidecar)(command.file, command.args, `${JSON.stringify(invocation)}\n`);
|
|
149
|
+
if (result.exitCode !== 0)
|
|
150
|
+
throw new CliError("BROKER_FAILED", result.stderr.trim() || `reader broker exited with ${result.exitCode}`);
|
|
151
|
+
const line = result.stdout.split(/\r?\n/).find(value => value.trim());
|
|
152
|
+
if (!line)
|
|
153
|
+
throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned no JSON response");
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(line);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw new CliError("BROKER_PROTOCOL_ERROR", "reader broker returned invalid JSON", { stdout: result.stdout });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const windowsBrokerManagers = new Map();
|
|
162
|
+
function persistentWindowsBroker(home, cwd) {
|
|
163
|
+
const key = resolve(home).toLowerCase();
|
|
164
|
+
let manager = windowsBrokerManagers.get(key);
|
|
165
|
+
if (!manager) {
|
|
166
|
+
manager = new WindowsBrokerManager({ home, readerEntry: resolve(cwd, "dist", "reader-main.js") });
|
|
167
|
+
windowsBrokerManagers.set(key, manager);
|
|
168
|
+
}
|
|
169
|
+
return manager;
|
|
170
|
+
}
|
|
171
|
+
async function listProfileFiles(directory) {
|
|
172
|
+
try {
|
|
173
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
174
|
+
return entries
|
|
175
|
+
.filter(entry => entry.isFile() && extname(entry.name).toLowerCase() === ".json")
|
|
176
|
+
.map(entry => join(directory, entry.name))
|
|
177
|
+
.sort((left, right) => left.localeCompare(right));
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code === "ENOENT")
|
|
181
|
+
return [];
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function readProfileSummary(file) {
|
|
186
|
+
const profile = jsonRecord(await readFile(file, "utf8"), file);
|
|
187
|
+
return {
|
|
188
|
+
id: typeof profile.id === "string" ? profile.id : basename(file, extname(file)),
|
|
189
|
+
file,
|
|
190
|
+
buildKey: profile.buildKey ?? null,
|
|
191
|
+
confidence: profile.confidence ?? "unverified",
|
|
192
|
+
executableSha256: profile.executableSha256
|
|
193
|
+
?? (profile.executable && typeof profile.executable === "object" && !Array.isArray(profile.executable)
|
|
194
|
+
? profile.executable.sha256 ?? null
|
|
195
|
+
: null)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function profileFile(profileRoot, idOrFile) {
|
|
199
|
+
if (isAbsolute(idOrFile))
|
|
200
|
+
return resolve(idOrFile);
|
|
201
|
+
const safeId = idOrFile.replace(/\.json$/i, "");
|
|
202
|
+
if (!/^[A-Za-z0-9_.@-]+$/.test(safeId))
|
|
203
|
+
throw new CliError("ARGUMENT_INVALID", "profile ID contains unsupported characters");
|
|
204
|
+
return join(profileRoot, `${safeId}.json`);
|
|
205
|
+
}
|
|
206
|
+
function selected(source, keys) {
|
|
207
|
+
return Object.fromEntries(keys.filter(key => source[key] !== undefined).map(key => [key, source[key]]));
|
|
208
|
+
}
|
|
209
|
+
function requireRuntimeConfirmation(options) {
|
|
210
|
+
if (options.confirm === true)
|
|
211
|
+
return;
|
|
212
|
+
throw new CliError("CONFIRMATION_REQUIRED", "runtime export requires --confirm", {
|
|
213
|
+
pid: options.pid,
|
|
214
|
+
buildKey: options.build,
|
|
215
|
+
operation: "runtime-export",
|
|
216
|
+
kind: options.kind,
|
|
217
|
+
maxHooks: options.maxHooks,
|
|
218
|
+
durationMs: options.durationMs,
|
|
219
|
+
maxEvents: options.maxEvents,
|
|
220
|
+
cleanupDeadlineMs: options.cleanupDeadlineMs
|
|
221
|
+
});
|
|
222
|
+
}
|
|
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
|
+
export function createWowdumpCli(dependencies = {}) {
|
|
255
|
+
const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
256
|
+
const env = dependencies.env ?? process.env;
|
|
257
|
+
const cwd = resolve(dependencies.cwd ?? process.cwd());
|
|
258
|
+
const home = resolveWowdumpHome(env);
|
|
259
|
+
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;
|
|
264
|
+
const program = new Command()
|
|
265
|
+
.name("wowdump")
|
|
266
|
+
.description("Build-aware WoW native memory analysis CLI")
|
|
267
|
+
.version("0.3.0")
|
|
268
|
+
.showHelpAfterError()
|
|
269
|
+
.configureOutput({ writeOut: value => io.stdout.write(value), writeErr: value => io.stderr.write(value) });
|
|
270
|
+
program.command("target")
|
|
271
|
+
.description("Show target, reader broker, profile, monitor, and Frida state")
|
|
272
|
+
.option("--pid <pid>", "target process ID")
|
|
273
|
+
.option("--build <buildKey>", "expected build key")
|
|
274
|
+
.action(async (options) => {
|
|
275
|
+
const payload = {
|
|
276
|
+
...(options.pid ? { pid: positiveInteger(options.pid, "pid") } : {}),
|
|
277
|
+
...(options.build ? { buildKey: options.build } : {})
|
|
278
|
+
};
|
|
279
|
+
const status = await broker({ command: "status", payload });
|
|
280
|
+
writeJson(io, { ok: true, command: "target", home, broker: status, toolchain: resolveToolchain({ home, env }) });
|
|
281
|
+
});
|
|
282
|
+
program.command("profiles")
|
|
283
|
+
.description("List profiles or describe one profile")
|
|
284
|
+
.argument("[id-or-file]", "profile ID or absolute JSON file")
|
|
285
|
+
.option("--directory <path>", "profile directory", join(home, "profiles"))
|
|
286
|
+
.action(async (idOrFile, options) => {
|
|
287
|
+
const directory = resolve(options.directory);
|
|
288
|
+
if (idOrFile) {
|
|
289
|
+
const file = profileFile(directory, idOrFile);
|
|
290
|
+
const profile = jsonRecord(await readFile(file, "utf8"), file);
|
|
291
|
+
writeJson(io, { ok: true, command: "profiles.describe", file, profile });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const files = await listProfileFiles(directory);
|
|
295
|
+
writeJson(io, { ok: true, command: "profiles.list", directory, profiles: await Promise.all(files.map(readProfileSummary)) });
|
|
296
|
+
});
|
|
297
|
+
const memory = program.command("memory").description("Read or monitor profile-defined native memory");
|
|
298
|
+
memory.command("read")
|
|
299
|
+
.description("Perform one bounded read")
|
|
300
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
301
|
+
.option("--build <buildKey>", "expected build key")
|
|
302
|
+
.option("--profile <id>", "profile ID")
|
|
303
|
+
.option("--field <name...>", "field names")
|
|
304
|
+
.option("--address <hex>", "explicit address as hexadecimal text")
|
|
305
|
+
.option("--size <bytes>", "bounded byte count")
|
|
306
|
+
.option("--request <json>", "complete read request as JSON")
|
|
307
|
+
.action(async (options) => {
|
|
308
|
+
const payload = options.request ? jsonRecord(options.request, "request") : {
|
|
309
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
310
|
+
...(options.build ? { buildKey: options.build } : {}),
|
|
311
|
+
...(options.profile ? { profileId: options.profile } : {}),
|
|
312
|
+
...(options.field ? { fields: options.field } : {}),
|
|
313
|
+
...(options.address ? { address: options.address } : {}),
|
|
314
|
+
...(options.size ? { size: positiveInteger(options.size, "size") } : {})
|
|
315
|
+
};
|
|
316
|
+
writeJson(io, await broker({ command: "read", payload }));
|
|
317
|
+
});
|
|
318
|
+
const watch = memory.command("watch").description("Manage broker-owned memory monitors");
|
|
319
|
+
watch.command("start")
|
|
320
|
+
.description("Start a monitor")
|
|
321
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
322
|
+
.option("--build <buildKey>", "expected build key")
|
|
323
|
+
.option("--profile <id>", "profile ID")
|
|
324
|
+
.option("--field <name...>", "field names")
|
|
325
|
+
.option("--address <hex>", "explicit address as hexadecimal text")
|
|
326
|
+
.option("--size <bytes>", "bounded byte count")
|
|
327
|
+
.option("--interval <ms>", "sample interval", "250")
|
|
328
|
+
.option("--max-samples <count>", "sample limit", "1000")
|
|
329
|
+
.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
|
+
} })));
|
|
341
|
+
watch.command("poll")
|
|
342
|
+
.description("Poll monitor events")
|
|
343
|
+
.requiredOption("--id <watchId>", "monitor ID")
|
|
344
|
+
.option("--after <sequence>", "sequence cursor", "0")
|
|
345
|
+
.action(async (options) => writeJson(io, await broker({ command: "watch.poll", payload: {
|
|
346
|
+
watchId: options.id,
|
|
347
|
+
afterSequence: nonNegativeInteger(options.after, "after")
|
|
348
|
+
} })));
|
|
349
|
+
watch.command("stop")
|
|
350
|
+
.description("Stop a monitor and release its resources")
|
|
351
|
+
.requiredOption("--id <watchId>", "monitor ID")
|
|
352
|
+
.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")
|
|
356
|
+
.requiredOption("--exe <path>", "path to Wow.exe")
|
|
357
|
+
.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"),
|
|
383
|
+
dryRun: options.dryRun === true
|
|
384
|
+
})));
|
|
385
|
+
analyze.command("runtime-export")
|
|
386
|
+
.description("Export runtime evidence through the isolated Frida sidecar")
|
|
387
|
+
.requiredOption("--pid <pid>", "target process ID")
|
|
388
|
+
.requiredOption("--build <buildKey>", "build key")
|
|
389
|
+
.option("--build-key <buildKey>", "alias for --build")
|
|
390
|
+
.option("--kind <kind>", "text or providers", "providers")
|
|
391
|
+
.option("--worker <path>", "Frida worker entry")
|
|
392
|
+
.option("--static-profile <path>", "static profile containing candidate RVAs")
|
|
393
|
+
.option("--max-hooks <count>", "maximum hooks", "1")
|
|
394
|
+
.option("--duration-ms <ms>", "maximum duration", "5000")
|
|
395
|
+
.option("--max-events <count>", "maximum events", "100")
|
|
396
|
+
.option("--cleanup-deadline-ms <ms>", "cleanup deadline", "2000")
|
|
397
|
+
.option("--confirm", "confirm the exact bounded Frida operation")
|
|
398
|
+
.action(async (options) => {
|
|
399
|
+
const normalized = {
|
|
400
|
+
pid: positiveInteger(options.pid, "pid"),
|
|
401
|
+
build: options.build ?? options.buildKey,
|
|
402
|
+
kind: options.kind,
|
|
403
|
+
maxHooks: positiveInteger(options.maxHooks, "max-hooks"),
|
|
404
|
+
durationMs: positiveInteger(options.durationMs, "duration-ms"),
|
|
405
|
+
maxEvents: positiveInteger(options.maxEvents, "max-events"),
|
|
406
|
+
cleanupDeadlineMs: positiveInteger(options.cleanupDeadlineMs, "cleanup-deadline-ms"),
|
|
407
|
+
confirm: options.confirm === true
|
|
408
|
+
};
|
|
409
|
+
if (!["text", "providers"].includes(normalized.kind))
|
|
410
|
+
throw new CliError("ARGUMENT_INVALID", "kind must be text or providers");
|
|
411
|
+
requireRuntimeConfirmation(normalized);
|
|
412
|
+
const worker = resolve(options.worker ?? env.WOWDUMP_FRIDA_WORKER ?? join(cwd, "dist", "frida-worker.js"));
|
|
413
|
+
if (!existsSync(worker))
|
|
414
|
+
throw new CliError("FRIDA_WORKER_NOT_FOUND", `Frida worker was not found: ${worker}`);
|
|
415
|
+
const request = {
|
|
416
|
+
command: "runtime-export",
|
|
417
|
+
...selected(normalized, ["pid", "build", "kind", "maxHooks", "durationMs", "maxEvents", "cleanupDeadlineMs"]),
|
|
418
|
+
...(options.staticProfile ? { staticProfile: resolve(options.staticProfile) } : {})
|
|
419
|
+
};
|
|
420
|
+
const result = await sidecar(process.execPath, [worker], `${JSON.stringify(request)}\n`);
|
|
421
|
+
if (result.exitCode !== 0)
|
|
422
|
+
throw new CliError("FRIDA_WORKER_FAILED", result.stderr.trim() || `Frida worker exited with ${result.exitCode}`);
|
|
423
|
+
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 });
|
|
425
|
+
});
|
|
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")
|
|
431
|
+
.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
|
|
445
|
+
};
|
|
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);
|
|
451
|
+
});
|
|
452
|
+
program.command("init")
|
|
453
|
+
.description("Initialize WOWDUMP_HOME without overwriting existing files")
|
|
454
|
+
.option("--skip-toolchain-download", "only detect existing Java/Ghidra installations")
|
|
455
|
+
.action(async (options) => {
|
|
456
|
+
const installMissing = options.skipToolchainDownload !== true && env.WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD !== "1";
|
|
457
|
+
const result = await bootstrapToolchain({ home, env, installMissing });
|
|
458
|
+
writeJson(io, {
|
|
459
|
+
ok: result.toolchain.ready,
|
|
460
|
+
command: "init",
|
|
461
|
+
...result,
|
|
462
|
+
toolchain: result.toolchain
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
return program;
|
|
466
|
+
}
|
|
467
|
+
export async function runWowdumpCli(argv = process.argv, dependencies = {}) {
|
|
468
|
+
const io = dependencies.io ?? { stdout: process.stdout, stderr: process.stderr };
|
|
469
|
+
try {
|
|
470
|
+
await createWowdumpCli(dependencies).parseAsync([...argv]);
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
const failure = error;
|
|
475
|
+
io.stderr.write(`${JSON.stringify({
|
|
476
|
+
ok: false,
|
|
477
|
+
error: {
|
|
478
|
+
code: typeof failure.code === "string" ? failure.code : "CLI_FAILED",
|
|
479
|
+
message: error instanceof Error ? error.message : String(error),
|
|
480
|
+
...(failure.details !== undefined ? { details: failure.details } : {})
|
|
481
|
+
}
|
|
482
|
+
})}\n`);
|
|
483
|
+
return 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
let invokedFile = "";
|
|
487
|
+
if (process.argv[1]) {
|
|
488
|
+
const invokedPath = resolve(process.argv[1]);
|
|
489
|
+
try {
|
|
490
|
+
invokedFile = pathToFileURL(realpathSync(invokedPath)).href;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
invokedFile = pathToFileURL(invokedPath).href;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (invokedFile === import.meta.url)
|
|
497
|
+
process.exitCode = await runWowdumpCli();
|
package/dist/discovery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
-
import { basename, join
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
function parseBuildInfo(file) {
|
|
4
4
|
if (!existsSync(file))
|
|
5
5
|
return [];
|
|
@@ -46,14 +46,3 @@ export function discoverInstalls(gameRoot) {
|
|
|
46
46
|
return [{ flavor, root, executable, build }];
|
|
47
47
|
}).sort((a, b) => a.flavor.localeCompare(b.flavor));
|
|
48
48
|
}
|
|
49
|
-
export function discoverInstallsAcrossRoots(gameRoots, discover = discoverInstalls) {
|
|
50
|
-
const installs = new Map();
|
|
51
|
-
for (const gameRoot of gameRoots) {
|
|
52
|
-
for (const install of discover(gameRoot)) {
|
|
53
|
-
const key = resolve(install.executable).toLowerCase();
|
|
54
|
-
if (!installs.has(key))
|
|
55
|
-
installs.set(key, install);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return [...installs.values()].sort((a, b) => a.flavor.localeCompare(b.flavor) || a.executable.localeCompare(b.executable));
|
|
59
|
-
}
|
package/dist/dry-run.js
CHANGED
|
@@ -29,8 +29,6 @@ export function createDryRunReport(installs, processes, registry) {
|
|
|
29
29
|
processes: processes
|
|
30
30
|
.map(process => ({
|
|
31
31
|
pid: process.pid,
|
|
32
|
-
executable: process.executable,
|
|
33
|
-
...(process.startTime ? { startTime: process.startTime } : {}),
|
|
34
32
|
...installStatus(process.install, registry)
|
|
35
33
|
}))
|
|
36
34
|
.sort((a, b) => a.pid - b.pid)
|