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.
Files changed (134) hide show
  1. package/CLAUDE_INSTALLATION.md +129 -0
  2. package/CODEX_INSTALLATION.md +25 -1
  3. package/COPILOT_INSTALLATION.md +29 -1
  4. package/LICENSE +21 -21
  5. package/README.md +232 -110
  6. package/dist/analysis-provenance.d.ts +50 -2
  7. package/dist/analysis-provenance.js +1 -0
  8. package/dist/analysis-task-runner.js +92 -0
  9. package/dist/config.d.ts +35 -16
  10. package/dist/config.js +37 -3
  11. package/dist/database.js +115 -115
  12. package/dist/decompiler-worker.d.ts +10 -5
  13. package/dist/decompiler-worker.js +240 -58
  14. package/dist/dynamic-trace.d.ts +6 -0
  15. package/dist/dynamic-trace.js +52 -1
  16. package/dist/ghidra-config.js +17 -1
  17. package/dist/index.js +38 -8
  18. package/dist/pe-runtime-functions.d.ts +90 -0
  19. package/dist/pe-runtime-functions.js +452 -0
  20. package/dist/prompts/module-reconstruction-review.d.ts +5 -0
  21. package/dist/prompts/module-reconstruction-review.js +65 -0
  22. package/dist/runtime-correlation.d.ts +5 -0
  23. package/dist/runtime-correlation.js +179 -2
  24. package/dist/sample-workspace.d.ts +28 -0
  25. package/dist/sample-workspace.js +186 -0
  26. package/dist/semantic-name-suggestion-artifacts.d.ts +56 -0
  27. package/dist/semantic-name-suggestion-artifacts.js +87 -0
  28. package/dist/server.js +1 -1
  29. package/dist/setup-guidance.d.ts +73 -0
  30. package/dist/setup-guidance.js +211 -0
  31. package/dist/tools/artifact-read.d.ts +10 -10
  32. package/dist/tools/artifacts-diff.d.ts +8 -8
  33. package/dist/tools/artifacts-list.d.ts +16 -16
  34. package/dist/tools/attack-map.d.ts +8 -8
  35. package/dist/tools/binary-role-profile.d.ts +1197 -0
  36. package/dist/tools/binary-role-profile.js +575 -0
  37. package/dist/tools/code-function-explain-apply.d.ts +12 -12
  38. package/dist/tools/code-function-explain-prepare.d.ts +12 -12
  39. package/dist/tools/code-function-explain-review.d.ts +58 -58
  40. package/dist/tools/code-function-rename-apply.d.ts +12 -12
  41. package/dist/tools/code-function-rename-prepare.d.ts +14 -14
  42. package/dist/tools/code-function-rename-review.d.ts +66 -66
  43. package/dist/tools/code-functions-define.d.ts +337 -0
  44. package/dist/tools/code-functions-define.js +337 -0
  45. package/dist/tools/code-functions-list.js +1 -1
  46. package/dist/tools/code-functions-rank.js +1 -1
  47. package/dist/tools/code-functions-reconstruct.d.ts +368 -58
  48. package/dist/tools/code-functions-reconstruct.js +130 -10
  49. package/dist/tools/code-functions-smart-recover.d.ts +352 -0
  50. package/dist/tools/code-functions-smart-recover.js +190 -0
  51. package/dist/tools/code-module-review-apply.d.ts +233 -0
  52. package/dist/tools/code-module-review-apply.js +209 -0
  53. package/dist/tools/code-module-review-prepare.d.ts +756 -0
  54. package/dist/tools/code-module-review-prepare.js +343 -0
  55. package/dist/tools/code-module-review.d.ts +474 -0
  56. package/dist/tools/code-module-review.js +566 -0
  57. package/dist/tools/code-reconstruct-export.d.ts +193 -21
  58. package/dist/tools/code-reconstruct-export.js +578 -21
  59. package/dist/tools/code-reconstruct-plan.d.ts +20 -20
  60. package/dist/tools/com-role-profile.d.ts +353 -0
  61. package/dist/tools/com-role-profile.js +186 -0
  62. package/dist/tools/dll-export-profile.d.ts +625 -0
  63. package/dist/tools/dll-export-profile.js +230 -0
  64. package/dist/tools/dotnet-metadata-extract.d.ts +122 -122
  65. package/dist/tools/dotnet-reconstruct-export.d.ts +49 -49
  66. package/dist/tools/dotnet-types-list.d.ts +66 -66
  67. package/dist/tools/dynamic-dependencies.d.ts +144 -8
  68. package/dist/tools/dynamic-dependencies.js +20 -1
  69. package/dist/tools/dynamic-memory-import.js +105 -2
  70. package/dist/tools/ghidra-analyze.d.ts +20 -0
  71. package/dist/tools/ghidra-analyze.js +10 -2
  72. package/dist/tools/ghidra-health.d.ts +177 -0
  73. package/dist/tools/ghidra-health.js +28 -0
  74. package/dist/tools/ioc-export.d.ts +8 -8
  75. package/dist/tools/packer-detect.d.ts +14 -14
  76. package/dist/tools/packer-detect.js +6 -9
  77. package/dist/tools/pe-exports-extract.d.ts +8 -8
  78. package/dist/tools/pe-exports-extract.js +6 -9
  79. package/dist/tools/pe-fingerprint.d.ts +36 -36
  80. package/dist/tools/pe-imports-extract.d.ts +8 -8
  81. package/dist/tools/pe-imports-extract.js +6 -9
  82. package/dist/tools/pe-pdata-extract.d.ts +448 -0
  83. package/dist/tools/pe-pdata-extract.js +219 -0
  84. package/dist/tools/pe-symbols-recover.d.ts +227 -0
  85. package/dist/tools/pe-symbols-recover.js +316 -0
  86. package/dist/tools/report-generate.d.ts +15 -1
  87. package/dist/tools/report-generate.js +139 -16
  88. package/dist/tools/report-summarize.d.ts +2242 -8
  89. package/dist/tools/report-summarize.js +246 -10
  90. package/dist/tools/runtime-detect.d.ts +8 -8
  91. package/dist/tools/runtime-detect.js +2 -12
  92. package/dist/tools/rust-binary-analyze.d.ts +2047 -0
  93. package/dist/tools/rust-binary-analyze.js +443 -0
  94. package/dist/tools/sample-profile-get.d.ts +135 -16
  95. package/dist/tools/sample-profile-get.js +35 -32
  96. package/dist/tools/sandbox-execute.d.ts +10 -10
  97. package/dist/tools/strings-extract.d.ts +8 -8
  98. package/dist/tools/strings-extract.js +2 -12
  99. package/dist/tools/strings-floss-decode.d.ts +8 -8
  100. package/dist/tools/system-health.d.ts +172 -36
  101. package/dist/tools/system-health.js +14 -0
  102. package/dist/tools/system-setup-guide.d.ts +179 -0
  103. package/dist/tools/system-setup-guide.js +62 -0
  104. package/dist/tools/tool-help.d.ts +16 -16
  105. package/dist/tools/tool-help.js +172 -0
  106. package/dist/tools/yara-scan.d.ts +8 -8
  107. package/dist/types.d.ts +2 -0
  108. package/dist/workflows/function-explanation-review.d.ts +3034 -76
  109. package/dist/workflows/function-explanation-review.js +172 -42
  110. package/dist/workflows/function-index-recover.d.ts +333 -0
  111. package/dist/workflows/function-index-recover.js +291 -0
  112. package/dist/workflows/module-reconstruction-review.d.ts +3537 -0
  113. package/dist/workflows/module-reconstruction-review.js +354 -0
  114. package/dist/workflows/reconstruct.d.ts +2182 -223
  115. package/dist/workflows/reconstruct.js +575 -32
  116. package/dist/workflows/semantic-name-review.d.ts +1436 -85
  117. package/dist/workflows/semantic-name-review.js +150 -58
  118. package/dist/workflows/triage.d.ts +8 -8
  119. package/ghidra_scripts/DecompileFunction.java +487 -487
  120. package/ghidra_scripts/ExtractCFG.java +256 -256
  121. package/ghidra_scripts/ExtractFunctions.java +442 -442
  122. package/ghidra_scripts/README.md +112 -112
  123. package/ghidra_scripts/SearchFunctionReferences.java +380 -380
  124. package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -9
  125. package/helpers/DotNetMetadataProbe/Program.cs +566 -566
  126. package/install-to-claude.ps1 +432 -0
  127. package/install-to-codex.ps1 +256 -178
  128. package/install-to-copilot.ps1 +389 -303
  129. package/package.json +4 -2
  130. package/requirements.txt +9 -9
  131. package/workers/requirements-dynamic.txt +11 -11
  132. package/workers/requirements.txt +8 -8
  133. package/workers/speakeasy_compat.py +175 -175
  134. 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
