windows-exe-decompiler-mcp-server 0.1.0

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 (190) hide show
  1. package/CODEX_INSTALLATION.md +69 -0
  2. package/COPILOT_INSTALLATION.md +77 -0
  3. package/LICENSE +21 -0
  4. package/README.md +314 -0
  5. package/bin/windows-exe-decompiler-mcp-server.js +3 -0
  6. package/dist/analysis-provenance.d.ts +184 -0
  7. package/dist/analysis-provenance.js +74 -0
  8. package/dist/analysis-task-runner.d.ts +31 -0
  9. package/dist/analysis-task-runner.js +160 -0
  10. package/dist/artifact-inventory.d.ts +23 -0
  11. package/dist/artifact-inventory.js +175 -0
  12. package/dist/cache-manager.d.ts +128 -0
  13. package/dist/cache-manager.js +454 -0
  14. package/dist/confidence-semantics.d.ts +66 -0
  15. package/dist/confidence-semantics.js +122 -0
  16. package/dist/config.d.ts +335 -0
  17. package/dist/config.js +193 -0
  18. package/dist/database.d.ts +227 -0
  19. package/dist/database.js +601 -0
  20. package/dist/decompiler-worker.d.ts +441 -0
  21. package/dist/decompiler-worker.js +1962 -0
  22. package/dist/dynamic-trace.d.ts +95 -0
  23. package/dist/dynamic-trace.js +629 -0
  24. package/dist/env-validator.d.ts +15 -0
  25. package/dist/env-validator.js +249 -0
  26. package/dist/error-handler.d.ts +28 -0
  27. package/dist/error-handler.example.d.ts +22 -0
  28. package/dist/error-handler.example.js +141 -0
  29. package/dist/error-handler.js +139 -0
  30. package/dist/ghidra-analysis-status.d.ts +49 -0
  31. package/dist/ghidra-analysis-status.js +178 -0
  32. package/dist/ghidra-config.d.ts +134 -0
  33. package/dist/ghidra-config.js +464 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +200 -0
  36. package/dist/job-queue.d.ts +169 -0
  37. package/dist/job-queue.js +407 -0
  38. package/dist/logger.d.ts +106 -0
  39. package/dist/logger.js +176 -0
  40. package/dist/policy-guard.d.ts +115 -0
  41. package/dist/policy-guard.js +243 -0
  42. package/dist/process-output.d.ts +15 -0
  43. package/dist/process-output.js +90 -0
  44. package/dist/prompts/function-explanation-review.d.ts +5 -0
  45. package/dist/prompts/function-explanation-review.js +64 -0
  46. package/dist/prompts/semantic-name-review.d.ts +5 -0
  47. package/dist/prompts/semantic-name-review.js +63 -0
  48. package/dist/runtime-correlation.d.ts +34 -0
  49. package/dist/runtime-correlation.js +279 -0
  50. package/dist/runtime-paths.d.ts +3 -0
  51. package/dist/runtime-paths.js +11 -0
  52. package/dist/selection-diff.d.ts +667 -0
  53. package/dist/selection-diff.js +53 -0
  54. package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
  55. package/dist/semantic-name-suggestion-artifacts.js +314 -0
  56. package/dist/server.d.ts +129 -0
  57. package/dist/server.js +578 -0
  58. package/dist/tools/artifact-read.d.ts +235 -0
  59. package/dist/tools/artifact-read.js +317 -0
  60. package/dist/tools/artifacts-diff.d.ts +728 -0
  61. package/dist/tools/artifacts-diff.js +304 -0
  62. package/dist/tools/artifacts-list.d.ts +515 -0
  63. package/dist/tools/artifacts-list.js +389 -0
  64. package/dist/tools/attack-map.d.ts +290 -0
  65. package/dist/tools/attack-map.js +519 -0
  66. package/dist/tools/cache-observability.d.ts +4 -0
  67. package/dist/tools/cache-observability.js +36 -0
  68. package/dist/tools/code-function-cfg.d.ts +50 -0
  69. package/dist/tools/code-function-cfg.js +102 -0
  70. package/dist/tools/code-function-decompile.d.ts +55 -0
  71. package/dist/tools/code-function-decompile.js +103 -0
  72. package/dist/tools/code-function-disassemble.d.ts +43 -0
  73. package/dist/tools/code-function-disassemble.js +185 -0
  74. package/dist/tools/code-function-explain-apply.d.ts +255 -0
  75. package/dist/tools/code-function-explain-apply.js +225 -0
  76. package/dist/tools/code-function-explain-prepare.d.ts +535 -0
  77. package/dist/tools/code-function-explain-prepare.js +276 -0
  78. package/dist/tools/code-function-explain-review.d.ts +397 -0
  79. package/dist/tools/code-function-explain-review.js +589 -0
  80. package/dist/tools/code-function-rename-apply.d.ts +248 -0
  81. package/dist/tools/code-function-rename-apply.js +220 -0
  82. package/dist/tools/code-function-rename-prepare.d.ts +506 -0
  83. package/dist/tools/code-function-rename-prepare.js +279 -0
  84. package/dist/tools/code-function-rename-review.d.ts +574 -0
  85. package/dist/tools/code-function-rename-review.js +761 -0
  86. package/dist/tools/code-functions-list.d.ts +37 -0
  87. package/dist/tools/code-functions-list.js +91 -0
  88. package/dist/tools/code-functions-rank.d.ts +34 -0
  89. package/dist/tools/code-functions-rank.js +90 -0
  90. package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
  91. package/dist/tools/code-functions-reconstruct.js +2807 -0
  92. package/dist/tools/code-functions-search.d.ts +39 -0
  93. package/dist/tools/code-functions-search.js +90 -0
  94. package/dist/tools/code-reconstruct-export.d.ts +1212 -0
  95. package/dist/tools/code-reconstruct-export.js +4002 -0
  96. package/dist/tools/code-reconstruct-plan.d.ts +274 -0
  97. package/dist/tools/code-reconstruct-plan.js +342 -0
  98. package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
  99. package/dist/tools/dotnet-metadata-extract.js +355 -0
  100. package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
  101. package/dist/tools/dotnet-reconstruct-export.js +1151 -0
  102. package/dist/tools/dotnet-types-list.d.ts +325 -0
  103. package/dist/tools/dotnet-types-list.js +201 -0
  104. package/dist/tools/dynamic-dependencies.d.ts +115 -0
  105. package/dist/tools/dynamic-dependencies.js +213 -0
  106. package/dist/tools/dynamic-memory-import.d.ts +10 -0
  107. package/dist/tools/dynamic-memory-import.js +567 -0
  108. package/dist/tools/dynamic-trace-import.d.ts +10 -0
  109. package/dist/tools/dynamic-trace-import.js +235 -0
  110. package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
  111. package/dist/tools/entrypoint-fallback-disasm.js +89 -0
  112. package/dist/tools/ghidra-analyze.d.ts +88 -0
  113. package/dist/tools/ghidra-analyze.js +208 -0
  114. package/dist/tools/ghidra-health.d.ts +37 -0
  115. package/dist/tools/ghidra-health.js +212 -0
  116. package/dist/tools/ioc-export.d.ts +209 -0
  117. package/dist/tools/ioc-export.js +542 -0
  118. package/dist/tools/packer-detect.d.ts +165 -0
  119. package/dist/tools/packer-detect.js +284 -0
  120. package/dist/tools/pe-exports-extract.d.ts +175 -0
  121. package/dist/tools/pe-exports-extract.js +253 -0
  122. package/dist/tools/pe-fingerprint.d.ts +234 -0
  123. package/dist/tools/pe-fingerprint.js +269 -0
  124. package/dist/tools/pe-imports-extract.d.ts +105 -0
  125. package/dist/tools/pe-imports-extract.js +245 -0
  126. package/dist/tools/report-generate.d.ts +157 -0
  127. package/dist/tools/report-generate.js +457 -0
  128. package/dist/tools/report-summarize.d.ts +2131 -0
  129. package/dist/tools/report-summarize.js +596 -0
  130. package/dist/tools/runtime-detect.d.ts +135 -0
  131. package/dist/tools/runtime-detect.js +247 -0
  132. package/dist/tools/sample-ingest.d.ts +94 -0
  133. package/dist/tools/sample-ingest.js +327 -0
  134. package/dist/tools/sample-profile-get.d.ts +183 -0
  135. package/dist/tools/sample-profile-get.js +121 -0
  136. package/dist/tools/sandbox-execute.d.ts +441 -0
  137. package/dist/tools/sandbox-execute.js +392 -0
  138. package/dist/tools/strings-extract.d.ts +375 -0
  139. package/dist/tools/strings-extract.js +314 -0
  140. package/dist/tools/strings-floss-decode.d.ts +143 -0
  141. package/dist/tools/strings-floss-decode.js +259 -0
  142. package/dist/tools/system-health.d.ts +434 -0
  143. package/dist/tools/system-health.js +446 -0
  144. package/dist/tools/task-cancel.d.ts +21 -0
  145. package/dist/tools/task-cancel.js +70 -0
  146. package/dist/tools/task-status.d.ts +27 -0
  147. package/dist/tools/task-status.js +106 -0
  148. package/dist/tools/task-sweep.d.ts +22 -0
  149. package/dist/tools/task-sweep.js +77 -0
  150. package/dist/tools/tool-help.d.ts +340 -0
  151. package/dist/tools/tool-help.js +261 -0
  152. package/dist/tools/yara-scan.d.ts +554 -0
  153. package/dist/tools/yara-scan.js +313 -0
  154. package/dist/types.d.ts +266 -0
  155. package/dist/types.js +41 -0
  156. package/dist/worker-pool.d.ts +204 -0
  157. package/dist/worker-pool.js +650 -0
  158. package/dist/workflows/deep-static.d.ts +104 -0
  159. package/dist/workflows/deep-static.js +276 -0
  160. package/dist/workflows/function-explanation-review.d.ts +655 -0
  161. package/dist/workflows/function-explanation-review.js +440 -0
  162. package/dist/workflows/reconstruct.d.ts +2053 -0
  163. package/dist/workflows/reconstruct.js +666 -0
  164. package/dist/workflows/semantic-name-review.d.ts +2418 -0
  165. package/dist/workflows/semantic-name-review.js +521 -0
  166. package/dist/workflows/triage.d.ts +659 -0
  167. package/dist/workflows/triage.js +1374 -0
  168. package/dist/workspace-manager.d.ts +150 -0
  169. package/dist/workspace-manager.js +411 -0
  170. package/ghidra_scripts/DecompileFunction.java +487 -0
  171. package/ghidra_scripts/DecompileFunction.py +150 -0
  172. package/ghidra_scripts/ExtractCFG.java +256 -0
  173. package/ghidra_scripts/ExtractCFG.py +233 -0
  174. package/ghidra_scripts/ExtractFunctions.java +442 -0
  175. package/ghidra_scripts/ExtractFunctions.py +101 -0
  176. package/ghidra_scripts/README.md +125 -0
  177. package/ghidra_scripts/SearchFunctionReferences.java +380 -0
  178. package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
  179. package/helpers/DotNetMetadataProbe/Program.cs +566 -0
  180. package/install-to-codex.ps1 +178 -0
  181. package/install-to-copilot.ps1 +303 -0
  182. package/package.json +101 -0
  183. package/requirements.txt +9 -0
  184. package/workers/requirements-dynamic.txt +11 -0
  185. package/workers/requirements.txt +8 -0
  186. package/workers/speakeasy_compat.py +175 -0
  187. package/workers/static_worker.py +5183 -0
  188. package/workers/yara_rules/default.yar +33 -0
  189. package/workers/yara_rules/malware_families.yar +93 -0
  190. package/workers/yara_rules/packers.yar +80 -0
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Ghidra Headless configuration and validation
3
+ * Handles detection, validation, and configuration of Ghidra installation
4
+ */
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { spawnSync } from 'child_process';
8
+ import { logger } from './logger.js';
9
+ import { buildRawCommandLine, decodeProcessStreams } from './process-output.js';
10
+ function getPythonCommand(platform = process.platform) {
11
+ return platform === 'win32' ? 'python' : 'python3';
12
+ }
13
+ function probePyGhidra(timeoutMs) {
14
+ const pythonCommand = getPythonCommand();
15
+ const probeCode = 'import importlib.util,sys;spec=importlib.util.find_spec("pyghidra");' +
16
+ 'print("PYGHIDRA_MISSING" if spec is None else "PYGHIDRA_OK")';
17
+ try {
18
+ const result = spawnSync(pythonCommand, ['-c', probeCode], {
19
+ timeout: timeoutMs,
20
+ windowsHide: true,
21
+ encoding: 'utf8',
22
+ });
23
+ if (result.error) {
24
+ return {
25
+ command: pythonCommand,
26
+ available: false,
27
+ error: result.error.message,
28
+ };
29
+ }
30
+ const stdout = String(result.stdout || '').trim();
31
+ if (stdout.includes('PYGHIDRA_OK')) {
32
+ const versionProbe = spawnSync(pythonCommand, ['-c', 'import pyghidra; print(getattr(pyghidra, "__version__", "unknown"))'], {
33
+ timeout: timeoutMs,
34
+ windowsHide: true,
35
+ encoding: 'utf8',
36
+ });
37
+ const version = String(versionProbe.stdout || '').trim() || 'unknown';
38
+ return {
39
+ command: pythonCommand,
40
+ available: true,
41
+ version,
42
+ };
43
+ }
44
+ return {
45
+ command: pythonCommand,
46
+ available: false,
47
+ error: 'pyghidra package not found in active Python environment.',
48
+ };
49
+ }
50
+ catch (error) {
51
+ return {
52
+ command: pythonCommand,
53
+ available: false,
54
+ error: error instanceof Error ? error.message : String(error),
55
+ };
56
+ }
57
+ }
58
+ export function probePyGhidraAvailability(timeoutMs = 5000) {
59
+ return probePyGhidra(timeoutMs);
60
+ }
61
+ function looksLikeAnalyzeHeadlessHelp(stdout, stderr) {
62
+ const combined = `${stdout}\n${stderr}`.toLowerCase();
63
+ const hasToolMarker = combined.includes('analyzeheadless');
64
+ const hasUsageSignal = combined.includes('usage') ||
65
+ combined.includes('-import') ||
66
+ combined.includes('-process') ||
67
+ combined.includes('-scriptpath');
68
+ return hasToolMarker && hasUsageSignal;
69
+ }
70
+ /**
71
+ * Quote argument for Windows cmd.exe command line.
72
+ * Keeps special characters literal, including &, (), and spaces.
73
+ */
74
+ export function quoteForWindowsCmd(value) {
75
+ if (value.length === 0) {
76
+ return '""';
77
+ }
78
+ // In cmd.exe quoted context, double quotes are escaped by doubling.
79
+ const escaped = value.replace(/"/g, '""');
80
+ return `"${escaped}"`;
81
+ }
82
+ /**
83
+ * Build explicit cmd.exe invocation for .bat/.cmd entry points on Windows.
84
+ * Avoids `shell: true` and path-splitting issues on special characters.
85
+ */
86
+ export function buildWindowsBatchInvocation(command, args) {
87
+ const commandLine = [quoteForWindowsCmd(command), ...args.map(quoteForWindowsCmd)].join(' ');
88
+ // cmd.exe /s /c requires one outer quote pair around the full command line;
89
+ // without this, paths containing '&' are split and fail to execute.
90
+ const wrappedCommandLine = `"${commandLine}"`;
91
+ return {
92
+ command: 'cmd.exe',
93
+ args: ['/d', '/s', '/c', wrappedCommandLine],
94
+ windowsVerbatimArguments: true,
95
+ };
96
+ }
97
+ /**
98
+ * Build portable process invocation.
99
+ * On Windows batch scripts we explicitly route through cmd.exe.
100
+ */
101
+ export function buildProcessInvocation(command, args, platform = process.platform) {
102
+ if (platform === 'win32' && /\.(bat|cmd)$/i.test(command)) {
103
+ return buildWindowsBatchInvocation(command, args);
104
+ }
105
+ return {
106
+ command,
107
+ args: [...args],
108
+ };
109
+ }
110
+ /**
111
+ * Detect Ghidra installation from environment variable or common paths
112
+ */
113
+ export function detectGhidraInstallation() {
114
+ // 1. Check explicit environment variables
115
+ const envCandidates = [
116
+ ['GHIDRA_INSTALL_DIR', process.env.GHIDRA_INSTALL_DIR],
117
+ ['GHIDRA_PATH', process.env.GHIDRA_PATH],
118
+ ];
119
+ for (const [envName, envPath] of envCandidates) {
120
+ if (envPath && fs.existsSync(envPath)) {
121
+ logger.info({ path: envPath, env: envName }, `Found Ghidra installation from ${envName}`);
122
+ return envPath;
123
+ }
124
+ }
125
+ // 2. Check common installation paths
126
+ const commonPaths = [
127
+ '/opt/ghidra',
128
+ '/usr/local/ghidra',
129
+ 'C:\\Program Files\\ghidra',
130
+ 'C:\\ghidra',
131
+ path.join(process.env.HOME || '', 'ghidra'),
132
+ ];
133
+ for (const commonPath of commonPaths) {
134
+ if (fs.existsSync(commonPath)) {
135
+ logger.info({ path: commonPath }, 'Found Ghidra installation at common path');
136
+ return commonPath;
137
+ }
138
+ }
139
+ logger.warn('Ghidra installation not found');
140
+ return null;
141
+ }
142
+ /**
143
+ * Validate Ghidra installation directory
144
+ */
145
+ export function validateGhidraInstallation(installDir) {
146
+ try {
147
+ // Check if directory exists
148
+ if (!fs.existsSync(installDir)) {
149
+ logger.error({ installDir }, 'Ghidra installation directory does not exist');
150
+ return false;
151
+ }
152
+ // Check for analyzeHeadless script
153
+ const analyzeHeadlessPath = getAnalyzeHeadlessPath(installDir);
154
+ if (!fs.existsSync(analyzeHeadlessPath)) {
155
+ logger.error({ installDir, analyzeHeadlessPath }, 'analyzeHeadless script not found in Ghidra installation');
156
+ return false;
157
+ }
158
+ // Check if script is executable (Unix-like systems)
159
+ if (process.platform !== 'win32') {
160
+ try {
161
+ fs.accessSync(analyzeHeadlessPath, fs.constants.X_OK);
162
+ }
163
+ catch {
164
+ logger.error({ analyzeHeadlessPath }, 'analyzeHeadless script is not executable');
165
+ return false;
166
+ }
167
+ }
168
+ logger.info({ installDir }, 'Ghidra installation validated successfully');
169
+ return true;
170
+ }
171
+ catch (error) {
172
+ logger.error({ error, installDir }, 'Error validating Ghidra installation');
173
+ return false;
174
+ }
175
+ }
176
+ /**
177
+ * Get path to analyzeHeadless script based on platform
178
+ */
179
+ export function getAnalyzeHeadlessPath(installDir) {
180
+ const supportDir = path.join(installDir, 'support');
181
+ if (process.platform === 'win32') {
182
+ return path.join(supportDir, 'analyzeHeadless.bat');
183
+ }
184
+ else {
185
+ return path.join(supportDir, 'analyzeHeadless');
186
+ }
187
+ }
188
+ /**
189
+ * Get Ghidra version from installation
190
+ */
191
+ export function getGhidraVersion(installDir) {
192
+ try {
193
+ const analyzeHeadlessPath = getAnalyzeHeadlessPath(installDir);
194
+ const invocation = buildProcessInvocation(analyzeHeadlessPath, ['-help']);
195
+ const result = spawnSync(invocation.command, invocation.args, {
196
+ timeout: 5000,
197
+ windowsHide: true,
198
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments === true,
199
+ });
200
+ const decoded = decodeProcessStreams(result.stdout, result.stderr);
201
+ if (result.error || result.status !== 0) {
202
+ logger.warn({
203
+ raw_cmd: buildRawCommandLine(invocation.command, invocation.args),
204
+ error: result.error?.message,
205
+ status: result.status,
206
+ stderr: decoded.stderr.text.trim(),
207
+ stderr_encoding: decoded.stderr.encoding,
208
+ }, 'Failed to get Ghidra version');
209
+ return null;
210
+ }
211
+ const output = decoded.stdout.text;
212
+ // Parse version from output (format: "Ghidra Version X.Y.Z")
213
+ const versionMatch = output.match(/Ghidra\s+Version\s+(\d+\.\d+(?:\.\d+)?)/i);
214
+ if (versionMatch) {
215
+ return versionMatch[1];
216
+ }
217
+ logger.warn('Could not parse Ghidra version from output');
218
+ return null;
219
+ }
220
+ catch (error) {
221
+ logger.warn({ error }, 'Failed to get Ghidra version');
222
+ return null;
223
+ }
224
+ }
225
+ /**
226
+ * Ensure Ghidra scripts directory exists
227
+ */
228
+ export function ensureScriptsDirectory(baseDir = './ghidra_scripts') {
229
+ const scriptsDir = path.resolve(baseDir);
230
+ if (!fs.existsSync(scriptsDir)) {
231
+ fs.mkdirSync(scriptsDir, { recursive: true });
232
+ logger.info({ scriptsDir }, 'Created Ghidra scripts directory');
233
+ }
234
+ return scriptsDir;
235
+ }
236
+ /**
237
+ * Initialize Ghidra configuration
238
+ */
239
+ export function initializeGhidraConfig(installDir) {
240
+ // Detect installation directory
241
+ const detectedInstallDir = installDir || detectGhidraInstallation();
242
+ if (!detectedInstallDir) {
243
+ logger.warn('Ghidra installation not detected. Ghidra features will be disabled.');
244
+ return {
245
+ installDir: '',
246
+ analyzeHeadlessPath: '',
247
+ scriptsDir: '',
248
+ isValid: false,
249
+ };
250
+ }
251
+ // Validate installation
252
+ const isValid = validateGhidraInstallation(detectedInstallDir);
253
+ if (!isValid) {
254
+ logger.error('Ghidra installation validation failed. Ghidra features will be disabled.');
255
+ return {
256
+ installDir: detectedInstallDir,
257
+ analyzeHeadlessPath: '',
258
+ scriptsDir: '',
259
+ isValid: false,
260
+ };
261
+ }
262
+ // Get paths and version
263
+ const analyzeHeadlessPath = getAnalyzeHeadlessPath(detectedInstallDir);
264
+ const scriptsDir = ensureScriptsDirectory();
265
+ const version = getGhidraVersion(detectedInstallDir);
266
+ const config = {
267
+ installDir: detectedInstallDir,
268
+ analyzeHeadlessPath,
269
+ scriptsDir,
270
+ version: version || undefined,
271
+ isValid: true,
272
+ };
273
+ logger.info({
274
+ installDir: config.installDir,
275
+ version: config.version,
276
+ scriptsDir: config.scriptsDir,
277
+ }, 'Ghidra configuration initialized');
278
+ return config;
279
+ }
280
+ /**
281
+ * Generate unique project key for Ghidra project
282
+ * Format: <timestamp>_<random>
283
+ * Ensures uniqueness for concurrent analyses
284
+ *
285
+ * Requirements: 8.1, 8.7
286
+ */
287
+ export function generateProjectKey() {
288
+ const timestamp = Date.now();
289
+ const random = Math.random().toString(36).substring(2, 10);
290
+ return `${timestamp}_${random}`;
291
+ }
292
+ /**
293
+ * Create Ghidra project directory
294
+ * Creates isolated project space to avoid concurrent conflicts
295
+ *
296
+ * Requirements: 8.1, 8.7
297
+ *
298
+ * @param ghidraWorkspaceDir - Base Ghidra workspace directory (from workspace manager)
299
+ * @param projectKey - Unique project key (optional, will be generated if not provided)
300
+ * @returns Object with project path and project key
301
+ */
302
+ export function createGhidraProject(ghidraWorkspaceDir, projectKey) {
303
+ // Generate project key if not provided
304
+ const key = projectKey || generateProjectKey();
305
+ // Create project directory: workspace/ghidra/project_<key>/
306
+ const projectPath = path.join(ghidraWorkspaceDir, `project_${key}`);
307
+ // Create directory if it doesn't exist
308
+ if (!fs.existsSync(projectPath)) {
309
+ fs.mkdirSync(projectPath, { recursive: true });
310
+ logger.info({ projectPath, projectKey: key }, 'Created Ghidra project directory');
311
+ }
312
+ return {
313
+ projectPath,
314
+ projectKey: key,
315
+ };
316
+ }
317
+ /**
318
+ * Clean up old Ghidra projects
319
+ * Removes project directories older than specified age
320
+ *
321
+ * @param ghidraWorkspaceDir - Base Ghidra workspace directory
322
+ * @param maxAgeMs - Maximum age in milliseconds (default: 7 days)
323
+ * @returns Number of projects cleaned up
324
+ */
325
+ export function cleanupOldGhidraProjects(ghidraWorkspaceDir, maxAgeMs = 7 * 24 * 60 * 60 * 1000) {
326
+ if (!fs.existsSync(ghidraWorkspaceDir)) {
327
+ return 0;
328
+ }
329
+ const cutoffTime = Date.now() - maxAgeMs;
330
+ let cleanedCount = 0;
331
+ try {
332
+ const entries = fs.readdirSync(ghidraWorkspaceDir, { withFileTypes: true });
333
+ for (const entry of entries) {
334
+ // Only process project directories (format: project_<key>)
335
+ if (!entry.isDirectory() || !entry.name.startsWith('project_')) {
336
+ continue;
337
+ }
338
+ const projectPath = path.join(ghidraWorkspaceDir, entry.name);
339
+ try {
340
+ const stats = fs.statSync(projectPath);
341
+ // Check if project is older than cutoff time
342
+ if (stats.mtimeMs < cutoffTime) {
343
+ // Delete old project directory
344
+ fs.rmSync(projectPath, { recursive: true, force: true });
345
+ cleanedCount++;
346
+ logger.info({ projectPath }, 'Cleaned up old Ghidra project');
347
+ }
348
+ }
349
+ catch (error) {
350
+ logger.warn({ error, projectPath }, 'Failed to clean up Ghidra project');
351
+ }
352
+ }
353
+ }
354
+ catch (error) {
355
+ logger.error({ error, ghidraWorkspaceDir }, 'Failed to cleanup Ghidra projects');
356
+ }
357
+ return cleanedCount;
358
+ }
359
+ /**
360
+ * Perform a lightweight Ghidra environment health check.
361
+ * This verifies path resolution and attempts a short `analyzeHeadless -help` launch.
362
+ */
363
+ export function checkGhidraHealth(timeoutMs = 8000) {
364
+ const checkedAt = new Date().toISOString();
365
+ const errors = [];
366
+ const warnings = [];
367
+ const installDir = ghidraConfig.installDir || detectGhidraInstallation() || '';
368
+ const analyzeHeadlessPath = installDir ? getAnalyzeHeadlessPath(installDir) : '';
369
+ const scriptsDir = ghidraConfig.scriptsDir || ensureScriptsDirectory();
370
+ const installDirExists = Boolean(installDir && fs.existsSync(installDir));
371
+ const analyzeHeadlessExists = Boolean(analyzeHeadlessPath && fs.existsSync(analyzeHeadlessPath));
372
+ const scriptsDirExists = Boolean(scriptsDir && fs.existsSync(scriptsDir));
373
+ const pyghidraProbe = probePyGhidra(Math.min(timeoutMs, 5000));
374
+ const pyghidraAvailable = pyghidraProbe.available === true;
375
+ if (!installDirExists) {
376
+ errors.push('Ghidra install directory was not found. Set GHIDRA_PATH or GHIDRA_INSTALL_DIR.');
377
+ }
378
+ if (!analyzeHeadlessExists) {
379
+ errors.push('analyzeHeadless script was not found in the Ghidra install directory.');
380
+ }
381
+ if (!scriptsDirExists) {
382
+ errors.push('Ghidra script directory does not exist.');
383
+ }
384
+ if (!pyghidraAvailable) {
385
+ warnings.push('PyGhidra is unavailable in current Python environment; Python post-scripts may fail and Java fallback will be used.');
386
+ }
387
+ let launchOk = false;
388
+ let launchProbe;
389
+ if (installDirExists && analyzeHeadlessExists) {
390
+ try {
391
+ const invocation = buildProcessInvocation(analyzeHeadlessPath, ['-help']);
392
+ const result = spawnSync(invocation.command, invocation.args, {
393
+ timeout: timeoutMs,
394
+ windowsHide: true,
395
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments === true,
396
+ });
397
+ const decoded = decodeProcessStreams(result.stdout, result.stderr);
398
+ const timedOut = (result.error?.code === 'ETIMEDOUT') ||
399
+ Boolean(result.error?.message && result.error.message.includes('ETIMEDOUT'));
400
+ launchProbe = {
401
+ raw_cmd: buildRawCommandLine(invocation.command, invocation.args),
402
+ exit_code: result.status ?? null,
403
+ timed_out: timedOut,
404
+ stdout: decoded.stdout.text.trim(),
405
+ stderr: decoded.stderr.text.trim(),
406
+ stdout_encoding: decoded.stdout.encoding,
407
+ stderr_encoding: decoded.stderr.encoding,
408
+ };
409
+ if (result.error) {
410
+ if (timedOut) {
411
+ errors.push(`analyzeHeadless launch probe timed out after ${timeoutMs}ms.`);
412
+ }
413
+ else {
414
+ errors.push(`Failed to launch analyzeHeadless: ${result.error.message}`);
415
+ }
416
+ }
417
+ else if (result.status !== 0) {
418
+ const stdoutText = decoded.stdout.text.trim();
419
+ const stderrText = decoded.stderr.text.trim();
420
+ if (looksLikeAnalyzeHeadlessHelp(stdoutText, stderrText)) {
421
+ launchOk = true;
422
+ warnings.push(`analyzeHeadless returned non-zero exit code (${result.status}) but help output was detected.`);
423
+ }
424
+ else {
425
+ errors.push(`analyzeHeadless returned non-zero exit code (${result.status}). ${stderrText}`.trim());
426
+ }
427
+ }
428
+ else {
429
+ launchOk = true;
430
+ }
431
+ }
432
+ catch (error) {
433
+ const message = error instanceof Error ? error.message : String(error);
434
+ errors.push(`Ghidra launch check failed: ${message}`);
435
+ }
436
+ }
437
+ if (!ghidraConfig.isValid) {
438
+ warnings.push('Global ghidraConfig is marked invalid; decompiler tools will fail until fixed.');
439
+ }
440
+ return {
441
+ ok: errors.length === 0,
442
+ checked_at: checkedAt,
443
+ install_dir: installDir,
444
+ analyze_headless_path: analyzeHeadlessPath,
445
+ scripts_dir: scriptsDir,
446
+ version: ghidraConfig.version,
447
+ checks: {
448
+ install_dir_exists: installDirExists,
449
+ analyze_headless_exists: analyzeHeadlessExists,
450
+ scripts_dir_exists: scriptsDirExists,
451
+ launch_ok: launchOk,
452
+ pyghidra_available: pyghidraAvailable,
453
+ },
454
+ launch_probe: launchProbe,
455
+ pyghidra_probe: pyghidraProbe,
456
+ errors,
457
+ warnings,
458
+ };
459
+ }
460
+ /**
461
+ * Global Ghidra configuration instance
462
+ */
463
+ export const ghidraConfig = initializeGhidraConfig();
464
+ //# sourceMappingURL=ghidra-config.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Windows EXE Decompiler MCP Server
3
+ * Entry point
4
+ */
5
+ export { MCPServer } from './server.js';
6
+ export { loadConfig } from './config.js';
7
+ export { WorkspaceManager } from './workspace-manager.js';
8
+ export * from './types.js';
9
+ //# sourceMappingURL=index.d.ts.map