From 2b75fcfaac8432b08b2d88cd95492333df2674d4 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Thu, 16 Jul 2026 18:21:28 +0800 Subject: [PATCH] test(dotnet): add managed conformance verifier --- AGENTS.md | 3 +- README.md | 2 +- docs/managed-code-analysis.md | 7 +- package.json | 1 + scripts/lib/managed-pe-fixture.mjs | 386 +++++++++++++++++++++++++ scripts/verify-managed-conformance.mjs | 274 ++++++++++++++++++ scripts/verify-package.mjs | 80 ++++- 7 files changed, 748 insertions(+), 5 deletions(-) create mode 100644 scripts/lib/managed-pe-fixture.mjs create mode 100644 scripts/verify-managed-conformance.mjs diff --git a/AGENTS.md b/AGENTS.md index a496c90f0..d00339e70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,7 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec - `npm run verify:hopper`: build and run the real-Hopper verifier with two distinct binaries. - `npm run verify:ghidra`: build and run the real-Ghidra verifier against `GHIDRA_INSTALL_DIR` and optional `GHIDRA_TARGET_PATH`. - `npm run verify:browser`: build and run the real Chrome verifier against `REA_BROWSER_EXECUTABLE` or a platform-default Chrome-family executable. +- `npm run verify:managed`: build and run the source-owned managed PE/CLI conformance verifier for artifact triage, member inspection, managed/native boundary declarations, token drift comparison, malformed metadata, and non-managed degradation. - `npm run verify:replay`: build and run the real Linux Bubblewrap/seccomp/cgroup verifier against source-owned replay fixtures; set `REA_REPLAY_INPUT_PATH` to verify an operator-local manifest. - `npm run verify:package`: pack and test the CLI, setup transaction, skill, and 87-tool target-free MCP server in an isolated environment. - `npm run docs:generate`: generate API documentation from JSDoc comments into `docs/api/` using TypeDoc. @@ -95,7 +96,7 @@ Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler ## Testing Guidelines -Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the beta.3 client. Preserve the 33 direct, 10 enhanced, 5 native, 2 artifact, 4 managed, 8 browser, 4 Electron, 3 application, and 18 session tool inventory (87 total). Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper, Ghidra, browser, JavaScript replay, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command. +Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the beta.3 client. Preserve the 33 direct, 10 enhanced, 5 native, 2 artifact, 4 managed, 8 browser, 4 Electron, 3 application, and 18 session tool inventory (87 total). Cover malformed input, cancellation, timeouts, process exit, concurrency, limits, and clean shutdown. Real Hopper, Ghidra, browser, JavaScript replay, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command. ## Commit & Pull Request Guidelines diff --git a/README.md b/README.md index e4d05a13d..36ef61145 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ REA is growing into a toolkit for understanding software across static artifacts 1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser or Electron interaction remains a different future authority. 2. **Broader application graph evidence** — extend authenticated cross-layer traces with additional static extractors and separately approved runtime authorities. -3. **Professional managed-code analysis** — extend shipped PE/CLI triage into CIL evidence, obfuscation-resistant comparisons, and explicit managed/native composition under the accepted [managed-code boundary](docs/managed-code-analysis.md). +3. **Professional managed-code analysis** — extend shipped PE/CLI triage, CIL evidence, managed/native declaration inventory, source-owned conformance, and obfuscation-resistant comparisons toward verified native-provider composition under the accepted [managed-code boundary](docs/managed-code-analysis.md). 4. **Deterministic behavior harnesses** — extend process ownership, protocol fixtures, filesystem observation, reconnects, and cross-version behavioral comparison. ### Later diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 926d1f0b6..93c325391 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -302,8 +302,11 @@ The managed-code track advances as reviewable pull requests: 3. bounded metadata, signatures, method bodies, and normalized CIL (shipped); 4. obfuscation-resistant slices and cross-version comparison (shipped for static member observations); -5. managed/native composition and truthful deployment degradation; -6. source-built, pinned real-tool, package, CLI, and MCP conformance; and +5. managed/native composition and truthful deployment degradation (declaration + inventory shipped; verified native-provider matching remains planned); +6. source-built managed conformance and package/CLI verification (source-owned + PE/CLI corpus shipped through `npm run verify:managed`; pinned external + ILSpy/dnSpy/Windows checks remain planned); and 7. separately authorized runtime correlation. Each implementation PR updates generated product facts only for behavior it diff --git a/package.json b/package.json index 512211691..1c2352228 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "verify:browser": "npm run build && node scripts/verify-real-browser.mjs", "verify:javascript-runtime": "npm run build && node scripts/verify-local-javascript-runtime.mjs", "verify:application-workflows": "npm run build && node scripts/verify-local-application-workflows.mjs", + "verify:managed": "npm run build && node scripts/verify-managed-conformance.mjs", "verify:replay": "npm run build && node scripts/verify-real-javascript-replay.mjs", "verify:hopper:linux": "npm run build && node scripts/verify-real-hopper-linux.mjs", "verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs", diff --git a/scripts/lib/managed-pe-fixture.mjs b/scripts/lib/managed-pe-fixture.mjs new file mode 100644 index 000000000..dffc742f9 --- /dev/null +++ b/scripts/lib/managed-pe-fixture.mjs @@ -0,0 +1,386 @@ +import { Buffer } from "node:buffer"; + +const textEncoder = new TextEncoder(); + +const DEFAULT_MVID = Buffer.from([ + 0x33, 0x22, 0x11, 0x00, 0x55, 0x44, 0x77, 0x66, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, +]); + +const align4 = (value) => (value + 3) & ~3; +const cString = (value) => Buffer.from(`${value}\0`, "utf8"); + +class StringHeap { + #chunks = [Buffer.from([0])]; + #size = 1; + + add(value) { + if (value.length === 0) return 0; + const index = this.#size; + const chunk = cString(value); + this.#chunks.push(chunk); + this.#size += chunk.length; + return index; + } + + toBuffer() { + return Buffer.concat(this.#chunks); + } +} + +class BlobHeap { + #chunks = [Buffer.from([0])]; + #size = 1; + + add(value) { + if (value.length === 0) return 0; + if (value.length > 0x7f) + throw new RangeError("Managed PE fixture blob is too large"); + const index = this.#size; + this.#chunks.push(Buffer.from([value.length]), value); + this.#size += value.length + 1; + return index; + } + + toBuffer() { + return Buffer.concat(this.#chunks); + } +} + +const u16 = (value) => { + const bytes = Buffer.alloc(2); + bytes.writeUInt16LE(value, 0); + return bytes; +}; + +const u32 = (value) => { + const bytes = Buffer.alloc(4); + bytes.writeUInt32LE(value, 0); + return bytes; +}; + +const guidHeap = (mvid = DEFAULT_MVID) => { + if (mvid.length !== 16) + throw new RangeError("Managed PE fixture MVID must be 16 bytes"); + return Buffer.from(mvid); +}; + +const moduleRow = (name) => + Buffer.concat([u16(0), u16(name), u16(1), u16(0), u16(0)]); + +const typeRefRow = (name, namespace) => + Buffer.concat([u16(0), u16(name), u16(namespace)]); + +const typeDefRow = (name, namespace, fieldList, methodList) => + Buffer.concat([ + u32(0x0010_0001), + u16(name), + u16(namespace), + u16((1 << 2) | 1), + u16(fieldList), + u16(methodList), + ]); + +const fieldRow = (name, signature) => + Buffer.concat([u16(0x0001), u16(name), u16(signature)]); + +const methodDefRow = ({ + flags = 0x0016, + implFlags = 0, + name, + rva, + signature, +}) => + Buffer.concat([ + u32(rva), + u16(implFlags), + u16(flags), + u16(name), + u16(signature), + u16(0), + ]); + +const memberRefRow = (name, signature) => + Buffer.concat([u16((1 << 3) | 1), u16(name), u16(signature)]); + +const moduleRefRow = (name) => Buffer.concat([u16(name)]); + +const implMapRow = (importName, importScope, mappingFlags, memberRow = 1) => + Buffer.concat([ + u16(mappingFlags), + u16((memberRow << 1) | 1), + u16(importName), + u16(importScope), + ]); + +const assemblyRow = (name) => + Buffer.concat([ + u32(0x0000_8004), + u16(1), + u16(2), + u16(3), + u16(4), + u32(0), + u16(0), + u16(name), + u16(0), + ]); + +const assemblyRefRow = (name, keyOrToken) => + Buffer.concat([ + u16(8), + u16(0), + u16(0), + u16(0), + u32(0), + u16(keyOrToken), + u16(name), + u16(0), + u16(0), + ]); + +const customAttributeRow = (value) => + Buffer.concat([u16((1 << 5) | 14), u16((1 << 3) | 3), u16(value)]); + +const manifestResourceRow = (name) => + Buffer.concat([u32(0), u32(2), u16(name), u16(0)]); + +const fixedStringAttributeBlob = (value) => { + const bytes = Buffer.from(textEncoder.encode(value)); + if (bytes.length > 0x7f) + throw new RangeError("Managed PE fixture target framework is too long"); + return Buffer.concat([Buffer.from([1, 0, bytes.length]), bytes, u16(0)]); +}; + +const metadataStreamHeader = (relativeOffset, size, name) => { + const named = cString(name); + const paddedName = Buffer.alloc(align4(named.length)); + named.copy(paddedName); + return Buffer.concat([u32(relativeOffset), u32(size), paddedName]); +}; + +const metadataRoot = (tables, strings, guid, blob) => { + const version = Buffer.from("v4.0.30319\0\0", "utf8"); + const header = Buffer.concat([ + u32(0x424a_5342), + u16(1), + u16(1), + u32(0), + u32(version.length), + version, + u16(0), + u16(4), + ]); + const streamHeaderSize = + metadataStreamHeader(0, 0, "#~").length + + metadataStreamHeader(0, 0, "#Strings").length + + metadataStreamHeader(0, 0, "#GUID").length + + metadataStreamHeader(0, 0, "#Blob").length; + let offset = align4(header.length + streamHeaderSize); + const tablesOffset = offset; + offset = align4(offset + tables.length); + const stringsOffset = offset; + offset = align4(offset + strings.length); + const guidOffset = offset; + offset = align4(offset + guid.length); + const blobOffset = offset; + const headers = Buffer.concat([ + metadataStreamHeader(tablesOffset, tables.length, "#~"), + metadataStreamHeader(stringsOffset, strings.length, "#Strings"), + metadataStreamHeader(guidOffset, guid.length, "#GUID"), + metadataStreamHeader(blobOffset, blob.length, "#Blob"), + ]); + const root = Buffer.alloc(align4(blobOffset + blob.length)); + header.copy(root, 0); + headers.copy(root, header.length); + tables.copy(root, tablesOffset); + strings.copy(root, stringsOffset); + guid.copy(root, guidOffset); + blob.copy(root, blobOffset); + return root; +}; + +const tablesStream = (rows, extraValidMask = 0n) => { + let valid = extraValidMask; + for (const table of rows.keys()) valid |= 1n << BigInt(table); + const header = Buffer.alloc(24); + header.writeUInt8(2, 4); + header.writeUInt8(1, 7); + header.writeBigUInt64LE(valid, 8); + const counts = []; + for (let index = 0; index <= 44; index += 1) { + const tableRows = rows.get(index); + if (tableRows !== undefined) counts.push(u32(tableRows.length)); + } + const data = []; + for (let index = 0; index <= 44; index += 1) { + const tableRows = rows.get(index); + if (tableRows !== undefined) data.push(...tableRows); + } + return Buffer.concat([header, ...counts, ...data]); +}; + +const defaultIlBody = Buffer.from([ + 0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a, 0x2a, +]); + +const retOnlyIlBody = Buffer.from([0x2a]); + +export const alternateMvid = Buffer.from([ + 0xde, 0xad, 0xbe, 0xef, 0x44, 0x33, 0x66, 0x55, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, + 0xff, 0x11, 0x22, +]); + +/** Build a source-owned PE/CLI fixture without committing compiled binaries. */ +export const buildManagedPeFixture = (options = {}) => { + const strings = new StringHeap(); + const blobs = new BlobHeap(); + const methods = options.methods ?? [ + { + name: options.methodName ?? "Main", + body: options.ilBody ?? defaultIlBody, + flags: options.pinvoke === undefined ? 0x0016 : 0x2016, + implFlags: options.methodImplFlags ?? 0, + }, + ]; + const stringsForMethods = methods.map((method) => strings.add(method.name)); + const moduleName = strings.add("Fixture.dll"); + const assemblyName = strings.add("Fixture.Managed"); + const attributeName = strings.add("TargetFrameworkAttribute"); + const attributeNamespace = strings.add("System.Runtime.Versioning"); + const constructorName = strings.add(".ctor"); + const typeName = strings.add(options.typeName ?? "Program"); + const typeNamespace = strings.add(options.typeNamespace ?? "Fixture"); + const fieldName = strings.add(options.fieldName ?? "counter"); + const pinvokeModuleName = + options.pinvoke === undefined + ? null + : strings.add(options.pinvoke.moduleName ?? "user32.dll"); + const pinvokeImportName = + options.pinvoke === undefined + ? null + : strings.add(options.pinvoke.importName ?? "MessageBoxW"); + const resourceName = strings.add("Fixture.resources"); + const referenceNames = options.references ?? ["System.Runtime"]; + const referenceStringIndexes = referenceNames.map((name) => + strings.add(name), + ); + const tokenBlob = blobs.add(Buffer.from("b77a5c561934e089", "hex")); + const fieldSignature = blobs.add(Buffer.from([0x06, 0x08])); + const methodSignature = blobs.add(Buffer.from([0x00, 0x00, 0x01])); + const constructorSignature = blobs.add(Buffer.from([0x20, 0x01, 0x01, 0x0e])); + const attributeBlob = blobs.add( + fixedStringAttributeBlob( + options.targetFramework ?? ".NETCoreApp,Version=v8.0", + ), + ); + const methodRows = methods.map((method, index) => + methodDefRow({ + name: stringsForMethods[index], + signature: methodSignature, + rva: method.rva ?? 0x2800 + index * 0x20, + flags: method.flags ?? 0x0016, + implFlags: method.implFlags ?? 0, + }), + ); + const rows = new Map([ + [0, [moduleRow(moduleName)]], + [1, [typeRefRow(attributeName, attributeNamespace)]], + [2, [typeDefRow(typeName, typeNamespace, 1, 1)]], + [4, [fieldRow(fieldName, fieldSignature)]], + [6, methodRows], + [10, [memberRefRow(constructorName, constructorSignature)]], + [12, [customAttributeRow(attributeBlob)]], + ...(pinvokeModuleName === null + ? [] + : [[26, [moduleRefRow(pinvokeModuleName)]]]), + ...(pinvokeImportName === null + ? [] + : [ + [ + 28, + [ + implMapRow( + pinvokeImportName, + 1, + options.pinvoke?.mappingFlags ?? 0x0344, + options.pinvoke?.memberRow ?? 1, + ), + ], + ], + ]), + [32, [assemblyRow(assemblyName)]], + [35, referenceStringIndexes.map((name) => assemblyRefRow(name, tokenBlob))], + [40, [manifestResourceRow(resourceName)]], + ]); + const metadata = metadataRoot( + tablesStream(rows, options.metadataValidMaskExtra ?? 0n), + strings.toBuffer(), + guidHeap(options.mvid), + blobs.toBuffer(), + ); + if (options.corruptMetadataSignature === true) metadata.writeUInt32LE(0, 0); + const resourceData = options.resourceData ?? Buffer.from("resource-data"); + const resourceDirectory = Buffer.concat([ + u32(resourceData.length), + resourceData, + ]); + const image = Buffer.alloc(0x1000); + image.write("MZ", 0, "ascii"); + image.writeUInt32LE(0x80, 0x3c); + image.writeUInt32LE(0x0000_4550, 0x80); + const coff = 0x84; + image.writeUInt16LE(options.machine ?? 0x014c, coff); + image.writeUInt16LE(1, coff + 2); + image.writeUInt16LE(0x00e0, coff + 16); + image.writeUInt16LE(0x0102, coff + 18); + const optional = coff + 20; + image.writeUInt16LE(0x010b, optional); + image.writeUInt32LE(0x2000, optional + 20); + image.writeUInt32LE(0x0040_0000, optional + 28); + image.writeUInt32LE(0x1000, optional + 32); + image.writeUInt32LE(0x200, optional + 36); + image.writeUInt32LE(0x3000, optional + 56); + image.writeUInt32LE(0x200, optional + 60); + image.writeUInt32LE(16, optional + 92); + image.writeUInt32LE(0x2000, optional + 96 + 14 * 8); + image.writeUInt32LE(72, optional + 96 + 14 * 8 + 4); + const section = optional + 0x00e0; + image.write(".text\0\0\0", section, "ascii"); + image.writeUInt32LE(0x1000, section + 8); + image.writeUInt32LE(0x2000, section + 12); + image.writeUInt32LE(0x0e00, section + 16); + image.writeUInt32LE(0x0200, section + 20); + image.writeUInt32LE(0x6000_0020, section + 36); + const cli = 0x0200; + image.writeUInt32LE(72, cli); + image.writeUInt16LE(2, cli + 4); + image.writeUInt16LE(5, cli + 6); + image.writeUInt32LE(0x2100, cli + 8); + image.writeUInt32LE(metadata.length, cli + 12); + image.writeUInt32LE(options.cliFlags ?? 1, cli + 16); + image.writeUInt32LE(0x0600_0001, cli + 20); + image.writeUInt32LE(0x2600, cli + 24); + image.writeUInt32LE(resourceDirectory.length, cli + 28); + if (options.readyToRun === true) { + image.writeUInt32LE(0x2700, cli + 64); + image.writeUInt32LE(4, cli + 68); + image.write("RTR\0", 0x0900, "ascii"); + } + methods.forEach((method, index) => { + const offset = 0x0a00 + index * 0x20; + (method.body ?? retOnlyIlBody).copy(image, offset); + }); + metadata.copy(image, 0x0300); + resourceDirectory.copy(image, 0x0800); + return image; +}; + +/** Build a syntactically valid PE32 fixture with no CLI directory. */ +export const buildNativePeFixture = () => { + const image = buildManagedPeFixture(); + const cliDirectory = 0x84 + 20 + 96 + 14 * 8; + image.writeUInt32LE(0, cliDirectory); + image.writeUInt32LE(0, cliDirectory + 4); + return image; +}; diff --git a/scripts/verify-managed-conformance.mjs b/scripts/verify-managed-conformance.mjs new file mode 100644 index 000000000..646863044 --- /dev/null +++ b/scripts/verify-managed-conformance.mjs @@ -0,0 +1,274 @@ +import { createHash } from "node:crypto"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import assert from "node:assert/strict"; + +import { parseBinaryTarget } from "../dist/domain/binaryTarget.js"; +import { inspectManagedArtifactBytes } from "../dist/dotnet/ManagedArtifactInspector.js"; +import { inspectManagedMembersBytes } from "../dist/dotnet/ManagedMemberInspector.js"; +import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativeBoundaryInspector.js"; +import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js"; +import { + alternateMvid, + buildManagedPeFixture, + buildNativePeFixture, +} from "./lib/managed-pe-fixture.mjs"; + +const inspectionLimits = { + 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, +}; + +const comparisonLimits = { + max_method_matches: 100, + max_field_matches: 100, + max_candidates: 20, +}; + +const defaultIlBody = Buffer.from([ + 0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a, 0x2a, +]); + +const workspace = await mkdtemp(join(tmpdir(), "rea-managed-conformance-")); + +try { + const modern = await fixture("modern-anycpu.exe", { + references: ["System.Runtime"], + resourceData: Buffer.from("source-owned managed resource"), + }); + const modernArtifact = inspectManagedArtifactBytes( + modern.bytes, + modern.target, + inspectionLimits, + ); + assert.equal(modernArtifact.classification.status, "managed"); + assert.equal(modernArtifact.classification.runtime_family, "modern-dotnet"); + assert.equal(modernArtifact.classification.managed_architecture, "anycpu"); + assert.deepEqual(modernArtifact.target_frameworks, [ + ".NETCoreApp,Version=v8.0", + ]); + assert.equal( + modernArtifact.resources.items[0]?.data_sha256, + sha256(Buffer.from("source-owned managed resource")), + ); + + const framework = await fixture("framework-x86-pinvoke.exe", { + cliFlags: 0x0000_0003, + targetFramework: ".NETFramework,Version=v4.8", + pinvoke: { + moduleName: "user32.dll", + importName: "MessageBoxW", + mappingFlags: 0x0345, + }, + }); + const frameworkArtifact = inspectManagedArtifactBytes( + framework.bytes, + framework.target, + inspectionLimits, + ); + assert.equal( + frameworkArtifact.classification.runtime_family, + "dotnet-framework", + ); + assert.equal(frameworkArtifact.classification.managed_architecture, "x86"); + const frameworkBoundaries = inspectManagedNativeBoundariesBytes( + framework.bytes, + framework.target, + nativeBoundaryLimits, + ); + assert.equal(frameworkBoundaries.pinvoke_imports.total, 1); + assert.equal( + frameworkBoundaries.pinvoke_imports.items[0]?.verification, + "managed-declaration-only", + ); + assert.equal( + frameworkBoundaries.pinvoke_imports.items[0]?.char_set, + "unicode", + ); + assert.equal( + frameworkBoundaries.pinvoke_imports.items[0]?.call_convention, + "stdcall", + ); + + const readyToRun = await fixture("r2r-x64.exe", { + machine: 0x8664, + readyToRun: true, + methods: [ + { + name: "NativeBody", + implFlags: 0x0001, + body: Buffer.from([0x2a]), + }, + ], + }); + const r2rArtifact = inspectManagedArtifactBytes( + readyToRun.bytes, + readyToRun.target, + inspectionLimits, + ); + assert.equal(r2rArtifact.pe.architecture, "x86_64"); + assert.equal( + r2rArtifact.classification.implementation, + "cil-and-ready-to-run", + ); + const r2rBoundaries = inspectManagedNativeBoundariesBytes( + readyToRun.bytes, + readyToRun.target, + nativeBoundaryLimits, + ); + assert.equal(r2rBoundaries.summary.ready_to_run, true); + assert.equal( + r2rBoundaries.native_implementations.items[0]?.code_type, + "native", + ); + assert.equal( + r2rBoundaries.native_implementations.items[0]?.boundary_kind, + "native-body", + ); + + const obfuscated = await fixture("obfuscated.exe", { + typeName: "ꙮType", + methodName: "λ⛧", + fieldName: "字段", + }); + const obfuscatedMembers = inspectManagedMembersBytes( + obfuscated.bytes, + obfuscated.target, + memberLimits, + ); + assert.equal(obfuscatedMembers.types.items[0]?.full_name, "Fixture.ꙮType"); + assert.equal(obfuscatedMembers.methods.items[0]?.name, "λ⛧"); + assert.equal(obfuscatedMembers.fields.items[0]?.name, "字段"); + + const left = await fixture("token-drift-left.exe", { + methodName: "StableSemanticSlice", + }); + const right = await fixture("token-drift-right.exe", { + mvid: alternateMvid, + methods: [ + { name: "InsertedHelper", body: Buffer.from([0x2a]) }, + { name: "RenamedSemanticSlice", body: defaultIlBody }, + ], + }); + const comparison = await compareManagedMemberPaths({ + leftPath: left.path, + rightPath: right.path, + memberLimits: { + maxFileBytes: 1024 * 1024, + ...memberLimits, + }, + comparisonLimits, + }); + assert.equal(comparison.ok, true); + const comparisonResult = comparison.value.normalized_result; + assert.equal(comparisonResult.algorithm.name_matching, "not-used"); + assert.equal(comparisonResult.matching.exact_il_signature, 1); + assert.equal( + comparisonResult.methods.some( + ({ left, right, match }) => + left.token === "0x06000001" && + right?.token === "0x06000002" && + match.basis === "exact-il-signature", + ), + true, + ); + + const nativeOnly = await fixtureBytes( + "native-only.exe", + buildNativePeFixture(), + ); + const nativeResult = inspectManagedArtifactBytes( + nativeOnly.bytes, + nativeOnly.target, + inspectionLimits, + ); + assert.equal(nativeResult.classification.status, "not-managed"); + + const malformed = await fixture("malformed-metadata.exe", { + corruptMetadataSignature: true, + }); + const malformedResult = inspectManagedArtifactBytes( + malformed.bytes, + malformed.target, + inspectionLimits, + ); + assert.equal(malformedResult.classification.status, "malformed"); + + process.stdout.write( + `${JSON.stringify({ + verified: 7, + managedSurfaces: [ + "inspect_managed_artifact", + "inspect_managed_members", + "inspect_managed_native_boundaries", + "compare_managed_members", + ], + coverage: [ + "modern-dotnet-anycpu", + "dotnet-framework-x86-pinvoke", + "x64-ready-to-run-native-body", + "unicode-obfuscated-identifiers", + "mvid-and-token-drift", + "not-managed", + "malformed-metadata", + ], + })}\n`, + ); +} finally { + await rm(workspace, { recursive: true, force: true }); +} + +async function fixture(name, options) { + return fixtureBytes(name, buildManagedPeFixture(options)); +} + +async function fixtureBytes(name, bytes) { + const path = join(workspace, name); + await writeFile(path, bytes); + const parsed = await parseBinaryTarget(path); + if (!parsed.ok) throw parsed.error; + return { bytes, path, target: parsed.value }; +} + +function sha256(bytes) { + return createHash("sha256").update(bytes).digest("hex"); +} diff --git a/scripts/verify-package.mjs b/scripts/verify-package.mjs index 75565f598..7888a2cfa 100644 --- a/scripts/verify-package.mjs +++ b/scripts/verify-package.mjs @@ -20,6 +20,7 @@ import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js"; import * as prompts from "./verify-package-prompts.mjs"; import { verifyPackagedInvestigation } from "./verify-package-investigation.mjs"; import { verifyPackedBridge } from "./verify-packed-bridge.mjs"; +import { buildManagedPeFixture } from "./lib/managed-pe-fixture.mjs"; const exec = promisify(execFile); const root = process.cwd(); @@ -215,6 +216,83 @@ try { ?.paired_renderer_transmissions !== 1 ) throw new Error("packaged JavaScript application analysis CLI failed"); + + const managedPath = join(workspace, "managed-fixture.exe"); + const managedRightPath = join(workspace, "managed-fixture-right.exe"); + await Promise.all([ + writeFile( + managedPath, + buildManagedPeFixture({ + pinvoke: { + moduleName: "user32.dll", + importName: "MessageBoxW", + mappingFlags: 0x0345, + }, + }), + ), + writeFile( + managedRightPath, + buildManagedPeFixture({ + mvid: Buffer.from([ + 0xde, 0xad, 0xbe, 0xef, 0x44, 0x33, 0x66, 0x55, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x11, 0x22, + ]), + }), + ), + ]); + const managedArtifact = json( + await run( + cli, + ["inspect-managed-artifact", managedPath, "--json"], + environment, + ), + ); + if ( + managedArtifact.operation !== "inspect_managed_artifact" || + managedArtifact.provider?.id !== "rea-dotnet-static" || + managedArtifact.normalized_result?.classification?.status !== "managed" + ) + throw new Error("packaged managed artifact CLI failed"); + const managedMembers = json( + await run( + cli, + ["inspect-managed-members", managedPath, "--json"], + environment, + ), + ); + if ( + managedMembers.operation !== "inspect_managed_members" || + managedMembers.normalized_result?.methods?.total !== 1 + ) + throw new Error("packaged managed member CLI failed"); + const managedBoundaries = json( + await run( + cli, + ["inspect-managed-native-boundaries", managedPath, "--json"], + environment, + ), + ); + if ( + managedBoundaries.operation !== "inspect_managed_native_boundaries" || + managedBoundaries.normalized_result?.pinvoke_imports?.total !== 1 || + managedBoundaries.normalized_result?.pinvoke_imports?.items?.[0] + ?.verification !== "managed-declaration-only" + ) + throw new Error("packaged managed native-boundary CLI failed"); + const managedComparison = json( + await run( + cli, + ["compare-managed-members", managedPath, managedRightPath, "--json"], + environment, + ), + ); + if ( + managedComparison.operation !== "compare_managed_members" || + managedComparison.provider?.id !== "rea-dotnet-workflows" || + managedComparison.normalized_result?.algorithm?.name_matching !== "not-used" + ) + throw new Error("packaged managed comparison CLI failed"); + // prettier-ignore const investigationReplay = await verifyPackagedInvestigation({ cli, workspace, evidenceRoot, artifactArchive, environment }); const unknownProviderExecution = await runWithStatus( @@ -766,7 +844,7 @@ try { } process.stdout.write( - `${JSON.stringify({ cli: true, analysisCli: true, artifactCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents mcp", doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true })}\n`, + `${JSON.stringify({ cli: true, analysisCli: true, artifactCli: true, managedCli: true, evidenceCli: true, incurMcpCommand: "npx -y rea-agents mcp", doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true })}\n`, ); } finally { if (tarball) await rm(join(root, tarball), { force: true });