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 <cursoragent@cursor.com>
This commit is contained in:
morluto 2026-10-07 13:22:23 +08:00 committed by GitHub
parent 4aa2b60c1d
commit b7df5b7805
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 443 additions and 363 deletions

View File

@ -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<Buffer, GhidraSessionError>
>;
};
/** 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<Result<JsonValue, AnalysisError>> => {
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));
};

View File

@ -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<GhidraClient, "start" | "callTool" | "close"> &
Partial<Pick<GhidraClient, "runtimeLineage" | "readTargetSnapshot">>;
/** 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<AnalysisError | undefined> => {
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<GhidraClientCoordinates, AnalysisError> => {
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,
});
};

View File

@ -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<GhidraClient, "start" | "callTool" | "close"> &
Partial<Pick<GhidraClient, "runtimeLineage" | "readTargetSnapshot">>;
/** 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<AnalysisError | undefined> => {
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<GhidraClientCoordinates, AnalysisError> => {
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,
});
};