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/toolchain.js CHANGED
@@ -1,117 +1,55 @@
1
1
  import { createHash } from "node:crypto";
2
- import { execFile as execFileCallback } from "node:child_process";
3
- import { constants, createReadStream, createWriteStream, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
- import { access, cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
5
- import { homedir, tmpdir } from "node:os";
6
- import { basename, dirname, join, resolve } from "node:path";
7
- import { pathToFileURL } from "node:url";
2
+ import { createReadStream, readdirSync, statSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, extname, join, resolve } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
7
  import { pipeline } from "node:stream/promises";
9
- import { promisify } from "node:util";
10
- const execFile = promisify(execFileCallback);
11
- export const WOWDUMP_HOME_DIRECTORIES = [
12
- "profiles",
13
- "logs",
14
- "runtime",
15
- "cache",
16
- "skills",
17
- "monitors",
18
- "toolchains"
19
- ];
20
- export const PINNED_TOOLCHAINS = {
21
- java: {
22
- name: "Temurin JDK",
23
- version: "21.0.8+9",
24
- directory: "temurin-21.0.8+9",
25
- windowsUrl: "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.8%2B9/OpenJDK21U-jdk_x64_windows_hotspot_21.0.8_9.zip"
26
- },
27
- ghidra: {
28
- name: "Ghidra",
29
- version: "11.3.2",
30
- directory: "ghidra-11.3.2",
31
- windowsUrl: "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.3.2_build/ghidra_11.3.2_PUBLIC_20250415.zip"
32
- }
33
- };
34
- export const DEFAULT_WOWDUMP_SKILL = `---
35
- name: wowdump
36
- description: 使用 wowdump CLI 进行 WoW 原生内存分析和静态取证。
37
- ---
38
-
39
- # wowdump
40
-
41
- 使用唯一入口 \`wowdump\` 按依赖顺序完成:静态发现候选 RVA;经确认后用 Frida 导出运行时基址和字节验证;验证通过后落盘为 \`reader_ready\` profile,后续只用 reader。
42
-
43
- 所有结论都要保留 build、文件哈希、模块基址、RVA、命令和证据来源。详细命令、参数和输出字段见同目录的 \`commands.md\`。
8
+ export const WOWDUMP_HOME_DIRECTORIES = ["profiles", "logs", "runtime", "cache", "skills", "monitors", "toolchains"];
9
+ export const DEFAULT_WOWDUMP_SKILL = `---
10
+ name: wowdump
11
+ description: 使用 Frida 运行时证据和 IDA/iced-x86 定位 WoW 字段,生成 Reader 可用 profile。
12
+ ---
13
+
14
+ # wowdump
15
+
16
+ 查字段时先读取 targets,再用显式 GumJS 导出运行时线索。静态定位优先交给可用的 IDA Pro MCP;没有时运行 \`wowdump analyze disassemble\`,由 Agent 根据 iced-x86 输出分析并补齐字段定义。两条路径都必须最终生成 \`wowdump.profile.v1\`,且只有地址、类型和证据齐全时才标记 \`reader_ready\`。详情见 references/workflow.md、references/dynamic.md、references/disassemble.md 和 references/profiles.md。
44
17
  `;
45
- export const DEFAULT_WOWDUMP_COMMANDS = `# wowdump 命令参考
46
-
47
- ## 初始化和工具链
48
-
49
- \`wowdump init\` 初始化运行目录并探测 JDK/Ghidra。缺少默认依赖时,npm 安装阶段会下载到 \`WOWDUMP_HOME/toolchains\`;也可以用 \`--skip-toolchain-download\` 只探测本机安装。
50
-
51
- 默认目录:运行数据为 \`~/.wowdump\`,Skill \`~/.agents/skills/wowdump\`。可用 \`WOWDUMP_HOME\` \`WOWDUMP_SKILL_HOME\` 覆盖。
52
-
53
- ## 1. 静态发现
54
-
55
- 命令:wowdump analyze static --exe "C:\\path\\Wow.exe" --build "retail@VERSION"
56
-
57
- 输出候选 profile,包含模块 imageBase、段范围、函数/字符串/交叉引用和候选 RVA;结果 confidence 为 candidate。
58
-
59
- ## 2. Frida 运行时导出
60
-
61
- 命令:wowdump analyze runtime-export --pid 1234 --build "retail@VERSION" --static-profile .wowdump\\profiles\\static.json --kind providers --confirm
62
-
63
- Frida 只负责一次性验证 moduleBase 和静态 profile 指定 RVA 的现场字节/事件,并记录 runtimeAddress = moduleBase + rva;完成后卸载脚本并断开。
64
-
65
- ## 3. 验证并落盘
66
-
67
- 命令:wowdump analyze verify --static-profile .wowdump\\profiles\\static.json --runtime-export .wowdump\\runtime\\export.json --output .wowdump\\profiles\\verified.json
68
-
69
- 只有检查通过的记录才可标为 reader_ready;不匹配项原样保留。
70
-
71
- ## 4. 后续只读 reader
72
-
73
- 命令:wowdump target;wowdump profiles;wowdump memory read --pid 1234 --profile reader-ready;wowdump memory watch start --pid 1234 --profile reader-ready;wowdump memory watch poll --id WATCH_ID;wowdump memory watch stop --id WATCH_ID
74
-
75
- Windows 首次读取会由 Node 自动请求 UAC 并启动 broker;后续调用复用 broker、句柄和监控状态。broker 仅监听本机 loopback,20 分钟无请求后退出。
76
-
77
- Frida 导出必须绑定 PID、build、目标、限制和清理期限,并显式传 \`--confirm\`。写操作和失败结果都以结构化 JSON 返回;candidate/unverified 不能当作已确认偏移。
18
+ export const DEFAULT_WOWDUMP_COMMANDS = `# wowdump 命令参考
19
+
20
+ 1. \`wowdump targets\`
21
+ 2. \`wowdump analyze runtime --pid <pid> --build <buildKey> --kind text --confirm\`
22
+ 3. \`wowdump analyze dynamic --pid <pid> --build <buildKey> --script <GumJS> --export collect --confirm\`
23
+ 4. \`wowdump analyze disassemble --exe <Wow.exe> --build <buildKey> --runtime-export <runtime.json> --output <profile.json>\`
24
+ 5. \`wowdump memory read --pid <pid> --build <buildKey> --profile <profile.json> --field <name>\`
25
+
26
+ Agent 能调用 IDA Pro MCP,先用 MCP 分析 runtime text,再把结果保存为 JSON 并通过 \`--ida-evidence\` 交给 disassemble 命令。未确认的候选 profile 放在 runtime 目录;确认后才复制到当前 build 的 profile 目录。
78
27
  `;
79
- function normalizePath(value) {
80
- return resolve(value.trim().replace(/^"|"$/g, ""));
81
- }
82
- function pathExists(value) {
83
- try {
84
- return statSync(value).isFile();
85
- }
86
- catch {
87
- return false;
88
- }
89
- }
90
- function directoryExists(value) {
91
- try {
92
- return statSync(value).isDirectory();
93
- }
94
- catch {
95
- return false;
96
- }
97
- }
98
- export function resolveWowdumpHome(env = process.env, userHome = homedir()) {
99
- return normalizePath(env.WOWDUMP_HOME || join(userHome, ".wowdump"));
100
- }
101
- export function resolveWowdumpSkillHome(env = process.env, userHome = homedir()) {
102
- return normalizePath(env.WOWDUMP_SKILL_HOME || join(userHome, ".agents", "skills"));
103
- }
104
- async function createFileIfMissing(file, contents, created, preserved) {
105
- try {
106
- await writeFile(file, contents, { encoding: "utf8", flag: "wx", mode: 0o600 });
107
- created.push(file);
108
- }
109
- catch (error) {
110
- if (error.code !== "EEXIST")
111
- throw error;
28
+ function normalizePath(value) { return resolve(value.trim().replace(/^"|"$/g, "")); }
29
+ function directoryExists(value) { try {
30
+ return statSync(value).isDirectory();
31
+ }
32
+ catch {
33
+ return false;
34
+ } }
35
+ function fileExists(value) { try {
36
+ return statSync(value).isFile();
37
+ }
38
+ catch {
39
+ return false;
40
+ } }
41
+ async function createFileIfMissing(file, contents, created, preserved) { try {
42
+ await writeFile(file, contents, { encoding: "utf8", flag: "wx", mode: 0o600 });
43
+ created.push(file);
44
+ }
45
+ catch (error) {
46
+ if (error.code === "EEXIST")
112
47
  preserved.push(file);
113
- }
114
- }
48
+ else
49
+ throw error;
50
+ } }
51
+ export function resolveWowdumpHome(env = process.env, userHome = homedir()) { return normalizePath(env.WOWDUMP_HOME || join(userHome, ".wowdump")); }
52
+ export function resolveWowdumpSkillHome(env = process.env, userHome = homedir()) { return normalizePath(env.WOWDUMP_SKILL_HOME || join(userHome, ".agents", "skills")); }
115
53
  export async function initializeWowdumpHome(options = {}) {
116
54
  const env = options.env ?? process.env;
117
55
  const home = normalizePath(options.home ?? resolveWowdumpHome(env, options.userHome));
@@ -119,476 +57,67 @@ export async function initializeWowdumpHome(options = {}) {
119
57
  const preserved = [];
120
58
  await mkdir(home, { recursive: true });
121
59
  for (const name of WOWDUMP_HOME_DIRECTORIES) {
122
- const directory = join(home, name);
123
- if (directoryExists(directory))
124
- preserved.push(directory);
60
+ const dir = join(home, name);
61
+ if (directoryExists(dir))
62
+ preserved.push(dir);
125
63
  else {
126
- await mkdir(directory, { recursive: true });
127
- created.push(directory);
64
+ await mkdir(dir, { recursive: true });
65
+ created.push(dir);
128
66
  }
129
67
  }
130
68
  const configFile = join(home, "config.json");
131
- await createFileIfMissing(configFile, `${JSON.stringify({ schema: "wowdump.config.v1", toolchains: {} }, null, 2)}\n`, created, preserved);
132
- const skillRoot = normalizePath(options.skillHome ?? resolveWowdumpSkillHome(env, options.userHome));
133
- const skillDirectory = join(skillRoot, "wowdump");
134
- if (!directoryExists(skillDirectory)) {
69
+ await createFileIfMissing(configFile, `${JSON.stringify({ schema: "wowdump.config.v1" }, null, 2)}\n`, created, preserved);
70
+ const skillDirectory = join(normalizePath(options.skillHome ?? resolveWowdumpSkillHome(env, options.userHome)), "wowdump");
71
+ if (directoryExists(skillDirectory))
72
+ preserved.push(skillDirectory);
73
+ else {
135
74
  await mkdir(skillDirectory, { recursive: true });
136
75
  created.push(skillDirectory);
137
76
  }
138
- else
139
- preserved.push(skillDirectory);
77
+ const bundledSkill = resolve(dirname(fileURLToPath(import.meta.url)), "..", "skills", "wowdump", "SKILL.md");
78
+ const skillSource = options.skillSource ? normalizePath(options.skillSource) : fileExists(bundledSkill) ? bundledSkill : undefined;
140
79
  const skillFile = join(skillDirectory, "SKILL.md");
141
- const skill = options.skillSource
142
- ? await readFile(normalizePath(options.skillSource), "utf8")
143
- : DEFAULT_WOWDUMP_SKILL;
144
- await createFileIfMissing(skillFile, skill.endsWith("\n") ? skill : `${skill}\n`, created, preserved);
145
- const commandsFile = join(skillDirectory, "commands.md");
146
- const commands = options.commandsSource
147
- ? await readFile(normalizePath(options.commandsSource), "utf8")
148
- : DEFAULT_WOWDUMP_COMMANDS;
149
- await createFileIfMissing(commandsFile, commands.endsWith("\n") ? commands : `${commands}\n`, created, preserved);
150
- return { home, created, preserved, configFile, skillFile, commandsFile };
151
- }
152
- function readConfig(file) {
153
- try {
154
- const value = JSON.parse(readFileSync(file, "utf8"));
155
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
156
- }
157
- catch {
158
- return {};
159
- }
160
- }
161
- function configuredTool(config, name) {
162
- const toolchains = config.toolchains;
163
- if (!toolchains || typeof toolchains !== "object" || Array.isArray(toolchains))
164
- return undefined;
165
- const value = toolchains[name];
166
- if (typeof value === "string" && value.trim())
167
- return value;
168
- if (value && typeof value === "object" && !Array.isArray(value)) {
169
- const path = value.path;
170
- return typeof path === "string" && path.trim() ? path : undefined;
171
- }
172
- return undefined;
173
- }
174
- function executableNames(base, platform) {
175
- if (platform !== "win32")
176
- return [base];
177
- return [base, `${base}.exe`, `${base}.cmd`, `${base}.bat`];
178
- }
179
- function pathCandidates(name, pathValue, platform) {
180
- if (!pathValue)
181
- return [];
182
- const separator = platform === "win32" ? ";" : ":";
183
- return pathValue.split(separator).filter(Boolean).flatMap(directory => executableNames(name, platform).map(executable => join(directory.replace(/^"|"$/g, ""), executable)));
184
- }
185
- function childDirectories(parent, prefix) {
186
- try {
187
- return readdirSync(parent, { withFileTypes: true })
188
- .filter(entry => entry.isDirectory() && prefix.test(entry.name))
189
- .map(entry => join(parent, entry.name))
190
- .sort((left, right) => right.localeCompare(left, undefined, { numeric: true }));
191
- }
192
- catch {
193
- return [];
194
- }
195
- }
196
- function javaExecutable(input, platform) {
197
- const value = normalizePath(input);
198
- if (pathExists(value))
199
- return value;
200
- for (const name of executableNames("java", platform)) {
201
- const candidate = join(value, "bin", name);
202
- if (pathExists(candidate))
203
- return candidate;
204
- }
205
- return undefined;
206
- }
207
- function ghidraExecutable(input, platform) {
208
- const value = normalizePath(input);
209
- if (pathExists(value) && /^analyzeHeadless(?:\.(?:bat|cmd|sh))?$/i.test(basename(value)))
210
- return value;
211
- const names = platform === "win32"
212
- ? ["analyzeHeadless.bat", "analyzeHeadless.cmd", "analyzeHeadless.exe", "analyzeHeadless"]
213
- : executableNames("analyzeHeadless", platform);
214
- for (const name of names) {
215
- for (const candidate of [join(value, "support", name), join(value, name)]) {
216
- if (pathExists(candidate))
217
- return candidate;
218
- }
219
- for (const child of childDirectories(value, /^ghidra[_-]/i)) {
220
- const candidate = join(child, "support", name);
221
- if (pathExists(candidate))
222
- return candidate;
223
- }
224
- }
225
- return undefined;
226
- }
227
- function unique(values) {
228
- const seen = new Set();
229
- const result = [];
230
- for (const item of values) {
231
- if (!item?.trim())
232
- continue;
233
- const value = normalizePath(item);
234
- const key = process.platform === "win32" ? value.toLowerCase() : value;
235
- if (!seen.has(key)) {
236
- seen.add(key);
237
- result.push(value);
238
- }
239
- }
240
- return result;
241
- }
242
- function resolvedTool(candidates, resolver) {
243
- const searched = [];
244
- for (const candidate of candidates) {
245
- if (!candidate.value?.trim())
246
- continue;
247
- const normalized = normalizePath(candidate.value);
248
- searched.push(normalized);
249
- const executable = resolver(normalized);
250
- if (executable) {
251
- const root = basename(dirname(executable)).toLowerCase() === "bin" || basename(dirname(executable)).toLowerCase() === "support"
252
- ? dirname(dirname(executable))
253
- : dirname(executable);
254
- return { tool: { executable, root, source: candidate.source }, searched: unique(searched) };
255
- }
256
- }
257
- return { tool: null, searched: unique(searched) };
258
- }
259
- function windowsRoots(env) {
260
- const user = env.USERPROFILE || homedir();
261
- const programFiles = [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA && join(env.LOCALAPPDATA, "Programs")].filter((value) => Boolean(value));
262
- return {
263
- java: programFiles.flatMap(root => [
264
- ...childDirectories(join(root, "Eclipse Adoptium"), /^(?:jdk|temurin)/i),
265
- ...childDirectories(join(root, "Java"), /^jdk/i),
266
- ...childDirectories(join(root, "Microsoft"), /^jdk/i)
267
- ]),
268
- ghidra: [
269
- ...programFiles.flatMap(root => childDirectories(root, /^ghidra/i)),
270
- ...childDirectories(user, /^ghidra/i),
271
- ...childDirectories(join(user, "tools"), /^ghidra/i)
272
- ]
273
- };
274
- }
275
- export function resolveToolchain(options = {}) {
276
- const env = options.env ?? process.env;
277
- const platform = options.platform ?? process.platform;
278
- const home = normalizePath(options.home ?? resolveWowdumpHome(env, options.userHome));
279
- const config = readConfig(normalizePath(options.configFile ?? join(home, "config.json")));
280
- const managedRoot = join(home, "toolchains");
281
- const standard = platform === "win32"
282
- ? windowsRoots(env)
283
- : {
284
- java: ["/usr/lib/jvm/default-java", "/usr/lib/jvm/java-21-openjdk", "/opt/java"],
285
- ghidra: ["/opt/ghidra", "/usr/local/ghidra", join(options.userHome ?? homedir(), "ghidra")]
286
- };
287
- const javaPathMatches = pathCandidates("java", options.path ?? env.PATH, platform);
288
- const ghidraPathMatches = pathCandidates("analyzeHeadless", options.path ?? env.PATH, platform);
289
- const javaCandidates = [
290
- { value: options.java ?? env.WOWDUMP_JAVA, source: "explicit" },
291
- { value: configuredTool(config, "java"), source: "config" },
292
- { value: env.JAVA_HOME, source: "system" },
293
- ...javaPathMatches.map(value => ({ value, source: "system" })),
294
- ...standard.java.map(value => ({ value, source: "system" })),
295
- ...childDirectories(managedRoot, /^(?:temurin|jdk|java)/i).map(value => ({ value, source: "managed" }))
296
- ];
297
- const ghidraCandidates = [
298
- { value: options.ghidra ?? env.WOWDUMP_GHIDRA, source: "explicit" },
299
- { value: configuredTool(config, "ghidra"), source: "config" },
300
- { value: env.GHIDRA_HOME ?? env.GHIDRA_INSTALL_DIR, source: "system" },
301
- ...ghidraPathMatches.map(value => ({ value, source: "system" })),
302
- ...standard.ghidra.map(value => ({ value, source: "system" })),
303
- ...childDirectories(managedRoot, /^ghidra/i).map(value => ({ value, source: "managed" }))
304
- ];
305
- const java = resolvedTool(javaCandidates, value => javaExecutable(value, platform));
306
- const ghidra = resolvedTool(ghidraCandidates, value => ghidraExecutable(value, platform));
307
- const missing = [];
308
- if (!java.tool)
309
- missing.push("java");
310
- if (!ghidra.tool)
311
- missing.push("ghidra");
312
- return {
313
- home,
314
- ready: missing.length === 0,
315
- java: java.tool,
316
- ghidra: ghidra.tool,
317
- missing,
318
- searched: { java: java.searched, ghidra: ghidra.searched }
319
- };
320
- }
321
- async function download(url, file) {
322
- const response = await fetch(url, { redirect: "follow" });
323
- if (!response.ok || !response.body)
324
- throw new Error(`download failed with HTTP ${response.status}: ${url}`);
325
- await pipeline(response.body, createWriteStream(file, { flags: "wx", mode: 0o600 }));
326
- }
327
- async function extractZip(archive, directory) {
328
- if (process.platform === "win32") {
329
- await execFile("powershell.exe", [
330
- "-NoProfile",
331
- "-NonInteractive",
332
- "-Command",
333
- "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force",
334
- archive,
335
- directory
336
- ], { windowsHide: true, maxBuffer: 4 * 1024 * 1024 });
337
- return;
338
- }
339
- await execFile("unzip", ["-q", archive, "-d", directory], { maxBuffer: 4 * 1024 * 1024 });
340
- }
341
- function firstDirectory(directory) {
342
- const entries = readdirSync(directory, { withFileTypes: true }).filter(entry => entry.isDirectory());
343
- return entries.length === 1 ? join(directory, entries[0].name) : directory;
344
- }
345
- export const defaultToolchainInstaller = async (input) => {
346
- if (process.platform !== "win32") {
347
- throw new Error(`managed ${input.component} installation currently requires Windows; configure an existing installation`);
348
- }
349
- if (directoryExists(input.destination))
350
- return;
351
- await mkdir(dirname(input.destination), { recursive: true });
352
- const staging = await mkdtemp(join(tmpdir(), `wowdump-${input.component}-`));
353
- const archive = join(staging, `${input.component}.zip`);
354
- const extracted = join(staging, "extracted");
355
- try {
356
- await download(input.url, archive);
357
- await mkdir(extracted, { recursive: true });
358
- await extractZip(archive, extracted);
359
- const source = firstDirectory(extracted);
360
- try {
361
- await rename(source, input.destination);
362
- }
363
- catch (error) {
364
- if (error.code === "EXDEV")
365
- await cp(source, input.destination, { recursive: true, errorOnExist: true });
366
- else if (error.code !== "EEXIST")
367
- throw error;
368
- }
369
- const manifest = join(input.destination, ".wowdump-toolchain.json");
370
- if (!existsSync(manifest)) {
371
- await writeFile(manifest, `${JSON.stringify({
372
- schema: "wowdump.toolchain.v1",
373
- component: input.component,
374
- version: input.version,
375
- source: input.url,
376
- installedAt: new Date().toISOString()
377
- }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
378
- }
379
- }
380
- finally {
381
- await rm(staging, { recursive: true, force: true });
382
- }
383
- };
384
- export async function bootstrapToolchain(options = {}) {
385
- const initialized = await initializeWowdumpHome(options);
386
- let toolchain = resolveToolchain({ ...options, home: initialized.home });
387
- const installed = [];
388
- if (options.installMissing === true && toolchain.missing.length > 0) {
389
- const installer = options.installer ?? defaultToolchainInstaller;
390
- for (const component of toolchain.missing) {
391
- const pinned = PINNED_TOOLCHAINS[component];
392
- const destination = join(initialized.home, "toolchains", pinned.directory);
393
- await installer({ component, destination, url: pinned.windowsUrl, version: pinned.version });
394
- installed.push(component);
395
- }
396
- toolchain = resolveToolchain({ ...options, home: initialized.home });
397
- }
398
- return { ...initialized, toolchain, installed };
399
- }
400
- export function ghidraHeadlessArguments(options) {
401
- const args = [
402
- normalizePath(options.projectDirectory),
403
- options.projectName,
404
- "-import",
405
- normalizePath(options.executable),
406
- "-analysisTimeoutPerFile",
407
- String(options.timeoutSeconds ?? 1800)
408
- ];
409
- if (options.overwrite !== false)
410
- args.push("-overwrite");
411
- if (options.scriptDirectory)
412
- args.push("-scriptPath", normalizePath(options.scriptDirectory));
413
- if (options.exporterScript) {
414
- args.push("-postScript", options.exporterScript);
415
- if (options.evidenceFile)
416
- args.push(normalizePath(options.evidenceFile));
417
- if (options.buildKey)
418
- args.push(options.buildKey);
419
- }
420
- return args;
421
- }
422
- export const defaultExternalCommandRunner = async (command) => {
423
- try {
424
- const result = await execFile(command.file, command.args, {
425
- cwd: command.cwd,
426
- env: command.env,
427
- windowsHide: true,
428
- maxBuffer: 64 * 1024 * 1024
429
- });
430
- return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
431
- }
432
- catch (error) {
433
- const failure = error;
434
- return {
435
- exitCode: typeof failure.code === "number" ? failure.code : 1,
436
- stdout: failure.stdout ?? "",
437
- stderr: failure.stderr ?? failure.message
438
- };
439
- }
440
- };
441
- export async function sha256File(file) {
442
- const hash = createHash("sha256");
443
- await pipeline(createReadStream(file), hash);
444
- return hash.digest("hex");
445
- }
446
- function hex(value) {
447
- return `0x${BigInt(value).toString(16)}`;
448
- }
449
- export function inspectPeSections(buffer) {
450
- if (buffer.length < 0x40 || buffer.toString("ascii", 0, 2) !== "MZ")
451
- return [];
452
- const peOffset = buffer.readUInt32LE(0x3c);
453
- if (peOffset + 24 > buffer.length || buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\0\0")
454
- return [];
455
- const sectionCount = buffer.readUInt16LE(peOffset + 6);
456
- const optionalSize = buffer.readUInt16LE(peOffset + 20);
457
- const table = peOffset + 24 + optionalSize;
458
- const sections = [];
459
- for (let index = 0; index < sectionCount; index += 1) {
460
- const offset = table + index * 40;
461
- if (offset + 40 > buffer.length)
462
- break;
463
- const name = buffer.subarray(offset, offset + 8).toString("ascii").replace(/\0+$/, "");
464
- sections.push({
465
- name,
466
- virtualSize: hex(buffer.readUInt32LE(offset + 8)),
467
- rva: hex(buffer.readUInt32LE(offset + 12)),
468
- rawSize: hex(buffer.readUInt32LE(offset + 16)),
469
- rawOffset: hex(buffer.readUInt32LE(offset + 20)),
470
- characteristics: hex(buffer.readUInt32LE(offset + 36))
471
- });
472
- }
473
- return sections;
474
- }
475
- function safeBuildName(buildKey) {
476
- const value = buildKey.trim().replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
477
- if (!value)
478
- throw new Error("buildKey must contain a filesystem-safe character");
479
- return value;
480
- }
481
- function record(value) {
482
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
483
- }
484
- function evidenceArray(evidence, ...keys) {
485
- for (const key of keys)
486
- if (Array.isArray(evidence[key]))
487
- return evidence[key];
488
- return [];
489
- }
490
- export async function runGhidraStaticAnalysis(options) {
491
- const executable = normalizePath(options.executable);
492
- await access(executable, constants.R_OK);
493
- if (!options.buildKey.trim())
494
- throw new Error("buildKey is required");
495
- const toolchain = resolveToolchain(options);
496
- if (!toolchain.ghidra || !toolchain.java) {
497
- throw new Error(`Ghidra toolchain is incomplete: missing ${toolchain.missing.join(", ")}; run npm install or configure --java/--ghidra`);
498
- }
499
- const suffix = safeBuildName(options.buildKey);
500
- const outputDirectory = normalizePath(options.outputDirectory ?? join(toolchain.home, "profiles", suffix));
501
- const projectDirectory = normalizePath(options.projectDirectory ?? join(toolchain.home, "cache", "ghidra", suffix));
502
- const projectName = options.projectName ?? `wowdump-${suffix}`;
503
- const evidenceFile = normalizePath(options.evidenceFile ?? join(outputDirectory, "ghidra-evidence.json"));
504
- const outputFile = join(outputDirectory, "profile.json");
505
- await mkdir(outputDirectory, { recursive: true });
506
- await mkdir(projectDirectory, { recursive: true });
507
- const args = ghidraHeadlessArguments({
508
- projectDirectory,
509
- projectName,
510
- executable,
511
- timeoutSeconds: options.timeoutSeconds,
512
- overwrite: options.overwrite,
513
- scriptDirectory: options.scriptDirectory,
514
- exporterScript: options.exporterScript,
515
- evidenceFile,
516
- buildKey: options.buildKey
517
- });
518
- const command = {
519
- file: toolchain.ghidra.executable,
520
- args,
521
- cwd: outputDirectory,
522
- env: { ...process.env, JAVA_HOME: toolchain.java.root }
523
- };
524
- let processResult;
525
- if (!options.dryRun) {
526
- processResult = await (options.runner ?? defaultExternalCommandRunner)(command);
527
- if (processResult.exitCode !== 0) {
528
- throw new Error(`Ghidra Headless exited with ${processResult.exitCode}: ${processResult.stderr.trim() || "no diagnostic"}`);
529
- }
530
- }
531
- let evidence = {};
532
- if (!options.dryRun && existsSync(evidenceFile))
533
- evidence = record(JSON.parse(await readFile(evidenceFile, "utf8")));
534
- const executableSha256 = await sha256File(executable);
535
- const sections = evidenceArray(evidence, "sections").length > 0
536
- ? evidenceArray(evidence, "sections")
537
- : inspectPeSections(await readFile(executable));
538
- const profile = {
539
- schema: "wowdump.profile.v1",
540
- buildKey: options.buildKey,
541
- executable: { path: executable, sha256: executableSha256, module: basename(executable) },
542
- sections,
543
- functions: evidenceArray(evidence, "functions"),
544
- strings: evidenceArray(evidence, "strings"),
545
- xrefs: evidenceArray(evidence, "xrefs", "crossReferences"),
546
- providers: evidenceArray(evidence, "providers", "providerMatches"),
547
- signatures: evidenceArray(evidence, "signatures", "signatureMatches"),
548
- analysis: {
549
- engine: "ghidra-headless",
550
- engineVersion: evidence.ghidraVersion ?? PINNED_TOOLCHAINS.ghidra.version,
551
- javaVersion: evidence.javaVersion ?? PINNED_TOOLCHAINS.java.version,
552
- generatedAt: new Date().toISOString(),
553
- command: { file: command.file, args: command.args },
554
- evidenceFile
555
- },
556
- confidence: "candidate"
557
- };
558
- if (!options.dryRun)
559
- await writeFile(outputFile, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
560
- return {
561
- ok: true,
562
- command: "analyze.static",
563
- dryRun: options.dryRun === true,
564
- buildKey: options.buildKey,
565
- executable,
566
- executableSha256,
567
- outputFile,
568
- commandLine: { file: command.file, args: command.args, cwd: outputDirectory },
569
- toolchain,
570
- profile,
571
- ...(processResult ? { process: processResult } : {})
572
- };
573
- }
574
- async function postinstall() {
575
- const installMissing = process.env.WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD !== "1";
576
- try {
577
- const result = await bootstrapToolchain({ installMissing });
578
- process.stdout.write(`${JSON.stringify({
579
- ok: result.toolchain.ready,
580
- command: "postinstall",
581
- home: result.home,
582
- installed: result.installed,
583
- missing: result.toolchain.missing,
584
- skillFile: result.skillFile
585
- })}\n`);
586
- }
587
- catch (error) {
588
- process.stderr.write(`wowdump postinstall: ${error instanceof Error ? error.message : String(error)}\n`);
589
- process.stderr.write("Set WOWDUMP_SKIP_TOOLCHAIN_DOWNLOAD=1 to initialize without downloading, then configure WOWDUMP_JAVA and WOWDUMP_GHIDRA.\n");
590
- }
591
- }
80
+ await createFileIfMissing(skillFile, `${(skillSource ? await readFile(skillSource, "utf8") : DEFAULT_WOWDUMP_SKILL).replace(/\n?$/, "\n")}`, created, preserved);
81
+ const referencesDirectory = join(skillDirectory, "references");
82
+ if (directoryExists(referencesDirectory))
83
+ preserved.push(referencesDirectory);
84
+ else {
85
+ await mkdir(referencesDirectory, { recursive: true });
86
+ created.push(referencesDirectory);
87
+ }
88
+ const commandsFile = join(referencesDirectory, "commands.md");
89
+ await createFileIfMissing(commandsFile, `${(options.commandsSource ? await readFile(normalizePath(options.commandsSource), "utf8") : DEFAULT_WOWDUMP_COMMANDS).replace(/\n?$/, "\n")}`, created, preserved);
90
+ const bundledReferences = resolve(dirname(fileURLToPath(import.meta.url)), "..", "skills", "wowdump", "references");
91
+ if (directoryExists(bundledReferences))
92
+ for (const entry of readdirSync(bundledReferences, { withFileTypes: true }))
93
+ if (entry.isFile() && extname(entry.name).toLowerCase() === ".md")
94
+ await createFileIfMissing(join(referencesDirectory, entry.name), await readFile(join(bundledReferences, entry.name), "utf8"), created, preserved);
95
+ const scriptsDirectory = join(skillDirectory, "scripts");
96
+ if (directoryExists(scriptsDirectory))
97
+ preserved.push(scriptsDirectory);
98
+ else {
99
+ await mkdir(scriptsDirectory, { recursive: true });
100
+ created.push(scriptsDirectory);
101
+ }
102
+ const bundledScripts = resolve(dirname(fileURLToPath(import.meta.url)), "..", "skills", "wowdump", "scripts");
103
+ if (directoryExists(bundledScripts))
104
+ for (const entry of readdirSync(bundledScripts, { withFileTypes: true }))
105
+ if (entry.isFile() && extname(entry.name).toLowerCase() === ".js")
106
+ await createFileIfMissing(join(scriptsDirectory, entry.name), await readFile(join(bundledScripts, entry.name), "utf8"), created, preserved);
107
+ return { home, created, preserved, configFile, skillFile, commandsFile, scriptsDirectory };
108
+ }
109
+ export async function bootstrapToolchain(options = {}) { const home = await initializeWowdumpHome(options); return { ...home, toolchain: { ready: true, disassembler: "iced-x86" }, installed: [] }; }
110
+ export function resolveToolchain(options = {}) { return { home: normalizePath(options.home ?? resolveWowdumpHome(options.env ?? process.env, options.userHome)), ready: true, disassembler: "iced-x86" }; }
111
+ export async function sha256File(file) { const hash = createHash("sha256"); await pipeline(createReadStream(file), hash); return hash.digest("hex"); }
112
+ export function inspectPeSections(buffer) { if (buffer.length < 0x40 || buffer.toString("ascii", 0, 2) !== "MZ")
113
+ return []; const pe = buffer.readUInt32LE(0x3c); if (pe + 24 > buffer.length || buffer.toString("ascii", pe, pe + 4) !== "PE\0\0")
114
+ return []; const count = buffer.readUInt16LE(pe + 6); const optional = buffer.readUInt16LE(pe + 20); const table = pe + 24 + optional; const out = []; for (let i = 0; i < count; i++) {
115
+ const off = table + i * 40;
116
+ if (off + 40 > buffer.length)
117
+ break;
118
+ out.push({ name: buffer.subarray(off, off + 8).toString("ascii").replace(/\0+$/, ""), virtualSize: `0x${buffer.readUInt32LE(off + 8).toString(16)}`, rva: `0x${buffer.readUInt32LE(off + 12).toString(16)}`, rawSize: `0x${buffer.readUInt32LE(off + 16).toString(16)}`, rawOffset: `0x${buffer.readUInt32LE(off + 20).toString(16)}` });
119
+ } return out; }
592
120
  const invokedFile = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : "";
593
- if (invokedFile === import.meta.url && process.argv.includes("--postinstall"))
594
- await postinstall();
121
+ if (invokedFile === import.meta.url && process.argv.includes("--postinstall")) {
122
+ await bootstrapToolchain();
123
+ }