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);