diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index 0ad7917de..aa7477662 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -11,6 +11,7 @@ import { } from "../../../tests/fixtures/analysisView.js"; import { inspectAnalysisView, + inspectAnalysisViewValidated, summarizeRetainedAnalysis, } from "./AnalysisViewService.js"; import type { Evidence } from "../../domain/evidence.js"; @@ -58,6 +59,35 @@ it.each(["/fixtures/unknown.exe", ""])( }, ); +it("reports inline Evidence identity failures at the complete source path", () => { + const parent = analysisViewLayoutEvidence(); + const input = { + source: { + kind: "inline", + evidence: { ...parent, evidence_id: `ev_${"0".repeat(64)}` }, + }, + view: { kind: "summary" }, + } as const; + + for (const result of [ + inspectAnalysisView(input), + inspectAnalysisViewValidated(input), + ]) + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + issues: [ + { + path: ["source", "evidence", "evidence_id"], + reason: "invalid_value", + message: "Evidence semantic identifier does not match its record", + }, + ], + }, + }); +}); + it("projects a small native view from an authenticated >10 MiB retained dossier", () => { const original = functionDossierSchema.parse(ghidraFunctionDossier()); const parent = createEvidence( diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index ea079802e..380feb59e 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -132,8 +132,11 @@ const resolveAnalysisViewSource = ( return err( analysisInputErrorFromIssues( INSPECT_ANALYSIS_VIEW_OPERATION, - cause.issues, - source.evidence, + cause.issues.map((issue) => ({ + ...issue, + path: ["source", "evidence", ...issue.path], + })), + { source }, { cause }, ), ); diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts index 3c15312aa..0de1f0b02 100644 --- a/tests/boundary/mcp/analysisViewMcp.test.ts +++ b/tests/boundary/mcp/analysisViewMcp.test.ts @@ -19,6 +19,7 @@ import { analysisViewJavaScriptEvidence, analysisViewJavaScriptAnalysisWithSource, analysisViewBindJavaScriptGraphs, + analysisViewLayoutEvidence, analysisViewLayoutFixture, } from "../../fixtures/analysisView.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; @@ -111,6 +112,35 @@ it("advertises exact schemas and projects one layout section from retained Evide ); }); +it("reports a tampered inline Evidence ID at its full MCP source path", async () => { + const { client } = await connect(); + const parent = analysisViewLayoutEvidence(); + const response = await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { + kind: "inline", + evidence: { ...parent, evidence_id: `ev_${"0".repeat(64)}` }, + }, + view: { kind: "summary" }, + }, + }); + + expect(parseMcpToolError(response).error).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + issues: [ + { + path: ["source", "evidence", "evidence_id"], + reason: "invalid_value", + message: "Evidence semantic identifier does not match its record", + }, + ], + }, + }); +}); + it("delivers a bounded native dossier view over MCP without a Ghidra provider or lost connection", async () => { const { client, session } = await connect(); const original = functionDossierSchema.parse(ghidraFunctionDossier());