windows-exe-decompiler-mcp-server 0.1.0 → 0.1.3
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/CLAUDE_INSTALLATION.md +129 -0
- package/CODEX_INSTALLATION.md +25 -1
- package/COPILOT_INSTALLATION.md +29 -1
- package/LICENSE +21 -21
- package/README.md +232 -110
- package/dist/analysis-provenance.d.ts +50 -2
- package/dist/analysis-provenance.js +1 -0
- package/dist/analysis-task-runner.js +92 -0
- package/dist/config.d.ts +35 -16
- package/dist/config.js +37 -3
- package/dist/database.js +115 -115
- package/dist/decompiler-worker.d.ts +10 -5
- package/dist/decompiler-worker.js +240 -58
- package/dist/dynamic-trace.d.ts +6 -0
- package/dist/dynamic-trace.js +52 -1
- package/dist/ghidra-config.js +17 -1
- package/dist/index.js +38 -8
- package/dist/pe-runtime-functions.d.ts +90 -0
- package/dist/pe-runtime-functions.js +452 -0
- package/dist/prompts/module-reconstruction-review.d.ts +5 -0
- package/dist/prompts/module-reconstruction-review.js +65 -0
- package/dist/runtime-correlation.d.ts +5 -0
- package/dist/runtime-correlation.js +179 -2
- package/dist/sample-workspace.d.ts +28 -0
- package/dist/sample-workspace.js +186 -0
- package/dist/semantic-name-suggestion-artifacts.d.ts +56 -0
- package/dist/semantic-name-suggestion-artifacts.js +87 -0
- package/dist/server.js +1 -1
- package/dist/setup-guidance.d.ts +73 -0
- package/dist/setup-guidance.js +211 -0
- package/dist/tools/artifact-read.d.ts +10 -10
- package/dist/tools/artifacts-diff.d.ts +8 -8
- package/dist/tools/artifacts-list.d.ts +16 -16
- package/dist/tools/attack-map.d.ts +8 -8
- package/dist/tools/binary-role-profile.d.ts +1197 -0
- package/dist/tools/binary-role-profile.js +575 -0
- package/dist/tools/code-function-explain-apply.d.ts +12 -12
- package/dist/tools/code-function-explain-prepare.d.ts +12 -12
- package/dist/tools/code-function-explain-review.d.ts +58 -58
- package/dist/tools/code-function-rename-apply.d.ts +12 -12
- package/dist/tools/code-function-rename-prepare.d.ts +14 -14
- package/dist/tools/code-function-rename-review.d.ts +66 -66
- package/dist/tools/code-functions-define.d.ts +337 -0
- package/dist/tools/code-functions-define.js +337 -0
- package/dist/tools/code-functions-list.js +1 -1
- package/dist/tools/code-functions-rank.js +1 -1
- package/dist/tools/code-functions-reconstruct.d.ts +368 -58
- package/dist/tools/code-functions-reconstruct.js +130 -10
- package/dist/tools/code-functions-smart-recover.d.ts +352 -0
- package/dist/tools/code-functions-smart-recover.js +190 -0
- package/dist/tools/code-module-review-apply.d.ts +233 -0
- package/dist/tools/code-module-review-apply.js +209 -0
- package/dist/tools/code-module-review-prepare.d.ts +756 -0
- package/dist/tools/code-module-review-prepare.js +343 -0
- package/dist/tools/code-module-review.d.ts +474 -0
- package/dist/tools/code-module-review.js +566 -0
- package/dist/tools/code-reconstruct-export.d.ts +193 -21
- package/dist/tools/code-reconstruct-export.js +578 -21
- package/dist/tools/code-reconstruct-plan.d.ts +20 -20
- package/dist/tools/com-role-profile.d.ts +353 -0
- package/dist/tools/com-role-profile.js +186 -0
- package/dist/tools/dll-export-profile.d.ts +625 -0
- package/dist/tools/dll-export-profile.js +230 -0
- package/dist/tools/dotnet-metadata-extract.d.ts +122 -122
- package/dist/tools/dotnet-reconstruct-export.d.ts +49 -49
- package/dist/tools/dotnet-types-list.d.ts +66 -66
- package/dist/tools/dynamic-dependencies.d.ts +144 -8
- package/dist/tools/dynamic-dependencies.js +20 -1
- package/dist/tools/dynamic-memory-import.js +105 -2
- package/dist/tools/ghidra-analyze.d.ts +20 -0
- package/dist/tools/ghidra-analyze.js +10 -2
- package/dist/tools/ghidra-health.d.ts +177 -0
- package/dist/tools/ghidra-health.js +28 -0
- package/dist/tools/ioc-export.d.ts +8 -8
- package/dist/tools/packer-detect.d.ts +14 -14
- package/dist/tools/packer-detect.js +6 -9
- package/dist/tools/pe-exports-extract.d.ts +8 -8
- package/dist/tools/pe-exports-extract.js +6 -9
- package/dist/tools/pe-fingerprint.d.ts +36 -36
- package/dist/tools/pe-imports-extract.d.ts +8 -8
- package/dist/tools/pe-imports-extract.js +6 -9
- package/dist/tools/pe-pdata-extract.d.ts +448 -0
- package/dist/tools/pe-pdata-extract.js +219 -0
- package/dist/tools/pe-symbols-recover.d.ts +227 -0
- package/dist/tools/pe-symbols-recover.js +316 -0
- package/dist/tools/report-generate.d.ts +15 -1
- package/dist/tools/report-generate.js +139 -16
- package/dist/tools/report-summarize.d.ts +2242 -8
- package/dist/tools/report-summarize.js +246 -10
- package/dist/tools/runtime-detect.d.ts +8 -8
- package/dist/tools/runtime-detect.js +2 -12
- package/dist/tools/rust-binary-analyze.d.ts +2047 -0
- package/dist/tools/rust-binary-analyze.js +443 -0
- package/dist/tools/sample-profile-get.d.ts +135 -16
- package/dist/tools/sample-profile-get.js +35 -32
- package/dist/tools/sandbox-execute.d.ts +10 -10
- package/dist/tools/strings-extract.d.ts +8 -8
- package/dist/tools/strings-extract.js +2 -12
- package/dist/tools/strings-floss-decode.d.ts +8 -8
- package/dist/tools/system-health.d.ts +172 -36
- package/dist/tools/system-health.js +14 -0
- package/dist/tools/system-setup-guide.d.ts +179 -0
- package/dist/tools/system-setup-guide.js +62 -0
- package/dist/tools/tool-help.d.ts +16 -16
- package/dist/tools/tool-help.js +172 -0
- package/dist/tools/yara-scan.d.ts +8 -8
- package/dist/types.d.ts +2 -0
- package/dist/workflows/function-explanation-review.d.ts +3034 -76
- package/dist/workflows/function-explanation-review.js +172 -42
- package/dist/workflows/function-index-recover.d.ts +333 -0
- package/dist/workflows/function-index-recover.js +291 -0
- package/dist/workflows/module-reconstruction-review.d.ts +3537 -0
- package/dist/workflows/module-reconstruction-review.js +354 -0
- package/dist/workflows/reconstruct.d.ts +2182 -223
- package/dist/workflows/reconstruct.js +575 -32
- package/dist/workflows/semantic-name-review.d.ts +1436 -85
- package/dist/workflows/semantic-name-review.js +150 -58
- package/dist/workflows/triage.d.ts +8 -8
- package/ghidra_scripts/DecompileFunction.java +487 -487
- package/ghidra_scripts/ExtractCFG.java +256 -256
- package/ghidra_scripts/ExtractFunctions.java +442 -442
- package/ghidra_scripts/README.md +112 -112
- package/ghidra_scripts/SearchFunctionReferences.java +380 -380
- package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -9
- package/helpers/DotNetMetadataProbe/Program.cs +566 -566
- package/install-to-claude.ps1 +432 -0
- package/install-to-codex.ps1 +256 -178
- package/install-to-copilot.ps1 +389 -303
- package/package.json +4 -2
- package/requirements.txt +9 -9
- package/workers/requirements-dynamic.txt +11 -11
- package/workers/requirements.txt +8 -8
- package/workers/speakeasy_compat.py +175 -175
- package/workers/static_worker.py +3474 -3474
|
@@ -16,6 +16,8 @@ import { logger } from './logger.js';
|
|
|
16
16
|
import { ghidraConfig, createGhidraProject, buildProcessInvocation, } from './ghidra-config.js';
|
|
17
17
|
import { findBestGhidraAnalysis, getGhidraCapabilityStatus, isGhidraCapabilityReady, parseGhidraAnalysisMetadata, } from './ghidra-analysis-status.js';
|
|
18
18
|
import { buildRawCommandLine, decodeProcessStreams, } from './process-output.js';
|
|
19
|
+
import { smartRecoverFunctionsFromPE } from './pe-runtime-functions.js';
|
|
20
|
+
import { formatMissingOriginalError, resolvePrimarySamplePath } from './sample-workspace.js';
|
|
19
21
|
export class GhidraProcessError extends Error {
|
|
20
22
|
errorCode;
|
|
21
23
|
diagnostics;
|
|
@@ -250,27 +252,75 @@ export class DecompilerWorker {
|
|
|
250
252
|
}
|
|
251
253
|
throw lastError instanceof Error ? lastError : new Error(`${operationLabel} failed`);
|
|
252
254
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
async resolveSamplePathForSample(sampleId) {
|
|
256
|
+
const { samplePath, integrity } = await resolvePrimarySamplePath(this.workspaceManager, sampleId);
|
|
257
|
+
if (!samplePath) {
|
|
258
|
+
throw new Error(formatMissingOriginalError(sampleId, integrity));
|
|
259
|
+
}
|
|
260
|
+
return samplePath;
|
|
261
|
+
}
|
|
262
|
+
getAlternateWorkspaceRoot() {
|
|
263
|
+
const currentRoot = this.workspaceManager.getWorkspaceRoot();
|
|
264
|
+
const siblingRoot = path.join(path.dirname(path.dirname(currentRoot)), 'workspaces');
|
|
265
|
+
if (path.resolve(siblingRoot) === path.resolve(currentRoot)) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return fs.existsSync(siblingRoot) ? siblingRoot : null;
|
|
269
|
+
}
|
|
270
|
+
findLatestProjectInGhidraDir(ghidraDir) {
|
|
271
|
+
if (!fs.existsSync(ghidraDir)) {
|
|
272
|
+
return null;
|
|
261
273
|
}
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
const candidates = fs
|
|
275
|
+
.readdirSync(ghidraDir, { withFileTypes: true })
|
|
276
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith('project_'))
|
|
277
|
+
.map((entry) => {
|
|
278
|
+
const projectPath = path.join(ghidraDir, entry.name);
|
|
279
|
+
const projectKey = entry.name.slice('project_'.length);
|
|
280
|
+
const gprPath = path.join(projectPath, `${projectKey}.gpr`);
|
|
281
|
+
const stats = fs.statSync(projectPath);
|
|
282
|
+
return {
|
|
283
|
+
projectPath,
|
|
284
|
+
projectKey,
|
|
285
|
+
gprPath,
|
|
286
|
+
mtimeMs: stats.mtimeMs,
|
|
287
|
+
};
|
|
288
|
+
})
|
|
289
|
+
.filter((item) => fs.existsSync(item.gprPath))
|
|
290
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
291
|
+
if (candidates.length === 0) {
|
|
292
|
+
return null;
|
|
264
293
|
}
|
|
265
|
-
|
|
266
|
-
.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
294
|
+
return {
|
|
295
|
+
projectPath: candidates[0].projectPath,
|
|
296
|
+
projectKey: candidates[0].projectKey,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
findRecoveredProject(sampleId, originalProjectPath) {
|
|
300
|
+
const sha256 = sampleId.startsWith('sha256:') ? sampleId.slice('sha256:'.length) : sampleId;
|
|
301
|
+
const bucket1 = sha256.slice(0, 2);
|
|
302
|
+
const bucket2 = sha256.slice(2, 4);
|
|
303
|
+
const projectDirName = path.basename(originalProjectPath);
|
|
304
|
+
const currentRoot = this.workspaceManager.getWorkspaceRoot();
|
|
305
|
+
const roots = [currentRoot, this.getAlternateWorkspaceRoot()].filter((value) => Boolean(value));
|
|
306
|
+
for (const root of roots) {
|
|
307
|
+
const ghidraDir = path.join(root, bucket1, bucket2, sha256, 'ghidra');
|
|
308
|
+
const mappedProjectPath = path.join(ghidraDir, projectDirName);
|
|
309
|
+
if (fs.existsSync(mappedProjectPath)) {
|
|
310
|
+
const projectKey = projectDirName.startsWith('project_')
|
|
311
|
+
? projectDirName.slice('project_'.length)
|
|
312
|
+
: projectDirName;
|
|
313
|
+
return {
|
|
314
|
+
projectPath: mappedProjectPath,
|
|
315
|
+
projectKey,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const latestProject = this.findLatestProjectInGhidraDir(ghidraDir);
|
|
319
|
+
if (latestProject) {
|
|
320
|
+
return latestProject;
|
|
321
|
+
}
|
|
272
322
|
}
|
|
273
|
-
return
|
|
323
|
+
return null;
|
|
274
324
|
}
|
|
275
325
|
/**
|
|
276
326
|
* Spawn Ghidra process with Windows batch-script compatibility.
|
|
@@ -391,14 +441,28 @@ export class DecompilerWorker {
|
|
|
391
441
|
});
|
|
392
442
|
});
|
|
393
443
|
}
|
|
394
|
-
buildAnalyzeBaseArgs(projectPath, projectKey) {
|
|
395
|
-
|
|
444
|
+
buildAnalyzeBaseArgs(projectPath, projectKey, options) {
|
|
445
|
+
const args = [projectPath, projectKey];
|
|
446
|
+
const processor = options?.languageId || options?.processor;
|
|
447
|
+
if (processor) {
|
|
448
|
+
args.push('-processor', processor);
|
|
449
|
+
}
|
|
450
|
+
if (options?.cspec) {
|
|
451
|
+
args.push('-cspec', options.cspec);
|
|
452
|
+
}
|
|
453
|
+
return args;
|
|
454
|
+
}
|
|
455
|
+
buildScriptPath(options) {
|
|
456
|
+
const scriptPaths = Array.from(new Set([ghidraConfig.scriptsDir, ...(options?.scriptPaths || [])]
|
|
457
|
+
.map((item) => item?.trim())
|
|
458
|
+
.filter((item) => Boolean(item))));
|
|
459
|
+
return scriptPaths.join(path.delimiter);
|
|
396
460
|
}
|
|
397
461
|
buildAnalysisArgs(projectPath, projectKey, samplePath, options) {
|
|
398
462
|
const timeout = options.timeout || 300000; // Default 5 minutes
|
|
399
463
|
const maxCpu = options.maxCpu || '4';
|
|
400
464
|
return [
|
|
401
|
-
...this.buildAnalyzeBaseArgs(projectPath, projectKey),
|
|
465
|
+
...this.buildAnalyzeBaseArgs(projectPath, projectKey, options),
|
|
402
466
|
'-import',
|
|
403
467
|
samplePath,
|
|
404
468
|
'-max-cpu',
|
|
@@ -407,13 +471,13 @@ export class DecompilerWorker {
|
|
|
407
471
|
String(Math.floor(timeout / 1000)),
|
|
408
472
|
];
|
|
409
473
|
}
|
|
410
|
-
buildExtractFunctionsArgs(projectPath, projectKey, samplePath, scriptName) {
|
|
474
|
+
buildExtractFunctionsArgs(projectPath, projectKey, samplePath, scriptName, options) {
|
|
411
475
|
return [
|
|
412
|
-
...this.buildAnalyzeBaseArgs(projectPath, projectKey),
|
|
476
|
+
...this.buildAnalyzeBaseArgs(projectPath, projectKey, options),
|
|
413
477
|
'-process',
|
|
414
478
|
path.basename(samplePath),
|
|
415
479
|
'-scriptPath',
|
|
416
|
-
|
|
480
|
+
this.buildScriptPath(options),
|
|
417
481
|
'-postScript',
|
|
418
482
|
scriptName,
|
|
419
483
|
'-noanalysis',
|
|
@@ -445,9 +509,9 @@ export class DecompilerWorker {
|
|
|
445
509
|
throw error;
|
|
446
510
|
}
|
|
447
511
|
}
|
|
448
|
-
async executeFunctionExtractionScript(projectPath, projectKey, samplePath, scriptName, timeoutMs) {
|
|
512
|
+
async executeFunctionExtractionScript(projectPath, projectKey, samplePath, scriptName, timeoutMs, options) {
|
|
449
513
|
const command = ghidraConfig.analyzeHeadlessPath;
|
|
450
|
-
const args = this.buildExtractFunctionsArgs(projectPath, projectKey, samplePath, scriptName);
|
|
514
|
+
const args = this.buildExtractFunctionsArgs(projectPath, projectKey, samplePath, scriptName, options);
|
|
451
515
|
logger.debug({
|
|
452
516
|
command,
|
|
453
517
|
args,
|
|
@@ -456,7 +520,7 @@ export class DecompilerWorker {
|
|
|
456
520
|
}, 'Executing Ghidra function extraction post-script');
|
|
457
521
|
return this.runGhidraCommand(command, args, projectPath, timeoutMs, undefined, `E_TIMEOUT: Function extraction (${scriptName}) exceeded timeout of ${timeoutMs}ms`, `Function extraction (${scriptName}) failed`);
|
|
458
522
|
}
|
|
459
|
-
async tryExtractFunctionsWithFallback(projectPath, projectKey, samplePath, timeoutMs) {
|
|
523
|
+
async tryExtractFunctionsWithFallback(projectPath, projectKey, samplePath, timeoutMs, options) {
|
|
460
524
|
const warnings = [];
|
|
461
525
|
const attempts = [];
|
|
462
526
|
const primaryScript = 'ExtractFunctions.java';
|
|
@@ -464,7 +528,7 @@ export class DecompilerWorker {
|
|
|
464
528
|
const runAndParse = async (scriptName) => {
|
|
465
529
|
let output;
|
|
466
530
|
try {
|
|
467
|
-
output = await this.executeFunctionExtractionScript(projectPath, projectKey, samplePath, scriptName, timeoutMs);
|
|
531
|
+
output = await this.executeFunctionExtractionScript(projectPath, projectKey, samplePath, scriptName, timeoutMs, options);
|
|
468
532
|
}
|
|
469
533
|
catch (error) {
|
|
470
534
|
const diagnostics = error instanceof GhidraProcessError || error instanceof GhidraOutputParseError
|
|
@@ -547,6 +611,53 @@ export class DecompilerWorker {
|
|
|
547
611
|
functions[0];
|
|
548
612
|
return preferred?.address;
|
|
549
613
|
}
|
|
614
|
+
tryRecoverFunctionsFromPE(samplePath) {
|
|
615
|
+
const recovery = smartRecoverFunctionsFromPE(samplePath);
|
|
616
|
+
const functions = recovery.functions.map((item) => ({
|
|
617
|
+
address: item.address,
|
|
618
|
+
name: item.name,
|
|
619
|
+
size: item.size,
|
|
620
|
+
is_thunk: false,
|
|
621
|
+
is_external: false,
|
|
622
|
+
calling_convention: 'unknown',
|
|
623
|
+
signature: `${item.name}()`,
|
|
624
|
+
callers: [],
|
|
625
|
+
caller_count: 0,
|
|
626
|
+
callees: [],
|
|
627
|
+
callee_count: 0,
|
|
628
|
+
caller_relationships: [],
|
|
629
|
+
callee_relationships: [],
|
|
630
|
+
is_entry_point: item.isEntryPoint,
|
|
631
|
+
is_exported: item.isExported,
|
|
632
|
+
}));
|
|
633
|
+
return {
|
|
634
|
+
functions,
|
|
635
|
+
warnings: recovery.warnings,
|
|
636
|
+
recoveryMetadata: {
|
|
637
|
+
strategy: recovery.strategy,
|
|
638
|
+
machine: recovery.machine,
|
|
639
|
+
machine_name: recovery.machineName,
|
|
640
|
+
image_base: recovery.imageBase,
|
|
641
|
+
entry_point_rva: recovery.entryPointRva,
|
|
642
|
+
count: recovery.count,
|
|
643
|
+
recovered_functions: recovery.functions.map((item) => ({
|
|
644
|
+
address: item.address,
|
|
645
|
+
rva: item.rva,
|
|
646
|
+
size: item.size,
|
|
647
|
+
name: item.name,
|
|
648
|
+
name_source: item.nameSource,
|
|
649
|
+
confidence: item.confidence,
|
|
650
|
+
source: item.source,
|
|
651
|
+
section_name: item.sectionName,
|
|
652
|
+
executable_section: item.executableSection,
|
|
653
|
+
is_entry_point: item.isEntryPoint,
|
|
654
|
+
is_exported: item.isExported,
|
|
655
|
+
export_name: item.exportName,
|
|
656
|
+
evidence: item.evidence,
|
|
657
|
+
})),
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
|
550
661
|
buildCapabilityReadyStatus(target, warnings) {
|
|
551
662
|
return {
|
|
552
663
|
available: true,
|
|
@@ -614,13 +725,35 @@ export class DecompilerWorker {
|
|
|
614
725
|
};
|
|
615
726
|
}
|
|
616
727
|
}
|
|
617
|
-
resolveAnalysisProject(analysis) {
|
|
728
|
+
resolveAnalysisProject(sampleId, analysis) {
|
|
618
729
|
const metadata = parseGhidraAnalysisMetadata(analysis.output_json);
|
|
619
730
|
const projectPath = typeof metadata.project_path === 'string' ? metadata.project_path : '';
|
|
620
731
|
const projectKey = typeof metadata.project_key === 'string' ? metadata.project_key : '';
|
|
621
732
|
if (!projectPath || !projectKey) {
|
|
622
733
|
throw new Error(`Ghidra analysis ${analysis.id} has no reusable project metadata for downstream scripts.`);
|
|
623
734
|
}
|
|
735
|
+
if (fs.existsSync(projectPath)) {
|
|
736
|
+
return {
|
|
737
|
+
analysis,
|
|
738
|
+
projectPath,
|
|
739
|
+
projectKey,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
const recoveredProject = this.findRecoveredProject(sampleId, projectPath);
|
|
743
|
+
if (recoveredProject) {
|
|
744
|
+
logger.warn({
|
|
745
|
+
sampleId,
|
|
746
|
+
analysisId: analysis.id,
|
|
747
|
+
originalProjectPath: projectPath,
|
|
748
|
+
recoveredProjectPath: recoveredProject.projectPath,
|
|
749
|
+
recoveredProjectKey: recoveredProject.projectKey,
|
|
750
|
+
}, 'Recorded Ghidra project path is missing; using recovered project path from sample ghidra workspace');
|
|
751
|
+
return {
|
|
752
|
+
analysis,
|
|
753
|
+
projectPath: recoveredProject.projectPath,
|
|
754
|
+
projectKey: recoveredProject.projectKey,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
624
757
|
return {
|
|
625
758
|
analysis,
|
|
626
759
|
projectPath,
|
|
@@ -643,7 +776,7 @@ export class DecompilerWorker {
|
|
|
643
776
|
const reason = readiness.reason ? ` ${readiness.reason}` : '';
|
|
644
777
|
throw new Error(`Ghidra ${capability} is not ready for sample: ${sampleId}.${reason}`.trim());
|
|
645
778
|
}
|
|
646
|
-
const project = this.resolveAnalysisProject(selected);
|
|
779
|
+
const project = this.resolveAnalysisProject(sampleId, selected);
|
|
647
780
|
return {
|
|
648
781
|
...project,
|
|
649
782
|
readiness,
|
|
@@ -690,7 +823,7 @@ export class DecompilerWorker {
|
|
|
690
823
|
}
|
|
691
824
|
// 3. Get workspace paths
|
|
692
825
|
const workspace = await this.workspaceManager.getWorkspace(sampleId);
|
|
693
|
-
const samplePath = this.
|
|
826
|
+
const samplePath = await this.resolveSamplePathForSample(sampleId);
|
|
694
827
|
// Verify sample file exists
|
|
695
828
|
if (!fs.existsSync(samplePath)) {
|
|
696
829
|
throw new Error(`Sample file not found: ${samplePath}`);
|
|
@@ -706,21 +839,47 @@ export class DecompilerWorker {
|
|
|
706
839
|
await this.executeMainAnalysis(projectPath, projectKey, samplePath, options);
|
|
707
840
|
// 6. Execute post-processing function extraction with fallback chain.
|
|
708
841
|
// If extraction fails but main analysis succeeded, persist partial_success.
|
|
709
|
-
const extraction = await this.tryExtractFunctionsWithFallback(projectPath, projectKey, samplePath, options.timeout || 300000);
|
|
842
|
+
const extraction = await this.tryExtractFunctionsWithFallback(projectPath, projectKey, samplePath, options.timeout || 300000, options);
|
|
710
843
|
const analysisOutput = extraction.output;
|
|
711
844
|
const extractionWarnings = extraction.warnings || [];
|
|
712
845
|
const extractionAttempts = extraction.attempts || [];
|
|
713
|
-
|
|
714
|
-
|
|
846
|
+
let recoveredFunctions;
|
|
847
|
+
const combinedWarnings = [...extractionWarnings];
|
|
848
|
+
const analysisOutputHasFunctions = Boolean(analysisOutput && analysisOutput.functions.length > 0);
|
|
849
|
+
const shouldRecoverFromPE = !analysisOutput || !analysisOutputHasFunctions;
|
|
850
|
+
if (analysisOutput && !analysisOutputHasFunctions) {
|
|
851
|
+
combinedWarnings.push('Ghidra post-script extraction completed but returned zero functions; attempting PE metadata recovery from .pdata / exception directory.');
|
|
852
|
+
}
|
|
853
|
+
if (shouldRecoverFromPE) {
|
|
854
|
+
try {
|
|
855
|
+
recoveredFunctions = this.tryRecoverFunctionsFromPE(samplePath);
|
|
856
|
+
if (recoveredFunctions.functions.length > 0) {
|
|
857
|
+
combinedWarnings.push(`Recovered ${recoveredFunctions.functions.length} function candidates from PE exception metadata after Ghidra post-script extraction failed.`);
|
|
858
|
+
}
|
|
859
|
+
if (recoveredFunctions.warnings.length > 0) {
|
|
860
|
+
combinedWarnings.push(...recoveredFunctions.warnings);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
catch (recoveryError) {
|
|
864
|
+
combinedWarnings.push(`PE metadata recovery failed after Ghidra extraction failure: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const effectiveFunctions = analysisOutputHasFunctions
|
|
868
|
+
? analysisOutput?.functions || []
|
|
869
|
+
: recoveredFunctions?.functions || [];
|
|
870
|
+
const probeTarget = analysisOutputHasFunctions
|
|
871
|
+
? this.selectProbeTarget((analysisOutput?.functions || []))
|
|
715
872
|
: undefined;
|
|
716
873
|
let decompileProbe;
|
|
717
874
|
let cfgProbe;
|
|
718
|
-
if (
|
|
875
|
+
if (effectiveFunctions.length > 0) {
|
|
719
876
|
// 7. Store functions to database (Requirement 8.4)
|
|
720
|
-
await this.storeFunctions(sampleId,
|
|
877
|
+
await this.storeFunctions(sampleId, effectiveFunctions);
|
|
721
878
|
// 8. Store analysis artifact
|
|
722
879
|
const artifactId = randomUUID();
|
|
723
|
-
const artifactPath =
|
|
880
|
+
const artifactPath = analysisOutput
|
|
881
|
+
? `ghidra/functions_${projectKey}.json`
|
|
882
|
+
: `ghidra/recovered_functions_${projectKey}.json`;
|
|
724
883
|
const artifactFullPath = path.join(workspace.root, artifactPath);
|
|
725
884
|
// Ensure directory exists
|
|
726
885
|
const artifactDir = path.dirname(artifactFullPath);
|
|
@@ -728,22 +887,30 @@ export class DecompilerWorker {
|
|
|
728
887
|
fs.mkdirSync(artifactDir, { recursive: true });
|
|
729
888
|
}
|
|
730
889
|
// Write artifact
|
|
731
|
-
|
|
890
|
+
const artifactPayload = (analysisOutputHasFunctions ? analysisOutput : undefined) ||
|
|
891
|
+
{
|
|
892
|
+
program_name: path.basename(samplePath),
|
|
893
|
+
program_path: samplePath,
|
|
894
|
+
function_count: recoveredFunctions?.functions.length || 0,
|
|
895
|
+
functions: recoveredFunctions?.functions || [],
|
|
896
|
+
recovery: recoveredFunctions?.recoveryMetadata || {},
|
|
897
|
+
};
|
|
898
|
+
fs.writeFileSync(artifactFullPath, JSON.stringify(artifactPayload, null, 2));
|
|
732
899
|
// Compute artifact SHA256
|
|
733
900
|
const artifactSha256 = createHash('sha256')
|
|
734
|
-
.update(JSON.stringify(
|
|
901
|
+
.update(JSON.stringify(artifactPayload))
|
|
735
902
|
.digest('hex');
|
|
736
903
|
// Insert artifact record
|
|
737
904
|
this.database.insertArtifact({
|
|
738
905
|
id: artifactId,
|
|
739
906
|
sample_id: sampleId,
|
|
740
|
-
type: 'ghidra_functions',
|
|
907
|
+
type: analysisOutputHasFunctions ? 'ghidra_functions' : 'function_recovery',
|
|
741
908
|
path: artifactPath,
|
|
742
909
|
sha256: artifactSha256,
|
|
743
910
|
mime: 'application/json',
|
|
744
911
|
created_at: new Date().toISOString()
|
|
745
912
|
});
|
|
746
|
-
if (probeTarget) {
|
|
913
|
+
if (analysisOutput && probeTarget) {
|
|
747
914
|
const probeTimeoutMs = Math.max(5000, Math.min(15000, Math.floor((options.timeout || 300000) / 6)));
|
|
748
915
|
decompileProbe = await this.probeCapability('decompile', projectPath, projectKey, samplePath, probeTarget, probeTimeoutMs);
|
|
749
916
|
cfgProbe = await this.probeCapability('cfg', projectPath, projectKey, samplePath, probeTarget, probeTimeoutMs);
|
|
@@ -751,15 +918,21 @@ export class DecompilerWorker {
|
|
|
751
918
|
}
|
|
752
919
|
// 9. Update analysis status
|
|
753
920
|
const elapsedMs = Date.now() - startTime;
|
|
754
|
-
const status =
|
|
755
|
-
const functionCount =
|
|
756
|
-
|
|
921
|
+
const status = analysisOutputHasFunctions ? 'done' : 'partial_success';
|
|
922
|
+
const functionCount = analysisOutputHasFunctions
|
|
923
|
+
? analysisOutput?.function_count || analysisOutput?.functions.length || 0
|
|
924
|
+
: recoveredFunctions?.functions.length || 0;
|
|
925
|
+
const functionIndexRecovered = !analysisOutputHasFunctions && Boolean(recoveredFunctions && functionCount > 0);
|
|
926
|
+
const functionIndexReady = Boolean(analysisOutputHasFunctions && functionCount > 0);
|
|
757
927
|
const readiness = {
|
|
758
928
|
function_index: {
|
|
759
|
-
available: functionIndexReady,
|
|
760
|
-
status: functionIndexReady ? 'ready' : 'missing',
|
|
929
|
+
available: functionIndexReady || functionIndexRecovered,
|
|
930
|
+
status: functionIndexReady ? 'ready' : functionIndexRecovered ? 'degraded' : 'missing',
|
|
931
|
+
reason: functionIndexRecovered
|
|
932
|
+
? 'Function candidates were recovered from PE exception metadata (.pdata) after Ghidra post-script extraction failed.'
|
|
933
|
+
: undefined,
|
|
761
934
|
checked_at: new Date().toISOString(),
|
|
762
|
-
warnings:
|
|
935
|
+
warnings: combinedWarnings.length > 0 ? combinedWarnings : undefined,
|
|
763
936
|
},
|
|
764
937
|
decompile: functionIndexReady
|
|
765
938
|
? decompileProbe?.status ||
|
|
@@ -772,7 +945,9 @@ export class DecompilerWorker {
|
|
|
772
945
|
: {
|
|
773
946
|
available: false,
|
|
774
947
|
status: 'missing',
|
|
775
|
-
reason:
|
|
948
|
+
reason: functionIndexRecovered
|
|
949
|
+
? 'Function candidates were recovered from PE metadata only; Ghidra decompile readiness was not established.'
|
|
950
|
+
: 'Function index is unavailable, so decompile readiness was not probed.',
|
|
776
951
|
checked_at: new Date().toISOString(),
|
|
777
952
|
},
|
|
778
953
|
cfg: functionIndexReady
|
|
@@ -786,7 +961,9 @@ export class DecompilerWorker {
|
|
|
786
961
|
: {
|
|
787
962
|
available: false,
|
|
788
963
|
status: 'missing',
|
|
789
|
-
reason:
|
|
964
|
+
reason: functionIndexRecovered
|
|
965
|
+
? 'Function candidates were recovered from PE metadata only; Ghidra CFG readiness was not established.'
|
|
966
|
+
: 'Function index is unavailable, so CFG readiness was not probed.',
|
|
790
967
|
checked_at: new Date().toISOString(),
|
|
791
968
|
},
|
|
792
969
|
};
|
|
@@ -799,11 +976,16 @@ export class DecompilerWorker {
|
|
|
799
976
|
project_key: projectKey,
|
|
800
977
|
readiness,
|
|
801
978
|
function_extraction: {
|
|
802
|
-
status:
|
|
979
|
+
status: analysisOutputHasFunctions
|
|
980
|
+
? 'success'
|
|
981
|
+
: functionIndexRecovered
|
|
982
|
+
? 'recovered_via_smart_recover'
|
|
983
|
+
: 'failed',
|
|
803
984
|
script_used: extraction.scriptUsed,
|
|
804
|
-
warnings:
|
|
985
|
+
warnings: combinedWarnings,
|
|
805
986
|
attempts: extractionAttempts,
|
|
806
987
|
},
|
|
988
|
+
function_recovery: recoveredFunctions?.recoveryMetadata,
|
|
807
989
|
end_to_end_probe: {
|
|
808
990
|
target: probeTarget,
|
|
809
991
|
decompile: decompileProbe?.status,
|
|
@@ -841,7 +1023,7 @@ export class DecompilerWorker {
|
|
|
841
1023
|
functionCount,
|
|
842
1024
|
projectPath,
|
|
843
1025
|
status,
|
|
844
|
-
warnings:
|
|
1026
|
+
warnings: combinedWarnings.length > 0 ? combinedWarnings : undefined,
|
|
845
1027
|
readiness,
|
|
846
1028
|
};
|
|
847
1029
|
}
|
|
@@ -1126,8 +1308,8 @@ export class DecompilerWorker {
|
|
|
1126
1308
|
}
|
|
1127
1309
|
const resolved = this.resolveGhidraAnalysisForCapability(sampleId, 'decompile');
|
|
1128
1310
|
// 2. Get workspace and project paths
|
|
1129
|
-
|
|
1130
|
-
const samplePath = this.
|
|
1311
|
+
await this.workspaceManager.getWorkspace(sampleId);
|
|
1312
|
+
const samplePath = await this.resolveSamplePathForSample(sampleId);
|
|
1131
1313
|
// Verify sample file exists
|
|
1132
1314
|
if (!fs.existsSync(samplePath)) {
|
|
1133
1315
|
throw new Error(`Sample file not found: ${samplePath}`);
|
|
@@ -1533,8 +1715,8 @@ export class DecompilerWorker {
|
|
|
1533
1715
|
}
|
|
1534
1716
|
const resolved = this.resolveGhidraAnalysisForCapability(sampleId, 'cfg');
|
|
1535
1717
|
// 2. Get workspace and project paths
|
|
1536
|
-
|
|
1537
|
-
const samplePath = this.
|
|
1718
|
+
await this.workspaceManager.getWorkspace(sampleId);
|
|
1719
|
+
const samplePath = await this.resolveSamplePathForSample(sampleId);
|
|
1538
1720
|
// Verify sample file exists
|
|
1539
1721
|
if (!fs.existsSync(samplePath)) {
|
|
1540
1722
|
throw new Error(`Sample file not found: ${samplePath}`);
|
|
@@ -1628,8 +1810,8 @@ export class DecompilerWorker {
|
|
|
1628
1810
|
}
|
|
1629
1811
|
async searchFunctionsWithGhidra(sampleId, apiQuery, stringQuery, limit, timeout) {
|
|
1630
1812
|
const resolved = this.resolveGhidraAnalysisForCapability(sampleId, 'function_index');
|
|
1631
|
-
|
|
1632
|
-
const samplePath = this.
|
|
1813
|
+
await this.workspaceManager.getWorkspace(sampleId);
|
|
1814
|
+
const samplePath = await this.resolveSamplePathForSample(sampleId);
|
|
1633
1815
|
if (!fs.existsSync(samplePath)) {
|
|
1634
1816
|
throw new Error(`Sample file not found: ${samplePath}`);
|
|
1635
1817
|
}
|
package/dist/dynamic-trace.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface NormalizedDynamicTraceMemoryRegion {
|
|
|
24
24
|
base_address?: string;
|
|
25
25
|
size?: number;
|
|
26
26
|
protection?: string;
|
|
27
|
+
module_name?: string;
|
|
28
|
+
segment_name?: string;
|
|
27
29
|
indicators: string[];
|
|
28
30
|
}
|
|
29
31
|
export interface NormalizedDynamicTrace {
|
|
@@ -66,7 +68,11 @@ export interface DynamicTraceSummary {
|
|
|
66
68
|
high_signal_apis: string[];
|
|
67
69
|
memory_regions: string[];
|
|
68
70
|
region_types?: string[];
|
|
71
|
+
protections?: string[];
|
|
72
|
+
address_ranges?: string[];
|
|
73
|
+
region_owners?: string[];
|
|
69
74
|
observed_modules?: string[];
|
|
75
|
+
segment_names?: string[];
|
|
70
76
|
observed_strings?: string[];
|
|
71
77
|
stages: string[];
|
|
72
78
|
risk_hints: string[];
|
package/dist/dynamic-trace.js
CHANGED
|
@@ -221,6 +221,8 @@ function normalizeRegion(entry) {
|
|
|
221
221
|
base_address: normalizeAddress(obj.base_address ?? obj.base ?? obj.start),
|
|
222
222
|
size: readFirstNumber(obj, ['size']),
|
|
223
223
|
protection: readFirstString(obj, ['protection', 'protect']),
|
|
224
|
+
module_name: readFirstString(obj, ['module_name', 'module', 'image']),
|
|
225
|
+
segment_name: readFirstString(obj, ['segment_name', 'section', 'segment']),
|
|
224
226
|
indicators,
|
|
225
227
|
};
|
|
226
228
|
}
|
|
@@ -280,6 +282,7 @@ export function normalizeDynamicTrace(raw, options) {
|
|
|
280
282
|
const modules = dedupeStrings([
|
|
281
283
|
...toStringArray(record.modules),
|
|
282
284
|
...apiCalls.map((item) => item.module || ''),
|
|
285
|
+
...memoryRegions.map((item) => item.module_name || ''),
|
|
283
286
|
]);
|
|
284
287
|
const strings = dedupeStrings([
|
|
285
288
|
...toStringArray(record.strings),
|
|
@@ -379,6 +382,24 @@ export function summarizeDynamicTrace(trace) {
|
|
|
379
382
|
const highSignalApis = observedApis.filter((item) => HIGH_SIGNAL_APIS.has(normalizeApiName(item)));
|
|
380
383
|
const memoryRegions = trace.memory_regions.map((item) => item.purpose || item.region_type);
|
|
381
384
|
const regionTypes = trace.memory_regions.map((item) => item.region_type);
|
|
385
|
+
const protections = trace.memory_regions.map((item) => item.protection || '');
|
|
386
|
+
const addressRanges = trace.memory_regions
|
|
387
|
+
.map((item) => {
|
|
388
|
+
if (!item.base_address) {
|
|
389
|
+
return '';
|
|
390
|
+
}
|
|
391
|
+
if (!item.size) {
|
|
392
|
+
return item.base_address;
|
|
393
|
+
}
|
|
394
|
+
const start = Number.parseInt(item.base_address.replace(/^0x/i, ''), 16);
|
|
395
|
+
if (!Number.isFinite(start)) {
|
|
396
|
+
return item.base_address;
|
|
397
|
+
}
|
|
398
|
+
return `${item.base_address}-0x${(start + item.size).toString(16)}`;
|
|
399
|
+
})
|
|
400
|
+
.filter((item) => item.length > 0);
|
|
401
|
+
const regionOwners = trace.memory_regions.map((item) => item.module_name || '');
|
|
402
|
+
const segmentNames = trace.memory_regions.map((item) => item.segment_name || '');
|
|
382
403
|
const evidence = [];
|
|
383
404
|
evidence.push(trace.executed
|
|
384
405
|
? `Imported runtime trace observed ${trace.api_calls.length} unique API(s).`
|
|
@@ -389,6 +410,9 @@ export function summarizeDynamicTrace(trace) {
|
|
|
389
410
|
if (memoryRegions.length > 0) {
|
|
390
411
|
evidence.push(`Memory regions or plans: ${dedupeStrings(memoryRegions, 8).join(', ')}`);
|
|
391
412
|
}
|
|
413
|
+
if (protections.some((item) => item.length > 0)) {
|
|
414
|
+
evidence.push(`Observed protections: ${dedupeStrings(protections, 8).join(', ')}`);
|
|
415
|
+
}
|
|
392
416
|
if (trace.stages.length > 0) {
|
|
393
417
|
evidence.push(`Derived runtime stages: ${trace.stages.join(', ')}`);
|
|
394
418
|
}
|
|
@@ -406,7 +430,11 @@ export function summarizeDynamicTrace(trace) {
|
|
|
406
430
|
high_signal_apis: dedupeStrings(highSignalApis, 12),
|
|
407
431
|
memory_regions: dedupeStrings(memoryRegions, 12),
|
|
408
432
|
region_types: dedupeStrings(regionTypes, 12),
|
|
433
|
+
protections: dedupeStrings(protections, 12),
|
|
434
|
+
address_ranges: dedupeStrings(addressRanges, 12),
|
|
435
|
+
region_owners: dedupeStrings(regionOwners, 12),
|
|
409
436
|
observed_modules: dedupeStrings(trace.modules, 12),
|
|
437
|
+
segment_names: dedupeStrings(segmentNames, 12),
|
|
410
438
|
observed_strings: dedupeStrings(trace.strings, 12),
|
|
411
439
|
stages: trace.stages,
|
|
412
440
|
risk_hints: trace.risk_hints,
|
|
@@ -512,7 +540,26 @@ export async function loadDynamicTraceEvidence(workspaceManager, database, sampl
|
|
|
512
540
|
observed_apis: dedupeStrings(normalizedTraces.flatMap((item) => item.api_calls.map((entry) => entry.api)), 30),
|
|
513
541
|
memory_regions: dedupeStrings(normalizedTraces.flatMap((item) => item.memory_regions.map((entry) => entry.purpose || entry.region_type)), 20),
|
|
514
542
|
region_types: dedupeStrings(normalizedTraces.flatMap((item) => item.memory_regions.map((entry) => entry.region_type)), 20),
|
|
515
|
-
|
|
543
|
+
protections: dedupeStrings(normalizedTraces.flatMap((item) => item.memory_regions.map((entry) => entry.protection || '')), 20),
|
|
544
|
+
address_ranges: dedupeStrings(normalizedTraces.flatMap((item) => item.memory_regions.map((entry) => {
|
|
545
|
+
if (!entry.base_address) {
|
|
546
|
+
return '';
|
|
547
|
+
}
|
|
548
|
+
if (!entry.size) {
|
|
549
|
+
return entry.base_address;
|
|
550
|
+
}
|
|
551
|
+
const start = Number.parseInt(entry.base_address.replace(/^0x/i, ''), 16);
|
|
552
|
+
if (!Number.isFinite(start)) {
|
|
553
|
+
return entry.base_address;
|
|
554
|
+
}
|
|
555
|
+
return `${entry.base_address}-0x${(start + entry.size).toString(16)}`;
|
|
556
|
+
})), 20),
|
|
557
|
+
region_owners: dedupeStrings(normalizedTraces.flatMap((item) => item.memory_regions.map((entry) => entry.module_name || '')), 20),
|
|
558
|
+
observed_modules: dedupeStrings(normalizedTraces.flatMap((item) => [
|
|
559
|
+
...item.modules,
|
|
560
|
+
...item.memory_regions.map((entry) => entry.module_name || ''),
|
|
561
|
+
]), 20),
|
|
562
|
+
segment_names: dedupeStrings(normalizedTraces.flatMap((item) => item.memory_regions.map((entry) => entry.segment_name || '')), 20),
|
|
516
563
|
observed_strings: dedupeStrings(normalizedTraces.flatMap((item) => item.strings), 20),
|
|
517
564
|
stages: dedupeStrings(normalizedTraces.flatMap((item) => item.stages), 20),
|
|
518
565
|
risk_hints: dedupeStrings(normalizedTraces.flatMap((item) => item.risk_hints), 20),
|
|
@@ -605,7 +652,11 @@ export async function loadDynamicTraceEvidence(workspaceManager, database, sampl
|
|
|
605
652
|
high_signal_apis: dedupeStrings(highSignalApis, 12),
|
|
606
653
|
memory_regions: aggregated.memory_regions,
|
|
607
654
|
region_types: aggregated.region_types,
|
|
655
|
+
protections: aggregated.protections,
|
|
656
|
+
address_ranges: aggregated.address_ranges,
|
|
657
|
+
region_owners: aggregated.region_owners,
|
|
608
658
|
observed_modules: aggregated.observed_modules,
|
|
659
|
+
segment_names: aggregated.segment_names,
|
|
609
660
|
observed_strings: aggregated.observed_strings,
|
|
610
661
|
stages: aggregated.stages,
|
|
611
662
|
risk_hints: aggregated.risk_hints,
|
package/dist/ghidra-config.js
CHANGED
|
@@ -7,6 +7,22 @@ import path from 'path';
|
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
8
|
import { logger } from './logger.js';
|
|
9
9
|
import { buildRawCommandLine, decodeProcessStreams } from './process-output.js';
|
|
10
|
+
function getWindowsCommandInterpreter() {
|
|
11
|
+
const envCandidates = [process.env.ComSpec, process.env.COMSPEC].filter((value) => Boolean(value && value.trim()));
|
|
12
|
+
for (const candidate of envCandidates) {
|
|
13
|
+
if (fs.existsSync(candidate)) {
|
|
14
|
+
return candidate;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const systemRootCandidates = [process.env.SystemRoot, process.env.SYSTEMROOT].filter((value) => Boolean(value && value.trim()));
|
|
18
|
+
for (const systemRoot of systemRootCandidates) {
|
|
19
|
+
const candidate = path.join(systemRoot, 'System32', 'cmd.exe');
|
|
20
|
+
if (fs.existsSync(candidate)) {
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return 'cmd.exe';
|
|
25
|
+
}
|
|
10
26
|
function getPythonCommand(platform = process.platform) {
|
|
11
27
|
return platform === 'win32' ? 'python' : 'python3';
|
|
12
28
|
}
|
|
@@ -89,7 +105,7 @@ export function buildWindowsBatchInvocation(command, args) {
|
|
|
89
105
|
// without this, paths containing '&' are split and fail to execute.
|
|
90
106
|
const wrappedCommandLine = `"${commandLine}"`;
|
|
91
107
|
return {
|
|
92
|
-
command:
|
|
108
|
+
command: getWindowsCommandInterpreter(),
|
|
93
109
|
args: ['/d', '/s', '/c', wrappedCommandLine],
|
|
94
110
|
windowsVerbatimArguments: true,
|
|
95
111
|
};
|