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.
- package/CODEX_INSTALLATION.md +69 -0
- package/COPILOT_INSTALLATION.md +77 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/bin/windows-exe-decompiler-mcp-server.js +3 -0
- package/dist/analysis-provenance.d.ts +184 -0
- package/dist/analysis-provenance.js +74 -0
- package/dist/analysis-task-runner.d.ts +31 -0
- package/dist/analysis-task-runner.js +160 -0
- package/dist/artifact-inventory.d.ts +23 -0
- package/dist/artifact-inventory.js +175 -0
- package/dist/cache-manager.d.ts +128 -0
- package/dist/cache-manager.js +454 -0
- package/dist/confidence-semantics.d.ts +66 -0
- package/dist/confidence-semantics.js +122 -0
- package/dist/config.d.ts +335 -0
- package/dist/config.js +193 -0
- package/dist/database.d.ts +227 -0
- package/dist/database.js +601 -0
- package/dist/decompiler-worker.d.ts +441 -0
- package/dist/decompiler-worker.js +1962 -0
- package/dist/dynamic-trace.d.ts +95 -0
- package/dist/dynamic-trace.js +629 -0
- package/dist/env-validator.d.ts +15 -0
- package/dist/env-validator.js +249 -0
- package/dist/error-handler.d.ts +28 -0
- package/dist/error-handler.example.d.ts +22 -0
- package/dist/error-handler.example.js +141 -0
- package/dist/error-handler.js +139 -0
- package/dist/ghidra-analysis-status.d.ts +49 -0
- package/dist/ghidra-analysis-status.js +178 -0
- package/dist/ghidra-config.d.ts +134 -0
- package/dist/ghidra-config.js +464 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +200 -0
- package/dist/job-queue.d.ts +169 -0
- package/dist/job-queue.js +407 -0
- package/dist/logger.d.ts +106 -0
- package/dist/logger.js +176 -0
- package/dist/policy-guard.d.ts +115 -0
- package/dist/policy-guard.js +243 -0
- package/dist/process-output.d.ts +15 -0
- package/dist/process-output.js +90 -0
- package/dist/prompts/function-explanation-review.d.ts +5 -0
- package/dist/prompts/function-explanation-review.js +64 -0
- package/dist/prompts/semantic-name-review.d.ts +5 -0
- package/dist/prompts/semantic-name-review.js +63 -0
- package/dist/runtime-correlation.d.ts +34 -0
- package/dist/runtime-correlation.js +279 -0
- package/dist/runtime-paths.d.ts +3 -0
- package/dist/runtime-paths.js +11 -0
- package/dist/selection-diff.d.ts +667 -0
- package/dist/selection-diff.js +53 -0
- package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
- package/dist/semantic-name-suggestion-artifacts.js +314 -0
- package/dist/server.d.ts +129 -0
- package/dist/server.js +578 -0
- package/dist/tools/artifact-read.d.ts +235 -0
- package/dist/tools/artifact-read.js +317 -0
- package/dist/tools/artifacts-diff.d.ts +728 -0
- package/dist/tools/artifacts-diff.js +304 -0
- package/dist/tools/artifacts-list.d.ts +515 -0
- package/dist/tools/artifacts-list.js +389 -0
- package/dist/tools/attack-map.d.ts +290 -0
- package/dist/tools/attack-map.js +519 -0
- package/dist/tools/cache-observability.d.ts +4 -0
- package/dist/tools/cache-observability.js +36 -0
- package/dist/tools/code-function-cfg.d.ts +50 -0
- package/dist/tools/code-function-cfg.js +102 -0
- package/dist/tools/code-function-decompile.d.ts +55 -0
- package/dist/tools/code-function-decompile.js +103 -0
- package/dist/tools/code-function-disassemble.d.ts +43 -0
- package/dist/tools/code-function-disassemble.js +185 -0
- package/dist/tools/code-function-explain-apply.d.ts +255 -0
- package/dist/tools/code-function-explain-apply.js +225 -0
- package/dist/tools/code-function-explain-prepare.d.ts +535 -0
- package/dist/tools/code-function-explain-prepare.js +276 -0
- package/dist/tools/code-function-explain-review.d.ts +397 -0
- package/dist/tools/code-function-explain-review.js +589 -0
- package/dist/tools/code-function-rename-apply.d.ts +248 -0
- package/dist/tools/code-function-rename-apply.js +220 -0
- package/dist/tools/code-function-rename-prepare.d.ts +506 -0
- package/dist/tools/code-function-rename-prepare.js +279 -0
- package/dist/tools/code-function-rename-review.d.ts +574 -0
- package/dist/tools/code-function-rename-review.js +761 -0
- package/dist/tools/code-functions-list.d.ts +37 -0
- package/dist/tools/code-functions-list.js +91 -0
- package/dist/tools/code-functions-rank.d.ts +34 -0
- package/dist/tools/code-functions-rank.js +90 -0
- package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
- package/dist/tools/code-functions-reconstruct.js +2807 -0
- package/dist/tools/code-functions-search.d.ts +39 -0
- package/dist/tools/code-functions-search.js +90 -0
- package/dist/tools/code-reconstruct-export.d.ts +1212 -0
- package/dist/tools/code-reconstruct-export.js +4002 -0
- package/dist/tools/code-reconstruct-plan.d.ts +274 -0
- package/dist/tools/code-reconstruct-plan.js +342 -0
- package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
- package/dist/tools/dotnet-metadata-extract.js +355 -0
- package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
- package/dist/tools/dotnet-reconstruct-export.js +1151 -0
- package/dist/tools/dotnet-types-list.d.ts +325 -0
- package/dist/tools/dotnet-types-list.js +201 -0
- package/dist/tools/dynamic-dependencies.d.ts +115 -0
- package/dist/tools/dynamic-dependencies.js +213 -0
- package/dist/tools/dynamic-memory-import.d.ts +10 -0
- package/dist/tools/dynamic-memory-import.js +567 -0
- package/dist/tools/dynamic-trace-import.d.ts +10 -0
- package/dist/tools/dynamic-trace-import.js +235 -0
- package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
- package/dist/tools/entrypoint-fallback-disasm.js +89 -0
- package/dist/tools/ghidra-analyze.d.ts +88 -0
- package/dist/tools/ghidra-analyze.js +208 -0
- package/dist/tools/ghidra-health.d.ts +37 -0
- package/dist/tools/ghidra-health.js +212 -0
- package/dist/tools/ioc-export.d.ts +209 -0
- package/dist/tools/ioc-export.js +542 -0
- package/dist/tools/packer-detect.d.ts +165 -0
- package/dist/tools/packer-detect.js +284 -0
- package/dist/tools/pe-exports-extract.d.ts +175 -0
- package/dist/tools/pe-exports-extract.js +253 -0
- package/dist/tools/pe-fingerprint.d.ts +234 -0
- package/dist/tools/pe-fingerprint.js +269 -0
- package/dist/tools/pe-imports-extract.d.ts +105 -0
- package/dist/tools/pe-imports-extract.js +245 -0
- package/dist/tools/report-generate.d.ts +157 -0
- package/dist/tools/report-generate.js +457 -0
- package/dist/tools/report-summarize.d.ts +2131 -0
- package/dist/tools/report-summarize.js +596 -0
- package/dist/tools/runtime-detect.d.ts +135 -0
- package/dist/tools/runtime-detect.js +247 -0
- package/dist/tools/sample-ingest.d.ts +94 -0
- package/dist/tools/sample-ingest.js +327 -0
- package/dist/tools/sample-profile-get.d.ts +183 -0
- package/dist/tools/sample-profile-get.js +121 -0
- package/dist/tools/sandbox-execute.d.ts +441 -0
- package/dist/tools/sandbox-execute.js +392 -0
- package/dist/tools/strings-extract.d.ts +375 -0
- package/dist/tools/strings-extract.js +314 -0
- package/dist/tools/strings-floss-decode.d.ts +143 -0
- package/dist/tools/strings-floss-decode.js +259 -0
- package/dist/tools/system-health.d.ts +434 -0
- package/dist/tools/system-health.js +446 -0
- package/dist/tools/task-cancel.d.ts +21 -0
- package/dist/tools/task-cancel.js +70 -0
- package/dist/tools/task-status.d.ts +27 -0
- package/dist/tools/task-status.js +106 -0
- package/dist/tools/task-sweep.d.ts +22 -0
- package/dist/tools/task-sweep.js +77 -0
- package/dist/tools/tool-help.d.ts +340 -0
- package/dist/tools/tool-help.js +261 -0
- package/dist/tools/yara-scan.d.ts +554 -0
- package/dist/tools/yara-scan.js +313 -0
- package/dist/types.d.ts +266 -0
- package/dist/types.js +41 -0
- package/dist/worker-pool.d.ts +204 -0
- package/dist/worker-pool.js +650 -0
- package/dist/workflows/deep-static.d.ts +104 -0
- package/dist/workflows/deep-static.js +276 -0
- package/dist/workflows/function-explanation-review.d.ts +655 -0
- package/dist/workflows/function-explanation-review.js +440 -0
- package/dist/workflows/reconstruct.d.ts +2053 -0
- package/dist/workflows/reconstruct.js +666 -0
- package/dist/workflows/semantic-name-review.d.ts +2418 -0
- package/dist/workflows/semantic-name-review.js +521 -0
- package/dist/workflows/triage.d.ts +659 -0
- package/dist/workflows/triage.js +1374 -0
- package/dist/workspace-manager.d.ts +150 -0
- package/dist/workspace-manager.js +411 -0
- package/ghidra_scripts/DecompileFunction.java +487 -0
- package/ghidra_scripts/DecompileFunction.py +150 -0
- package/ghidra_scripts/ExtractCFG.java +256 -0
- package/ghidra_scripts/ExtractCFG.py +233 -0
- package/ghidra_scripts/ExtractFunctions.java +442 -0
- package/ghidra_scripts/ExtractFunctions.py +101 -0
- package/ghidra_scripts/README.md +125 -0
- package/ghidra_scripts/SearchFunctionReferences.java +380 -0
- package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
- package/helpers/DotNetMetadataProbe/Program.cs +566 -0
- package/install-to-codex.ps1 +178 -0
- package/install-to-copilot.ps1 +303 -0
- package/package.json +101 -0
- package/requirements.txt +9 -0
- package/workers/requirements-dynamic.txt +11 -0
- package/workers/requirements.txt +8 -0
- package/workers/speakeasy_compat.py +175 -0
- package/workers/static_worker.py +5183 -0
- package/workers/yara_rules/default.yar +33 -0
- package/workers/yara_rules/malware_families.yar +93 -0
- 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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|