mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(managed): preserve malformed signature uncertainty in comparisons
Consume the signature producer status before inferring operand-resolution limits. Preserve exact byte-match provenance while malformed paired signatures remain unknown through current and historical Evidence, CLI and MCP.
This commit is contained in:
1 parent
6298c84eea
commit
263a98c6ce
4 files changed
+162
-4
No files matched your search
@@ -544,7 +544,11 @@ establish which library the runtime loaded.
|
||||
|
||||
Signatures retain their decoded fields, but a Field or MethodDef row whose
|
||||
signature has the other member kind is malformed and cannot establish an
|
||||
unchanged signature. Runtime-provided methods are counted separately in
|
||||
unchanged signature. Comparison retains exact byte-based pairs for malformed
|
||||
signatures, including empty, truncated, or invalid headers, but reports them as
|
||||
unknown with a `signature-malformed` limitation when no change is observed.
|
||||
This applies to historical Evidence without signature-resolution attestation as
|
||||
well. Runtime-provided methods are counted separately in
|
||||
`native_implementations.runtime_provided`; they have no file-backed native body
|
||||
for this workflow to map and do not increase `native_body_unresolved`. Historical
|
||||
verification Evidence without this separate count leaves it unknown (`null`).
|
||||
|
||||
@@ -549,17 +549,25 @@ const unresolved = (member: Method | Field, resolved: boolean): boolean =>
|
||||
member.signature.kind !== "unknown" &&
|
||||
member.signature.resolved_sha256 === null;
|
||||
|
||||
/** Equal build-local bytes cannot establish equal unresolved type operands. */
|
||||
/** Exact bytes retain a pair without establishing a valid or resolved signature. */
|
||||
const pairedSignatureLimitations = (
|
||||
left: Method | Field,
|
||||
right: Method | Field,
|
||||
examined: boolean,
|
||||
): string[] =>
|
||||
unresolved(left, examined) || unresolved(right, examined)
|
||||
): string[] => {
|
||||
if (
|
||||
left.signature.parse_status === "malformed" ||
|
||||
right.signature.parse_status === "malformed"
|
||||
)
|
||||
return [
|
||||
"signature-malformed: At least one signature is malformed; equal signature bytes do not establish an unchanged member signature.",
|
||||
];
|
||||
return unresolved(left, examined) || unresolved(right, examined)
|
||||
? [
|
||||
"signature-not-resolved: At least one signature names a type REA could not resolve; equal build-local row bytes do not establish equivalent signature types.",
|
||||
]
|
||||
: [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Declared names of unpaired members whose signature was not decoded: one-sided
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js";
|
||||
import { managedMemberComparisonResultSchema } from "../../../src/domain/managed/managedMemberComparison.js";
|
||||
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
@@ -85,3 +89,98 @@ cliTest(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"keeps malformed signatures unknown through CLI and stdio MCP comparison while a valid control stays unchanged",
|
||||
async ({ cli }) => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-managed-signature-cli-",
|
||||
);
|
||||
const call = await cli.sharedMcpCall!();
|
||||
for (const malformed of [true, false]) {
|
||||
const name = malformed ? "truncated" : "control";
|
||||
const path = join(directory, `${name}.dll`);
|
||||
const peerPath = join(directory, `${name}-peer.dll`);
|
||||
const bytes = buildManagedPeFixture({
|
||||
methodSignature: Buffer.from(
|
||||
malformed ? [0x00, 0x80] : [0x00, 0x00, 0x01],
|
||||
),
|
||||
ilBody: Buffer.from([0x06, 0x2a]),
|
||||
});
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
await writeFile(path, bytes);
|
||||
await writeFile(peerPath, bytes);
|
||||
const inspected = await cli.sharedCliRun!({
|
||||
arguments: ["inspect-managed-members", path, "--json"],
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
||||
const cliEvidence = parseEvidence(inspected.json);
|
||||
const mcpInspected = await call("inspect_managed_members", {
|
||||
path: peerPath,
|
||||
});
|
||||
expect(mcpInspected.isError, JSON.stringify(mcpInspected)).not.toBe(true);
|
||||
const mcpEvidence = parseEvidence(mcpInspected.structuredContent);
|
||||
for (const [evidence, sourcePath] of [
|
||||
[cliEvidence, path],
|
||||
[mcpEvidence, peerPath],
|
||||
] as const) {
|
||||
const report = managedMemberInspectionSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
expect(report.artifact).toMatchObject({ path: sourcePath, sha256 });
|
||||
expect(report.coverage.state).toBe(malformed ? "partial" : "complete");
|
||||
expect(report.methods[0]?.signature).toMatchObject({
|
||||
parse_status: malformed ? "malformed" : "decoded",
|
||||
...(malformed
|
||||
? { issue: "truncated compressed integer", resolved_sha256: null }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
const compared = await cli.sharedCliRun!({
|
||||
arguments: ["compare-managed-members", path, path, "--json"],
|
||||
});
|
||||
expect(compared.exitCode, compared.stderr).toBe(0);
|
||||
// Identical bytes under separate paths provide distinct authenticated
|
||||
// observations for MCP, whose Evidence inputs must have different IDs.
|
||||
const mcpCompared = await call("compare_managed_members", {
|
||||
left: cliEvidence,
|
||||
right: mcpEvidence,
|
||||
});
|
||||
expect(mcpCompared.isError, JSON.stringify(mcpCompared)).not.toBe(true);
|
||||
for (const evidence of [
|
||||
parseEvidence(compared.json),
|
||||
parseEvidence(mcpCompared.structuredContent),
|
||||
]) {
|
||||
const report = managedMemberComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
expect(report.left.artifact_sha256).toBe(sha256);
|
||||
expect(report.right.artifact_sha256).toBe(sha256);
|
||||
expect(report.coverage.status).toBe(
|
||||
malformed ? "partial" : "complete-within-inputs",
|
||||
);
|
||||
expect(report.summary).toEqual({
|
||||
unchanged: malformed ? 1 : 2,
|
||||
changed: 0,
|
||||
added: 0,
|
||||
removed: 0,
|
||||
unknown: malformed ? 1 : 0,
|
||||
});
|
||||
expect(report.methods).toEqual([
|
||||
expect.objectContaining({
|
||||
status: malformed ? "unknown" : "unchanged",
|
||||
match: expect.objectContaining({
|
||||
status: "matched",
|
||||
basis: "exact-il-signature",
|
||||
confidence: "exact",
|
||||
}),
|
||||
limitations: malformed
|
||||
? [expect.stringMatching(/^signature-malformed:/)]
|
||||
: [],
|
||||
}),
|
||||
]);
|
||||
expect(report.fields[0]?.status).toBe("unchanged");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1462,6 +1462,53 @@ describe("managed member comparison beside user strings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["wrong member kinds", [0x06, 0x08], [0x00, 0x00, 0x01]],
|
||||
["empty blobs", [], []],
|
||||
["truncated blobs", [0x00, 0x80], [0x06]],
|
||||
["invalid headers", [0x08, 0x00, 0x08], [0x26, 0x08]],
|
||||
] as const)(
|
||||
"keeps equal malformed %s unknown regardless of resolution attestation",
|
||||
(_label, methodBytes, fieldBytes) => {
|
||||
const side = inspect(
|
||||
buildManagedPeFixture({
|
||||
methodSignature: Buffer.from(methodBytes),
|
||||
fieldSignature: Buffer.from(fieldBytes),
|
||||
ilBody: Buffer.from([0x06, 0x2a]),
|
||||
}),
|
||||
"/tmp/malformed-signatures.dll",
|
||||
);
|
||||
expect(side.result.methods[0]?.signature.parse_status).toBe("malformed");
|
||||
expect(side.result.fields[0]?.signature.parse_status).toBe("malformed");
|
||||
expect(side.result.coverage.state).toBe("partial");
|
||||
|
||||
for (const signature_resolution of [
|
||||
side.result.signature_resolution,
|
||||
null,
|
||||
]) {
|
||||
const observation = {
|
||||
...side,
|
||||
result: { ...side.result, signature_resolution },
|
||||
};
|
||||
const comparison = compareManagedMembers(observation, observation);
|
||||
expect(comparison.coverage.status).toBe("partial");
|
||||
expect(comparison.summary).toMatchObject({
|
||||
unchanged: 0,
|
||||
changed: 0,
|
||||
added: 0,
|
||||
removed: 0,
|
||||
unknown: 2,
|
||||
});
|
||||
for (const item of [...comparison.methods, ...comparison.fields])
|
||||
expect(item).toMatchObject({
|
||||
status: "unknown",
|
||||
match: { status: "matched", confidence: "exact" },
|
||||
limitations: [expect.stringMatching(/^signature-malformed:/)],
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe("managed member comparison of undecoded signatures", () => {
|
||||
it("remaps renamed methods only when raw signatures and complete normalized IL match", () => {
|
||||
const signature = Buffer.from([0x00, 0x00, 0x7f]);
|
||||
|
||||
Reference in new issue
Block a user