rea/tests/managedArtifactProvider.tes...

713 lines
22 KiB
TypeScript

import { createHash } from "node:crypto";
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { runProviderAnalysis } from "../src/application/DirectAnalysis.js";
import type { AnalysisExecution } from "../src/application/AnalysisProvider.js";
import {
managedArtifactInspectionSchema,
managedMemberInspectionSchema,
managedNativeBoundaryInspectionSchema,
} from "../src/domain/managedArtifact.js";
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
import { parseBinaryTarget } from "../src/domain/binaryTarget.js";
import { parseEvidence } from "../src/domain/evidence.js";
import { ManagedStaticProvider } from "../src/dotnet/ManagedStaticProvider.js";
import { inspectManagedArtifactBytes } from "../src/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../src/dotnet/ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "../src/dotnet/ManagedNativeBoundaryInspector.js";
import {
buildManagedPeFixture,
buildNativePeFixture,
} from "./fixtures/managedPe.js";
const limits = {
referenceOffset: 0,
referenceLimit: 100,
resourceOffset: 0,
resourceLimit: 100,
attributeOffset: 0,
attributeLimit: 100,
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
};
const memberLimits = {
typeOffset: 0,
typeLimit: 100,
methodOffset: 0,
methodLimit: 100,
fieldOffset: 0,
fieldLimit: 100,
memberRefOffset: 0,
memberRefLimit: 100,
edgeOffset: 0,
edgeLimit: 100,
instructionAnchorLimit: 100,
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
maxMethodBodyBytes: 1024 * 1024,
maxMethodInstructions: 1_000,
};
const nativeBoundaryLimits = {
moduleRefOffset: 0,
moduleRefLimit: 100,
importOffset: 0,
importLimit: 100,
implementationOffset: 0,
implementationLimit: 100,
maxMetadataBytes: 1024 * 1024,
maxTableRows: 1_000,
maxHeapItemBytes: 1024 * 1024,
};
describe("managed PE/CLI static provider", () => {
it("inventories module, assembly, framework, references, and resources without loading CLR code", () => {
const resource = Buffer.from("source-owned resource");
const bytes = buildManagedPeFixture({ resourceData: resource });
const result = inspectManagedArtifactBytes(bytes, target(bytes), limits);
expect(result.classification).toMatchObject({
status: "managed",
runtime_family: "modern-dotnet",
implementation: "cil",
managed_architecture: "anycpu",
});
expect(result.pe.cli).toMatchObject({
flag_names: ["il-only"],
entry_point: { kind: "metadata-token", value: "0x06000001" },
});
expect(result.module).toMatchObject({
name: "Fixture.dll",
mvid: "00112233-4455-6677-8899-aabbccddeeff",
token: "0x00000001",
});
expect(result.assembly).toMatchObject({
name: "Fixture.Managed",
version: "1.2.3.4",
token: "0x20000001",
});
expect(result.target_frameworks).toEqual([".NETCoreApp,Version=v8.0"]);
expect(result.references).toMatchObject({
total: 1,
returned: 1,
complete: true,
items: [expect.objectContaining({ name: "System.Runtime" })],
});
expect(result.resources.items).toEqual([
expect.objectContaining({
name: "Fixture.resources",
embedded: true,
visibility: "private",
data_length: resource.length,
data_sha256: createHash("sha256").update(resource).digest("hex"),
}),
]);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
it("keeps classification evidence complete when caller pages visible references", () => {
const bytes = buildManagedPeFixture({
references: ["System.Runtime", "UnityEngine.CoreModule"],
});
const result = inspectManagedArtifactBytes(bytes, target(bytes), {
...limits,
referenceLimit: 1,
});
expect(result.classification.runtime_family).toBe("unity-mono");
expect(result.references).toMatchObject({
total: 2,
returned: 1,
complete: false,
});
expect(result.coverage.state).toBe("partial");
expect(result.coverage.issues).toEqual([]);
});
it("accepts CLI metadata GUIDs without RFC UUID version or variant bits", () => {
const bytes = buildManagedPeFixture({
mvid: Buffer.from("3aebc60edc4a544b1f458b4ed40b33b1", "hex"),
});
const result = inspectManagedArtifactBytes(bytes, target(bytes), limits);
const members = inspectManagedMembersBytes(
bytes,
target(bytes),
memberLimits,
);
const boundaries = inspectManagedNativeBoundariesBytes(
bytes,
target(bytes),
nativeBoundaryLimits,
);
expect(result.module?.mvid).toBe("0ec6eb3a-4adc-4b54-1f45-8b4ed40b33b1");
expect(result.classification.status).toBe("managed");
expect(members.identity_scope.requires_mvid).toBe(result.module?.mvid);
expect(boundaries.identity_scope.requires_mvid).toBe(result.module?.mvid);
});
it("decodes ECMA-335 pointer and byref element types without swapping them", () => {
const bytes = buildManagedPeFixture({
fieldSignature: Buffer.from([0x06, 0x1d, 0x0f, 0x0f, 0x08]),
methodSignature: Buffer.from([
0x00, 0x02, 0x10, 0x08, 0x0f, 0x08, 0x10, 0x0e,
]),
});
const result = inspectManagedMembersBytes(
bytes,
target(bytes),
memberLimits,
);
expect(result.fields.items[0]?.signature).toMatchObject({
parse_status: "decoded",
field_type: "i4**[]",
});
expect(result.methods.items[0]?.signature).toMatchObject({
parse_status: "decoded",
return_type: "i4&",
parameter_types: ["i4*", "string&"],
});
});
it("reads fat method header size from the full flags-and-size word", () => {
const il = Buffer.from([
0x21, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x22, 0x00,
0x00, 0x80, 0x3f, 0x26, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0,
0x3f, 0x26, 0xfe, 0x06, 0x01, 0x00, 0x00, 0x06, 0x26, 0x2a,
]);
const header = Buffer.alloc(12);
header.writeUInt16LE(0x3013, 0);
header.writeUInt16LE(8, 2);
header.writeUInt32LE(il.length, 4);
const bytes = buildManagedPeFixture({
ilBody: Buffer.concat([header, il]),
});
const result = inspectManagedMembersBytes(
bytes,
target(bytes),
memberLimits,
);
expect(result.methods.items[0]?.body).toMatchObject({
status: "present",
header_format: "fat",
il_size: il.length,
il_sha256: createHash("sha256").update(il).digest("hex"),
opcode_counts: {
"ldc.i8": 1,
pop: 4,
"ldc.r4": 1,
"ldc.r8": 1,
ldftn: 1,
ret: 1,
},
issue: null,
});
});
it("does not normalize reserved CIL opcodes as operand-free instructions", () => {
const bytes = buildManagedPeFixture({
ilBody: Buffer.from([0x0a, 0x24, 0x2a]),
});
const result = inspectManagedMembersBytes(
bytes,
target(bytes),
memberLimits,
);
expect(result.methods.items[0]?.body).toMatchObject({
status: "malformed",
il_size: 2,
normalized_il_sha256: null,
decoded_instruction_count: 0,
issue: "Unsupported CIL opcode 0x24 at IL offset 0",
});
});
it("keeps the documented decoded-CIL v1 golden vector stable", () => {
const il = Buffer.from([0x00, 0x2a]);
const tinyBytes = buildManagedPeFixture({
ilBody: Buffer.from([0x0a, ...il]),
});
const fatHeader = Buffer.alloc(12);
fatHeader.writeUInt16LE(0x3013, 0);
fatHeader.writeUInt16LE(32, 2);
fatHeader.writeUInt32LE(il.length, 4);
fatHeader.writeUInt32LE(0x1100_0001, 8);
const fatBytes = buildManagedPeFixture({
ilBody: Buffer.concat([fatHeader, il]),
});
const tiny = inspectManagedMembersBytes(
tinyBytes,
target(tinyBytes),
memberLimits,
);
const fat = inspectManagedMembersBytes(
fatBytes,
target(fatBytes),
memberLimits,
);
expect(tiny.methods.items[0]?.body).toMatchObject({
status: "present",
header_format: "tiny",
max_stack: 8,
init_locals: false,
local_var_sig_token: null,
il_size: 2,
il_sha256: createHash("sha256").update(il).digest("hex"),
normalized_il_sha256:
"5e5fad7741cb44bca3a4f045546b7449990da343f612f5f34c0ca30e9eee0636",
opcode_counts: { nop: 1, ret: 1 },
exception_regions: [],
});
expect(fat.methods.items[0]?.body).toMatchObject({
header_format: "fat",
max_stack: 32,
init_locals: true,
local_var_sig_token: "0x11000001",
il_sha256: tiny.methods.items[0]?.body.il_sha256,
normalized_il_sha256: tiny.methods.items[0]?.body.normalized_il_sha256,
});
});
it("marks instruction-limited CIL as partial without assigning normalized identity", () => {
const bytes = buildManagedPeFixture();
const result = inspectManagedMembersBytes(bytes, target(bytes), {
...memberLimits,
maxMethodInstructions: 1,
});
const il = bytes.subarray(0x0a01, 0x0a0d);
expect(result.metadata.status).toBe("complete");
expect(result.methods.items[0]?.body).toMatchObject({
status: "partial",
il_size: 12,
il_sha256: createHash("sha256").update(il).digest("hex"),
normalized_il_sha256: null,
instruction_count: 1,
decoded_instruction_count: 1,
truncated_instructions: 1,
issue:
"Instruction decode reached max_method_instructions 1 before the end of the method body",
});
expect(result.coverage).toEqual({
state: "partial",
issues: [
{
code: "limit-exceeded",
scope: "method.0x06000001.body.instructions",
offset: 0x0a00,
detail:
"Instruction decode reached max_method_instructions 1 before the end of the method body",
},
],
});
expect(result.limitations.join(" ")).toContain(
"has no normalized CIL identity",
);
});
it("inspects metadata members, signatures, CIL hashes, call edges, and field anchors", () => {
const bytes = buildManagedPeFixture();
const result = inspectManagedMembersBytes(
bytes,
target(bytes),
memberLimits,
);
expect(result.identity_scope).toEqual({
token_identity: "build-local",
requires_artifact_sha256: target(bytes).sha256,
requires_mvid: "00112233-4455-6677-8899-aabbccddeeff",
});
expect(result.types.items).toEqual([
expect.objectContaining({
token: "0x02000001",
full_name: "Fixture.Program",
field_list: { first_row: 1, last_row: 1, count: 1 },
method_list: { first_row: 1, last_row: 1, count: 1 },
}),
]);
expect(result.fields.items).toEqual([
expect.objectContaining({
token: "0x04000001",
declaring_type: "Fixture.Program",
name: "counter",
signature: expect.objectContaining({
kind: "field",
parse_status: "decoded",
field_type: "i4",
}),
}),
]);
expect(result.methods.items).toEqual([
expect.objectContaining({
token: "0x06000001",
declaring_type: "Fixture.Program",
name: "Main",
rva: 0x2800,
signature: expect.objectContaining({
kind: "method",
parse_status: "decoded",
return_type: "void",
parameter_types: [],
}),
body: expect.objectContaining({
status: "present",
header_format: "tiny",
file_offset: 0x0a00,
il_size: 12,
instruction_count: 4,
decoded_instruction_count: 4,
truncated_instructions: 0,
opcode_counts: { "ldarg.0": 1, ldfld: 1, call: 1, ret: 1 },
anchors: [
{
il_offset: 1,
opcode: "ldfld",
operand_kind: "field",
operand: "0x04000001",
},
{
il_offset: 6,
opcode: "call",
operand_kind: "method",
operand: "0x0a000001",
},
],
}),
}),
]);
expect(result.member_refs.items).toEqual([
expect.objectContaining({
token: "0x0a000001",
name: ".ctor",
signature: expect.objectContaining({
kind: "method",
parse_status: "decoded",
parameter_types: ["string"],
}),
}),
]);
expect(result.call_edges.items).toEqual([
{
caller_token: "0x06000001",
caller: "Fixture.Program.Main",
opcode: "call",
target_token: "0x0a000001",
target_kind: "member-ref",
target_name: ".ctor",
},
]);
expect(result.field_accesses.items).toEqual([
{
method_token: "0x06000001",
method: "Fixture.Program.Main",
opcode: "ldfld",
field_token: "0x04000001",
field_name: "counter",
},
]);
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
it("inspects managed/native PInvoke declarations without verifying native exports", () => {
const bytes = buildManagedPeFixture({
pinvoke: {
moduleName: "user32.dll",
importName: "MessageBoxW",
mappingFlags: 0x0345,
},
readyToRun: true,
});
const result = inspectManagedNativeBoundariesBytes(
bytes,
target(bytes),
nativeBoundaryLimits,
);
expect(result.cli_native).toMatchObject({
il_only: true,
ready_to_run_signature: true,
managed_native_header_rva: 0x2700,
managed_native_header_size: 4,
});
expect(result.module_refs.items).toEqual([
{
token: "0x1a000001",
row_offset: expect.any(Number),
name: "user32.dll",
},
]);
expect(result.pinvoke_imports.items).toEqual([
expect.objectContaining({
token: "0x1c000001",
member_token: "0x06000001",
member_kind: "method",
member_name: "Main",
import_name: "MessageBoxW",
import_scope_token: "0x1a000001",
import_scope_name: "user32.dll",
no_mangle: true,
char_set: "unicode",
call_convention: "stdcall",
supports_last_error: true,
verification: "managed-declaration-only",
}),
]);
expect(result.native_implementations.items).toEqual([
expect.objectContaining({
token: "0x06000001",
name: "Main",
pinvoke_declared: true,
boundary_kind: "pinvoke",
body_interpretation: "native-or-runtime",
}),
]);
expect(result.summary).toMatchObject({
module_ref_count: 1,
pinvoke_import_count: 1,
native_implementation_count: 1,
ready_to_run: true,
mixed_mode_or_native_header: true,
});
expect(result.limitations).toContain(
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
);
});
it("keeps member evidence bounded and typed for pagination and unavailable metadata", () => {
const bytes = buildManagedPeFixture();
const paged = inspectManagedMembersBytes(bytes, target(bytes), {
...memberLimits,
methodLimit: 0 + 1,
instructionAnchorLimit: 1,
});
expect(paged.methods).toMatchObject({
total: 1,
returned: 1,
complete: true,
});
expect(paged.methods.items[0]?.body.anchors).toHaveLength(1);
const nativeBytes = buildNativePeFixture();
const native = inspectManagedMembersBytes(
nativeBytes,
target(nativeBytes),
memberLimits,
);
expect(native.metadata.status).toBe("absent");
expect(native.coverage.state).toBe("unavailable");
const malformedBytes = buildManagedPeFixture({
corruptMetadataSignature: true,
});
const malformed = inspectManagedMembersBytes(
malformedBytes,
target(malformedBytes),
memberLimits,
);
expect(malformed.metadata.status).toBe("malformed");
expect(malformed.coverage.issues).toEqual([
expect.objectContaining({ code: "invalid-metadata-root" }),
]);
});
it("reports native PE, malformed metadata, and bounded metadata failures as typed results", () => {
const native = inspectManagedArtifactBytes(
buildNativePeFixture(),
target(buildNativePeFixture()),
limits,
);
expect(native.classification.status).toBe("not-managed");
expect(native.metadata.status).toBe("absent");
expect(native.coverage.state).toBe("unavailable");
const malformedBytes = buildManagedPeFixture({
corruptMetadataSignature: true,
});
const malformed = inspectManagedArtifactBytes(
malformedBytes,
target(malformedBytes),
limits,
);
expect(malformed.classification.status).toBe("malformed");
expect(malformed.coverage.issues).toEqual([
expect.objectContaining({ code: "invalid-metadata-root" }),
]);
const unsupportedTableBytes = buildManagedPeFixture({
metadataValidMaskExtra: 1n << 50n,
});
const unsupportedTable = inspectManagedArtifactBytes(
unsupportedTableBytes,
target(unsupportedTableBytes),
limits,
);
expect(unsupportedTable.coverage.issues).toEqual([
expect.objectContaining({ code: "invalid-tables" }),
]);
const limited = inspectManagedArtifactBytes(
buildManagedPeFixture(),
target(buildManagedPeFixture()),
{ ...limits, maxMetadataBytes: 256 },
);
expect(limited.metadata.status).toBe("partial");
expect(limited.coverage.issues).toEqual([
expect.objectContaining({ code: "limit-exceeded" }),
]);
});
it("executes as a read-only auxiliary provider with digest, cancellation, and format boundaries", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-managed-provider-"));
const bytes = buildManagedPeFixture();
const path = join(directory, "fixture.exe");
await writeFile(path, bytes);
const parsed = await parseBinaryTarget(path);
expect(parsed.ok).toBe(true);
if (!parsed.ok) return;
const client = new ManagedStaticProvider().createClient(parsed.value);
const observed = await client.execute("inspect_managed_artifact", {});
expect(observed.ok).toBe(true);
if (!observed.ok) return;
expect(asManagedResult(observed.value)).toMatchObject({
classification: { status: "managed", runtime_family: "modern-dotnet" },
artifact: { path, sha256: parsed.value.sha256, format: "pe" },
});
expect(observed.value.provider.id).toBe("rea-dotnet-static");
expect(observed.value.subject).toMatchObject({ path, format: "pe" });
const members = await client.execute("inspect_managed_members", {});
expect(members.ok).toBe(true);
if (!members.ok) return;
expect(asManagedMemberResult(members.value)).toMatchObject({
artifact: { path, sha256: parsed.value.sha256, format: "pe" },
methods: { total: 1 },
call_edges: { total: 1 },
field_accesses: { total: 1 },
});
const boundaries = await client.execute(
"inspect_managed_native_boundaries",
{},
);
expect(boundaries.ok).toBe(true);
if (!boundaries.ok) return;
expect(asManagedNativeBoundaryResult(boundaries.value)).toMatchObject({
artifact: { path, sha256: parsed.value.sha256, format: "pe" },
module_refs: { total: 0 },
pinvoke_imports: { total: 0 },
native_implementations: { total: 0 },
});
const cancelled = new AbortController();
cancelled.abort();
const cancelledResult = await client.execute(
"inspect_managed_artifact",
{},
{ signal: cancelled.signal },
);
expect(cancelledResult).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
const changed = Buffer.from(bytes);
changed[0x300] = 0;
await writeFile(path, changed);
const staleDigest = await client.execute("inspect_managed_artifact", {});
expect(staleDigest).toMatchObject({
ok: false,
error: { _tag: "EvidenceIntegrityError" },
});
await client.close();
const nativeClient = new ManagedStaticProvider().createClient({
...parsed.value,
format: "elf",
});
await expect(
nativeClient.execute("inspect_managed_artifact", {}),
).resolves.toMatchObject({
ok: false,
error: { _tag: "AnalysisCapabilityUnavailableError" },
});
});
it("has the same evidence shape through direct CLI analysis plumbing", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-managed-cli-"));
const path = join(directory, "fixture.exe");
await writeFile(path, buildManagedPeFixture());
const evidence = parseEvidence(
await runProviderAnalysis(path, "inspect_managed_artifact", {
reference_limit: 1,
}),
);
expect(evidence).toMatchObject({
operation: "inspect_managed_artifact",
provider: { id: "rea-dotnet-static" },
subject: { local_path: path, format: "pe" },
normalized_result: {
classification: { status: "managed", runtime_family: "modern-dotnet" },
references: { limit: 1 },
},
});
const memberEvidence = parseEvidence(
await runProviderAnalysis(path, "inspect_managed_members", {}),
);
expect(memberEvidence).toMatchObject({
operation: "inspect_managed_members",
provider: { id: "rea-dotnet-static" },
subject: { local_path: path, format: "pe" },
normalized_result: {
identity_scope: { token_identity: "build-local" },
methods: { total: 1 },
},
});
const boundaryEvidence = parseEvidence(
await runProviderAnalysis(path, "inspect_managed_native_boundaries", {}),
);
expect(boundaryEvidence).toMatchObject({
operation: "inspect_managed_native_boundaries",
provider: { id: "rea-dotnet-static" },
subject: { local_path: path, format: "pe" },
normalized_result: {
identity_scope: { token_identity: "build-local" },
pinvoke_imports: { total: 0 },
},
});
});
});
const target = (bytes: Buffer): BinaryTarget => ({
path: "/fixture.exe",
sha256: createHash("sha256").update(bytes).digest("hex"),
kind: "executable",
format: "pe",
architecture: "x86",
availableArchitectures: ["x86"],
});
const asManagedResult = (execution: AnalysisExecution) =>
managedArtifactInspectionSchema.parse(execution.result);
const asManagedMemberResult = (execution: AnalysisExecution) =>
managedMemberInspectionSchema.parse(execution.result);
const asManagedNativeBoundaryResult = (execution: AnalysisExecution) =>
managedNativeBoundaryInspectionSchema.parse(execution.result);