diff --git a/src/application/binary/AnalysisSnapshotCache.test.ts b/src/application/binary/AnalysisSnapshotCache.test.ts index 86640eb2aa..dae7084061 100644 --- a/src/application/binary/AnalysisSnapshotCache.test.ts +++ b/src/application/binary/AnalysisSnapshotCache.test.ts @@ -33,6 +33,66 @@ import { import { EvidenceLedger } from "../investigation/EvidenceLedger.js"; import { enhancedOutputSchemas } from "../../contracts/toolOutputSchemaGroups.js"; +it.each(["provider", "workflow"] as const)( + "owns nested %s execution data before returning control to callers", + (kind) => { + const cache = new AnalysisSnapshotCache(); + const target = structuredClone(ANALYSIS_SNAPSHOT_TARGET); + const profile = structuredClone(ANALYSIS_SNAPSHOT_PROFILE); + const analysisProfile = + kind === "provider" ? profile : workflowAnalysisProfile(profile); + const execution = createAnalysisExecution( + { nested: { value: "original" } }, + structuredClone( + kind === "provider" + ? ANALYSIS_SNAPSHOT_PROVIDER + : REA_WORKFLOW_PROVIDER, + ), + { + rawResult: { nested: { value: "raw" } }, + subject: target, + analysisProfile, + locations: [{ kind: "address", address: "0x1000" }], + relationshipCoverage: { + status: "partial", + limitations: ["original coverage"], + }, + }, + ); + const input = { + target, + profile, + operation: "procedure_info" as const, + parameters: { address: "0x1000", document: "fixture" }, + execution: { ...execution, analysisProfile }, + }; + if (kind === "provider") cache.record(input); + else cache.recordWorkflow(input); + const entries = () => + kind === "provider" ? cache.entries() : cache.workflowEntries(); + const expected = entries(); + for (const payload of [execution.result, execution.rawResult]) { + if (payload === null || typeof payload !== "object") + throw new Error("Expected structured observation payload"); + Reflect.set(payload, "nested", { value: "caller edit" }); + } + const location = execution.locations[0]; + if (location !== undefined) Reflect.set(location, "address", "0x2000"); + if (execution.relationshipCoverage?.status === "partial") + execution.relationshipCoverage.limitations.push("caller edit"); + Reflect.set(execution.limitations, 0, "caller limitation"); + Reflect.set(execution.provider, "name", "caller provider"); + Reflect.set(execution.subject, "path", "/tmp/caller-edited-subject"); + if (kind === "workflow") { + const upstream = analysisProfile.parameters.upstream_analysis_profile; + if (upstream === null || typeof upstream !== "object") + throw new Error("Expected nested workflow profile"); + Reflect.set(upstream, "parameters", { loader: "caller profile" }); + } + expect(entries()).toEqual(expected); + }, +); + it("exports independent nested query, workflow and Evidence payloads", () => { const cache = new AnalysisSnapshotCache(); const target = ANALYSIS_SNAPSHOT_TARGET; diff --git a/src/domain/analysisSnapshot.ts b/src/domain/analysisSnapshot.ts index a4722fa3c2..2cb1084a78 100644 --- a/src/domain/analysisSnapshot.ts +++ b/src/domain/analysisSnapshot.ts @@ -358,25 +358,26 @@ type SnapshotExecutionProjection = Pick< | "subject" >; -/** Project one authenticated execution into the shared persisted snapshot shape. */ +/** Own one execution's snapshot data independently of its caller-visible result. */ const snapshotExecutionProjection = ( execution: SnapshotExecution, provider: AnalysisSnapshotEntry["execution"]["provider"], -): SnapshotExecutionProjection => ({ - result: execution.result, - raw_result: execution.rawResult, - provider, - limitations: [...execution.limitations], - relationship_coverage: execution.relationshipCoverage ?? null, - locations: [...execution.locations], - subject: - execution.subject === null - ? null - : subjectSchema.parse({ - ...execution.subject, - architecture: execution.subject.architecture ?? null, - }), -}); +): SnapshotExecutionProjection => + entrySchema.shape.execution.parse({ + result: execution.result, + raw_result: execution.rawResult, + provider, + limitations: execution.limitations, + relationship_coverage: execution.relationshipCoverage ?? null, + locations: execution.locations, + subject: + execution.subject === null + ? null + : { + ...execution.subject, + architecture: execution.subject.architecture ?? null, + }, + }); const canonicalJson = (value: unknown): string => { const json = jsonValueSchema.parse(value); @@ -549,7 +550,7 @@ export const createAnalysisSnapshotWorkflowEntry = (input: { execution, execution.analysisProfile.provider, ), - analysis_profile: execution.analysisProfile, + analysis_profile: analysisProfileSchema.parse(execution.analysisProfile), }, }; }; diff --git a/tests/boundary/filesystem/analysisSnapshotProvenance.test.ts b/tests/boundary/filesystem/analysisSnapshotProvenance.test.ts index 3b42be0b73..e7b3dba979 100644 --- a/tests/boundary/filesystem/analysisSnapshotProvenance.test.ts +++ b/tests/boundary/filesystem/analysisSnapshotProvenance.test.ts @@ -14,6 +14,59 @@ import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js" import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +it("keeps real provider replay and persisted Evidence independent of caller edits", async () => { + const directory = await createTestTempDirectory( + "rea-snapshot-result-ownership-", + ); + const path = join(directory, "application.exe"); + await writeFile(path, buildManagedPeFixture()); + const provider = new ManagedStaticProvider(); + const profile = createAnalysisProfile(MANAGED_STATIC_PROVIDER, { + fixture: "snapshot-result-ownership", + }); + const session = createTestBinarySession(provider, { + resolveAnalysisProfile: () => Promise.resolve(ok({ profile })), + }); + onTestFinished(async () => { + const closed = await session.close(); + if (!closed.ok) throw closed.error; + }); + const opened = await session.open(path); + if (!opened.ok) throw opened.error; + const observed = await session.execute("inspect_managed_artifact", {}); + if (!observed.ok) throw observed.error; + const expected = structuredClone(observed.value.result); + if (observed.value.evidenceId === undefined) + throw new Error("Real managed observation has no Evidence identity"); + const historical = session.evidenceById(observed.value.evidenceId); + if (historical === undefined) throw new Error("Observation was not retained"); + if ( + observed.value.result === null || + typeof observed.value.result !== "object" + ) + throw new Error("Expected a structured managed artifact result"); + Reflect.set(observed.value.result, "caller_edit", "must not enter replay"); + + const replayed = await session.execute("inspect_managed_artifact", {}); + if (!replayed.ok) throw replayed.error; + expect(replayed.value.evidenceId).toBe(observed.value.evidenceId); + expect(replayed.value.result).toEqual(expected); + expect(session.evidenceById(observed.value.evidenceId)).toEqual(historical); + const snapshot = session.exportAnalysisSnapshot(); + if (!snapshot.ok) throw snapshot.error; + const snapshotPath = join(directory, "analysis.json"); + const written = await session.writeAnalysisSnapshot( + snapshot.value, + snapshotPath, + false, + ); + if (!written.ok) throw written.error; + const restored = await readAnalysisSnapshot(snapshotPath); + if (!restored.ok) throw restored.error; + expect(restored.value.entries[0]?.execution.result).toEqual(expected); + expect(restored.value.evidence_bundle.records).toContainEqual(historical); +}); + it("reruns ambiguous imported observations without changing their historical Evidence", async () => { const directory = await createTestTempDirectory("rea-snapshot-provenance-"); const path = join(directory, "application.exe");