diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index e1b5b255d8..b603bc4967 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -365,8 +365,9 @@ admitted digest, inspection and comparison report retryable `artifact_changed`; any completed hash retains the observed digest and byte count. Retry against a stable source. This differs from corrupted retained Evidence, which remains an Evidence-integrity failure. Malformed selected PE input has the same input-format -classification in inspection and comparison, and a failed right-side comparison -retains the completed left observation. +classification in inspection and comparison. Its issue identifies the selected +filename and argument: `path` for inspection, or `leftPath` / `rightPath` for CLI +comparison. A failed right-side comparison retains the completed left observation. ## Member comparison diff --git a/src/application/managed/ManagedMemberComparisonService.ts b/src/application/managed/ManagedMemberComparisonService.ts index 6bb2594909..daa5cc799c 100644 --- a/src/application/managed/ManagedMemberComparisonService.ts +++ b/src/application/managed/ManagedMemberComparisonService.ts @@ -118,9 +118,10 @@ export const compareManagedMemberPaths = async ( if (!rightTarget.ok) return err(rightTarget.error); const inspectTarget = async ( target: BinaryTarget, - selectedPath: string, + inputField: "leftPath" | "rightPath", ) => { const { path } = target; + const selectedPath = input[inputField]; let snapshot: Awaited>; try { snapshot = await readSnapshot( @@ -143,7 +144,12 @@ export const compareManagedMemberPaths = async ( }); } catch (cause: unknown) { throw ( - managedInspectionInputFailure(operation, selectedPath, cause) ?? cause + managedInspectionInputFailure( + operation, + selectedPath, + cause, + inputField, + ) ?? cause ); } const evidence = createEvidence(target, MANAGED_STATIC_PROVIDER, { @@ -156,9 +162,9 @@ export const compareManagedMemberPaths = async ( }); return { inspection, evidence }; }; - const left = await inspectTarget(leftTarget.value, input.leftPath); + const left = await inspectTarget(leftTarget.value, "leftPath"); partialEvidence = left.evidence; - const right = await inspectTarget(rightTarget.value, input.rightPath); + const right = await inspectTarget(rightTarget.value, "rightPath"); const result = compareManagedMembers( { evidenceId: left.evidence.evidence_id, diff --git a/src/dotnet/ManagedReaderFailure.ts b/src/dotnet/ManagedReaderFailure.ts index d74306d770..2308438833 100644 --- a/src/dotnet/ManagedReaderFailure.ts +++ b/src/dotnet/ManagedReaderFailure.ts @@ -35,12 +35,13 @@ export const managedInspectionInputFailure = ( operation: string, path: string, cause: unknown, + inputField: "path" | "leftPath" | "rightPath" = "path", ): AnalysisInputError | undefined => cause instanceof ManagedReaderFailure && managedMetadataFailureStatus(cause.issue) === "malformed" ? new AnalysisInputError(operation, { cause }, [ { - path: ["path"], + path: [inputField], reason: "invalid_format", message: `Selected artifact ${path}: ${cause.issue.detail}`, }, diff --git a/tests/boundary/cli/managedMemberComparisonCli.test.ts b/tests/boundary/cli/managedMemberComparisonCli.test.ts new file mode 100644 index 0000000000..3f131a9025 --- /dev/null +++ b/tests/boundary/cli/managedMemberComparisonCli.test.ts @@ -0,0 +1,87 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect } from "vitest"; + +import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest( + "attributes malformed managed comparison input to its selected argument and retains completed peer Evidence", + async ({ cli }) => { + const directory = await createTestTempDirectory( + "rea-managed-malformed-cli-", + ); + const good = join(directory, "good.dll"); + const bad = join(directory, "bad.dll"); + const bytes = buildManagedPeFixture(); + await writeFile(good, bytes); + bytes.writeUInt16LE(0, bytes.readUInt32LE(0x3c) + 6); + await writeFile(bad, bytes); + + const healthy = await cli.run({ + arguments: ["compare-managed-members", good, good, "--json"], + }); + expect(healthy.exitCode, healthy.stderr).toBe(0); + expect(healthy.json).toMatchObject({ + operation: "compare_managed_members", + parameters: { left_path: good, right_path: good }, + }); + + for (const field of ["leftPath", "rightPath"] as const) { + const result = await cli.run({ + arguments: [ + "compare-managed-members", + field === "leftPath" ? bad : good, + field === "rightPath" ? bad : good, + "--json", + ], + }); + expect(result.exitCode, result.stderr).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + retryable: false, + details: { + issues: [ + { + path: [field], + reason: "invalid_format", + message: `Selected artifact ${bad}: PE section count must be positive`, + }, + ], + }, + }); + if (field === "rightPath") + expect(result.json).toMatchObject({ + details: { + partial_observation: { + operation: "inspect_managed_members", + subject: { local_path: good }, + }, + }, + }); + else + expect(result.json).not.toHaveProperty("details.partial_observation"); + } + + const inspection = await cli.run({ + arguments: ["inspect-managed-members", bad, "--json"], + }); + expect(inspection.exitCode, inspection.stderr).toBe(1); + expect(inspection.json).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + issues: [ + { + path: ["path"], + reason: "invalid_format", + message: `Selected artifact ${bad}: PE section count must be positive`, + }, + ], + }, + }); + }, +); diff --git a/tests/boundary/providers/managed/managedMemberComparison.test.ts b/tests/boundary/providers/managed/managedMemberComparison.test.ts index 6657132b9b..2ddaa6018c 100644 --- a/tests/boundary/providers/managed/managedMemberComparison.test.ts +++ b/tests/boundary/providers/managed/managedMemberComparison.test.ts @@ -912,41 +912,6 @@ describe("managed comparison snapshot admission and cleanup", () => { }); }); -describe("managed comparison malformed artifact admission", () => { - it.each(["left", "right"] as const)( - "retains malformed %s source classification and completed peer Evidence", - async (side) => { - const directory = await createTestTempDirectory("rea-managed-malformed-"); - const good = join(directory, "good.dll"); - const bad = join(directory, "bad.dll"); - const bytes = buildManagedPeFixture(); - await writeFile(good, bytes); - bytes.writeUInt16LE(0, bytes.readUInt32LE(0x3c) + 6); - await writeFile(bad, bytes); - const result = await compareManagedMemberPaths({ - leftPath: side === "left" ? bad : good, - rightPath: side === "right" ? bad : good, - }); - expect(result.ok).toBe(false); - if (result.ok) return; - expect(result.error._tag).toBe("AnalysisInputError"); - expect(projectAnalysisError(result.error)).toMatchObject({ - code: "invalid_request", - details: { - issues: [ - expect.objectContaining({ - message: expect.stringContaining("PE section count"), - }), - ], - }, - }); - expect(result.error.partialObservation !== undefined).toBe( - side === "right", - ); - }, - ); -}); - describe("managed comparison target identity", () => { it("rejects a file that changes after target identity is resolved", async () => { const directory = await createTestTempDirectory(