rea-agents 1.6.0 → 2.0.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/README.md +157 -66
- package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
- package/bridge/hopper_bridge.py +13 -3
- package/dist/application/AbortablePromise.js +25 -0
- package/dist/application/AnalysisProvider.js +3 -0
- package/dist/application/AnalysisProviderEvaluation.js +60 -0
- package/dist/application/AnalysisProviderRegistry.js +228 -0
- package/dist/application/AnalysisSnapshotCache.js +41 -21
- package/dist/application/AnalysisSnapshotFiles.js +5 -6
- package/dist/application/BinarySession.js +218 -101
- package/dist/application/CapabilityInventory.js +18 -0
- package/dist/application/CompositeProvider.js +20 -8
- package/dist/application/CrossVersionInvestigation.js +10 -6
- package/dist/application/DirectAnalysis.js +49 -35
- package/dist/application/Doctor.js +110 -8
- package/dist/application/ElectronBoundaryAnalysis.js +96 -0
- package/dist/application/ElectronBoundaryGraph.js +9 -0
- package/dist/application/ElectronBoundaryGraphContext.js +62 -0
- package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
- package/dist/application/ElectronBoundaryGraphNative.js +86 -0
- package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
- package/dist/application/EnhancedTools.js +39 -14
- package/dist/application/InvestigationProviders.js +41 -0
- package/dist/application/JavaScriptApplicationEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
- package/dist/application/JavaScriptApplicationService.js +74 -0
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
- package/dist/application/JavaScriptArtifactAnalysis.js +305 -0
- package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
- package/dist/application/JavaScriptArtifactFiles.js +231 -0
- package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
- package/dist/application/JavaScriptArtifactGraphBuilder.js +144 -0
- package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
- package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
- package/dist/application/JavaScriptArtifactGraphEvidence.js +99 -0
- package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
- package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
- package/dist/application/JavaScriptArtifactReconstruction.js +77 -0
- package/dist/application/JavaScriptArtifactReconstructionInput.js +18 -0
- package/dist/application/JavaScriptReplayPlanning.js +201 -0
- package/dist/application/JavaScriptReplayService.js +266 -0
- package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
- package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
- package/dist/application/MacHopper.js +20 -4
- package/dist/application/ManagedApplicationGraphService.js +55 -0
- package/dist/application/ManagedMemberComparisonService.js +122 -0
- package/dist/application/ManagedNativeVerificationService.js +39 -0
- package/dist/application/ManagedReconstructionService.js +58 -0
- package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
- package/dist/application/PermissionAuthority.js +1 -1
- package/dist/application/ProcessCaptureLifecycle.js +1 -1
- package/dist/application/ProcessHarness.js +1 -1
- package/dist/application/ProcessSampling.js +16 -12
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/SessionProviderRouter.js +219 -0
- package/dist/application/Setup.js +84 -60
- package/dist/application/SetupClients.js +1 -1
- package/dist/application/SetupPlan.js +9 -2
- package/dist/application/SupportedClients.js +40 -16
- package/dist/application/Upgrade.js +1 -1
- package/dist/application/runtime.js +11 -6
- package/dist/artifacts/AsarArtifactReader.js +23 -3
- package/dist/browser/CdpBrowserProvider.js +4 -2
- package/dist/browser/CdpCaptureEvents.js +49 -0
- package/dist/browser/CdpElectronInspection.js +106 -162
- package/dist/browser/CdpElectronProvider.js +9 -3
- package/dist/browser/CdpElectronScriptEvents.js +69 -0
- package/dist/browser/CdpElectronScripts.js +94 -0
- package/dist/browser/CdpElectronWorkers.js +56 -0
- package/dist/browser/CdpEndpoint.js +14 -1
- package/dist/browser/CdpPageCapture.js +8 -4
- package/dist/catalogIdentity.js +6 -42
- package/dist/cli.js +446 -34
- package/dist/cliApplicationCommands.js +62 -0
- package/dist/cliBrowserAdvancedCommands.js +5 -4
- package/dist/cliBrowserCommands.js +5 -4
- package/dist/cliCommandNames.js +56 -0
- package/dist/cliElectronCommands.js +106 -3
- package/dist/cliEvidenceCommands.js +4 -3
- package/dist/cliInvestigationCommands.js +2 -1
- package/dist/cliJsonInput.js +54 -0
- package/dist/cliPolicyCommands.js +2 -1
- package/dist/cliProcessCommands.js +3 -2
- package/dist/config.js +101 -2
- package/dist/contracts/applicationToolContracts.js +91 -0
- package/dist/contracts/artifactToolContracts.js +5 -9
- package/dist/contracts/browserToolContracts.js +14 -62
- package/dist/contracts/electronToolContracts.js +45 -19
- package/dist/contracts/enhancedInputs.js +2 -2
- package/dist/contracts/errorSchemas.js +1 -0
- package/dist/contracts/investigationExamples.js +0 -6
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
- package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
- package/dist/contracts/managedToolContracts.js +153 -0
- package/dist/contracts/managedWorkflowExamples.js +346 -0
- package/dist/contracts/managedWorkflowToolContracts.js +203 -0
- package/dist/contracts/nativeToolContracts.js +2 -6
- package/dist/contracts/promptContracts.js +18 -2
- package/dist/contracts/providerSelection.js +28 -0
- package/dist/contracts/sessionLifecycleInputs.js +2 -0
- package/dist/contracts/toolContractExamples.js +10 -25
- package/dist/contracts/toolContracts.js +69 -65
- package/dist/contracts/toolEffects.js +160 -0
- package/dist/contracts/toolOutputSchemas.js +152 -42
- package/dist/doctorRuntime.js +47 -0
- package/dist/domain/analysisProfile.js +78 -0
- package/dist/domain/analysisSnapshot.js +92 -46
- package/dist/domain/artifactComparison.js +3 -4
- package/dist/domain/artifactGraph.js +1 -0
- package/dist/domain/binaryTarget.js +4 -29
- package/dist/domain/browserObservation.js +3 -0
- package/dist/domain/bundleComparison.js +4 -4
- package/dist/domain/electronObservation.js +13 -0
- package/dist/domain/electronStaticAnalysis.js +9 -0
- package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
- package/dist/domain/electronStaticAnalysisIpc.js +171 -0
- package/dist/domain/electronStaticAnalysisNative.js +164 -0
- package/dist/domain/electronStaticAnalysisTypes.js +1 -0
- package/dist/domain/electronStaticAnalysisValues.js +103 -0
- package/dist/domain/errors.js +148 -7
- package/dist/domain/evidence.js +44 -5
- package/dist/domain/evidenceBundle.js +9 -7
- package/dist/domain/functionComparison.js +9 -3
- package/dist/domain/functionComparisonNormalization.js +37 -0
- package/dist/domain/functionComparisonSchemas.js +3 -8
- package/dist/domain/functionDossierEvidence.js +4 -1
- package/dist/domain/hopperValues.js +66 -9
- package/dist/domain/inputIssueProjection.js +52 -0
- package/dist/domain/investigationWorkspace.js +2 -1
- package/dist/domain/javascriptApplicationAnalysis.js +155 -0
- package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
- package/dist/domain/javascriptApplicationEvidenceSchemas.js +379 -0
- package/dist/domain/javascriptApplicationGraph.js +282 -0
- package/dist/domain/javascriptApplicationGraphSchemas.js +139 -0
- package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
- package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
- package/dist/domain/javascriptApplicationVersionItems.js +221 -0
- package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
- package/dist/domain/javascriptAstFingerprint.js +143 -0
- package/dist/domain/javascriptFeatureSeed.js +140 -0
- package/dist/domain/javascriptFeatureTrace.js +232 -0
- package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
- package/dist/domain/javascriptFeatureTraversal.js +88 -0
- package/dist/domain/javascriptNativeHandoff.js +85 -0
- package/dist/domain/javascriptReplay.js +361 -0
- package/dist/domain/javascriptRuntimeLoadState.js +106 -0
- package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
- package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
- package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
- package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
- package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
- package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
- package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
- package/dist/domain/javascriptStaticAnalysis.js +346 -0
- package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
- package/dist/domain/javascriptStaticAnalysisHelpers.js +274 -0
- package/dist/domain/javascriptStaticAnalysisState.js +23 -0
- package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
- package/dist/domain/managedApplicationGraph.js +556 -0
- package/dist/domain/managedArtifact.js +444 -0
- package/dist/domain/managedMemberComparison.js +565 -0
- package/dist/domain/managedNativeVerification.js +408 -0
- package/dist/domain/managedReconstruction.js +183 -0
- package/dist/domain/managedRuntimeCorrelation.js +175 -0
- package/dist/domain/reconstructionVerificationSchemas.js +1 -3
- package/dist/dotnet/ManagedArtifactInspector.js +318 -0
- package/dist/dotnet/ManagedMemberInspector.js +887 -0
- package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
- package/dist/dotnet/ManagedMetadataInventory.js +368 -0
- package/dist/dotnet/ManagedMetadataLayout.js +322 -0
- package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
- package/dist/dotnet/ManagedPeReader.js +157 -0
- package/dist/dotnet/ManagedReaderFailure.js +10 -0
- package/dist/dotnet/ManagedStaticProvider.js +203 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
- package/dist/ghidra/GhidraClient.js +400 -0
- package/dist/ghidra/GhidraClientTypes.js +1 -0
- package/dist/ghidra/GhidraDefaults.js +20 -0
- package/dist/ghidra/GhidraDiagnostics.js +43 -0
- package/dist/ghidra/GhidraDoctor.js +59 -0
- package/dist/ghidra/GhidraFunctionValues.js +264 -0
- package/dist/ghidra/GhidraInstallation.js +239 -0
- package/dist/ghidra/GhidraInventoryValues.js +217 -0
- package/dist/ghidra/GhidraLauncher.js +139 -0
- package/dist/ghidra/GhidraProvider.js +366 -0
- package/dist/ghidra/GhidraRequestQueue.js +98 -0
- package/dist/ghidra/GhidraResponseBuffer.js +31 -0
- package/dist/ghidra/GhidraResponseRouter.js +24 -0
- package/dist/ghidra/GhidraSessionError.js +39 -0
- package/dist/ghidra/GhidraSessionValues.js +62 -0
- package/dist/ghidra/GhidraSocketConnection.js +46 -0
- package/dist/ghidra/protocol.js +69 -0
- package/dist/hopper/BridgeLauncher.js +19 -54
- package/dist/hopper/HopperAnalysisProfile.js +106 -0
- package/dist/hopper/HopperClient.js +154 -162
- package/dist/hopper/HopperProvider.js +99 -7
- package/dist/hopper/HopperSessionValues.js +27 -0
- package/dist/hopper/HopperSocketConnection.js +31 -0
- package/dist/main.js +14 -0
- package/dist/native/NativeMacOSProvider.js +3 -1
- package/dist/process/PendingOperations.js +59 -0
- package/dist/process/PrivateRuntimeRoot.js +33 -0
- package/dist/process/ProviderDeadline.js +95 -0
- package/dist/process/ProviderProcess.js +275 -0
- package/dist/replay/JavaScriptReplayWorker.js +286 -0
- package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
- package/dist/replay/LinuxRuntimeClosure.js +46 -0
- package/dist/replay/LinuxSeccompPolicy.js +40 -0
- package/dist/replay/ReplayWorkerProtocol.js +55 -0
- package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
- package/dist/server/createServer.js +53 -0
- package/dist/server/promptCompletion.js +9 -3
- package/dist/server/registerApplicationTools.js +115 -0
- package/dist/server/registerArtifactComparisonTool.js +16 -9
- package/dist/server/registerBrowserTools.js +33 -8
- package/dist/server/registerBundleComparisonTool.js +17 -20
- package/dist/server/registerElectronTools.js +45 -3
- package/dist/server/registerEnhancedTools.js +67 -19
- package/dist/server/registerEvidenceTools.js +5 -1
- package/dist/server/registerFunctionComparisonTool.js +14 -7
- package/dist/server/registerInvestigationTools.js +21 -17
- package/dist/server/registerManagedTools.js +6 -0
- package/dist/server/registerManagedWorkflowTools.js +277 -0
- package/dist/server/registerOfficialTools.js +11 -8
- package/dist/server/registerProcessComparisonTool.js +25 -47
- package/dist/server/registerSessionStatusTool.js +7 -7
- package/dist/server/registerSessionTools.js +57 -87
- package/dist/server/sessionEvidence.js +14 -19
- package/dist/server/toolInputValidation.js +77 -0
- package/dist/server/toolRegistrationOptions.js +79 -2
- package/dist/server/toolResult.js +25 -17
- package/package.json +17 -7
- package/scripts/rea.mjs +36 -2
- package/skills/rea-analysis/SKILL.md +59 -3
- /package/dist/{application → process}/ProcessOwnership.js +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { createAnalysisExecution, } from "../application/AnalysisProvider.js";
|
|
3
|
+
import { AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisInputError, AnalysisTimeoutError, ProviderAdapterError, } from "../domain/errors.js";
|
|
4
|
+
import { err, ok } from "../domain/result.js";
|
|
5
|
+
import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, } from "../contracts/toolContracts.js";
|
|
6
|
+
import { GhidraClient } from "./GhidraClient.js";
|
|
7
|
+
import { GHIDRA_ANALYSIS_TIMEOUT_SECONDS, GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS, GHIDRA_DECOMPILE_TIMEOUT_SECONDS, GHIDRA_MAX_LINE_BYTES, GHIDRA_REQUEST_TIMEOUT_MS, GHIDRA_STARTUP_TIMEOUT_MS, } from "./GhidraDefaults.js";
|
|
8
|
+
import { GHIDRA_FUNCTION_OPERATIONS, isGhidraFunctionOperation, parseGhidraFunctionInput, parseGhidraFunctionResult, } from "./GhidraFunctionValues.js";
|
|
9
|
+
import { GHIDRA_INVENTORY_OPERATIONS, isGhidraInventoryOperation, parseGhidraInventoryInput, parseGhidraInventoryResult, } from "./GhidraInventoryValues.js";
|
|
10
|
+
import { ghidraInstallationDiagnostics, inspectGhidraInstallation, } from "./GhidraInstallation.js";
|
|
11
|
+
import { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
|
|
12
|
+
import { resolveGhidraAnalysisProfile } from "./GhidraAnalysisProfile.js";
|
|
13
|
+
/** Public identity committed by every Ghidra-backed observation. */
|
|
14
|
+
export const GHIDRA_PROVIDER_IDENTITY = Object.freeze({
|
|
15
|
+
id: "ghidra",
|
|
16
|
+
name: "Ghidra",
|
|
17
|
+
version: null,
|
|
18
|
+
});
|
|
19
|
+
const providerContractByName = new Map([...OFFICIAL_TOOL_CONTRACTS, ...ENHANCED_TOOL_CONTRACTS].map((contract) => [
|
|
20
|
+
contract.name,
|
|
21
|
+
contract,
|
|
22
|
+
]));
|
|
23
|
+
/** Provider-neutral read-only contracts implemented by the Ghidra adapter. */
|
|
24
|
+
export const GHIDRA_PROVIDER_TOOL_CONTRACTS = Object.freeze([...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].map((operation) => {
|
|
25
|
+
const contract = providerContractByName.get(operation);
|
|
26
|
+
if (contract === undefined)
|
|
27
|
+
throw new TypeError(`Missing provider-neutral contract for ${operation}`);
|
|
28
|
+
return contract;
|
|
29
|
+
}));
|
|
30
|
+
const PAGINATED_OPERATIONS = new Set([
|
|
31
|
+
"list_names",
|
|
32
|
+
"list_procedures",
|
|
33
|
+
"list_strings",
|
|
34
|
+
"search_procedures",
|
|
35
|
+
"search_strings",
|
|
36
|
+
"procedure_references",
|
|
37
|
+
"analyze_function",
|
|
38
|
+
]);
|
|
39
|
+
const SEARCH_OPERATIONS = new Set([
|
|
40
|
+
"search_procedures",
|
|
41
|
+
"search_strings",
|
|
42
|
+
]);
|
|
43
|
+
const DECOMPILE_OPERATIONS = new Set([
|
|
44
|
+
"procedure_pseudo_code",
|
|
45
|
+
"analyze_function",
|
|
46
|
+
]);
|
|
47
|
+
const healthLimitations = Object.freeze([
|
|
48
|
+
"The session serves operations only after default Ghidra auto-analysis completes; an analysis timeout fails the open instead of exposing partial results.",
|
|
49
|
+
"The imported Program and temporary project are ephemeral, read-only to REA, and deleted on close.",
|
|
50
|
+
]);
|
|
51
|
+
const limitationsFor = (operation) => {
|
|
52
|
+
const common = [
|
|
53
|
+
...healthLimitations,
|
|
54
|
+
"Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.",
|
|
55
|
+
];
|
|
56
|
+
switch (operation) {
|
|
57
|
+
case "list_documents":
|
|
58
|
+
return [
|
|
59
|
+
...common,
|
|
60
|
+
"A headless Ghidra session contains exactly one imported Program, unlike Hopper's multi-document GUI session.",
|
|
61
|
+
];
|
|
62
|
+
case "list_names":
|
|
63
|
+
return [
|
|
64
|
+
...common,
|
|
65
|
+
"The symbol inventory includes memory and external symbols, including dynamic symbols, but excludes variable and no-address namespace records.",
|
|
66
|
+
];
|
|
67
|
+
case "list_procedures":
|
|
68
|
+
case "procedure_address":
|
|
69
|
+
return [
|
|
70
|
+
...common,
|
|
71
|
+
"External functions and local thunks are distinct; procedure metadata identifies both and preserves a thunk target when Ghidra resolves one.",
|
|
72
|
+
];
|
|
73
|
+
case "list_strings":
|
|
74
|
+
return [
|
|
75
|
+
...common,
|
|
76
|
+
"Only Ghidra-defined string Data is observed; charset is reported, while a non-missing terminator cannot distinguish a present terminator from a fixed or Pascal layout.",
|
|
77
|
+
"Returned values are bounded to 1,024 Unicode code points and mark value_truncated instead of silently crossing the wire budget.",
|
|
78
|
+
];
|
|
79
|
+
case "list_segments":
|
|
80
|
+
return [
|
|
81
|
+
...common,
|
|
82
|
+
"Memory-block end addresses are exclusive; permissions come from Ghidra MemoryBlock flags rather than inference from section names.",
|
|
83
|
+
];
|
|
84
|
+
case "search_procedures":
|
|
85
|
+
case "search_strings":
|
|
86
|
+
return [
|
|
87
|
+
...common,
|
|
88
|
+
"Literal search enforces 1,000,000 cumulative work units; regex mode also accepts only a conservative finite Java-regex subset with 10,000 static paths and 4,096 UTF-16 code units per candidate.",
|
|
89
|
+
];
|
|
90
|
+
case "procedure_pseudo_code":
|
|
91
|
+
return [
|
|
92
|
+
...common,
|
|
93
|
+
"Pseudocode is Ghidra decompiler output, not original source and not text-equivalent to Hopper output; each decompile has a 30-second native deadline.",
|
|
94
|
+
"External functions and functions without an analyzable body return null; other decompiler failures remain explicit.",
|
|
95
|
+
];
|
|
96
|
+
case "procedure_assembly":
|
|
97
|
+
return [
|
|
98
|
+
...common,
|
|
99
|
+
"Assembly is Ghidra Listing text and fails rather than silently truncating when the 100,000-instruction or 1 MiB wire bound is exceeded.",
|
|
100
|
+
];
|
|
101
|
+
case "procedure_callers":
|
|
102
|
+
case "procedure_callees":
|
|
103
|
+
return [
|
|
104
|
+
...common,
|
|
105
|
+
"Only resolved Ghidra call references are returned; unresolved computed or indirect calls remain unknown, while function classifications distinguish thunks and externals.",
|
|
106
|
+
];
|
|
107
|
+
case "xrefs":
|
|
108
|
+
return [
|
|
109
|
+
...common,
|
|
110
|
+
"The direct address list projects exact Ghidra references to one address but does not expose their kinds; procedure_references and analyze_function preserve available kind metadata.",
|
|
111
|
+
"Synthetic Ghidra entry-point references without actionable memory sources are omitted.",
|
|
112
|
+
];
|
|
113
|
+
case "procedure_references":
|
|
114
|
+
return [
|
|
115
|
+
...common,
|
|
116
|
+
"Reference kinds are direct Ghidra ReferenceManager observations; unresolved computed flows without a target are absent and remain unknown.",
|
|
117
|
+
"Synthetic Ghidra entry-point references without actionable memory sources are omitted.",
|
|
118
|
+
"Instruction scans stop at max_instructions; a truncated scan reports an unknown total and no false continuation.",
|
|
119
|
+
];
|
|
120
|
+
case "analyze_function":
|
|
121
|
+
return [
|
|
122
|
+
...common,
|
|
123
|
+
"The dossier combines Ghidra FunctionManager, Listing, ReferenceManager, BasicBlockModel, and decompiler observations; provider-specific pseudocode and assembly are not cross-provider text invariants.",
|
|
124
|
+
"Resolved reference metadata identifies computed, indirect, external, call, jump, and data edges; unresolved targetless flows remain unknown, and function classifications distinguish thunks and externals.",
|
|
125
|
+
"Synthetic Ghidra entry-point references without actionable memory sources are omitted.",
|
|
126
|
+
"The Java bridge serializes one function request per Program through a bounded 32-request queue and applies a 30-second native decompilation deadline.",
|
|
127
|
+
];
|
|
128
|
+
default:
|
|
129
|
+
return common;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const CAPABILITIES = Object.freeze(GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
|
|
133
|
+
provider: GHIDRA_PROVIDER_IDENTITY,
|
|
134
|
+
operation: contract.name,
|
|
135
|
+
inputContractVersion: 1,
|
|
136
|
+
outputContractVersion: 1,
|
|
137
|
+
available: true,
|
|
138
|
+
reason: null,
|
|
139
|
+
pagination: PAGINATED_OPERATIONS.has(contract.name)
|
|
140
|
+
? "offset"
|
|
141
|
+
: "none",
|
|
142
|
+
exhaustive: !PAGINATED_OPERATIONS.has(contract.name),
|
|
143
|
+
effects: Object.freeze({
|
|
144
|
+
mutatesArtifact: false,
|
|
145
|
+
launchesProcess: true,
|
|
146
|
+
mayShowUi: false,
|
|
147
|
+
mayAccessNetwork: false,
|
|
148
|
+
mayWriteFilesystem: true,
|
|
149
|
+
changesPermissions: false,
|
|
150
|
+
requiresRoot: false,
|
|
151
|
+
}),
|
|
152
|
+
limits: Object.freeze({
|
|
153
|
+
maxResults: PAGINATED_OPERATIONS.has(contract.name)
|
|
154
|
+
? SEARCH_OPERATIONS.has(contract.name)
|
|
155
|
+
? 100
|
|
156
|
+
: 500
|
|
157
|
+
: null,
|
|
158
|
+
maxPayloadBytes: GHIDRA_MAX_LINE_BYTES,
|
|
159
|
+
timeoutMs: DECOMPILE_OPERATIONS.has(contract.name)
|
|
160
|
+
? GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS
|
|
161
|
+
: GHIDRA_REQUEST_TIMEOUT_MS,
|
|
162
|
+
}),
|
|
163
|
+
limitations: Object.freeze(limitationsFor(contract.name)),
|
|
164
|
+
})));
|
|
165
|
+
const SUPPORTED_ARCHITECTURES = new Set(["x86", "x86_64", "arm", "arm64"]);
|
|
166
|
+
const SUPPORTED_FORMATS = new Set(["elf", "pe", "mach-o"]);
|
|
167
|
+
/** Linux Ghidra candidate backed by an isolated read-only headless import. */
|
|
168
|
+
export class GhidraProvider {
|
|
169
|
+
config;
|
|
170
|
+
logger;
|
|
171
|
+
installationHost;
|
|
172
|
+
clientFactory;
|
|
173
|
+
#installation;
|
|
174
|
+
constructor(config, logger, installationHost, clientFactory = (options) => new GhidraClient(options)) {
|
|
175
|
+
this.config = config;
|
|
176
|
+
this.logger = logger;
|
|
177
|
+
this.installationHost = installationHost;
|
|
178
|
+
this.clientFactory = clientFactory;
|
|
179
|
+
}
|
|
180
|
+
identity() {
|
|
181
|
+
return GHIDRA_PROVIDER_IDENTITY;
|
|
182
|
+
}
|
|
183
|
+
capabilities() {
|
|
184
|
+
return CAPABILITIES;
|
|
185
|
+
}
|
|
186
|
+
inspectAvailability() {
|
|
187
|
+
const installation = this.#inspectInstallation();
|
|
188
|
+
const diagnostics = ghidraInstallationDiagnostics(installation);
|
|
189
|
+
return installation.available
|
|
190
|
+
? {
|
|
191
|
+
status: "available",
|
|
192
|
+
code: null,
|
|
193
|
+
reason: null,
|
|
194
|
+
diagnostics,
|
|
195
|
+
}
|
|
196
|
+
: {
|
|
197
|
+
status: "unavailable",
|
|
198
|
+
code: installation.rejectionCode ?? "not_configured",
|
|
199
|
+
reason: installation.reason ?? "Ghidra installation is not available.",
|
|
200
|
+
diagnostics,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
inspectTargetSupport(target) {
|
|
204
|
+
const diagnostics = {
|
|
205
|
+
target_kind: target.kind,
|
|
206
|
+
target_format: target.format,
|
|
207
|
+
architecture: target.architecture ?? null,
|
|
208
|
+
};
|
|
209
|
+
if (target.kind !== "executable")
|
|
210
|
+
return {
|
|
211
|
+
status: "unsupported",
|
|
212
|
+
code: "target_kind_unsupported",
|
|
213
|
+
reason: `Ghidra v1 imports executable targets, not ${target.kind} targets.`,
|
|
214
|
+
diagnostics,
|
|
215
|
+
};
|
|
216
|
+
if (!SUPPORTED_FORMATS.has(target.format))
|
|
217
|
+
return {
|
|
218
|
+
status: "unsupported",
|
|
219
|
+
code: "target_format_unsupported",
|
|
220
|
+
reason: `Ghidra v1 does not import ${target.format} through this adapter.`,
|
|
221
|
+
diagnostics,
|
|
222
|
+
};
|
|
223
|
+
if (target.architecture === undefined ||
|
|
224
|
+
!SUPPORTED_ARCHITECTURES.has(target.architecture))
|
|
225
|
+
return {
|
|
226
|
+
status: "unsupported",
|
|
227
|
+
code: "architecture_unsupported",
|
|
228
|
+
reason: "Ghidra v1 requires a concrete x86, x86_64, arm, or arm64 target architecture.",
|
|
229
|
+
diagnostics,
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
status: "supported",
|
|
233
|
+
code: null,
|
|
234
|
+
reason: null,
|
|
235
|
+
diagnostics,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
resolveAnalysisProfile(target, options) {
|
|
239
|
+
return resolveGhidraAnalysisProfile(target, GHIDRA_PROVIDER_IDENTITY, this.#inspectInstallation(), options?.signal);
|
|
240
|
+
}
|
|
241
|
+
createClient(target, profile) {
|
|
242
|
+
const installation = this.#inspectInstallation();
|
|
243
|
+
const prerequisites = ghidraClientPrerequisites(target, profile, installation);
|
|
244
|
+
if (!prerequisites.ok)
|
|
245
|
+
return unavailableClient(prerequisites.error);
|
|
246
|
+
const committedProfile = prerequisites.value.profile;
|
|
247
|
+
const client = this.clientFactory({
|
|
248
|
+
launcher: new GhidraHeadlessLauncher({
|
|
249
|
+
analyzeHeadlessPath: prerequisites.value.analyzeHeadlessPath,
|
|
250
|
+
...(this.config.ghidraJavaHome === undefined
|
|
251
|
+
? {}
|
|
252
|
+
: { javaHome: this.config.ghidraJavaHome }),
|
|
253
|
+
bridgeScriptPath: fileURLToPath(new URL("../../bridge/ghidra/ReaGhidraBridge.java", import.meta.url)),
|
|
254
|
+
}),
|
|
255
|
+
targetPath: target.path,
|
|
256
|
+
providerVersion: prerequisites.value.providerVersion,
|
|
257
|
+
profileDigest: committedProfile.digest,
|
|
258
|
+
logger: this.logger.child({ layer: "ghidra-bridge" }),
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
execute: async (operation, parameters, options) => {
|
|
262
|
+
if (operation !== "health" &&
|
|
263
|
+
!isGhidraInventoryOperation(operation) &&
|
|
264
|
+
!isGhidraFunctionOperation(operation))
|
|
265
|
+
return err(new AnalysisCapabilityUnavailableError(GHIDRA_PROVIDER_IDENTITY.id, operation, "The Ghidra adapter does not declare this operation."));
|
|
266
|
+
if (operation === "health") {
|
|
267
|
+
const started = await client.start(options?.signal);
|
|
268
|
+
if (!started.ok)
|
|
269
|
+
return err(projectSessionError(operation, started.error));
|
|
270
|
+
return ok(createAnalysisExecution(started.value, committedProfile.provider, {
|
|
271
|
+
analysisProfile: committedProfile,
|
|
272
|
+
limitations: healthLimitations,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
const input = isGhidraFunctionOperation(operation)
|
|
276
|
+
? parseGhidraFunctionInput(operation, parameters)
|
|
277
|
+
: parseGhidraInventoryInput(operation, parameters);
|
|
278
|
+
if (!input.ok)
|
|
279
|
+
return input;
|
|
280
|
+
const called = await client.callTool(operation, input.value, {
|
|
281
|
+
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
|
282
|
+
...(DECOMPILE_OPERATIONS.has(operation)
|
|
283
|
+
? { timeoutMs: GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS }
|
|
284
|
+
: {}),
|
|
285
|
+
});
|
|
286
|
+
if (!called.ok)
|
|
287
|
+
return err(projectSessionError(operation, called.error));
|
|
288
|
+
const result = isGhidraFunctionOperation(operation)
|
|
289
|
+
? parseGhidraFunctionResult(operation, called.value)
|
|
290
|
+
: parseGhidraInventoryResult(operation, called.value);
|
|
291
|
+
if (!result.ok)
|
|
292
|
+
return result;
|
|
293
|
+
return ok(createAnalysisExecution(result.value, committedProfile.provider, {
|
|
294
|
+
rawResult: called.value,
|
|
295
|
+
analysisProfile: committedProfile,
|
|
296
|
+
limitations: limitationsFor(operation),
|
|
297
|
+
}));
|
|
298
|
+
},
|
|
299
|
+
close: () => client.close(),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
#inspectInstallation() {
|
|
303
|
+
const options = {
|
|
304
|
+
...(this.config.ghidraInstallDir === undefined
|
|
305
|
+
? {}
|
|
306
|
+
: { installDir: this.config.ghidraInstallDir }),
|
|
307
|
+
...(this.config.ghidraJavaHome === undefined
|
|
308
|
+
? {}
|
|
309
|
+
: { javaHome: this.config.ghidraJavaHome }),
|
|
310
|
+
...(this.installationHost?.platform === undefined
|
|
311
|
+
? {}
|
|
312
|
+
: { platform: this.installationHost.platform }),
|
|
313
|
+
...(this.installationHost?.architecture === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { architecture: this.installationHost.architecture }),
|
|
316
|
+
};
|
|
317
|
+
this.#installation ??=
|
|
318
|
+
this.installationHost === undefined
|
|
319
|
+
? inspectGhidraInstallation(options)
|
|
320
|
+
: inspectGhidraInstallation(options, this.installationHost);
|
|
321
|
+
return this.#installation;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const ghidraClientPrerequisites = (target, profile, installation) => {
|
|
325
|
+
if (target.kind !== "executable")
|
|
326
|
+
return err(new AnalysisCapabilityUnavailableError("ghidra", "health", `Ghidra cannot import ${target.kind} targets through this adapter.`));
|
|
327
|
+
if (!installation.available ||
|
|
328
|
+
installation.analyzeHeadlessPath === null ||
|
|
329
|
+
installation.providerVersion === null)
|
|
330
|
+
return err(new ProviderAdapterError("ghidra", "health", {
|
|
331
|
+
diagnostics: ghidraInstallationDiagnostics(installation),
|
|
332
|
+
}));
|
|
333
|
+
if (profile === undefined || profile.provider.id !== "ghidra")
|
|
334
|
+
return err(new ProviderAdapterError("ghidra", "health"));
|
|
335
|
+
return ok({
|
|
336
|
+
analyzeHeadlessPath: installation.analyzeHeadlessPath,
|
|
337
|
+
providerVersion: installation.providerVersion,
|
|
338
|
+
profile,
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
const unavailableClient = (failure) => ({
|
|
342
|
+
execute: () => Promise.resolve(err(failure)),
|
|
343
|
+
close: () => Promise.resolve(),
|
|
344
|
+
});
|
|
345
|
+
const projectSessionError = (operation, failure) => {
|
|
346
|
+
if (failure.kind === "cancelled")
|
|
347
|
+
return new AnalysisCancelledError(operation);
|
|
348
|
+
if (failure.kind === "timeout" || failure.kind === "analysis_timeout")
|
|
349
|
+
return new AnalysisTimeoutError(operation, failure.timeoutMs ??
|
|
350
|
+
(failure.kind === "analysis_timeout"
|
|
351
|
+
? GHIDRA_ANALYSIS_TIMEOUT_SECONDS * 1_000
|
|
352
|
+
: GHIDRA_STARTUP_TIMEOUT_MS));
|
|
353
|
+
if (failure.kind === "remote" && failure.remoteCode === "decompile_timeout")
|
|
354
|
+
return new AnalysisTimeoutError(operation, GHIDRA_DECOMPILE_TIMEOUT_SECONDS * 1_000);
|
|
355
|
+
if (failure.kind === "remote" && failure.remoteCode === "decompile_cancelled")
|
|
356
|
+
return new AnalysisCancelledError(operation);
|
|
357
|
+
if (failure.kind === "remote" &&
|
|
358
|
+
["invalid_request", "not_found", "ambiguous"].includes(failure.remoteCode ?? ""))
|
|
359
|
+
return new AnalysisInputError(operation, { cause: failure });
|
|
360
|
+
if (failure.kind === "remote" && failure.remoteCode === "method_unavailable")
|
|
361
|
+
return new AnalysisCapabilityUnavailableError("ghidra", operation, failure.message);
|
|
362
|
+
return new ProviderAdapterError("ghidra", operation, {
|
|
363
|
+
cause: failure,
|
|
364
|
+
diagnostics: failure.diagnostics,
|
|
365
|
+
});
|
|
366
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { err } from "../domain/result.js";
|
|
3
|
+
/** Bounded FIFO that admits one Ghidra Program request at a time. */
|
|
4
|
+
export class GhidraRequestQueue {
|
|
5
|
+
maximum;
|
|
6
|
+
execute;
|
|
7
|
+
failure;
|
|
8
|
+
#queue = [];
|
|
9
|
+
#active = false;
|
|
10
|
+
#size = 0;
|
|
11
|
+
constructor(maximum, execute, failure) {
|
|
12
|
+
this.maximum = maximum;
|
|
13
|
+
this.execute = execute;
|
|
14
|
+
this.failure = failure;
|
|
15
|
+
}
|
|
16
|
+
/** Queue one request, counting queue wait against its declared deadline. */
|
|
17
|
+
run(method, parameters, options) {
|
|
18
|
+
if (options.signal?.aborted === true)
|
|
19
|
+
return Promise.resolve(err(this.failure("cancelled", "Ghidra request was cancelled")));
|
|
20
|
+
if (options.timeoutMs <= 0)
|
|
21
|
+
return Promise.resolve(err(this.failure("timeout", "Ghidra request deadline elapsed", options.timeoutMs)));
|
|
22
|
+
if (this.#size >= this.maximum)
|
|
23
|
+
return Promise.resolve(err(this.failure("protocol", `Ghidra serial request queue reached its ${String(this.maximum)}-request limit`)));
|
|
24
|
+
this.#size += 1;
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let entry;
|
|
27
|
+
const onAbort = options.signal === undefined
|
|
28
|
+
? undefined
|
|
29
|
+
: () => this.#rejectQueued(entry, err(this.failure("cancelled", "Ghidra request was cancelled")));
|
|
30
|
+
const timer = setTimeout(() => this.#rejectQueued(entry, err(this.failure("timeout", "Ghidra request deadline elapsed in the serial queue", options.timeoutMs))), options.timeoutMs);
|
|
31
|
+
entry = {
|
|
32
|
+
method,
|
|
33
|
+
parameters,
|
|
34
|
+
signal: options.signal,
|
|
35
|
+
timeoutMs: options.timeoutMs,
|
|
36
|
+
deadline: performance.now() + options.timeoutMs,
|
|
37
|
+
resolve,
|
|
38
|
+
timer,
|
|
39
|
+
onAbort,
|
|
40
|
+
};
|
|
41
|
+
this.#queue.push(entry);
|
|
42
|
+
if (onAbort !== undefined)
|
|
43
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
this.#drain();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Fail requests that have not crossed the socket during session cleanup. */
|
|
48
|
+
failQueued(failure) {
|
|
49
|
+
for (const entry of this.#queue.splice(0)) {
|
|
50
|
+
this.#release(entry);
|
|
51
|
+
this.#size -= 1;
|
|
52
|
+
entry.resolve(err(failure));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
#drain() {
|
|
56
|
+
if (this.#active)
|
|
57
|
+
return;
|
|
58
|
+
const entry = this.#queue.shift();
|
|
59
|
+
if (entry === undefined)
|
|
60
|
+
return;
|
|
61
|
+
this.#release(entry);
|
|
62
|
+
const remaining = entry.deadline - performance.now();
|
|
63
|
+
if (entry.signal?.aborted === true || remaining <= 0) {
|
|
64
|
+
this.#size -= 1;
|
|
65
|
+
entry.resolve(err(entry.signal?.aborted === true
|
|
66
|
+
? this.failure("cancelled", "Ghidra request was cancelled")
|
|
67
|
+
: this.failure("timeout", "Ghidra request deadline elapsed in the serial queue", entry.timeoutMs)));
|
|
68
|
+
this.#drain();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.#active = true;
|
|
72
|
+
void this.execute(entry.method, entry.parameters, {
|
|
73
|
+
...(entry.signal === undefined ? {} : { signal: entry.signal }),
|
|
74
|
+
timeoutMs: remaining,
|
|
75
|
+
})
|
|
76
|
+
.then(entry.resolve)
|
|
77
|
+
.catch(() => entry.resolve(err(this.failure("protocol", "Ghidra serial request execution rejected unexpectedly"))))
|
|
78
|
+
.finally(() => {
|
|
79
|
+
this.#active = false;
|
|
80
|
+
this.#size -= 1;
|
|
81
|
+
this.#drain();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
#rejectQueued(entry, result) {
|
|
85
|
+
const index = this.#queue.indexOf(entry);
|
|
86
|
+
if (index < 0)
|
|
87
|
+
return;
|
|
88
|
+
this.#queue.splice(index, 1);
|
|
89
|
+
this.#release(entry);
|
|
90
|
+
this.#size -= 1;
|
|
91
|
+
entry.resolve(result);
|
|
92
|
+
}
|
|
93
|
+
#release(entry) {
|
|
94
|
+
clearTimeout(entry.timer);
|
|
95
|
+
if (entry.signal !== undefined && entry.onAbort !== undefined)
|
|
96
|
+
entry.signal.removeEventListener("abort", entry.onAbort);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Splits fragmented UTF-8 socket data while enforcing a byte limit per line. */
|
|
2
|
+
export class GhidraResponseBuffer {
|
|
3
|
+
#options;
|
|
4
|
+
#buffer = "";
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.#options = options;
|
|
7
|
+
}
|
|
8
|
+
/** Consume one decoded socket chunk. */
|
|
9
|
+
push(chunk) {
|
|
10
|
+
this.#buffer += chunk;
|
|
11
|
+
let newline = this.#buffer.indexOf("\n");
|
|
12
|
+
while (newline >= 0) {
|
|
13
|
+
const encodedLine = this.#buffer.slice(0, newline);
|
|
14
|
+
this.#buffer = this.#buffer.slice(newline + 1);
|
|
15
|
+
if (Buffer.byteLength(encodedLine) > this.#options.maxLineBytes) {
|
|
16
|
+
this.#options.onFailure("Ghidra response exceeded the maximum line size");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const line = encodedLine.trim();
|
|
20
|
+
if (line.length > 0)
|
|
21
|
+
this.#options.onLine(line);
|
|
22
|
+
newline = this.#buffer.indexOf("\n");
|
|
23
|
+
}
|
|
24
|
+
if (Buffer.byteLength(this.#buffer) > this.#options.maxLineBytes)
|
|
25
|
+
this.#options.onFailure("Ghidra response exceeded the maximum line size");
|
|
26
|
+
}
|
|
27
|
+
/** Drop any incomplete line when a session closes. */
|
|
28
|
+
reset() {
|
|
29
|
+
this.#buffer = "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { err } from "../domain/result.js";
|
|
2
|
+
import { ghidraResponseResult, parseGhidraResponseLine } from "./protocol.js";
|
|
3
|
+
/** Routes validated response IDs without giving the wire protocol client state. */
|
|
4
|
+
export class GhidraResponseRouter {
|
|
5
|
+
#options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.#options = options;
|
|
8
|
+
}
|
|
9
|
+
/** Parse and settle one complete response line. */
|
|
10
|
+
route(line) {
|
|
11
|
+
const parsed = parseGhidraResponseLine(line);
|
|
12
|
+
if (!parsed.ok) {
|
|
13
|
+
this.#options.protocolFailure(parsed.error.message, parsed.error);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!this.#options.pending.has(parsed.value.id)) {
|
|
17
|
+
if (parsed.value.id >= this.#options.nextId())
|
|
18
|
+
this.#options.protocolFailure("Ghidra returned an unknown response id");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const result = ghidraResponseResult(parsed.value);
|
|
22
|
+
this.#options.pending.settle(parsed.value.id, result.ok ? result : err(this.#options.remoteFailure(result.error)));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Provider-owned failure with bounded, token-redacted local diagnostics. */
|
|
2
|
+
export class GhidraSessionError extends Error {
|
|
3
|
+
kind;
|
|
4
|
+
diagnostics;
|
|
5
|
+
timeoutMs;
|
|
6
|
+
remoteCode;
|
|
7
|
+
constructor(kind, message, diagnostics = {}, options = {}) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.kind = kind;
|
|
10
|
+
this.name = "GhidraSessionError";
|
|
11
|
+
this.diagnostics = structuredClone(diagnostics);
|
|
12
|
+
this.timeoutMs = options.timeoutMs;
|
|
13
|
+
this.remoteCode = options.remoteCode;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Build a session failure while preserving bounded remote failure metadata. */
|
|
17
|
+
const createGhidraSessionError = (failure) => new GhidraSessionError(failure.kind, failure.message, failure.remoteCode === undefined
|
|
18
|
+
? failure.diagnostics
|
|
19
|
+
: {
|
|
20
|
+
...failure.diagnostics,
|
|
21
|
+
remote_code: failure.remoteCode,
|
|
22
|
+
remote_message: failure.message,
|
|
23
|
+
}, {
|
|
24
|
+
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
25
|
+
...(failure.timeoutMs === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { timeoutMs: failure.timeoutMs }),
|
|
28
|
+
...(failure.remoteCode === undefined
|
|
29
|
+
? {}
|
|
30
|
+
: { remoteCode: failure.remoteCode }),
|
|
31
|
+
});
|
|
32
|
+
/** Bind session-failure construction to a live, token-redacted diagnostic view. */
|
|
33
|
+
export const bindGhidraSessionFailure = (diagnostics) => (kind, message, cause, options = {}) => createGhidraSessionError({
|
|
34
|
+
kind,
|
|
35
|
+
message,
|
|
36
|
+
diagnostics: diagnostics(),
|
|
37
|
+
...(cause === undefined ? {} : { cause }),
|
|
38
|
+
...options,
|
|
39
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
import { GHIDRA_BRIDGE_VERSION } from "./GhidraDefaults.js";
|
|
4
|
+
import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
|
|
5
|
+
import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
|
|
6
|
+
/** Exact methods proved by the bridge handshake after auto-analysis. */
|
|
7
|
+
export const GHIDRA_SESSION_CAPABILITIES = [
|
|
8
|
+
"ping",
|
|
9
|
+
"shutdown",
|
|
10
|
+
...GHIDRA_INVENTORY_OPERATIONS,
|
|
11
|
+
...GHIDRA_FUNCTION_OPERATIONS,
|
|
12
|
+
];
|
|
13
|
+
const capabilitySchema = z.enum(GHIDRA_SESSION_CAPABILITIES);
|
|
14
|
+
const sessionInfoSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
name: z.literal("REA Ghidra bridge"),
|
|
17
|
+
bridge_version: z.literal(GHIDRA_BRIDGE_VERSION),
|
|
18
|
+
run_id: z.string().uuid(),
|
|
19
|
+
profile_digest: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
20
|
+
provider: z
|
|
21
|
+
.object({
|
|
22
|
+
id: z.literal("ghidra"),
|
|
23
|
+
version: z.string().min(1),
|
|
24
|
+
})
|
|
25
|
+
.strict(),
|
|
26
|
+
read_only: z.literal(true),
|
|
27
|
+
analysis_complete: z.boolean(),
|
|
28
|
+
analysis_timed_out: z.boolean(),
|
|
29
|
+
capabilities: z
|
|
30
|
+
.array(capabilitySchema)
|
|
31
|
+
.length(GHIDRA_SESSION_CAPABILITIES.length),
|
|
32
|
+
target: z
|
|
33
|
+
.object({
|
|
34
|
+
name: z.string().min(1),
|
|
35
|
+
language_id: z.string().min(1),
|
|
36
|
+
compiler_spec_id: z.string().min(1),
|
|
37
|
+
image_base: z.string().regex(/^0x[0-9a-f]+$/u),
|
|
38
|
+
default_address_space: z.string().min(1),
|
|
39
|
+
})
|
|
40
|
+
.strict(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
/** Require exact run, provider, and profile identity from the Java bridge. */
|
|
44
|
+
export const parseGhidraSessionInfo = (value, expected) => {
|
|
45
|
+
const parsed = sessionInfoSchema.safeParse(value);
|
|
46
|
+
if (!parsed.success ||
|
|
47
|
+
parsed.data.run_id !== expected.runId ||
|
|
48
|
+
parsed.data.provider.version !== expected.providerVersion ||
|
|
49
|
+
parsed.data.profile_digest !== expected.profileDigest ||
|
|
50
|
+
new Set(parsed.data.capabilities).size !==
|
|
51
|
+
GHIDRA_SESSION_CAPABILITIES.length ||
|
|
52
|
+
GHIDRA_SESSION_CAPABILITIES.some((capability) => !parsed.data.capabilities.includes(capability)) ||
|
|
53
|
+
parsed.data.analysis_complete === parsed.data.analysis_timed_out)
|
|
54
|
+
return err(new Error("Ghidra bridge handshake identity is invalid"));
|
|
55
|
+
return ok(parsed.data);
|
|
56
|
+
};
|
|
57
|
+
/** Require acknowledgement that the private bridge loop has ended. */
|
|
58
|
+
export const isGhidraShutdownAcknowledgement = (value) => typeof value === "object" &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
!Array.isArray(value) &&
|
|
61
|
+
value.shutdown === true &&
|
|
62
|
+
value.project_ephemeral === true;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
import { GhidraSessionError } from "./GhidraSessionError.js";
|
|
4
|
+
/** Attach decoded bridge listeners and return an idempotent detach callback. */
|
|
5
|
+
export const attachGhidraSocket = (socket, handlers) => {
|
|
6
|
+
socket.setEncoding("utf8");
|
|
7
|
+
socket.on("data", handlers.data);
|
|
8
|
+
socket.on("error", handlers.error);
|
|
9
|
+
socket.on("close", handlers.close);
|
|
10
|
+
return () => {
|
|
11
|
+
socket.off("data", handlers.data);
|
|
12
|
+
socket.off("error", handlers.error);
|
|
13
|
+
socket.off("close", handlers.close);
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/** Open one abort-aware Ghidra Unix-socket connection attempt. */
|
|
17
|
+
export const connectGhidraSocketOnce = (socketPath, signal) => new Promise((resolve) => {
|
|
18
|
+
const socket = createConnection(socketPath);
|
|
19
|
+
const detach = () => {
|
|
20
|
+
socket.off("connect", onConnect);
|
|
21
|
+
socket.off("error", onError);
|
|
22
|
+
signal.removeEventListener("abort", onAbort);
|
|
23
|
+
};
|
|
24
|
+
const onConnect = () => {
|
|
25
|
+
detach();
|
|
26
|
+
resolve(ok(socket));
|
|
27
|
+
};
|
|
28
|
+
const onError = (cause) => {
|
|
29
|
+
detach();
|
|
30
|
+
socket.destroy();
|
|
31
|
+
resolve(err(new GhidraSessionError("start", "Ghidra bridge connection failed", {
|
|
32
|
+
socket_path: socketPath,
|
|
33
|
+
error: cause.message,
|
|
34
|
+
})));
|
|
35
|
+
};
|
|
36
|
+
const onAbort = () => {
|
|
37
|
+
detach();
|
|
38
|
+
socket.destroy();
|
|
39
|
+
resolve(err(new GhidraSessionError("cancelled", "Ghidra bridge connection was cancelled", { socket_path: socketPath })));
|
|
40
|
+
};
|
|
41
|
+
socket.once("connect", onConnect);
|
|
42
|
+
socket.once("error", onError);
|
|
43
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
if (signal.aborted)
|
|
45
|
+
onAbort();
|
|
46
|
+
});
|