diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index 5e3799b0d1..d1d631d5aa 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -10,7 +10,10 @@ 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 type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js"; +import { + ARTIFACT_ANALYSIS_OPERATIONS, + type ArtifactAnalysisOperation, +} from "../contracts/artifactToolContracts.js"; import { isManagedToolName, type ManagedToolName, @@ -79,6 +82,10 @@ type DirectAnalysisTool = | "trace_native_ui_action" | "trace_native_values"; +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 @@ -129,16 +136,26 @@ export const runProviderAnalysis = async ( logger?: Logger, signal?: AbortSignal, ] -): Promise => - isManagedToolName(tool) - ? runManagedProviderAnalysis(dependencies, path, tool, arguments_, signal) - : withProcessCancellation(signal, (operationSignal) => - runAnalysis(dependencies, path, tool, arguments_, { - logger, - snapshotPath: undefined, - signal: operationSignal, - }), - ); +): 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 } + : {}), + }), + ); +}; /** Execute managed metadata inspection in an isolated managed-only session. */ export const runManagedProviderExecution = async ( @@ -205,6 +222,7 @@ const runAnalysis = async ( readonly logger: Logger; readonly snapshotPath: string | undefined; readonly signal: AbortSignal; + readonly createSession?: () => BinarySession | Promise; readonly providerId?: AnalysisProviderSelector; readonly formatHint?: ExecutableFormatHint; readonly architecture?: BinaryArchitecture; @@ -212,9 +230,14 @@ const runAnalysis = async ( }, ): Promise => { const { logger, signal, snapshotPath } = options; - const config = dependencies.readConfiguration(); - if (!config.ok) return cliError(config.error); - const session = await dependencies.createBinarySession(config.value, logger); + 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 dependencies.createBinarySession(config.value, logger); + } return withSessionCleanup( session, async () => { diff --git a/src/application/DirectAnalysisDependencies.ts b/src/application/DirectAnalysisDependencies.ts index df49dc2fe8..ae34ea4046 100644 --- a/src/application/DirectAnalysisDependencies.ts +++ b/src/application/DirectAnalysisDependencies.ts @@ -11,6 +11,10 @@ export interface DirectAnalysisDependencies { config: AppConfig, logger: Logger, ) => BinarySession | Promise; + /** Create an artifact-only session without selecting native analysis engines. */ + readonly createArtifactBinarySession: () => + | BinarySession + | Promise; readonly createManagedBinarySession: () => | BinarySession | Promise; diff --git a/src/composition/binary.ts b/src/composition/binary.ts index b4b86f6379..0cb438cb36 100644 --- a/src/composition/binary.ts +++ b/src/composition/binary.ts @@ -41,3 +41,17 @@ export const createManagedBinarySession = (): BinarySession => composeBinarySession(new AnalysisProviderRegistry([]), [ new ManagedStaticProvider(), ]); + +/** Compose the artifact operations without candidates for deep analysis engines. */ +export const createArtifactBinarySession = ( + selectedEnvironment: Readonly, + platform: NodeJS.Platform = process.platform, +): Promise => + import("../artifacts/ArtifactProvider.js").then(({ ArtifactProvider }) => + composeBinarySession(new AnalysisProviderRegistry([]), [ + new ArtifactProvider( + snapshotEnvironment(selectedEnvironment, platform), + platform, + ), + ]), + ); diff --git a/src/composition/directAnalysis.ts b/src/composition/directAnalysis.ts index c920626c44..0955c6f589 100644 --- a/src/composition/directAnalysis.ts +++ b/src/composition/directAnalysis.ts @@ -28,6 +28,10 @@ export const createDirectAnalysis = ( const { createManagedBinarySession } = await import("./binary.js"); return createManagedBinarySession(); }, + createArtifactBinarySession: async () => { + const { createArtifactBinarySession } = await import("./binary.js"); + return createArtifactBinarySession(environment); + }, }; const runDirectAnalysis: BoundOperation< DirectAnalysisRuntime["runDirectAnalysis"] diff --git a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts index 7adf193f4a..2ba32af0b3 100644 --- a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts +++ b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts @@ -243,6 +243,7 @@ const createSnapshotReplayFixture = async ( return { readConfiguration: () => parseConfig({}), createBinarySession: () => createTestBinarySession(provider), + createArtifactBinarySession: () => createTestBinarySession(provider), createManagedBinarySession: () => createTestBinarySession(provider), }; }, @@ -924,6 +925,7 @@ describe("workflow snapshot profile and cancellation binding", () => { const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: () => session, + createArtifactBinarySession: () => session, createManagedBinarySession: () => session, }; const result = await runDirectAnalysis( diff --git a/tests/boundary/filesystem/nibNativeHierarchy.test.ts b/tests/boundary/filesystem/nibNativeHierarchy.test.ts index e2b948f571..5f7c07b3ed 100644 --- a/tests/boundary/filesystem/nibNativeHierarchy.test.ts +++ b/tests/boundary/filesystem/nibNativeHierarchy.test.ts @@ -220,7 +220,7 @@ describe.skipIf(process.platform !== "darwin")( REA_ANALYSIS_PROVIDER: "auto", }, }); - expect(output.exitCode).toBe(0); + expect(output.exitCode, JSON.stringify(output)).toBe(0); const result = interfaceBuilderAnalysisSchema.parse( parseEvidence(output.json).normalized_result, ); diff --git a/tests/boundary/mcp/officialArguments.test.ts b/tests/boundary/mcp/officialArguments.test.ts index a5fe0f2b11..ec4ff3c47b 100644 --- a/tests/boundary/mcp/officialArguments.test.ts +++ b/tests/boundary/mcp/officialArguments.test.ts @@ -70,6 +70,7 @@ describe("official operation arguments", () => { const dependencies = { readConfiguration: () => parseConfig({}), createBinarySession: createSession, + createArtifactBinarySession: createSession, createManagedBinarySession: createSession, }; const client = await connect({ execute }); diff --git a/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts b/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts index b5a66df42e..59437a193d 100644 --- a/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts +++ b/tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts @@ -330,6 +330,7 @@ describe("MCP composed workflow snapshot replay", () => { const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: () => createWorkflowSession(provider), + createArtifactBinarySession: () => createWorkflowSession(provider), createManagedBinarySession: () => createWorkflowSession(provider), }; const replay = await runDirectAnalysis( diff --git a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts index 2b79fce06d..c614f7d75e 100644 --- a/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts +++ b/tests/boundary/providers/native/artifactProvider.directAnalysis.test.ts @@ -6,11 +6,51 @@ import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { createDirectAnalysis } from "../../../../src/composition/directAnalysis.js"; +import { createArtifactBinarySession } from "../../../../src/composition/binary.js"; +import { thinMach } from "../../../../src/domain/binaryTarget.fixture.js"; +import { interfaceBuilderAnalysisSchema } from "../../../../src/domain/apple/interfaceBuilderGraph.js"; import { artifactInspectionResultSchema } from "../../../../src/domain/artifactInspection.js"; import { parseEvidence } from "../../../../src/domain/evidence.js"; const { runProviderAnalysis } = createDirectAnalysis({}); +it("runs static .app decoding without registering native analysis engines", async () => { + const root = await createTestTempDirectory("rea-artifact-only-direct-"); + const app = join(root, "Fixture.app"); + const contents = join(app, "Contents"); + await mkdir(join(contents, "MacOS"), { recursive: true }); + await mkdir(join(contents, "Resources"), { recursive: true }); + await writeFile( + join(contents, "Info.plist"), + "CFBundleExecutableFixture", + ); + await writeFile( + join(contents, "MacOS", "Fixture"), + thinMach(0xfeedfacf, 0x0100000c), + ); + + const staticSession = await createArtifactBinarySession({ + REA_ANALYSIS_PROVIDER: "auto", + }); + expect(staticSession.status()).toMatchObject({ + analysis_provider_candidates: [], + }); + await staticSession.close(); + + const evidence = parseEvidence( + await createDirectAnalysis({ + REA_ANALYSIS_PROVIDER: "auto", + }).runProviderAnalysis(app, "decode_interface_builder", {}), + ); + expect(evidence.operation).toBe("decode_interface_builder"); + expect(evidence.provider.id).toBe("rea-artifact-graph"); + const result = interfaceBuilderAnalysisSchema.parse( + evidence.normalized_result, + ); + expect(result.documents).toEqual([]); + expect(result.graph.target_sha256).toBe(evidence.subject?.digest.sha256); +}); + it("inspects an artifact through the direct CLI workflow without an REA grant", async () => { const root = await createTestTempDirectory("rea-artifact-direct-"); const source = join(root, "source"); diff --git a/tests/composition/directAnalysisCleanup.test.ts b/tests/composition/directAnalysisCleanup.test.ts index 51992d0696..6e5bc24efa 100644 --- a/tests/composition/directAnalysisCleanup.test.ts +++ b/tests/composition/directAnalysisCleanup.test.ts @@ -60,6 +60,7 @@ const factories = ( const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: () => session, + createArtifactBinarySession: () => session, createManagedBinarySession: () => session, }; return { dependencies, session }; diff --git a/tests/composition/directAnalysisFactories.test.ts b/tests/composition/directAnalysisFactories.test.ts index c1f294b61e..16d899b87c 100644 --- a/tests/composition/directAnalysisFactories.test.ts +++ b/tests/composition/directAnalysisFactories.test.ts @@ -34,6 +34,7 @@ const recordingFactories = (failure?: Error) => { const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: createSession, + createArtifactBinarySession: createSession, createManagedBinarySession: createSession, }; return { dependencies, sessions, clientsClosed: () => clientsClosed }; diff --git a/tests/composition/directAnalysisXrefs.test.ts b/tests/composition/directAnalysisXrefs.test.ts index b68832afa7..3591c41088 100644 --- a/tests/composition/directAnalysisXrefs.test.ts +++ b/tests/composition/directAnalysisXrefs.test.ts @@ -82,6 +82,7 @@ const fixture = ( const dependencies: DirectAnalysisDependencies = { readConfiguration: () => parseConfig({}), createBinarySession: createSession, + createArtifactBinarySession: createSession, createManagedBinarySession: createSession, }; return { dependencies, closed: () => closed };