From 07eb044f77444de131d6c1bcdec082e5a5fcd221 Mon Sep 17 00:00:00 2001 From: Wenyu Chiou <162016108+WenyuChiou@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:35:27 -0400 Subject: [PATCH] fix(snapshot): decline ambiguous exact evidence replay (#7037) --- src/domain/analysisSnapshot.test.ts | 45 +++++++++ src/domain/analysisSnapshot.ts | 39 +++++--- .../composedWorkflowEvidenceLinks.test.ts | 99 +++++++++++++++++++ 3 files changed, 169 insertions(+), 14 deletions(-) diff --git a/src/domain/analysisSnapshot.test.ts b/src/domain/analysisSnapshot.test.ts index 841585bf86..e72c0740a3 100644 --- a/src/domain/analysisSnapshot.test.ts +++ b/src/domain/analysisSnapshot.test.ts @@ -422,6 +422,51 @@ describe("analysis snapshot canonical ordering and scope", () => { }); }); +it("does not select arbitrary provenance when multiple authenticated records match one replay entry", () => { + const { current, snapshot } = snapshotWithHistoricalEvidence(); + const source = snapshot.evidence_bundle.records.find( + (record) => record.operation === "unprofiled_query", + )!; + const alternative = createEvidence( + ANALYSIS_SNAPSHOT_TARGET, + ANALYSIS_SNAPSHOT_PROVIDER, + { + operation: current.operation, + parameters: current.parameters, + result: current.normalized_result, + analysisProfile: ANALYSIS_SNAPSHOT_PROFILE, + evidenceLinks: [source.evidence_id], + }, + ); + expect(alternative.evidence_id).not.toBe(current.evidence_id); + const ambiguous = parseAnalysisSnapshot({ + ...snapshot, + evidence_bundle: createEvidenceBundle([ + ...snapshot.evidence_bundle.records, + alternative, + ]), + }); + expect(ambiguous.entries).toEqual(snapshot.entries); + const reversedInsertion = parseAnalysisSnapshot({ + ...ambiguous, + evidence_bundle: createEvidenceBundle( + [...ambiguous.evidence_bundle.records].reverse(), + ), + }); + expect(reversedInsertion).toEqual(ambiguous); + for (const candidate of [ambiguous, reversedInsertion]) + expect( + snapshotEvidenceForQuery(candidate, { + target: ANALYSIS_SNAPSHOT_TARGET, + bindingProfile: ANALYSIS_SNAPSHOT_PROFILE, + operation: current.operation, + parameters: current.parameters, + provider: ANALYSIS_SNAPSHOT_PROVIDER, + evidenceProfile: ANALYSIS_SNAPSHOT_PROFILE, + }), + ).toBeUndefined(); +}); + describe("analysis snapshot Evidence binding", () => { it("finds only Evidence committed to the exact binding and profile", () => { const { current, snapshot } = snapshotWithHistoricalEvidence(); diff --git a/src/domain/analysisSnapshot.ts b/src/domain/analysisSnapshot.ts index 12c1b6cf9b..3d5d7df395 100644 --- a/src/domain/analysisSnapshot.ts +++ b/src/domain/analysisSnapshot.ts @@ -502,20 +502,31 @@ export const snapshotEvidenceForQuery = ( (candidate) => candidate.query_id === queryId, ); if (entry === undefined && workflowEntry === undefined) return undefined; - const evidence = snapshot.evidence_bundle.records.find( - (record) => - record.subject?.digest.sha256 === target.sha256 && - record.operation === operation && - record.provider.id === provider.id && - record.provider.name === provider.name && - record.provider.version === provider.version && - record.analysis_profile !== null && - analysisProfilesEqual(record.analysis_profile, evidenceProfile) && - canonicalJson(record.parameters) === encodedParameters && - ((entry !== undefined && evidenceMatchesEntry(record, entry, snapshot)) || - (workflowEntry !== undefined && - evidenceMatchesWorkflowEntry(record, workflowEntry, snapshot))), - ); + let evidence: Evidence | undefined; + for (const record of snapshot.evidence_bundle.records) { + if ( + !( + record.subject?.digest.sha256 === target.sha256 && + record.operation === operation && + record.provider.id === provider.id && + record.provider.name === provider.name && + record.provider.version === provider.version && + record.analysis_profile !== null && + analysisProfilesEqual(record.analysis_profile, evidenceProfile) && + canonicalJson(record.parameters) === encodedParameters && + ((entry !== undefined && + evidenceMatchesEntry(record, entry, snapshot)) || + (workflowEntry !== undefined && + evidenceMatchesWorkflowEntry(record, workflowEntry, snapshot))) + ) + ) + continue; + // Entries bind the result, not every part of the Evidence commitment + // (for example, upstream links). Do not choose provenance by ID sort order. + if (evidence !== undefined && evidence.evidence_id !== record.evidence_id) + return undefined; + evidence = record; + } return evidence === undefined ? undefined : rebindEvidenceToTargetPath(evidence, target); diff --git a/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts b/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts index c31c3f5662..11be0d2eea 100644 --- a/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts +++ b/tests/boundary/filesystem/composedWorkflowEvidenceLinks.test.ts @@ -6,6 +6,105 @@ import { expect, it } from "vitest"; import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { composedWorkflowFixture } from "../../fixtures/composedWorkflowEvidence.js"; +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; + +it.each([0, 1, 12])( + "does not replay an arbitrary historical workflow provenance when the snapshot binding is ambiguous (%i procedures)", + async (procedureCount) => { + const original = await composedWorkflowFixture(); + original.summary.procedure_count = procedureCount; + const target = await original.open(); + const snapshotPath = join(dirname(original.path), "analysis.json"); + const current = parseEvidence( + await runDirectAnalysis( + original.dependencies, + original.path, + "binary_overview", + {}, + { snapshotPath }, + ), + ); + expect(current.evidence_links).toHaveLength(1); + const snapshot = parseAnalysisSnapshot( + JSON.parse(await readFile(snapshotPath, "utf8")), + ); + const uniqueReplay = await composedWorkflowFixture(); + expect(uniqueReplay.session.importAnalysisSnapshot(snapshot).ok).toBe(true); + expect(uniqueReplay.session.importAnalysisSnapshot(snapshot)).toEqual( + ok(0), + ); + const unique = parseEvidence( + await runDirectAnalysis( + uniqueReplay.dependencies, + original.path, + "binary_overview", + {}, + { snapshotPath }, + ), + ); + expect(unique.evidence_id).toBe(current.evidence_id); + expect(unique.evidence_links).toEqual(current.evidence_links); + expect(uniqueReplay.calls).toEqual([]); + // Earlier composed observations did not carry links. Both are authentic, + // but the serialized replay binding does not identify which provenance won. + const historical = createEvidence(target, current.provider, { + operation: current.operation, + parameters: current.parameters, + result: current.normalized_result, + rawResult: current.raw_result, + analysisProfile: current.analysis_profile!, + confidence: "derived", + limitations: current.limitations, + }); + expect(historical.evidence_links).toEqual([]); + expect(historical.evidence_id).not.toBe(current.evidence_id); + const ambiguous = parseAnalysisSnapshot({ + ...snapshot, + evidence_bundle: createEvidenceBundle([ + ...snapshot.evidence_bundle.records, + historical, + ]), + }); + expect(ambiguous.workflow_entries).toEqual(snapshot.workflow_entries); + await writeFile(snapshotPath, JSON.stringify(ambiguous)); + const replay = await composedWorkflowFixture(); + const delivered = parseEvidence( + await runDirectAnalysis( + replay.dependencies, + original.path, + "binary_overview", + {}, + { snapshotPath }, + ), + ); + expect(delivered.normalized_result).toEqual(current.normalized_result); + expect([current.evidence_id, historical.evidence_id]).not.toContain( + delivered.evidence_id, + ); + expect(delivered.evidence_links).toEqual(current.evidence_links); + expect( + delivered.limitations.some((limitation) => + limitation.includes("this call did not re-run the provider"), + ), + ).toBe(true); + expect(replay.calls).toEqual([]); + const refreshedReplay = await composedWorkflowFixture(); + const refreshed = parseEvidence( + await runDirectAnalysis( + refreshedReplay.dependencies, + original.path, + "binary_overview", + {}, + { snapshotPath }, + ), + ); + expect(refreshed.evidence_id).toBe(delivered.evidence_id); + expect(refreshed.evidence_links).toEqual(delivered.evidence_links); + expect(refreshedReplay.calls).toEqual([]); + }, +); it.each([false, true])( "links a profiled binary overview only to the atomic record it consumed (cached=%s)",