import { projectPermissionFailure } from "./PermissionFailure.js"; import { artifactExtractionPermissionRequest } from "./ArtifactExtractionDestination.js"; import { parseConfig, type AppConfig } from "../config.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { EnhancedTools } from "./EnhancedTools.js"; import { createBinarySession } from "./runtime.js"; import { silentLogger, type Logger } from "../logger.js"; import { createEvidence } from "../domain/evidence.js"; import type { Evidence } from "../domain/evidence.js"; import type { NativeToolName } from "../contracts/nativeToolContracts.js"; import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js"; import type { ManagedToolName } from "../contracts/managedToolContracts.js"; import { EvidenceIntegrityError, projectAnalysisError, type AnalysisError, } from "../domain/errors.js"; import { access } from "node:fs/promises"; import { readAnalysisSnapshot, writeAnalysisSnapshot, } from "./AnalysisSnapshotFiles.js"; import { parseBinaryTarget } from "./BinaryTargetResolver.js"; import type { BinaryTarget } from "../domain/binaryTarget.js"; import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js"; import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js"; import { err, ok, type Result } from "../domain/result.js"; import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js"; import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js"; import type { PermissionAuthority } from "./PermissionAuthority.js"; import { REA_WORKFLOW_PROVIDER, workflowAnalysisProfile, } from "./InvestigationProviders.js"; import type { AnalysisProviderSelector } from "../contracts/providerSelection.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; export { runCapabilityStatus, runProviderStatus, } from "./DirectAnalysisStatus.js"; type DirectAnalysisTool = | "binary_overview" | "procedure_pseudo_code" | "read_function_instructions" | "analyze_function" | "inspect_native_api" | "search_strings" | "search_procedures" | "xrefs" | "trace_feature"; /** * 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 ( path: string, tool: DirectAnalysisTool, arguments_: Readonly>, options: { readonly logger?: Logger; readonly snapshotPath?: string | undefined; readonly signal?: AbortSignal; readonly permissionAuthority?: PermissionAuthority; readonly providerId?: AnalysisProviderSelector; } = {}, ): Promise => withProcessCancellation(options.signal, (signal) => runAnalysis(path, tool, arguments_, { logger: options.logger ?? silentLogger, snapshotPath: options.snapshotPath, signal, ...(options.providerId === undefined ? {} : { providerId: options.providerId }), ...(options.permissionAuthority === undefined ? {} : { permissionAuthority: options.permissionAuthority }), }), ); /** Execute one provider-native semantic operation with atomic provenance. */ export const runProviderAnalysis = async ( ...[path, tool, arguments_, logger = silentLogger, signal]: readonly [ path: string, tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName, arguments_: Readonly>, logger?: Logger, signal?: AbortSignal, ] ): Promise => withProcessCancellation(signal, (operationSignal) => runAnalysis(path, tool, arguments_, { logger, snapshotPath: undefined, signal: operationSignal, }), ); const authorizeAnalysis = async ( authority: PermissionAuthority, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, arguments_: Readonly>, ): Promise> => { const requests = []; if (tool === "extract_artifact" && typeof arguments_.output_root === "string") requests.push({ request: artifactExtractionPermissionRequest(), access: "write" as const, }); for (const request of requests) { const result = await authority.authorize(request.request, request.access); if (!result.ok) return err(projectPermissionFailure(result.error)); } if ( ["inventory_artifact", "inspect_artifact"].includes(tool) && arguments_.native_mount_approved === true ) { const result = await authority.authorize( { capability: "native_mount", roots: [], executables: [], environment_names: [], network: "none", mount: true, operation_identity: `${tool}:native_mount`, }, "read", ); if (!result.ok) return err(projectPermissionFailure(result.error)); } return ok(null); }; const permissionAuthorityFor = ( config: AppConfig, supplied: PermissionAuthority | undefined, ): ReturnType => supplied === undefined ? loadConfiguredPermissionAuthority(config) : Promise.resolve(ok(supplied)); const authorizeAnalysisRun = async (input: { readonly config: AppConfig; readonly suppliedAuthority: PermissionAuthority | undefined; readonly tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool; readonly arguments: Readonly>; }): Promise> => { const authority = await permissionAuthorityFor( input.config, input.suppliedAuthority, ); if (!authority.ok) return authority; const operation = await authorizeAnalysis( authority.value, input.tool, input.arguments, ); if (!operation.ok) return operation; return ok(null); }; const runAnalysis = async ( path: string, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, arguments_: Readonly>, options: { readonly logger: Logger; readonly snapshotPath: string | undefined; readonly signal: AbortSignal; readonly permissionAuthority?: PermissionAuthority; readonly providerId?: AnalysisProviderSelector; }, ): Promise => { const { logger, signal, snapshotPath } = options; const config = parseConfig(process.env); if (!config.ok) return cliError(config.error); const authorization = await authorizeAnalysisRun({ config: config.value, suppliedAuthority: options.permissionAuthority, tool, arguments: arguments_, }); if (!authorization.ok) return cliError(authorization.error); const session = createBinarySession(config.value, logger); try { const prepared = await prepareSnapshot({ path, snapshotPath, }); if (!prepared.ok) return cliError(prepared.error); const { snapshot } = prepared.value; const opened = await session.open(path, { signal, ...(snapshot === undefined ? {} : { snapshot }), ...(options.providerId === undefined ? {} : { providerId: options.providerId }), }); if (!opened.ok) return cliError(opened.error); const evidenceProfile = analysisProfileForEvidence(session, tool); const bindingProfile = session.analysisProfile(); if ( snapshot !== undefined && evidenceProfile !== undefined && bindingProfile !== undefined ) { const cached = snapshotEvidenceForQuery(snapshot, { target: opened.value, bindingProfile, operation: tool, parameters: arguments_, provider: replayProviderFor(session, tool), evidenceProfile, }); if (cached !== undefined) return cached; } const { output, evidence } = await executeAnalysisTool({ session, openedTarget: opened.value, tool, arguments: arguments_, signal, evidenceProfile, }); if (evidence !== undefined) session.recordEvidence(evidence); if (snapshotPath !== undefined && evidence !== undefined) { const snapshot = session.exportAnalysisSnapshot(); if (!snapshot.ok) return cliError(snapshot.error); const written = await writeAnalysisSnapshot( snapshot.value, snapshotPath, true, ); if (!written.ok) return cliError(written.error); } return output; } finally { await session.close(); } }; const executeAnalysisTool = async (input: { readonly session: ReturnType; 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 }> => { const { session, tool, signal, evidenceProfile } = input; if ( tool === "binary_overview" || tool === "analyze_function" || tool === "inspect_native_api" || tool === "trace_feature" ) { const result = await new EnhancedTools(session).execute( tool, input.arguments, signal, ); if (!result.ok) return { output: cliError(result.error) }; const evidence = createEvidence( input.openedTarget, tool === "analyze_function" ? session.providerIdentity(tool) : REA_WORKFLOW_PROVIDER, { operation: tool, parameters: input.arguments, result: result.value, ...(evidenceProfile === undefined ? {} : { analysisProfile: evidenceProfile }), confidence: "derived", limitations: ["Derived by an REA composed workflow."], }, ); return { output: evidence, evidence }; } const result = await session.execute(tool, input.arguments, { signal }); if (!result.ok) return { output: cliError(result.error) }; const evidence = createEvidence( result.value.subject ?? input.openedTarget, 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, 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(); process.once("SIGINT", cancel); try { return await operation(controller.signal); } finally { process.off("SIGINT", cancel); } }; const prepareSnapshot = async (options: { readonly path: string; readonly snapshotPath: string | undefined; }): Promise< Result<{ readonly snapshot?: AnalysisSnapshot }, AnalysisError> > => { const { path, snapshotPath } = options; if (snapshotPath === undefined || !(await fileExists(snapshotPath))) return ok({}); const loaded = await readAnalysisSnapshot(snapshotPath); if (!loaded.ok) return loaded; const target = await parseBinaryTarget(path); 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 }); }; const replayProviderFor = ( session: ReturnType, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, ) => isWorkflowEvidenceTool(tool) ? REA_WORKFLOW_PROVIDER : session.providerIdentity(tool); const analysisProfileForEvidence = ( session: ReturnType, tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, ): AnalysisProfileCommitment | undefined => { if (!isWorkflowEvidenceTool(tool)) return session.analysisProfile(tool); const upstream = session.analysisProfile(); return upstream === undefined ? undefined : workflowAnalysisProfile(upstream); }; const isWorkflowEvidenceTool = ( tool: | NativeToolName | ArtifactAnalysisOperation | ManagedToolName | DirectAnalysisTool, ): boolean => tool === "binary_overview" || tool === "inspect_native_api" || tool === "trace_feature"; 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), });