wowdump 0.2.0 → 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.
Files changed (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +56 -118
  3. package/dist/agent.js +5 -8
  4. package/dist/cli.js +497 -0
  5. package/dist/discovery.js +1 -12
  6. package/dist/dry-run.js +0 -2
  7. package/dist/focused-session.js +61 -1329
  8. package/dist/frida-runtime.js +51 -73
  9. package/dist/frida-worker.js +100 -0
  10. package/dist/ghidra.js +769 -0
  11. package/dist/main.js +66 -0
  12. package/dist/processes.js +2 -5
  13. package/dist/reader-broker.js +460 -0
  14. package/dist/reader-client.js +1 -0
  15. package/dist/reader-main.js +67 -0
  16. package/dist/session.js +17 -120
  17. package/dist/toolchain.js +594 -0
  18. package/dist/windows-launcher.js +207 -0
  19. package/dist/windows-reader.js +102 -0
  20. package/dist/wow-analysis.js +211 -236
  21. package/package.json +18 -35
  22. package/skills/wowdump/SKILL.md +15 -0
  23. package/skills/wowdump/commands.md +44 -0
  24. package/dist/analysis-path.js +0 -38
  25. package/dist/analysis-process-log.js +0 -146
  26. package/dist/broker-client.js +0 -411
  27. package/dist/broker-codec.js +0 -148
  28. package/dist/broker-core.js +0 -1045
  29. package/dist/broker-gateway.js +0 -447
  30. package/dist/broker-ledger.js +0 -196
  31. package/dist/broker-main.js +0 -291
  32. package/dist/broker-protocol.js +0 -119
  33. package/dist/broker-runtime.js +0 -1283
  34. package/dist/broker-server.js +0 -466
  35. package/dist/build-bundle-loader.js +0 -183
  36. package/dist/build-bundle.js +0 -11
  37. package/dist/focus-errors.js +0 -63
  38. package/dist/focus-service.js +0 -1855
  39. package/dist/mcp-main.js +0 -51
  40. package/dist/mcp.js +0 -924
  41. package/dist/process-log-lock.js +0 -181
  42. package/dist/runtime-config.js +0 -399
  43. package/resources/builds/retail/12.0.7.68974/build-profile.json +0 -290
  44. package/resources/builds/retail/12.0.7.68974/data-sources.json +0 -1633
  45. package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +0 -5130
  46. package/resources/builds/retail/12.0.7.68974/manifest.json +0 -63
  47. package/resources/builds/retail/12.0.7.68974/signatures.json +0 -260
@@ -1,8 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { basename, join, resolve } from "node:path";
4
- import { brokerManagedCommand } from "./frida-runtime.js";
5
- import { BuildBundleStore } from "./build-bundle-loader.js";
6
4
  export const WOW_ANALYSIS_BUILD_KEY = "retail@12.0.7.68974";
7
5
  export const WOW_ANALYSIS_MODULE = "Wow.exe";
8
6
  export const WOW_IDA_IMAGE_BASE = "0x140000000";
@@ -192,208 +190,206 @@ function requireSelection(input) {
192
190
  throw new Error("an explicit API, namespace, glob, RVA, cluster, or all=true selection is required");
193
191
  }
194
192
  }