- * Resolve sample file path in workspace/original.
255
- * Prefer legacy "sample.exe" name, then fall back to first regular file.
256
- */
257
- resolveSamplePath(originalDir) {
258
- const legacyPath = path.join(originalDir, 'sample.exe');
259
- if (fs.existsSync(legacyPath)) {
260
- return legacyPath;
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
- if (!fs.existsSync(originalDir)) {
263
- throw new Error(`Sample directory not found: ${originalDir}`);
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
- const files = fs
266
- .readdirSync(originalDir, { withFileTypes: true })
267
- .filter(entry => entry.isFile())
268
- .map(entry => entry.name)
269
- .sort((a, b) => a.localeCompare(b));
270
- if (files.length === 0) {
271
- throw new Error(`Sample file not found in workspace: ${originalDir}`);
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 path.join(originalDir, files[0]);
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
- return [projectPath, projectKey];
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
- ghidraConfig.scriptsDir,
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.resolveSamplePath(workspace.original);
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
- const probeTarget = analysisOutput
714
- ? this.selectProbeTarget(analysisOutput.functions)
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 (analysisOutput) {
875
+ if (effectiveFunctions.length > 0) {
719
876
  // 7. Store functions to database (Requirement 8.4)
720
- await this.storeFunctions(sampleId, analysisOutput.functions);
877
+ await this.storeFunctions(sampleId, effectiveFunctions);
721
878
  // 8. Store analysis artifact
722
879
  const artifactId = randomUUID();
723
- const artifactPath = `ghidra/functions_${projectKey}.json`;
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
- fs.writeFileSync(artifactFullPath, JSON.stringify(analysisOutput, null, 2));
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(analysisOutput))
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 = analysisOutput ? 'done' : 'partial_success';
755
- const functionCount = analysisOutput?.function_count || 0;
756
- const functionIndexReady = Boolean(analysisOutput && functionCount > 0);
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: extractionWarnings.length > 0 ? extractionWarnings : undefined,
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: 'Function index is unavailable, so decompile readiness was not probed.',
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: 'Function index is unavailable, so CFG readiness was not probed.',
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: analysisOutput ? 'success' : 'failed',
979
+ status: analysisOutputHasFunctions
980
+ ? 'success'
981
+ : functionIndexRecovered
982
+ ? 'recovered_via_smart_recover'
983
+ : 'failed',
803
984
  script_used: extraction.scriptUsed,
804
- warnings: extractionWarnings,
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: extractionWarnings.length > 0 ? extractionWarnings : undefined,
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
- const workspace = await this.workspaceManager.getWorkspace(sampleId);
1130
- const samplePath = this.resolveSamplePath(workspace.original);
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
- const workspace = await this.workspaceManager.getWorkspace(sampleId);
1537
- const samplePath = this.resolveSamplePath(workspace.original);
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
- const workspace = await this.workspaceManager.getWorkspace(sampleId);
1632
- const samplePath = this.resolveSamplePath(workspace.original);
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
  }
@@ -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[];
@@ -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
- observed_modules: dedupeStrings(normalizedTraces.flatMap((item) => item.modules), 20),
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,
@@ -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: 'cmd.exe',
108
+ command: getWindowsCommandInterpreter(),
93
109
  args: ['/d', '/s', '/c', wrappedCommandLine],
94
110
  windowsVerbatimArguments: true,
95
111
  };