import type { ExecutableFormatHint } from "../domain/dosCom.js"; import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js"; import type { BinarySession } from "./binary/BinarySession.js"; import type { SessionProviderRoute } from "./binary/SessionProviderRouter.js"; import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js"; import { silentLogger } from "../logger.js"; import type { Logger } from "pino"; import { createEvidence } from "../domain/evidence.js"; import type { Evidence } from "../domain/evidence.js"; import type { NativeToolName } from "../contracts/native/nativeToolContracts.js"; import { ARTIFACT_ANALYSIS_OPERATIONS, type ArtifactAnalysisOperation, } from "../contracts/artifactToolContracts.js"; import { isManagedToolName, type ManagedToolName, } from "../contracts/managed/managedToolContracts.js"; import { EvidenceFileError, EvidenceIntegrityError, } from "../domain/evidenceErrors.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { access, stat } from "node:fs/promises"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { readAnalysisSnapshot } from "./binary/AnalysisSnapshotFiles.js"; import { requiresNativeSourceAdmissionBeforeReplay } from "./binary/AnalysisSnapshotCache.js"; import { classifyBinaryTargetFilesystemFailure, parseBinaryTarget, } from "./BinaryTargetResolver.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js"; import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; import { profilelessNativeDispatchProfile } from "./binary/ProfilelessNativeWorkflowSnapshot.js"; import { err, ok, type Result } from "../domain/result.js"; import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js"; import type { AnalysisExecution, ProviderIdentity, } from "./AnalysisProvider.js"; import { createAnalysisExecutionEvidence } from "./AnalysisProvider.js"; import { REA_WORKFLOW_PROVIDER, workflowAnalysisProfile, } from "./InvestigationProviders.js"; import { executeComposedWorkflow, createWorkflowEvidence, workflowSnapshotRecord, recordWorkflowUnknowns, } from "./WorkflowEvidence.js"; import type { AnalysisProviderSelector } from "../contracts/providerSelection.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; import { resolveXrefsAddress } from "./XrefsAddressResolution.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import type { BinaryTargetError } from "../domain/configurationErrors.js"; import { directAnalysisCleanupFailure, managedAnalysisCleanupFailure, withSessionCleanup, } from "./DirectAnalysisCleanup.js"; import { BUNDLE_RESOURCE_OPERATIONS } from "./binary/BundleResourceSession.js"; type DirectAnalysisTool = | "annotate_native_function" | "inspect_native_load_image" | "read_bytes" | "search_bytes" | "address_to_file_offset" | "binary_overview" | "procedure_pseudo_code" | "read_function_instructions" | "inspect_native_instruction" | "inspect_native_data_type" | "resolve_native_call_targets" | "analyze_function" | "inspect_native_api" | "inspect_native_dispatch_metadata" | "search_strings" | "search_procedures" | "xrefs" | "trace_feature" | "trace_native_ui_action" | "trace_native_values"; // These one-shot CLI questions require native utilities, not a deep engine. // Runtime observation retains the ordinary session and authority checks. const NATIVE_INSPECTION_OPERATIONS: ReadonlySet = new Set([ "inspect_macho", "inspect_signature", "list_architectures", "inspect_plist", "demangle_swift", ] satisfies readonly NativeToolName[]); const ARTIFACT_ANALYSIS_OPERATION_SET: ReadonlySet = new Set( ARTIFACT_ANALYSIS_OPERATIONS, ); /** * Open one binary, execute one tool, and always release provider resources. * Unlike MCP mode, every CLI invocation is intentionally isolated and does not * retain a target or provider client for a subsequent command. */ export const runDirectAnalysis = async ( dependencies: DirectAnalysisDependencies, path: string, tool: DirectAnalysisTool, arguments_: Readonly>, options: { readonly logger?: Logger; readonly snapshotPath?: string | undefined; readonly signal?: AbortSignal; readonly providerId?: AnalysisProviderSelector; readonly formatHint?: ExecutableFormatHint; readonly architecture?: BinaryArchitecture; readonly entryPoint?: string; } = {}, ): Promise => withProcessCancellation(options.signal, (signal) => runAnalysis(dependencies, path, tool, arguments_, { logger: options.logger ?? silentLogger, snapshotPath: options.snapshotPath, signal, ...(options.formatHint === undefined ? {} : { formatHint: options.formatHint }), ...(options.architecture === undefined ? {} : { architecture: options.architecture }), ...(options.entryPoint === undefined ? {} : { entryPoint: options.entryPoint }), ...(options.providerId === undefined ? {} : { providerId: options.providerId }), }), ); /** Execute one provider-native semantic operation with atomic provenance. */ export const runProviderAnalysis = async ( dependencies: DirectAnalysisDependencies, ...[path, tool, arguments_, logger = silentLogger, signal]: readonly [ path: string, tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName, arguments_: Readonly>, logger?: Logger, signal?: AbortSignal, ] ): Promise => { if (isManagedToolName(tool)) return runManagedProviderAnalysis( dependencies, path, tool, arguments_, signal, ); return withProcessCancellation(signal, (operationSignal) => runAnalysis(dependencies, path, tool, arguments_, { logger, snapshotPath: undefined, signal: operationSignal, ...(ARTIFACT_ANALYSIS_OPERATION_SET.has(tool) ? { createSession: dependencies.createArtifactBinarySession } : NATIVE_INSPECTION_OPERATIONS.has(tool) ? { createSession: dependencies.createNativeBinarySession } : {}), }), ); }; /** Execute managed metadata inspection in an isolated managed-only session. */ export const runManagedProviderExecution = async ( dependencies: Pick, path: string, tool: ManagedToolName, signal?: AbortSignal, parameters: Readonly> = {}, ): Promise> => withProcessCancellation(signal, async (operationSignal) => { const session = await dependencies.createManagedBinarySession(); return withSessionCleanup( session, async () => { const opened = await session.open(path, { signal: operationSignal }); if (!opened.ok) return opened; return await session.execute(tool, parameters, { signal: operationSignal, }); }, managedAnalysisCleanupFailure, ); }); const runManagedProviderAnalysis = async ( dependencies: DirectAnalysisDependencies, path: string, tool: ManagedToolName, arguments_: Readonly>, signal?: AbortSignal, ): Promise => { const execution = await runManagedProviderExecution( dependencies, path, tool, signal, arguments_, ); if (!execution.ok) return cliError(execution.error); const value = execution.value; return createAnalysisExecutionEvidence(tool, arguments_, value, "observed"); }; const runAnalysis = async ( dependencies: DirectAnalysisDependencies, path: string, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, arguments_: Readonly>, options: { readonly logger: Logger; readonly snapshotPath: string | undefined; readonly signal: AbortSignal; readonly createSession?: () => BinarySession | Promise; readonly providerId?: AnalysisProviderSelector; readonly formatHint?: ExecutableFormatHint; readonly architecture?: BinaryArchitecture; readonly entryPoint?: string; }, ): Promise => { const { logger, signal, snapshotPath } = options; let session: BinarySession; if (options.createSession !== undefined) { session = await options.createSession(); } else { const config = dependencies.readConfiguration(); if (!config.ok) return cliError(config.error); session = await ( tool === "inspect_native_dispatch_metadata" && snapshotPath === undefined ? dependencies.createNativeDispatchBinarySession : dependencies.createBinarySession )(config.value, logger); } return withSessionCleanup( session, async () => { let isDirectory = false; if (BUNDLE_RESOURCE_OPERATIONS.has(tool)) { const inspected = await inspectBundleResourceInput(path); if (!inspected.ok) return cliError(inspected.error); isDirectory = inspected.value; } if (BUNDLE_RESOURCE_OPERATIONS.has(tool) && isDirectory) { const opened = await session.openBundleResources(path, { signal }); if (!opened.ok) return cliError(opened.error); const execution = await session.execute(tool, arguments_, { signal }); if (!execution.ok) return cliError(execution.error); return createAnalysisExecutionEvidence( tool, arguments_, execution.value, "observed", ); } const prepared = await prepareSnapshot({ path, snapshotPath, signal, ...(options.formatHint === undefined ? {} : { formatHint: options.formatHint }), ...(options.architecture === undefined ? {} : { architecture: options.architecture }), ...(options.entryPoint === undefined ? {} : { entryPoint: options.entryPoint }), }); if (!prepared.ok) return cliError(prepared.error); const { snapshot } = prepared.value; let resolvedTarget: ResolvedSessionOpen | undefined; if (snapshot !== undefined && prepared.value.target !== undefined) { const preview = await session.previewTarget(prepared.value.target, { signal, snapshot, ...(options.formatHint === undefined ? {} : { formatHint: options.formatHint }), ...(options.architecture === undefined ? {} : { architecture: options.architecture }), ...(options.entryPoint === undefined ? {} : { entryPoint: options.entryPoint }), ...(options.providerId === undefined ? {} : { providerId: options.providerId }), }); if (!preview.ok) return cliError(preview.error); resolvedTarget = preview.value; const route = preview.value.route; const bindingProfile = route.profile ?? nativeDispatchBindingProfile(route, tool); const evidenceProfile = analysisProfileForRoute(route, tool); // A parsed Windows path does not prove Ghidra's native snapshot owner // admitted the selected source; CLI replay must wait for that client. if ( bindingProfile !== undefined && evidenceProfile !== undefined && !requiresNativeSourceAdmissionBeforeReplay( route.binding?.identity.id, ) && allowsSnapshotReplay(route, tool) ) { const cached = snapshotEvidenceForQuery(snapshot, { target: preview.value.target, bindingProfile, operation: tool, parameters: arguments_, provider: isWorkflowEvidenceTool(tool) ? REA_WORKFLOW_PROVIDER : providerIdentityForRoute(route, tool), evidenceProfile, }); if (cached !== undefined) return cached; } } const openOptions = { signal, ...(options.formatHint === undefined ? {} : { formatHint: options.formatHint }), ...(options.architecture === undefined ? {} : { architecture: options.architecture }), ...(options.entryPoint === undefined ? {} : { entryPoint: options.entryPoint }), ...(snapshot === undefined ? {} : { snapshot }), ...(options.providerId === undefined ? {} : { providerId: options.providerId }), }; const opened = resolvedTarget === undefined ? await session.open(path, openOptions) : await session.openResolvedTarget(resolvedTarget, openOptions); if (!opened.ok) return cliError(opened.error); const evidenceProfile = analysisProfileForEvidence(session, tool); const { output, evidence, cachePolicy } = await executeAnalysisTool({ session, openedTarget: opened.value, tool, arguments: arguments_, signal, evidenceProfile, }); if (evidence !== undefined) { const recorded = session.recordEvidence(evidence); if (!recorded.ok) return cliError(recorded.error.retainPartialObservation(evidence)); if (isWorkflowEvidenceTool(tool)) { const unknowns = recordWorkflowUnknowns({ name: tool, result: evidence.normalized_result, evidenceId: evidence.evidence_id, recordUnknown: (unknown) => session.recordUnknown(unknown), }); if (!unknowns.ok) return cliError(unknowns.error.retainPartialObservation(evidence)); } } if ( isWorkflowEvidenceTool(tool) && tool !== "trace_native_ui_action" && snapshotPath !== undefined && evidence !== undefined && session.allowsSnapshotReplay(tool) ) { const workflowRecord = workflowSnapshotRecord( evidence, tool, evidenceProfile, cachePolicy, ); if (workflowRecord !== undefined) { const recorded = session.recordWorkflowSnapshot(workflowRecord); if (!recorded.ok) return cliError(recorded.error.retainPartialObservation(evidence)); } } if ( tool !== "trace_native_ui_action" && snapshotPath !== undefined && evidence !== undefined ) { const snapshot = session.exportAnalysisSnapshot(); if (!snapshot.ok) return cliError(snapshot.error.retainPartialObservation(evidence)); const written = await session.writeAnalysisSnapshot( snapshot.value, snapshotPath, true, ); if (!written.ok) { const recovered = written.error instanceof EvidenceFileError && written.error.published !== undefined && (await session.closeWriteResources()).ok; if (!recovered) return cliError(written.error.retainPartialObservation(evidence)); } } return output; }, directAnalysisCleanupFailure, ); }; const executeAnalysisTool = async (input: { readonly session: BinarySession; readonly openedTarget: BinaryTarget; readonly tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool; readonly arguments: Readonly>; readonly signal: AbortSignal; readonly evidenceProfile: AnalysisProfileCommitment | undefined; }): Promise<{ readonly output: JsonValue; readonly evidence?: Evidence; readonly cachePolicy?: "live"; }> => { const { session, tool, signal, evidenceProfile } = input; if (tool === "analyze_function") { const { executeFunctionAnalysisEvidence } = await import("./FunctionAnalysisEvidence.js"); const result = await executeFunctionAnalysisEvidence( session, input.arguments, input.openedTarget, { signal }, ); return result.ok ? { output: result.value, evidence: result.value } : { output: cliError(result.error) }; } if ( tool === "binary_overview" || tool === "inspect_native_api" || tool === "inspect_native_dispatch_metadata" || tool === "trace_feature" || tool === "trace_native_values" || tool === "trace_native_ui_action" ) { const { EnhancedTools } = await import("./EnhancedTools.js"); const result = await executeComposedWorkflow(session, (observed) => new EnhancedTools(observed).execute(tool, input.arguments, signal), ); if (!result.ok) return { output: cliError(result.error) }; const evidence = createWorkflowEvidence({ target: input.openedTarget, operation: tool, parameters: input.arguments, result: result.value.result, upstreamProfile: evidenceProfile, upstreamLimitations: result.value.limitations, }); return { output: evidence, evidence, ...(result.value.cachePolicy === undefined ? {} : { cachePolicy: result.value.cachePolicy }), }; } let result = await session.execute(tool, input.arguments, { signal }); if ( tool === "xrefs" && typeof input.arguments.address === "string" && !result.ok && result.error instanceof AnalysisInputError && !result.error.cleanupIncomplete && result.error.issues.length > 0 && result.error.issues.every( ({ path, reason }) => path.length === 1 && path[0] === "address" && reason === "invalid_format", ) ) { const address = await resolveXrefsAddress( session, input.arguments.address, signal, ); if (!address.ok) return { output: cliError(address.error) }; if (address.value !== input.arguments.address) result = await session.execute( tool, { ...input.arguments, address: address.value }, { signal }, ); } if (!result.ok) return { output: cliError(result.error) }; const evidence = createEvidence( result.value.subject === undefined ? input.openedTarget : (result.value.subject ?? undefined), result.value.provider, { operation: tool, parameters: input.arguments, result: result.value.result, ...(result.value.analysisProfile === undefined ? {} : { analysisProfile: result.value.analysisProfile }), rawResult: result.value.rawResult, limitations: result.value.limitations, ...(result.value.relationshipCoverage === undefined ? {} : { relationshipCoverage: result.value.relationshipCoverage }), locations: result.value.locations, evidenceLinks: tool === "inspect_artifact" ? artifactInspectionResultSchema.parse(result.value.result) .evidence_links : [], }, ); return { output: evidence, evidence }; }; const withProcessCancellation = async ( suppliedSignal: AbortSignal | undefined, operation: (signal: AbortSignal) => Promise, ): Promise => { if (suppliedSignal !== undefined) return operation(suppliedSignal); const controller = new AbortController(); const cancel = (): void => controller.abort(); // Package runners can forward a terminal interrupt after REA already received // it directly. Keep both guards installed until provider cleanup has settled. process.on("SIGINT", cancel); process.on("SIGTERM", cancel); try { return await operation(controller.signal); } finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); } }; const prepareSnapshot = async (options: { readonly path: string; readonly formatHint?: ExecutableFormatHint; readonly architecture?: BinaryArchitecture; readonly entryPoint?: string; readonly snapshotPath: string | undefined; readonly signal: AbortSignal; }): Promise< Result< { readonly snapshot?: AnalysisSnapshot; readonly target?: BinaryTarget }, AnalysisError > > => { const { path, snapshotPath } = options; if (snapshotPath === undefined || !(await fileExists(snapshotPath))) return ok({}); const loaded = await readAnalysisSnapshot(snapshotPath, options.signal); if (!loaded.ok) return loaded; const target = await parseBinaryTarget(path, { signal: options.signal, ...(options.formatHint === undefined ? {} : { formatHint: options.formatHint }), ...(options.architecture === undefined ? {} : { architecture: options.architecture }), ...(options.entryPoint === undefined ? {} : { entryPoint: options.entryPoint }), }); if (!target.ok) return target; if (!snapshotMatchesTarget(loaded.value.target, target.value)) return err( new EvidenceIntegrityError( "Analysis snapshot target does not match the requested binary", ), ); return ok({ snapshot: loaded.value, target: target.value }); }; const inspectBundleResourceInput = async ( path: string, ): Promise> => { const callerPath = absoluteCallerPath(path); try { return ok((await stat(callerPath)).isDirectory()); } catch (cause: unknown) { const failure = classifyBinaryTargetFilesystemFailure(callerPath, cause); if (failure !== undefined) return err(failure); throw cause; } }; const allowsSnapshotReplay = ( route: SessionProviderRoute, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, ): boolean => { if (tool === "trace_native_ui_action") return false; const descriptor = route.capabilities?.get(tool); return descriptor === undefined ? ![...(route.capabilities?.values() ?? [])].some( ({ cachePolicy }) => cachePolicy === "live", ) : descriptor.cachePolicy !== "live"; }; const analysisProfileForRoute = ( route: SessionProviderRoute, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, ): AnalysisProfileCommitment | undefined => { const profile = route.profile; const nativeDispatchProfile = profile === null || profile === undefined ? nativeDispatchBindingProfile(route, tool) : undefined; if (profile === null || profile === undefined) { if (nativeDispatchProfile === undefined) return undefined; return isWorkflowEvidenceTool(tool) ? workflowAnalysisProfile(nativeDispatchProfile, tool) : nativeDispatchProfile; } if (isWorkflowEvidenceTool(tool)) return workflowAnalysisProfile(profile, tool); const provider = providerIdentityForRoute(route, tool); return provider.id === profile.provider.id ? profile : undefined; }; const nativeDispatchBindingProfile = ( route: SessionProviderRoute, operation: string, ): AnalysisProfileCommitment | undefined => { if (operation !== "inspect_native_dispatch_metadata") return undefined; return profilelessNativeDispatchProfile(route); }; /** Mirror BinarySession.providerIdentity for a route not opened yet. */ const providerIdentityForRoute = ( route: SessionProviderRoute, operation: string, ): ProviderIdentity => route.capabilities?.get(operation)?.provider ?? route.identity; const analysisProfileForEvidence = ( session: BinarySession, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, ): AnalysisProfileCommitment | undefined => { if (!isWorkflowEvidenceTool(tool)) return session.analysisProfile(tool); return session.analysisProfile() ?? session.analysisProfile(tool); }; const isWorkflowEvidenceTool = ( tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, ): boolean => tool === "binary_overview" || tool === "inspect_native_api" || tool === "inspect_native_dispatch_metadata" || tool === "trace_feature" || tool === "trace_native_values" || tool === "trace_native_ui_action"; const fileExists = async (path: string): Promise => { try { await access(path); return true; } catch (cause: unknown) { if ( typeof cause === "object" && cause !== null && "code" in cause && cause.code === "ENOENT" ) return false; throw cause; } }; const cliError = (error: AnalysisError): JsonValue => ({ error: "Analysis failed", ...projectAnalysisError(error), });