195
- const CPP_TRACE_SOURCE = String.raw `
196
- let catalog = null;
197
- let listeners = [];
198
- let events = [];
199
- let nextSeq = 1;
200
- let droppedEvents = 0;
201
- let active = false;
202
- let timer = null;
203
- let startedAt = null;
204
- let stoppedAt = null;
205
- let stopReason = null;
206
- const depthByThread = new Map();
207
- function emit(event, maxEvents) {
208
- event.seq = nextSeq++;
209
- if (events.length >= maxEvents) { events.shift(); droppedEvents++; }
210
- events.push(event);
211
- }
212
- function stop(reason) {
213
- const errors = [];
214
- if (timer !== null) { clearTimeout(timer); timer = null; }
215
- const detachedHooks = listeners.length;
216
- for (const listener of listeners.splice(0)) {
217
- try { listener.detach(); } catch (error) { errors.push(String(error)); }
218
- }
219
- depthByThread.clear();
220
- active = false;
221
- stoppedAt = new Date().toISOString();
222
- stopReason = reason || 'requested';
223
- return { ...status(), detachedHooks, detachErrors: errors };
224
- }
225
- function status() {
226
- return {
227
- active, buildKey: catalog && catalog.buildKey, moduleName: catalog && catalog.moduleName,
228
- installedHooks: listeners.length, queuedEvents: events.length, droppedEvents,
229
- startedAt, stoppedAt, stopReason
230
- };
231
- }
232
- rpc.exports = {
233
- configureCppTrace(value) {
234
- stop('reconfigured');
235
- catalog = value;
236
- events = []; nextSeq = 1; droppedEvents = 0;
237
- return { configured: true, buildKey: value.buildKey, targets: value.targets.length };
238
- },
239
- wowCppTraceStart(request) {
240
- if (!catalog) throw new Error('C++ trace catalog is not configured');
241
- if (active) throw new Error('C++ trace is already active');
242
- if (request.buildKey !== catalog.buildKey) throw new Error('build mismatch');
243
- if (request.pid !== undefined && request.pid !== Process.id) throw new Error('PID mismatch');
244
- const module = Process.getModuleByName(catalog.moduleName);
245
- const maxEvents = request.maxEvents;
246
- const sampling = request.sampling;
247
- const argumentCount = request.argumentCount;
248
- for (const target of catalog.targets) {
249
- const address = module.base.add(target.rvaNumber);
250
- const range = Process.findRangeByAddress(address);
251
- if (range === null || range.protection.indexOf('x') < 0) throw new Error('target is not executable: ' + target.id);
252
- const listener = Interceptor.attach(address, {
253
- onEnter(args) {
254
- if (sampling < 1 && Math.random() > sampling) { this.__wowCppSkipped = true; return; }
255
- const threadId = this.threadId;
256
- const depth = (depthByThread.get(threadId) || 0) + 1;
257
- depthByThread.set(threadId, depth);
258
- this.__wowCppDepth = depth;
259
- const captured = [];
260
- if (request.captureArguments) {
261
- for (let index = 0; index < argumentCount; index++) captured.push(args[index].toString());
262
- }
263
- emit({ type: 'cpp_trace_enter', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
264
- moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
265
- arguments: captured }, maxEvents);
266
- },
267
- onLeave(retval) {
268
- if (this.__wowCppSkipped) return;
269
- const threadId = this.threadId;
270
- const depth = this.__wowCppDepth || depthByThread.get(threadId) || 1;
271
- emit({ type: 'cpp_trace_leave', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
272
- moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
273
- returnValue: request.captureReturn ? retval.toString() : undefined }, maxEvents);
274
- if (depth <= 1) depthByThread.delete(threadId); else depthByThread.set(threadId, depth - 1);
275
- }
276
- });
277
- listeners.push(listener);
278
- }
279
- active = true; startedAt = new Date().toISOString(); stoppedAt = null; stopReason = null;
280
- if (request.durationMs > 0) timer = setTimeout(() => stop('duration_elapsed'), request.durationMs);
281
- return { ...status(), pid: Process.id, moduleBase: module.base.toString(), selectedTargets: catalog.targets.length };
282
- },
283
- wowCppTraceRead(afterSeq, limit) {
284
- const selected = events.filter(event => event.seq > afterSeq).slice(0, limit);
285
- return { events: selected, oldestSeq: events[0] ? events[0].seq : null,
286
- newestSeq: events.length ? events[events.length - 1].seq : null,
287
- nextAfterSeq: selected.length ? selected[selected.length - 1].seq : afterSeq, droppedEvents };
288
- },
289
- wowCppTraceStatus() { return status(); },
290
- wowCppTraceStop(reason) { return stop(reason); },
291
- dispose() { const result = stop('disposed'); catalog = null; return result; }
292
- };
193
+ const CPP_TRACE_SOURCE = String.raw `
194
+ let catalog = null;
195
+ let listeners = [];
196
+ let events = [];
197
+ let nextSeq = 1;
198
+ let droppedEvents = 0;
199
+ let active = false;
200
+ let timer = null;
201
+ let startedAt = null;
202
+ let stoppedAt = null;
203
+ let stopReason = null;
204
+ const depthByThread = new Map();
205
+ function emit(event, maxEvents) {
206
+ event.seq = nextSeq++;
207
+ if (events.length >= maxEvents) { events.shift(); droppedEvents++; }
208
+ events.push(event);
209
+ }
210
+ function stop(reason) {
211
+ const errors = [];
212
+ if (timer !== null) { clearTimeout(timer); timer = null; }
213
+ const detachedHooks = listeners.length;
214
+ for (const listener of listeners.splice(0)) {
215
+ try { listener.detach(); } catch (error) { errors.push(String(error)); }
216
+ }
217
+ depthByThread.clear();
218
+ active = false;
219
+ stoppedAt = new Date().toISOString();
220
+ stopReason = reason || 'requested';
221
+ return { ...status(), detachedHooks, detachErrors: errors };
222
+ }
223
+ function status() {
224
+ return {
225
+ active, buildKey: catalog && catalog.buildKey, moduleName: catalog && catalog.moduleName,
226
+ installedHooks: listeners.length, queuedEvents: events.length, droppedEvents,
227
+ startedAt, stoppedAt, stopReason
228
+ };
229
+ }
230
+ rpc.exports = {
231
+ configureCppTrace(value) {
232
+ stop('reconfigured');
233
+ catalog = value;
234
+ events = []; nextSeq = 1; droppedEvents = 0;
235
+ return { configured: true, buildKey: value.buildKey, targets: value.targets.length };
236
+ },
237
+ wowCppTraceStart(request) {
238
+ if (!catalog) throw new Error('C++ trace catalog is not configured');
239
+ if (active) throw new Error('C++ trace is already active');
240
+ if (request.buildKey !== catalog.buildKey) throw new Error('build mismatch');
241
+ if (request.pid !== undefined && request.pid !== Process.id) throw new Error('PID mismatch');
242
+ const module = Process.getModuleByName(catalog.moduleName);
243
+ const maxEvents = request.maxEvents;
244
+ const sampling = request.sampling;
245
+ const argumentCount = request.argumentCount;
246
+ for (const target of catalog.targets) {
247
+ const address = module.base.add(target.rvaNumber);
248
+ const range = Process.findRangeByAddress(address);
249
+ if (range === null || range.protection.indexOf('x') < 0) throw new Error('target is not executable: ' + target.id);
250
+ const listener = Interceptor.attach(address, {
251
+ onEnter(args) {
252
+ if (sampling < 1 && Math.random() > sampling) { this.__wowCppSkipped = true; return; }
253
+ const threadId = this.threadId;
254
+ const depth = (depthByThread.get(threadId) || 0) + 1;
255
+ depthByThread.set(threadId, depth);
256
+ this.__wowCppDepth = depth;
257
+ const captured = [];
258
+ if (request.captureArguments) {
259
+ for (let index = 0; index < argumentCount; index++) captured.push(args[index].toString());
260
+ }
261
+ emit({ type: 'cpp_trace_enter', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
262
+ moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
263
+ arguments: captured }, maxEvents);
264
+ },
265
+ onLeave(retval) {
266
+ if (this.__wowCppSkipped) return;
267
+ const threadId = this.threadId;
268
+ const depth = this.__wowCppDepth || depthByThread.get(threadId) || 1;
269
+ emit({ type: 'cpp_trace_leave', timestamp: new Date().toISOString(), buildKey: catalog.buildKey,
270
+ moduleName: module.name, moduleBase: module.base.toString(), threadId, depth, target,
271
+ returnValue: request.captureReturn ? retval.toString() : undefined }, maxEvents);
272
+ if (depth <= 1) depthByThread.delete(threadId); else depthByThread.set(threadId, depth - 1);
273
+ }
274
+ });
275
+ listeners.push(listener);
276
+ }
277
+ active = true; startedAt = new Date().toISOString(); stoppedAt = null; stopReason = null;
278
+ if (request.durationMs > 0) timer = setTimeout(() => stop('duration_elapsed'), request.durationMs);
279
+ return { ...status(), pid: Process.id, moduleBase: module.base.toString(), selectedTargets: catalog.targets.length };
280
+ },
281
+ wowCppTraceRead(afterSeq, limit) {
282
+ const selected = events.filter(event => event.seq > afterSeq).slice(0, limit);
283
+ return { events: selected, oldestSeq: events[0] ? events[0].seq : null,
284
+ newestSeq: events.length ? events[events.length - 1].seq : null,
285
+ nextAfterSeq: selected.length ? selected[selected.length - 1].seq : afterSeq, droppedEvents };
286
+ },
287
+ wowCppTraceStatus() { return status(); },
288
+ wowCppTraceStop(reason) { return stop(reason); },
289
+ dispose() { const result = stop('disposed'); catalog = null; return result; }
290
+ };
293
291
  `;
294
- const DATA_READER_SOURCE = String.raw `
295
- function readable(address, size) {
296
- const range = Process.findRangeByAddress(address);
297
- if (range === null || range.protection.indexOf('r') < 0) throw new Error('unreadable address ' + address);
298
- const end = address.add(size);
299
- if (end.compare(range.base.add(range.size)) > 0) throw new Error('read crosses range at ' + address);
300
- }
301
- function readField(base, field, maxStringBytes) {
302
- const address = base.add(field.offset);
303
- const sizeByKind = { bool: 1, u8: 1, s8: 1, u16: 2, s16: 2, u32: 4, s32: 4,
304
- float: 4, u64: 8, s64: 8, double: 8, pointer: Process.pointerSize };
305
- if (field.kind === 'utf8') {
306
- const length = Math.min(field.maxBytes || maxStringBytes, maxStringBytes);
307
- readable(address, 1);
308
- return address.readUtf8String(length);
309
- }
310
- if (field.kind === 'bytes') {
311
- const length = field.size;
312
- readable(address, length);
313
- const value = address.readByteArray(length);
314
- return Array.from(new Uint8Array(value)).map(v => v.toString(16).padStart(2, '0')).join('');
315
- }
316
- const size = sizeByKind[field.kind];
317
- if (!size) throw new Error('unsupported field kind ' + field.kind);
318
- readable(address, size);
319
- switch (field.kind) {
320
- case 'bool': return address.readU8() !== 0;
321
- case 'u8': return address.readU8(); case 's8': return address.readS8();
322
- case 'u16': return address.readU16(); case 's16': return address.readS16();
323
- case 'u32': return address.readU32(); case 's32': return address.readS32();
324
- case 'float': return address.readFloat(); case 'double': return address.readDouble();
325
- case 'u64': return address.readU64().toString(); case 's64': return address.readS64().toString();
326
- case 'pointer': return address.readPointer().toString();
327
- }
328
- }
329
- function validateInvariant(rule, address, fields, module) {
330
- if (rule.kind === 'non_null') return { kind: rule.kind, passed: !address.isNull() };
331
- if (rule.kind === 'aligned') return { kind: rule.kind, passed: address.and(rule.alignment - 1).isNull() };
332
- if (rule.kind === 'field_range') {
333
- const value = Number(fields[rule.field]);
334
- return { kind: rule.kind, field: rule.field, passed: Number.isFinite(value) && value >= rule.minimum && value <= rule.maximum };
335
- }
336
- if (rule.kind === 'field_equals') return { kind: rule.kind, field: rule.field, passed: String(fields[rule.field]) === String(rule.value) };
337
- if (rule.kind === 'vtable_in_module') {
338
- const pointer = address.add(rule.offset || 0).readPointer();
339
- const passed = pointer.compare(module.base) >= 0 && pointer.compare(module.base.add(module.size)) < 0;
340
- return { kind: rule.kind, pointer: pointer.toString(), passed };
341
- }
342
- return { kind: rule.kind, passed: false, error: 'unsupported invariant' };
343
- }
344
- rpc.exports = {
345
- readDataSource(plan, requestedLimit) {
346
- const module = Process.getModuleByName(plan.moduleName);
347
- const rva = Number(plan.root.rva);
348
- if (!Number.isSafeInteger(rva) || rva < 0 || rva >= module.size) {
349
- return { allPassed: false, error: 'root RVA is outside module', moduleBase: module.base.toString() };
350
- }
351
- try {
352
- const rootAddress = module.base.add(rva);
353
- let address = rootAddress;
354
- if (plan.root.kind === 'global_pointer_rva') { readable(address, Process.pointerSize); address = address.readPointer(); }
355
- for (const step of plan.pointerChain) {
356
- address = address.add(step.offset);
357
- if (step.dereference) { readable(address, Process.pointerSize); address = address.readPointer(); }
358
- }
359
- if (address.isNull()) throw new Error('resolved root is null');
360
- const fields = {};
361
- for (const field of plan.fields) fields[field.name] = readField(address, field, plan.maxStringBytes);
362
- const validations = plan.invariants.map(rule => validateInvariant(rule, address, fields, module));
363
- const result = { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
364
- moduleBase: module.base.toString(), rootRva: plan.root.rva, rootAddress: rootAddress.toString(),
365
- resolvedAddress: address.toString(), fields, validations, items: [] };
366
- if (plan.container && plan.container.kind === 'vector') {
367
- const begin = address.add(plan.container.beginOffset).readPointer();
368
- const end = address.add(plan.container.endOffset).readPointer();
369
- const byteLength = Number(BigInt(end.toString()) - BigInt(begin.toString()));
370
- const elementSize = plan.container.elementSize;
371
- const count = byteLength >= 0 && byteLength % elementSize === 0 ? byteLength / elementSize : -1;
372
- const limit = Math.min(requestedLimit, plan.maxItems);
373
- validations.push({ kind: 'vector_bounds', passed: count >= 0 && count <= plan.maxItems, count });
374
- if (count >= 0 && count <= plan.maxItems) {
375
- for (let index = 0; index < Math.min(count, limit); index++) {
376
- const itemAddress = begin.add(index * elementSize);
377
- const item = {};
378
- for (const field of plan.container.fields || []) item[field.name] = readField(itemAddress, field, plan.maxStringBytes);
379
- result.items.push({ index, address: itemAddress.toString(), fields: item });
380
- }
381
- }
382
- }
383
- result.allPassed = validations.length > 0 && validations.every(item => item.passed === true);
384
- return result;
385
- } catch (error) {
386
- return { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
387
- moduleBase: module.base.toString(), rootRva: plan.root.rva, allPassed: false, error: String(error) };
388
- }
389
- }
390
- };
292
+ const DATA_READER_SOURCE = String.raw `
293
+ function readable(address, size) {
294
+ const range = Process.findRangeByAddress(address);
295
+ if (range === null || range.protection.indexOf('r') < 0) throw new Error('unreadable address ' + address);
296
+ const end = address.add(size);
297
+ if (end.compare(range.base.add(range.size)) > 0) throw new Error('read crosses range at ' + address);
298
+ }
299
+ function readField(base, field, maxStringBytes) {
300
+ const address = base.add(field.offset);
301
+ const sizeByKind = { bool: 1, u8: 1, s8: 1, u16: 2, s16: 2, u32: 4, s32: 4,
302
+ float: 4, u64: 8, s64: 8, double: 8, pointer: Process.pointerSize };
303
+ if (field.kind === 'utf8') {
304
+ const length = Math.min(field.maxBytes || maxStringBytes, maxStringBytes);
305
+ readable(address, 1);
306
+ return address.readUtf8String(length);
307
+ }
308
+ if (field.kind === 'bytes') {
309
+ const length = field.size;
310
+ readable(address, length);
311
+ const value = address.readByteArray(length);
312
+ return Array.from(new Uint8Array(value)).map(v => v.toString(16).padStart(2, '0')).join('');
313
+ }
314
+ const size = sizeByKind[field.kind];
315
+ if (!size) throw new Error('unsupported field kind ' + field.kind);
316
+ readable(address, size);
317
+ switch (field.kind) {
318
+ case 'bool': return address.readU8() !== 0;
319
+ case 'u8': return address.readU8(); case 's8': return address.readS8();
320
+ case 'u16': return address.readU16(); case 's16': return address.readS16();
321
+ case 'u32': return address.readU32(); case 's32': return address.readS32();
322
+ case 'float': return address.readFloat(); case 'double': return address.readDouble();
323
+ case 'u64': return address.readU64().toString(); case 's64': return address.readS64().toString();
324
+ case 'pointer': return address.readPointer().toString();
325
+ }
326
+ }
327
+ function validateInvariant(rule, address, fields, module) {
328
+ if (rule.kind === 'non_null') return { kind: rule.kind, passed: !address.isNull() };
329
+ if (rule.kind === 'aligned') return { kind: rule.kind, passed: address.and(rule.alignment - 1).isNull() };
330
+ if (rule.kind === 'field_range') {
331
+ const value = Number(fields[rule.field]);
332
+ return { kind: rule.kind, field: rule.field, passed: Number.isFinite(value) && value >= rule.minimum && value <= rule.maximum };
333
+ }
334
+ if (rule.kind === 'field_equals') return { kind: rule.kind, field: rule.field, passed: String(fields[rule.field]) === String(rule.value) };
335
+ if (rule.kind === 'vtable_in_module') {
336
+ const pointer = address.add(rule.offset || 0).readPointer();
337
+ const passed = pointer.compare(module.base) >= 0 && pointer.compare(module.base.add(module.size)) < 0;
338
+ return { kind: rule.kind, pointer: pointer.toString(), passed };
339
+ }
340
+ return { kind: rule.kind, passed: false, error: 'unsupported invariant' };
341
+ }
342
+ rpc.exports = {
343
+ readDataSource(plan, requestedLimit) {
344
+ const module = Process.getModuleByName(plan.moduleName);
345
+ const rva = Number(plan.root.rva);
346
+ if (!Number.isSafeInteger(rva) || rva < 0 || rva >= module.size) {
347
+ return { allPassed: false, error: 'root RVA is outside module', moduleBase: module.base.toString() };
348
+ }
349
+ try {
350
+ const rootAddress = module.base.add(rva);
351
+ let address = rootAddress;
352
+ if (plan.root.kind === 'global_pointer_rva') { readable(address, Process.pointerSize); address = address.readPointer(); }
353
+ for (const step of plan.pointerChain) {
354
+ address = address.add(step.offset);
355
+ if (step.dereference) { readable(address, Process.pointerSize); address = address.readPointer(); }
356
+ }
357
+ if (address.isNull()) throw new Error('resolved root is null');
358
+ const fields = {};
359
+ for (const field of plan.fields) fields[field.name] = readField(address, field, plan.maxStringBytes);
360
+ const validations = plan.invariants.map(rule => validateInvariant(rule, address, fields, module));
361
+ const result = { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
362
+ moduleBase: module.base.toString(), rootRva: plan.root.rva, rootAddress: rootAddress.toString(),
363
+ resolvedAddress: address.toString(), fields, validations, items: [] };
364
+ if (plan.container && plan.container.kind === 'vector') {
365
+ const begin = address.add(plan.container.beginOffset).readPointer();
366
+ const end = address.add(plan.container.endOffset).readPointer();
367
+ const byteLength = Number(BigInt(end.toString()) - BigInt(begin.toString()));
368
+ const elementSize = plan.container.elementSize;
369
+ const count = byteLength >= 0 && byteLength % elementSize === 0 ? byteLength / elementSize : -1;
370
+ const limit = Math.min(requestedLimit, plan.maxItems);
371
+ validations.push({ kind: 'vector_bounds', passed: count >= 0 && count <= plan.maxItems, count });
372
+ if (count >= 0 && count <= plan.maxItems) {
373
+ for (let index = 0; index < Math.min(count, limit); index++) {
374
+ const itemAddress = begin.add(index * elementSize);
375
+ const item = {};
376
+ for (const field of plan.container.fields || []) item[field.name] = readField(itemAddress, field, plan.maxStringBytes);
377
+ result.items.push({ index, address: itemAddress.toString(), fields: item });
378
+ }
379
+ }
380
+ }
381
+ result.allPassed = validations.length > 0 && validations.every(item => item.passed === true);
382
+ return result;
383
+ } catch (error) {
384
+ return { dataSourceId: plan.dataSourceId, buildKey: plan.buildKey, moduleName: module.name,
385
+ moduleBase: module.base.toString(), rootRva: plan.root.rva, allPassed: false, error: String(error) };
386
+ }
387
+ }
388
+ };
391
389
  `;
