From b7df5b78052539e57967db8e6562befd70c6ebdc Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:22:23 +0800 Subject: [PATCH] refactor(ghidra): extract createClient and load-image attestation (#856) Move AnalysisClient construction into GhidraProviderClient and the DOS load-image attestation path into GhidraLoadImageAttest so GhidraProvider stays a thin candidate adapter without behavior changes. Co-authored-by: Cursor Agent --- src/ghidra/GhidraLoadImageAttest.ts | 70 +++++ src/ghidra/GhidraProvider.ts | 381 ++-------------------------- src/ghidra/GhidraProviderClient.ts | 355 ++++++++++++++++++++++++++ 3 files changed, 443 insertions(+), 363 deletions(-) create mode 100644 src/ghidra/GhidraLoadImageAttest.ts create mode 100644 src/ghidra/GhidraProviderClient.ts diff --git a/src/ghidra/GhidraLoadImageAttest.ts b/src/ghidra/GhidraLoadImageAttest.ts new file mode 100644 index 000000000..e8319dce2 --- /dev/null +++ b/src/ghidra/GhidraLoadImageAttest.ts @@ -0,0 +1,70 @@ +import type { AnalysisOperation } from "../application/AnalysisProvider.js"; +import type { BinaryTarget } from "../domain/binaryTarget.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; +import { + nativeLoadImageObservationSchema, + nativeLoadImageSchema, +} from "../domain/nativeLoadImage.js"; +import { ProviderAdapterError } from "../domain/providerAdapterError.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { + attestGhidraDosComLoadImage, + attestGhidraDosLoadImage, +} from "./GhidraLoadImageValues.js"; +import type { GhidraSessionError } from "./GhidraSessionError.js"; + +type LoadImageSnapshotClient = { + readonly readTargetSnapshot?: () => Promise< + Result + >; +}; + +/** Independently verify measured load-image observations for admitted DOS targets. */ +export const attestGhidraNativeLoadImage = async ( + target: BinaryTarget, + operation: AnalysisOperation, + measured: JsonValue, + client: LoadImageSnapshotClient, + mapSessionError: (failure: GhidraSessionError) => AnalysisError, +): Promise> => { + const observations = nativeLoadImageObservationSchema.parse(measured); + if (target.format !== "dos-mz" && target.format !== "dos-com") { + return ok( + jsonValueSchema.parse( + nativeLoadImageSchema.parse({ + status: "unsupported", + reason: + "Independent load-image verification currently supports DOS MZ and explicit DOS COM targets only.", + observations, + limitations: [ + "Other formats expose measured mappings and source identities; no format-specific verification was performed.", + ], + }), + ), + ); + } + if (client.readTargetSnapshot === undefined) + return err( + new ProviderAdapterError("ghidra", operation, { + diagnostics: { + reason: + "The Ghidra client does not expose its immutable target snapshot for independent load-image verification.", + }, + }), + ); + const snapshot = await client.readTargetSnapshot(); + if (!snapshot.ok) return err(mapSessionError(snapshot.error)); + const attested = ( + target.format === "dos-com" + ? attestGhidraDosComLoadImage + : attestGhidraDosLoadImage + )(snapshot.value, target.sha256 ?? "", observations); + if (!attested.ok) + return err( + new ProviderAdapterError("ghidra", operation, { + diagnostics: { reason: attested.error }, + }), + ); + return ok(jsonValueSchema.parse(attested.value)); +}; diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index cfb1ddc64..84a7c6cd8 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -1,11 +1,6 @@ -import { ghidraExtensionFailure } from "./extensions/GhidraExtensionFailures.js"; -import { fileURLToPath } from "node:url"; - import { - createAnalysisExecution, type AnalysisClient, type AnalysisClientContext, - type AnalysisOperation, type AnalysisProfileResolutionOptions, type AnalysisProviderCandidate, type CapabilityDescriptor, @@ -14,71 +9,37 @@ import { type ProviderTargetSupport, } from "../application/AnalysisProvider.js"; import type { AppConfig } from "../config.js"; -import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; -import { createAnalysisProfile } from "../domain/analysisProfile.js"; +import { + createAnalysisProfile, + type AnalysisProfileCommitment, +} from "../domain/analysisProfile.js"; import type { BinaryTarget } from "../domain/binaryTarget.js"; import { jsonObjectSchema, jsonValueSchema, type JsonValue, } from "../domain/jsonValue.js"; -import { - nativeLoadImageObservationSchema, - nativeLoadImageSchema, -} from "../domain/nativeLoadImage.js"; -import { - attestGhidraDosLoadImage, - attestGhidraDosComLoadImage, -} from "./GhidraLoadImageValues.js"; -import { - AnalysisCancelledError, - AnalysisCapabilityUnavailableError, - AnalysisInputError, - AnalysisTimeoutError, -} from "../domain/analysisErrorCore.js"; -import { type AnalysisError } from "../domain/analysisErrorBase.js"; -import { ProviderAdapterError } from "../domain/providerAdapterError.js"; -import { err, ok, type Result } from "../domain/result.js"; +import { ok } from "../domain/result.js"; import type { Logger } from "../logger.js"; import { GhidraClient } from "./GhidraClient.js"; -import type { GhidraClientOptions } from "./GhidraClientTypes.js"; -import { GHIDRA_STARTUP_TIMEOUT_MS } from "./GhidraDefaults.js"; -import { - isGhidraFunctionOperation, - parseGhidraFunctionInput, - parseGhidraFunctionResult, -} from "./GhidraFunctionValues.js"; -import { - isGhidraInventoryOperation, - parseGhidraInventoryInput, - parseGhidraInventoryResult, -} from "./GhidraInventoryValues.js"; import { ghidraInstallationDiagnostics, inspectGhidraInstallation, type GhidraInstallationHost, type GhidraInstallationInspection, } from "./GhidraInstallation.js"; -import { unverifiedGhidraBuildLimitation } from "./GhidraInstallationPolicy.js"; -import { GhidraHeadlessLauncher } from "./GhidraLauncher.js"; import { resolveGhidraAnalysisProfile } from "./GhidraAnalysisProfile.js"; -import type { GhidraSessionError } from "./GhidraSessionError.js"; -import type { GhidraSessionInfo } from "./GhidraSessionValues.js"; -import { - ghidraExtensionSchema, - resolveGhidraExtensions, - validateGhidraExtensionProfile, - ghidraExtensionLimitations, -} from "./extensions/GhidraExtensions.js"; +import { resolveGhidraExtensions } from "./extensions/GhidraExtensions.js"; import { CAPABILITIES, windowsP0Capabilities, GHIDRA_PROVIDER_IDENTITY, GHIDRA_PROVIDER_TOOL_CONTRACTS, - healthLimitations, - windowsP0Limitations, - limitationsFor, } from "./GhidraProviderCapabilities.js"; +import { + createGhidraProviderClient, + type GhidraProviderClientFactory, +} from "./GhidraProviderClient.js"; import { windowsNativeAuthorityUnavailableReason, hasWindowsNativeAuthority, @@ -86,15 +47,10 @@ import { } from "../process/WindowsAuthority.js"; export { GHIDRA_PROVIDER_IDENTITY, GHIDRA_PROVIDER_TOOL_CONTRACTS }; +export type { GhidraProviderClientFactory }; const SUPPORTED_ARCHITECTURES = new Set(["x86", "x86_64", "arm", "arm64"]); -/** Production seam for exercising provider projection without a real process. */ -export type GhidraProviderClientFactory = ( - options: GhidraClientOptions, -) => Pick & - Partial>; - /** Ghidra candidate backed by an isolated ephemeral headless import. */ export class GhidraProvider implements AnalysisProviderCandidate { #installation: GhidraInstallationInspection | undefined; @@ -232,226 +188,15 @@ export class GhidraProvider implements AnalysisProviderCandidate { profile?: AnalysisProfileCommitment, context?: AnalysisClientContext, ): AnalysisClient { - const installation = this.#inspectInstallation(); - const prerequisites = ghidraClientPrerequisites( + return createGhidraProviderClient({ + config: this.config, + logger: this.logger, + clientFactory: this.clientFactory, target, - profile, - installation, - ); - if (!prerequisites.ok) return unavailableClient(prerequisites.error); - const committedProfile = prerequisites.value.profile; - const extensionProfile = ghidraExtensionSchema - .array() - .safeParse(committedProfile.parameters.analysis_extensions ?? []); - if ( - !extensionProfile.success || - (this.config.ghidraNativeAotJar !== undefined && - extensionProfile.data.length === 0) - ) - return unavailableClient( - new ProviderAdapterError("ghidra", "open_binary", { - diagnostics: { - reason: - "Resolve the configured Ghidra extensions into an analysis profile before opening the session.", - }, - }), - ); - const extensions = extensionProfile.data; - const invalidProfile = validateGhidraExtensionProfile( - extensions, - this.config, - target, - installation.platform, - ); - if (invalidProfile !== null) - return unavailableClient( - new ProviderAdapterError("ghidra", "open_binary", { - diagnostics: { reason: invalidProfile }, - }), - ); - let extensionFailure: AnalysisError | undefined; - const targetLimitations = - target.format === "dos-mz" - ? [ - "DOS MZ uses 16-bit x86 real mode with the Ghidra load segment 0x1000. Returned addresses are linear byte coordinates; they do not identify a unique segment:offset alias.", - "Static DOS analysis does not emulate BIOS, DOS interrupts, device ports, or self-modifying unpacking code. Packed targets require a separately identified unpacked artifact for original-program analysis; appended overlays are not the initialized load module.", - ] - : []; - const providerLimitations = - installation.platform === "win32" - ? windowsP0Limitations - : installation.platform === "darwin" - ? [ - "macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state.", - ] - : []; - const client = this.clientFactory({ - platform: installation.platform, - launcher: new GhidraHeadlessLauncher({ - analyzeHeadlessPath: prerequisites.value.analyzeHeadlessPath, - ...(this.config.ghidraJavaHome === undefined - ? {} - : { javaHome: this.config.ghidraJavaHome }), - bridgeScriptPath: fileURLToPath( - new URL("../../bridge/ghidra/ReaGhidraBridge.java", import.meta.url), - ), - ...(target.format === "dos-mz" ? { dosMz: true } : {}), - ...(target.format === "dos-com" ? { dosCom: true } : {}), - platform: installation.platform, - ...(extensions.length === 0 ? {} : { analysisExtensions: extensions }), - }), - targetPath: - installation.platform === "win32" - ? (target.sourcePath ?? target.path) - : target.path, - targetSha256: target.sha256, - transport: - installation.platform === "win32" - ? "authenticated-loopback-tcp" - : "unix-socket", - providerVersion: prerequisites.value.providerVersion, - profileDigest: committedProfile.digest, - ...(["dos-mz", "dos-com"].includes(target.format) - ? { - expectedLanguageId: "x86:LE:16:Real Mode", - expectedCompilerSpecId: "default", - } - : {}), - ...(context === undefined ? {} : { runId: context.runId }), - logger: this.logger.child({ layer: "ghidra-bridge" }), + ...(profile === undefined ? {} : { profile }), + ...(context === undefined ? {} : { context }), + installation: this.#inspectInstallation(), }); - const checkExtensions = async ( - operation: AnalysisOperation, - info: GhidraSessionInfo, - ): Promise => { - extensionFailure = ghidraExtensionFailure( - extensions, - info.analysis_extensions ?? [], - operation, - ); - if (extensionFailure === undefined) return undefined; - await client.close(); - return extensionFailure; - }; - const releaseLimitation = unverifiedGhidraBuildLimitation( - prerequisites.value.providerVersion, - ); - const sessionLimitations = [ - ...providerLimitations, - ...targetLimitations, - ...ghidraExtensionLimitations(extensions), - ...(releaseLimitation === undefined ? [] : [releaseLimitation]), - ]; - return { - execute: async (operation, parameters, options) => { - if (extensionFailure !== undefined) return err(extensionFailure); - if ( - operation !== "health" && - !isGhidraInventoryOperation(operation) && - !isGhidraFunctionOperation(operation) - ) - return err( - new AnalysisCapabilityUnavailableError( - GHIDRA_PROVIDER_IDENTITY.id, - operation, - "The Ghidra adapter does not declare this operation.", - ), - ); - if (operation === "health") { - const started = await client.start(options?.signal); - if (!started.ok) - return err(projectSessionError(operation, started.error)); - const failed = await checkExtensions(operation, started.value); - if (failed !== undefined) return err(failed); - return ok( - createAnalysisExecution(started.value, committedProfile.provider, { - analysisProfile: committedProfile, - limitations: [...healthLimitations, ...sessionLimitations], - }), - ); - } - const input = isGhidraFunctionOperation(operation) - ? parseGhidraFunctionInput(operation, parameters) - : parseGhidraInventoryInput(operation, parameters); - if (!input.ok) return input; - if (extensions.length > 0) { - const started = await client.start(options?.signal); - if (!started.ok) - return err(projectSessionError(operation, started.error)); - const failed = await checkExtensions(operation, started.value); - if (failed !== undefined) return err(failed); - } - const called = await client.callTool( - operation, - input.value, - options?.signal === undefined ? {} : { signal: options.signal }, - ); - if (!called.ok) - return err(projectSessionError(operation, called.error)); - const result = isGhidraFunctionOperation(operation) - ? parseGhidraFunctionResult(operation, called.value) - : parseGhidraInventoryResult(operation, called.value); - if (!result.ok) return result; - let normalized = result.value; - if (operation === "inspect_native_load_image") { - const observations = nativeLoadImageObservationSchema.parse( - result.value, - ); - if (target.format !== "dos-mz" && target.format !== "dos-com") { - normalized = jsonValueSchema.parse( - nativeLoadImageSchema.parse({ - status: "unsupported", - reason: - "Independent load-image verification currently supports DOS MZ and explicit DOS COM targets only.", - observations, - limitations: [ - "Other formats expose measured mappings and source identities; no format-specific verification was performed.", - ], - }), - ); - } else { - if (client.readTargetSnapshot === undefined) - return err( - new ProviderAdapterError("ghidra", operation, { - diagnostics: { - reason: - "The Ghidra client does not expose its immutable target snapshot for independent load-image verification.", - }, - }), - ); - const snapshot = await client.readTargetSnapshot(); - if (!snapshot.ok) - return err(projectSessionError(operation, snapshot.error)); - const attested = ( - target.format === "dos-com" - ? attestGhidraDosComLoadImage - : attestGhidraDosLoadImage - )(snapshot.value, target.sha256 ?? "", observations); - if (!attested.ok) - return err( - new ProviderAdapterError("ghidra", operation, { - diagnostics: { reason: attested.error }, - }), - ); - normalized = jsonValueSchema.parse(attested.value); - } - } - return ok( - createAnalysisExecution(normalized, committedProfile.provider, { - rawResult: called.value, - analysisProfile: committedProfile, - limitations: [...limitationsFor(operation), ...sessionLimitations], - }), - ); - }, - runtimeLineageSnapshots: () => { - const observation = client.runtimeLineage?.() ?? null; - return observation === null - ? [] - : [{ provider: committedProfile.provider, observation }]; - }, - close: () => client.close(), - }; } #inspectInstallation(): GhidraInstallationInspection { @@ -518,93 +263,3 @@ const inspectWindowsP0TargetSupport = ( diagnostics, }; }; - -interface GhidraClientCoordinates { - readonly analyzeHeadlessPath: string; - readonly providerVersion: string; - readonly profile: AnalysisProfileCommitment; -} - -const ghidraClientPrerequisites = ( - target: BinaryTarget, - profile: AnalysisProfileCommitment | undefined, - installation: GhidraInstallationInspection, -): Result => { - if (target.kind !== "executable") - return err( - new AnalysisCapabilityUnavailableError( - "ghidra", - "health", - `Ghidra cannot import ${target.kind} targets through this adapter.`, - ), - ); - if (installation.status === "unavailable") - return err( - new ProviderAdapterError("ghidra", "health", { - diagnostics: ghidraInstallationDiagnostics(installation), - }), - ); - if ( - installation.platform === "win32" && - !hasWindowsNativeAuthority(installation.platform) - ) - return err( - new AnalysisCapabilityUnavailableError( - "ghidra", - "health", - windowsNativeAuthorityUnavailableReason(installation.platform), - ), - ); - if (profile === undefined || profile.provider.id !== "ghidra") - return err(new ProviderAdapterError("ghidra", "health")); - return ok({ - analyzeHeadlessPath: installation.analyzeHeadlessPath, - providerVersion: installation.providerVersion, - profile, - }); -}; - -const unavailableClient = (failure: AnalysisError): AnalysisClient => ({ - execute: () => Promise.resolve(err(failure)), - close: () => Promise.resolve(), -}); - -const projectSessionError = ( - operation: AnalysisOperation, - failure: GhidraSessionError, -): AnalysisError => { - if ( - operation === "annotate_native_function" && - failure.kind === "remote" && - failure.remoteCode === "invalid_function_name" - ) - return new AnalysisInputError(operation, { cause: failure }, [ - { path: ["name"], reason: "invalid_value", message: failure.message }, - ]); - if (failure.kind === "cancelled") - return new AnalysisCancelledError(operation); - if (failure.kind === "timeout" || failure.kind === "analysis_timeout") - return new AnalysisTimeoutError( - operation, - failure.timeoutMs ?? GHIDRA_STARTUP_TIMEOUT_MS, - ); - if (failure.kind === "remote" && failure.remoteCode === "decompile_cancelled") - return new AnalysisCancelledError(operation); - if ( - failure.kind === "remote" && - ["invalid_request", "not_found", "ambiguous"].includes( - failure.remoteCode ?? "", - ) - ) - return new AnalysisInputError(operation, { cause: failure }); - if (failure.kind === "remote" && failure.remoteCode === "method_unavailable") - return new AnalysisCapabilityUnavailableError( - "ghidra", - operation, - failure.message, - ); - return new ProviderAdapterError("ghidra", operation, { - cause: failure, - diagnostics: failure.diagnostics, - }); -}; diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts new file mode 100644 index 000000000..2dad76201 --- /dev/null +++ b/src/ghidra/GhidraProviderClient.ts @@ -0,0 +1,355 @@ +import { fileURLToPath } from "node:url"; + +import { + createAnalysisExecution, + type AnalysisClient, + type AnalysisClientContext, + type AnalysisOperation, +} from "../application/AnalysisProvider.js"; +import type { AppConfig } from "../config.js"; +import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; +import type { BinaryTarget } from "../domain/binaryTarget.js"; +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisInputError, + AnalysisTimeoutError, +} from "../domain/analysisErrorCore.js"; +import { type AnalysisError } from "../domain/analysisErrorBase.js"; +import { ProviderAdapterError } from "../domain/providerAdapterError.js"; +import { err, ok, type Result } from "../domain/result.js"; +import type { Logger } from "../logger.js"; +import { GhidraClient } from "./GhidraClient.js"; +import type { GhidraClientOptions } from "./GhidraClientTypes.js"; +import { GHIDRA_STARTUP_TIMEOUT_MS } from "./GhidraDefaults.js"; +import { + isGhidraFunctionOperation, + parseGhidraFunctionInput, + parseGhidraFunctionResult, +} from "./GhidraFunctionValues.js"; +import { + isGhidraInventoryOperation, + parseGhidraInventoryInput, + parseGhidraInventoryResult, +} from "./GhidraInventoryValues.js"; +import { + ghidraInstallationDiagnostics, + type GhidraInstallationInspection, +} from "./GhidraInstallation.js"; +import { unverifiedGhidraBuildLimitation } from "./GhidraInstallationPolicy.js"; +import { GhidraHeadlessLauncher } from "./GhidraLauncher.js"; +import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js"; +import { + GHIDRA_PROVIDER_IDENTITY, + healthLimitations, + windowsP0Limitations, + limitationsFor, +} from "./GhidraProviderCapabilities.js"; +import type { GhidraSessionError } from "./GhidraSessionError.js"; +import type { GhidraSessionInfo } from "./GhidraSessionValues.js"; +import { ghidraExtensionFailure } from "./extensions/GhidraExtensionFailures.js"; +import { + ghidraExtensionSchema, + validateGhidraExtensionProfile, + ghidraExtensionLimitations, +} from "./extensions/GhidraExtensions.js"; +import { + windowsNativeAuthorityUnavailableReason, + hasWindowsNativeAuthority, +} from "../process/WindowsAuthority.js"; + +/** Production seam for exercising provider projection without a real process. */ +export type GhidraProviderClientFactory = ( + options: GhidraClientOptions, +) => Pick & + Partial>; + +/** Build one AnalysisClient for an admitted Ghidra target and profile. */ +export const createGhidraProviderClient = (input: { + readonly config: AppConfig; + readonly logger: Logger; + readonly clientFactory: GhidraProviderClientFactory; + readonly target: BinaryTarget; + readonly profile?: AnalysisProfileCommitment; + readonly context?: AnalysisClientContext; + readonly installation: GhidraInstallationInspection; +}): AnalysisClient => { + const { config, logger, clientFactory, target, context, installation } = + input; + const prerequisites = ghidraClientPrerequisites( + target, + input.profile, + installation, + ); + if (!prerequisites.ok) return unavailableClient(prerequisites.error); + const committedProfile = prerequisites.value.profile; + const extensionProfile = ghidraExtensionSchema + .array() + .safeParse(committedProfile.parameters.analysis_extensions ?? []); + if ( + !extensionProfile.success || + (config.ghidraNativeAotJar !== undefined && + extensionProfile.data.length === 0) + ) + return unavailableClient( + new ProviderAdapterError("ghidra", "open_binary", { + diagnostics: { + reason: + "Resolve the configured Ghidra extensions into an analysis profile before opening the session.", + }, + }), + ); + const extensions = extensionProfile.data; + const invalidProfile = validateGhidraExtensionProfile( + extensions, + config, + target, + installation.platform, + ); + if (invalidProfile !== null) + return unavailableClient( + new ProviderAdapterError("ghidra", "open_binary", { + diagnostics: { reason: invalidProfile }, + }), + ); + let extensionFailure: AnalysisError | undefined; + const targetLimitations = + target.format === "dos-mz" + ? [ + "DOS MZ uses 16-bit x86 real mode with the Ghidra load segment 0x1000. Returned addresses are linear byte coordinates; they do not identify a unique segment:offset alias.", + "Static DOS analysis does not emulate BIOS, DOS interrupts, device ports, or self-modifying unpacking code. Packed targets require a separately identified unpacked artifact for original-program analysis; appended overlays are not the initialized load module.", + ] + : []; + const providerLimitations = + installation.platform === "win32" + ? windowsP0Limitations + : installation.platform === "darwin" + ? [ + "macOS sessions require a matching executable Ghidra native decompiler; REA checks for it but does not build native components or change Gatekeeper quarantine state.", + ] + : []; + const client = clientFactory({ + platform: installation.platform, + launcher: new GhidraHeadlessLauncher({ + analyzeHeadlessPath: prerequisites.value.analyzeHeadlessPath, + ...(config.ghidraJavaHome === undefined + ? {} + : { javaHome: config.ghidraJavaHome }), + bridgeScriptPath: fileURLToPath( + new URL("../../bridge/ghidra/ReaGhidraBridge.java", import.meta.url), + ), + ...(target.format === "dos-mz" ? { dosMz: true } : {}), + ...(target.format === "dos-com" ? { dosCom: true } : {}), + platform: installation.platform, + ...(extensions.length === 0 ? {} : { analysisExtensions: extensions }), + }), + targetPath: + installation.platform === "win32" + ? (target.sourcePath ?? target.path) + : target.path, + targetSha256: target.sha256, + transport: + installation.platform === "win32" + ? "authenticated-loopback-tcp" + : "unix-socket", + providerVersion: prerequisites.value.providerVersion, + profileDigest: committedProfile.digest, + ...(["dos-mz", "dos-com"].includes(target.format) + ? { + expectedLanguageId: "x86:LE:16:Real Mode", + expectedCompilerSpecId: "default", + } + : {}), + ...(context === undefined ? {} : { runId: context.runId }), + logger: logger.child({ layer: "ghidra-bridge" }), + }); + const checkExtensions = async ( + operation: AnalysisOperation, + info: GhidraSessionInfo, + ): Promise => { + extensionFailure = ghidraExtensionFailure( + extensions, + info.analysis_extensions ?? [], + operation, + ); + if (extensionFailure === undefined) return undefined; + await client.close(); + return extensionFailure; + }; + const releaseLimitation = unverifiedGhidraBuildLimitation( + prerequisites.value.providerVersion, + ); + const sessionLimitations = [ + ...providerLimitations, + ...targetLimitations, + ...ghidraExtensionLimitations(extensions), + ...(releaseLimitation === undefined ? [] : [releaseLimitation]), + ]; + return { + execute: async (operation, parameters, options) => { + if (extensionFailure !== undefined) return err(extensionFailure); + if ( + operation !== "health" && + !isGhidraInventoryOperation(operation) && + !isGhidraFunctionOperation(operation) + ) + return err( + new AnalysisCapabilityUnavailableError( + GHIDRA_PROVIDER_IDENTITY.id, + operation, + "The Ghidra adapter does not declare this operation.", + ), + ); + if (operation === "health") { + const started = await client.start(options?.signal); + if (!started.ok) + return err(projectSessionError(operation, started.error)); + const failed = await checkExtensions(operation, started.value); + if (failed !== undefined) return err(failed); + return ok( + createAnalysisExecution(started.value, committedProfile.provider, { + analysisProfile: committedProfile, + limitations: [...healthLimitations, ...sessionLimitations], + }), + ); + } + const input = isGhidraFunctionOperation(operation) + ? parseGhidraFunctionInput(operation, parameters) + : parseGhidraInventoryInput(operation, parameters); + if (!input.ok) return input; + if (extensions.length > 0) { + const started = await client.start(options?.signal); + if (!started.ok) + return err(projectSessionError(operation, started.error)); + const failed = await checkExtensions(operation, started.value); + if (failed !== undefined) return err(failed); + } + const called = await client.callTool( + operation, + input.value, + options?.signal === undefined ? {} : { signal: options.signal }, + ); + if (!called.ok) return err(projectSessionError(operation, called.error)); + const result = isGhidraFunctionOperation(operation) + ? parseGhidraFunctionResult(operation, called.value) + : parseGhidraInventoryResult(operation, called.value); + if (!result.ok) return result; + let normalized = result.value; + if (operation === "inspect_native_load_image") { + const attested = await attestGhidraNativeLoadImage( + target, + operation, + result.value, + client, + (failure) => projectSessionError(operation, failure), + ); + if (!attested.ok) return attested; + normalized = attested.value; + } + return ok( + createAnalysisExecution(normalized, committedProfile.provider, { + rawResult: called.value, + analysisProfile: committedProfile, + limitations: [...limitationsFor(operation), ...sessionLimitations], + }), + ); + }, + runtimeLineageSnapshots: () => { + const observation = client.runtimeLineage?.() ?? null; + return observation === null + ? [] + : [{ provider: committedProfile.provider, observation }]; + }, + close: () => client.close(), + }; +}; + +interface GhidraClientCoordinates { + readonly analyzeHeadlessPath: string; + readonly providerVersion: string; + readonly profile: AnalysisProfileCommitment; +} + +const ghidraClientPrerequisites = ( + target: BinaryTarget, + profile: AnalysisProfileCommitment | undefined, + installation: GhidraInstallationInspection, +): Result => { + if (target.kind !== "executable") + return err( + new AnalysisCapabilityUnavailableError( + "ghidra", + "health", + `Ghidra cannot import ${target.kind} targets through this adapter.`, + ), + ); + if (installation.status === "unavailable") + return err( + new ProviderAdapterError("ghidra", "health", { + diagnostics: ghidraInstallationDiagnostics(installation), + }), + ); + if ( + installation.platform === "win32" && + !hasWindowsNativeAuthority(installation.platform) + ) + return err( + new AnalysisCapabilityUnavailableError( + "ghidra", + "health", + windowsNativeAuthorityUnavailableReason(installation.platform), + ), + ); + if (profile === undefined || profile.provider.id !== "ghidra") + return err(new ProviderAdapterError("ghidra", "health")); + return ok({ + analyzeHeadlessPath: installation.analyzeHeadlessPath, + providerVersion: installation.providerVersion, + profile, + }); +}; + +const unavailableClient = (failure: AnalysisError): AnalysisClient => ({ + execute: () => Promise.resolve(err(failure)), + close: () => Promise.resolve(), +}); + +const projectSessionError = ( + operation: AnalysisOperation, + failure: GhidraSessionError, +): AnalysisError => { + if ( + operation === "annotate_native_function" && + failure.kind === "remote" && + failure.remoteCode === "invalid_function_name" + ) + return new AnalysisInputError(operation, { cause: failure }, [ + { path: ["name"], reason: "invalid_value", message: failure.message }, + ]); + if (failure.kind === "cancelled") + return new AnalysisCancelledError(operation); + if (failure.kind === "timeout" || failure.kind === "analysis_timeout") + return new AnalysisTimeoutError( + operation, + failure.timeoutMs ?? GHIDRA_STARTUP_TIMEOUT_MS, + ); + if (failure.kind === "remote" && failure.remoteCode === "decompile_cancelled") + return new AnalysisCancelledError(operation); + if ( + failure.kind === "remote" && + ["invalid_request", "not_found", "ambiguous"].includes( + failure.remoteCode ?? "", + ) + ) + return new AnalysisInputError(operation, { cause: failure }); + if (failure.kind === "remote" && failure.remoteCode === "method_unavailable") + return new AnalysisCapabilityUnavailableError( + "ghidra", + operation, + failure.message, + ); + return new ProviderAdapterError("ghidra", operation, { + cause: failure, + diagnostics: failure.diagnostics, + }); +};