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.
- package/LICENSE +21 -21
- package/README.md +21 -53
- package/dist/adapters/reader.js +33 -0
- package/dist/analysis/disassemble.js +77 -0
- package/dist/{frida-runtime.js → analysis/frida-runtime.js} +48 -48
- package/dist/analysis/runtime-script.js +36 -0
- package/dist/cli.js +255 -189
- package/dist/core/profile-engine.js +238 -0
- package/dist/frida-worker.js +54 -55
- package/dist/{reader-broker.js → reader/broker.js} +15 -0
- package/dist/{reader-client.js → reader/client.js} +1 -1
- package/dist/{windows-launcher.js → reader/launcher.js} +16 -4
- package/dist/reader/main.js +100 -0
- package/dist/reader/protocol.js +1 -0
- package/dist/reader/windows.js +242 -0
- package/dist/reader-main.js +1 -66
- package/dist/toolchain.js +102 -573
- package/package.json +9 -10
- package/skills/wowdump/SKILL.md +22 -15
- package/skills/wowdump/references/commands.md +63 -0
- package/skills/wowdump/references/disassemble.md +18 -0
- package/skills/wowdump/references/dynamic.md +54 -0
- package/skills/wowdump/references/evidence-workflow.md +41 -0
- package/skills/wowdump/references/profiles.md +34 -0
- package/skills/wowdump/references/request-schema.md +28 -0
- package/skills/wowdump/references/workflow.md +44 -0
- package/skills/wowdump/scripts/dynamic-session.js +133 -0
- package/dist/agent.js +0 -1332
- package/dist/discovery.js +0 -48
- package/dist/dry-run.js +0 -36
- package/dist/error-log.js +0 -71
- package/dist/focused-session.js +0 -89
- package/dist/ghidra.js +0 -769
- package/dist/main.js +0 -66
- package/dist/observability.js +0 -41
- package/dist/processes.js +0 -44
- package/dist/session.js +0 -42
- package/dist/storage.js +0 -12
- package/dist/windows-reader.js +0 -102
- package/dist/wow-analysis.js +0 -1405
- package/skills/wowdump/commands.md +0 -44
- /package/dist/{adapters.js → core/build-adapters.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
package/dist/discovery.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
3
|
-
function parseBuildInfo(file) {
|
|
4
|
-
if (!existsSync(file))
|
|
5
|
-
return [];
|
|
6
|
-
const lines = readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean);
|
|
7
|
-
if (lines.length < 2)
|
|
8
|
-
return [];
|
|
9
|
-
const headers = lines[0].split("|").map(header => header.split("!")[0]);
|
|
10
|
-
return lines.slice(1).map(line => {
|
|
11
|
-
const values = line.split("|");
|
|
12
|
-
return Object.fromEntries(headers.map((header, i) => [header, values[i] ?? ""]));
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
function flavorFromDirectory(name) {
|
|
16
|
-
return name.startsWith("_") && name.endsWith("_") ? name.slice(1, -1) : name;
|
|
17
|
-
}
|
|
18
|
-
export function discoverInstalls(gameRoot) {
|
|
19
|
-
const rootBuildRows = parseBuildInfo(join(gameRoot, ".build.info"));
|
|
20
|
-
let entries = [];
|
|
21
|
-
try {
|
|
22
|
-
entries = readdirSync(gameRoot, { withFileTypes: true })
|
|
23
|
-
.filter(entry => entry.isDirectory())
|
|
24
|
-
.map(entry => join(gameRoot, entry.name));
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
return entries.flatMap(root => {
|
|
30
|
-
const executable = join(root, "Wow.exe");
|
|
31
|
-
if (!existsSync(executable))
|
|
32
|
-
return [];
|
|
33
|
-
const flavor = flavorFromDirectory(basename(root));
|
|
34
|
-
const wantedProduct = flavor.includes("classic") || flavor.includes("anniversary")
|
|
35
|
-
? "wow_classic" : "wow";
|
|
36
|
-
const rootBuild = rootBuildRows.find(row => row.Product === wantedProduct) ?? rootBuildRows[0] ?? {};
|
|
37
|
-
const version = rootBuild.Version ?? rootBuild.ProductVersion ?? rootBuild.Build;
|
|
38
|
-
const fileVersion = rootBuild.ProductVersion;
|
|
39
|
-
const build = {
|
|
40
|
-
product: rootBuild.Product ?? rootBuild.ProductName,
|
|
41
|
-
version,
|
|
42
|
-
branch: rootBuild.Branch,
|
|
43
|
-
fileVersion,
|
|
44
|
-
buildKey: [flavor, version || fileVersion || "unknown"].filter(Boolean).join("@")
|
|
45
|
-
};
|
|
46
|
-
return [{ flavor, root, executable, build }];
|
|
47
|
-
}).sort((a, b) => a.flavor.localeCompare(b.flavor));
|
|
48
|
-
}
|
package/dist/dry-run.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { getObservationTiers } from "./observability.js";
|
|
2
|
-
function toAdapterFields(lookup) {
|
|
3
|
-
if (lookup.status === "verified" && lookup.adapter) {
|
|
4
|
-
return {
|
|
5
|
-
adapterStatus: lookup.status,
|
|
6
|
-
observationTiers: getObservationTiers(lookup.adapter),
|
|
7
|
-
adapterName: lookup.adapter.name
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
return {
|
|
11
|
-
adapterStatus: lookup.status,
|
|
12
|
-
observationTiers: [],
|
|
13
|
-
adapterReason: lookup.reason
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
function installStatus(install, registry) {
|
|
17
|
-
return {
|
|
18
|
-
flavor: install.flavor,
|
|
19
|
-
buildKey: install.build.buildKey,
|
|
20
|
-
...toAdapterFields(registry.lookup(install.build.buildKey))
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
export function createDryRunReport(installs, processes, registry) {
|
|
24
|
-
return {
|
|
25
|
-
mode: "dry-run",
|
|
26
|
-
installs: installs
|
|
27
|
-
.map(install => installStatus(install, registry))
|
|
28
|
-
.sort((a, b) => a.flavor.localeCompare(b.flavor)),
|
|
29
|
-
processes: processes
|
|
30
|
-
.map(process => ({
|
|
31
|
-
pid: process.pid,
|
|
32
|
-
...installStatus(process.install, registry)
|
|
33
|
-
}))
|
|
34
|
-
.sort((a, b) => a.pid - b.pid)
|
|
35
|
-
};
|
|
36
|
-
}
|
package/dist/error-log.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { ErrorEventSchema } from "./types.js";
|
|
3
|
-
const DEFAULT_LIMIT = 100;
|
|
4
|
-
const MAX_LIMIT = 1000;
|
|
5
|
-
function validateLimit(limit) {
|
|
6
|
-
const value = limit ?? DEFAULT_LIMIT;
|
|
7
|
-
if (!Number.isInteger(value) || value < 1 || value > MAX_LIMIT) {
|
|
8
|
-
throw new RangeError(`limit must be an integer between 1 and ${MAX_LIMIT}`);
|
|
9
|
-
}
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
function matches(event, query) {
|
|
13
|
-
return (query.pid === undefined || event.pid === query.pid)
|
|
14
|
-
&& (query.buildKey === undefined || event.buildKey === query.buildKey)
|
|
15
|
-
&& (query.flavor === undefined || event.flavor === query.flavor)
|
|
16
|
-
&& (query.addon === undefined || event.addon === query.addon);
|
|
17
|
-
}
|
|
18
|
-
function schemaMessage(error) {
|
|
19
|
-
return error.issues
|
|
20
|
-
.map(issue => `${issue.path.length > 0 ? issue.path.join(".") : "event"}: ${issue.message}`)
|
|
21
|
-
.join("; ");
|
|
22
|
-
}
|
|
23
|
-
export async function readErrorLog(file, query = {}) {
|
|
24
|
-
const limit = validateLimit(query.limit);
|
|
25
|
-
let contents;
|
|
26
|
-
try {
|
|
27
|
-
contents = await readFile(file, "utf8");
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
if (error.code === "ENOENT") {
|
|
31
|
-
return { events: [], diagnostics: [] };
|
|
32
|
-
}
|
|
33
|
-
throw error;
|
|
34
|
-
}
|
|
35
|
-
const diagnostics = [];
|
|
36
|
-
const matchesByTime = [];
|
|
37
|
-
for (const [index, rawLine] of contents.split(/\r?\n/u).entries()) {
|
|
38
|
-
if (rawLine.trim().length === 0)
|
|
39
|
-
continue;
|
|
40
|
-
const line = index + 1;
|
|
41
|
-
let value;
|
|
42
|
-
try {
|
|
43
|
-
value = JSON.parse(rawLine);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
diagnostics.push({
|
|
47
|
-
line,
|
|
48
|
-
code: "invalid_json",
|
|
49
|
-
message: error instanceof Error ? error.message : "Invalid JSON"
|
|
50
|
-
});
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const parsed = ErrorEventSchema.safeParse(value);
|
|
54
|
-
if (!parsed.success) {
|
|
55
|
-
diagnostics.push({
|
|
56
|
-
line,
|
|
57
|
-
code: "invalid_event",
|
|
58
|
-
message: schemaMessage(parsed.error)
|
|
59
|
-
});
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (matches(parsed.data, query)) {
|
|
63
|
-
matchesByTime.push({ event: parsed.data, line });
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
matchesByTime.sort((left, right) => left.event.timestamp - right.event.timestamp || left.line - right.line);
|
|
67
|
-
return {
|
|
68
|
-
events: matchesByTime.slice(-limit).map(item => item.event),
|
|
69
|
-
diagnostics
|
|
70
|
-
};
|
|
71
|
-
}
|
package/dist/focused-session.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
export const TargetKindSchema = z.enum(["lua_wrapper", "cpp_function", "data_source", "object", "address_range"]);
|
|
4
|
-
export const FocusSelectorSchema = z.object({
|
|
5
|
-
schema: z.literal("selector-schema.v1").default("selector-schema.v1"),
|
|
6
|
-
kind: TargetKindSchema,
|
|
7
|
-
apis: z.array(z.string().min(1)).optional(),
|
|
8
|
-
namespace: z.string().min(1).optional(),
|
|
9
|
-
glob: z.string().min(1).optional(),
|
|
10
|
-
rva: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
|
|
11
|
-
address: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
|
|
12
|
-
start: z.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
|
|
13
|
-
end: z.string().regex(/^0x[0-9a-fA-F]+$/).optional()
|
|
14
|
-
}).superRefine((v, ctx) => {
|
|
15
|
-
if (v.kind === "address_range" && (!v.start || !v.end))
|
|
16
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "address_range requires start and end" });
|
|
17
|
-
if (v.kind !== "address_range" && v.kind !== "object" && !v.apis?.length && !v.namespace && !v.glob && !v.rva && !v.address)
|
|
18
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "selector must identify a target" });
|
|
19
|
-
});
|
|
20
|
-
export const validateSelector = (input) => FocusSelectorSchema.parse(input);
|
|
21
|
-
/** Canonical byte representation used for selector identity and replay checks. */
|
|
22
|
-
export const canonicalizeSelector = (input) => {
|
|
23
|
-
const value = validateSelector(input);
|
|
24
|
-
const ordered = {};
|
|
25
|
-
for (const key of ["schema", "kind", "apis", "namespace", "glob", "rva", "address", "start", "end"]) {
|
|
26
|
-
if (value[key] !== undefined)
|
|
27
|
-
ordered[key] = Array.isArray(value[key]) ? [...value[key]].sort() : value[key];
|
|
28
|
-
}
|
|
29
|
-
return JSON.stringify(ordered);
|
|
30
|
-
};
|
|
31
|
-
const systemClock = { now: () => Date.now() };
|
|
32
|
-
export class FocusedSession {
|
|
33
|
-
state = "created";
|
|
34
|
-
seq = 0;
|
|
35
|
-
bytes = 0;
|
|
36
|
-
hooks = [];
|
|
37
|
-
depth = new Map();
|
|
38
|
-
dropped = 0;
|
|
39
|
-
startedAt = 0;
|
|
40
|
-
stoppedAt;
|
|
41
|
-
cleanupStatus = { hooks: 0, scripts: 0, interceptors: 0 };
|
|
42
|
-
options;
|
|
43
|
-
constructor(options) { validateSelector(options.target); if (!Number.isInteger(options.pid) || options.pid <= 0)
|
|
44
|
-
throw new Error("pid must be positive"); this.options = options; }
|
|
45
|
-
get status() { return this.state; }
|
|
46
|
-
get manifest() { return { schema: "wow.focus-session.v1", sessionId: this.options.sessionId, pid: this.options.pid, buildKey: this.options.buildKey, moduleBase: this.options.moduleBase, imageBase: this.options.imageBase, target: this.options.target, environment: this.options.environment ?? {}, triggerPlan: this.options.triggerPlan ?? {}, startTime: this.startedAt ? new Date(this.startedAt).toISOString() : null, stopTime: this.stoppedAt ? new Date(this.stoppedAt).toISOString() : null, hookIds: this.hooks, droppedEventCount: this.dropped, cleanupStatus: this.cleanupStatus, state: this.state }; }
|
|
47
|
-
async start() { if (this.state !== "created")
|
|
48
|
-
throw new Error("session already started"); await this.options.transport.revalidateIdentity(this.identity()); this.hooks = await this.options.transport.installHooks(this.options.target); this.startedAt = this.clock.now(); this.state = "running"; await this.writeManifest(); return this.manifest; }
|
|
49
|
-
async pause() { await this.revalidate(); if (this.state !== "running")
|
|
50
|
-
throw new Error("session is not running"); this.state = "paused"; await this.writeManifest(); }
|
|
51
|
-
async resume() { await this.revalidate(); if (this.state !== "paused")
|
|
52
|
-
throw new Error("session is not paused"); this.state = "running"; await this.writeManifest(); }
|
|
53
|
-
async checkpoint(label = "checkpoint") { await this.revalidate(); await this.writeManifest({ checkpoint: label, seq: this.seq }); return this.manifest; }
|
|
54
|
-
async emit(input) {
|
|
55
|
-
if (this.state !== "running") {
|
|
56
|
-
this.dropped++;
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
if (this.options.sampling !== undefined && this.options.sampling < 1 && Math.random() > this.options.sampling) {
|
|
60
|
-
this.dropped++;
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
const event = { ...input, seq: this.seq + 1, sessionId: this.options.sessionId, buildKey: this.options.buildKey, pid: this.options.pid, moduleBase: this.options.moduleBase, environment: this.options.environment ?? {}, event: { ...input.event, timestamp: new Date(this.clock.now()).toISOString(), depth: input.event.depth ?? 0 } };
|
|
64
|
-
const size = Buffer.byteLength(JSON.stringify(event));
|
|
65
|
-
if ((this.options.maxEvents && this.seq >= this.options.maxEvents) || (this.options.maxBytes && this.bytes + size > this.options.maxBytes)) {
|
|
66
|
-
this.dropped++;
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
this.seq++;
|
|
70
|
-
this.bytes += size;
|
|
71
|
-
await this.options.eventSink.append(event);
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
async enter(threadId, input) { const d = (this.depth.get(threadId) ?? 0) + 1; this.depth.set(threadId, d); return this.emit({ ...input, event: { kind: "enter", threadId, depth: d } }); }
|
|
75
|
-
async leave(threadId, input) { const d = this.depth.get(threadId) ?? 1; const ok = await this.emit({ ...input, event: { kind: "leave", threadId, depth: d } }); if (d <= 1)
|
|
76
|
-
this.depth.delete(threadId);
|
|
77
|
-
else
|
|
78
|
-
this.depth.set(threadId, d - 1); return ok; }
|
|
79
|
-
async stop() { if (this.state === "stopped")
|
|
80
|
-
return this.manifest; this.state = "stopped"; this.stoppedAt = this.clock.now(); this.cleanupStatus = await this.options.transport.cleanup(this.hooks); await this.options.transport.detach?.(); await this.writeManifest(); return this.manifest; }
|
|
81
|
-
get clock() { return this.options.clock ?? systemClock; }
|
|
82
|
-
identity() { return { pid: this.options.pid, buildKey: this.options.buildKey, moduleBase: this.options.moduleBase, imageBase: this.options.imageBase }; }
|
|
83
|
-
async revalidate() { const actual = await this.options.transport.revalidateIdentity(this.identity()); if (actual.pid !== this.options.pid || actual.buildKey !== this.options.buildKey || actual.moduleBase.toLowerCase() !== this.options.moduleBase.toLowerCase()) {
|
|
84
|
-
this.state = "failed";
|
|
85
|
-
throw new Error("TARGET_IDENTITY_CHANGED");
|
|
86
|
-
} }
|
|
87
|
-
async writeManifest(extra = {}) { if (this.options.manifestSink)
|
|
88
|
-
await this.options.manifestSink.write({ ...this.manifest, ...extra, manifestHash: createHash("sha256").update(JSON.stringify(this.manifest)).digest("hex") }); }
|
|
89
|
-
}
|