392
390
  export class WowAnalysisService {
393
391
  executor;
394
- profileRoot;
395
- packagedProfiles;
396
- buildBundles;
392
+ artifactDir;
397
393
  moduleName;
398
394
  options;
399
395
  sessions = new Map();
@@ -404,9 +400,7 @@ export class WowAnalysisService {
404
400
  if (!options || typeof options.executor?.execute !== "function")
405
401
  throw new TypeError("executor is required");
406
402
  this.executor = options.executor;
407
- this.profileRoot = resolve(requiredString(options.profileRoot ?? options.artifactDir, "profileRoot"));
408
- this.packagedProfiles = options.profileRoot !== undefined;
409
- this.buildBundles = this.packagedProfiles ? new BuildBundleStore(this.profileRoot) : undefined;
403
+ this.artifactDir = resolve(requiredString(options.artifactDir, "artifactDir"));
410
404
  this.moduleName = options.moduleName ?? WOW_ANALYSIS_MODULE;
411
405
  this.options = options;
412
406
  }
@@ -432,17 +426,17 @@ export class WowAnalysisService {
432
426
  if (this.luaTrace)
433
427
  throw new Error("Lua trace is already loaded; stop it before starting another trace");
434
428
  const request = this.normalizeLuaStart(input);
435
- const catalog = await this.loadLuaCatalog(input.buildKey);
436
429
  const target = await this.acquireTarget(input.buildKey, input.pid);
437
430
  let scriptId;
438
431
  try {
439
432
  const source = await this.luaSource();
440
- const loaded = await this.executor.execute(brokerManagedCommand({
433
+ const loaded = await this.executor.execute({
441
434
  operation: "script_load",
442
435
  sessionId: target.sessionId,
443
436
  source
444
- }, "wow.lua_trace.script_load"), target.context);
437
+ }, target.context);
445
438
  scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
439
+ const catalog = await this.loadLuaCatalog(input.buildKey);
446
440
  const configured = await this.callScript(scriptId, "configureLuaTrace", [catalog]);
447
441
  const started = await this.callScript(scriptId, "wowLuaTraceStart", [{ ...request, pid: target.pid }]);
448
442
  const value = isRecord(started.value) ? started.value : {};
@@ -483,11 +477,11 @@ export class WowAnalysisService {
483
477
  const target = await this.acquireTarget(input.buildKey, input.pid);
484
478
  let scriptId;
485
479
  try {
486
- const loaded = await this.executor.execute(brokerManagedCommand({
480
+ const loaded = await this.executor.execute({
487
481
  operation: "script_load",
488
482
  sessionId: target.sessionId,
489
483
  source: this.options.cppTraceSource ?? CPP_TRACE_SOURCE
490
- }, "wow.cpp_trace.script_load"), target.context);
484
+ }, target.context);
491
485
  scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
492
486
  const configured = await this.callScript(scriptId, "configureCppTrace", [catalog]);
493
487
  const started = await this.callScript(scriptId, "wowCppTraceStart", [{ ...request, pid: target.pid }]);
@@ -566,11 +560,11 @@ export class WowAnalysisService {
566
560
  const target = await this.acquireTarget(buildKey, input.pid);
567
561
  let scriptId;
568
562
  try {
569
- const loaded = await this.executor.execute(brokerManagedCommand({
563
+ const loaded = await this.executor.execute({
570
564
  operation: "script_load",
571
565
  sessionId: target.sessionId,
572
566
  source: this.options.dataReaderSource ?? DATA_READER_SOURCE
573
- }, "wow.data_reader.script_load"), target.context);
567
+ }, target.context);
574
568
  scriptId = requiredString(loaded.scriptId, "script_load.scriptId");
575
569
  const result = await this.callScript(scriptId, "readDataSource", [plan, boundedInteger(input.limit, plan.maxItems, 1, plan.maxItems, "limit")]);
576
570
  const value = isRecord(result.value) ? result.value : { allPassed: false, error: "reader returned a non-object result" };
@@ -599,9 +593,6 @@ export class WowAnalysisService {
599
593
  async wowBuildProfileValidate(input) {
600
594
  const buildKey = requiredString(input.buildKey, "buildKey");
601
595
  const profile = await this.readBuildProfile(buildKey);
602
- const signatureSource = this.packagedProfiles
603
- ? await this.buildBundles.readJson(buildKey, "signatures.json")
604
- : profile;
605
596
  const target = await this.acquireTarget(buildKey, input.pid);
606
597
  const checks = [];
607
598
  try {
@@ -615,7 +606,7 @@ export class WowAnalysisService {
615
606
  checks.push({ name: "module_name", expected: profileModule, actual: target.moduleName, passed: profileModule.toLowerCase() === target.moduleName.toLowerCase() });
616
607
  checks.push({ name: "runtime_base_dynamic", actual: target.moduleBase, passed: /^0x[0-9a-f]+$/i.test(target.moduleBase) });
617
608
  checks.push({ name: "image_base", expected: WOW_IDA_IMAGE_BASE, actual: profile.imageBase, passed: normalizeHex(profile.imageBase ?? WOW_IDA_IMAGE_BASE, "imageBase") === WOW_IDA_IMAGE_BASE });
618
- const signatures = this.signatureRecords(signatureSource).slice(0, boundedInteger(input.maxChecks, 128, 1, MAX_SIGNATURE_CHECKS, "maxChecks"));
609
+ const signatures = this.signatureRecords(profile).slice(0, boundedInteger(input.maxChecks, 128, 1, MAX_SIGNATURE_CHECKS, "maxChecks"));
619
610
  if (signatures.length === 0) {
620
611
  checks.push({ name: "signature_evidence_present", passed: false, error: "build profile contains no verifiable entry signatures" });
621
612
  }
@@ -656,7 +647,7 @@ export class WowAnalysisService {
656
647
  const suffix = buildSuffix(buildKey);
657
648
  const artifacts = [];
658
649
  for (const spec of REQUIRED_ARTIFACTS) {
659
- const path = join(this.profileRoot, spec.file(suffix));
650
+ const path = join(this.artifactDir, spec.file(suffix));
660
651
  try {
661
652
  const data = await readFile(path);
662
653
  let parsed;
@@ -699,7 +690,7 @@ export class WowAnalysisService {
699
690
  const nonLuaRecords = recordsFrom(nonLua, ["systems", "records"]);
700
691
  return {
701
692
  buildKey,
702
- profileRoot: this.profileRoot,
693
+ artifactDir: this.artifactDir,
703
694
  generatedAt: new Date().toISOString(),
704
695
  requiredArtifacts: artifacts.length,
705
696
  presentArtifacts: artifacts.filter(item => item.exists === true).length,
@@ -890,10 +881,10 @@ export class WowAnalysisService {
890
881
  return readFile(path, "utf8");
891
882
  }
892
883
  async callScript(scriptId, exportName, args = []) {
893
- return this.executor.execute(brokerManagedCommand({ operation: "script_call", scriptId, exportName, args }, `wow.analysis.${exportName}`));
884
+ return this.executor.execute({ operation: "script_call", scriptId, exportName, args });
894
885
  }
895
886
  async unloadScript(scriptId) {
896
- await this.executor.execute(brokerManagedCommand({ operation: "script_unload", scriptId }, "wow.analysis.script_unload"));
887
+ await this.executor.execute({ operation: "script_unload", scriptId });
897
888
  }
898
889
  activeTrace(kind) {
899
890
  return kind === "lua" ? this.luaTrace : this.cppTrace;
@@ -978,27 +969,13 @@ export class WowAnalysisService {
978
969
  }
979
970
  }
980
971
  artifactPath(buildKey, stem) {
981
- if (!this.packagedProfiles)
982
- return join(this.profileRoot, `${stem}-${buildSuffix(buildKey)}.json`);
983
- const names = {
984
- "build-profile": "build-profile.json",
985
- "cpp-data-sources": "data-sources.json",
986
- "wow-lua-api-all-rva": "lua-targets.jsonl"
987
- };
988
- return names[stem];
972
+ return join(this.artifactDir, `${stem}-${buildSuffix(buildKey)}.json`);
989
973
  }
990
974
  async readArtifact(buildKey, stem) {
991
975
  const path = this.artifactPath(buildKey, stem);
992
- if (!path)
993
- throw new Error(`${stem} is not included in the packaged build bundle for ${buildKey}`);
994
976
  let value;
995
977
  try {
996
- const text = this.packagedProfiles
997
- ? await this.buildBundles.readText(buildKey, path)
998
- : await readFile(path, "utf8");
999
- value = path.endsWith(".jsonl")
1000
- ? { buildKey, records: text.split(/\r?\n/).filter(Boolean).map(line => JSON.parse(line)) }
1001
- : JSON.parse(text);
978
+ value = JSON.parse(await readFile(path, "utf8"));
1002
979
  }
1003
980
  catch (error) {
1004
981
  throw new Error(`failed to read ${basename(path)}: ${errorText(error)}`);
@@ -1011,8 +988,6 @@ export class WowAnalysisService {
1011
988
  return value;
1012
989
  }
1013
990
  async readArtifactOptional(buildKey, stem) {
1014
- if (this.packagedProfiles && !this.artifactPath(buildKey, stem))
1015
- return undefined;
1016
991
  try {
1017
992
  return await this.readArtifact(buildKey, stem);
1018
993
  }
package/package.json CHANGED
@@ -1,37 +1,21 @@
1
1
  {
2
2
  "name": "wowdump",
3
- "version": "0.2.0",
4
- "description": "Build-aware World of Warcraft runtime analysis MCP server backed by Frida",
5
- "type": "module",
6
- "bin": {
7
- "wowdump": "dist/mcp-main.js"
8
- },
9
- "files": [
10
- "dist/**",
11
- "!dist/main.js",
12
- "resources/**",
13
- "README.md",
14
- "LICENSE"
15
- ],
3
+ "version": "0.3.1",
4
+ "description": "WoW native memory analysis CLI with Ghidra, Frida validation, and an elevated reader broker",
16
5
  "license": "MIT",
17
- "author": "Follenfang",
18
6
  "repository": {
19
7
  "type": "git",
20
8
  "url": "git+https://github.com/Follen/wowdump.git"
21
9
  },
22
- "bugs": {
23
- "url": "https://github.com/Follen/wowdump/issues"
24
- },
25
- "homepage": "https://github.com/Follen/wowdump#readme",
26
- "keywords": [
27
- "mcp",
28
- "frida",
29
- "world-of-warcraft",
30
- "wow",
31
- "runtime-analysis"
10
+ "files": [
11
+ "dist",
12
+ "skills",
13
+ "README.md",
14
+ "LICENSE"
32
15
  ],
33
- "publishConfig": {
34
- "access": "public"
16
+ "type": "module",
17
+ "bin": {
18
+ "wowdump": "dist/cli.js"
35
19
  },
36
20
  "engines": {
37
21
  "node": ">=22"
@@ -40,19 +24,18 @@
40
24
  "build": "npm run build:host && npm run build:agent",
41
25
  "build:host": "tsc -p tsconfig.json",
42
26
  "build:agent": "frida-compile agent/index.ts -o dist/agent.js",
43
- "bundle:build": "node scripts/build-build-bundle.mjs --analysis-root analyze/vm --output-root resources/builds",
44
- "bundle:verify": "node scripts/verify-build-bundle.mjs resources/builds/retail/12.0.7.68974",
45
- "package:verify": "node scripts/verify-package.mjs",
46
- "release:verify": "node scripts/verify-release-workflow.mjs verify",
47
- "mcp": "node dist/mcp-main.js",
48
- "test": "npm run build:host && node --test tests/*.test.mjs && npm run release:verify",
49
- "typecheck": "tsc --noEmit -p tsconfig.json",
50
- "prepack": "npm run build && npm run bundle:verify"
27
+ "wowdump": "node dist/cli.js",
28
+ "postinstall": "npm run build:host --if-present && node dist/toolchain.js --postinstall",
29
+ "start": "node dist/main.js",
30
+ "discover": "node dist/main.js --discover",
31
+ "dry-run": "node dist/main.js --dry-run",
32
+ "test": "npm run build:host && node --test tests/*.test.mjs",
33
+ "typecheck": "tsc --noEmit -p tsconfig.json"
51
34
  },
52
35
  "dependencies": {
53
- "@modelcontextprotocol/server": "2.0.0",
54
36
  "commander": "^13.1.0",
55
37
  "frida": "^17.2.0",
38
+ "koffi": "^2.16.3",
56
39
  "pino": "^9.6.0",
57
40
  "zod": "^3.24.2"
58
41
  },