rea/scripts/verify-managed-conformance.mjs

1349 lines
41 KiB
JavaScript

import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import { constants } from "node:fs";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import assert from "node:assert/strict";
import { promisify } from "node:util";
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 { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
import { planManagedRuntimeCorrelationEvidence } from "../dist/application/ManagedRuntimeCorrelationService.js";
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../dist/application/JavaScriptApplicationWorkflowService.js";
import { createPermissionAuthority } from "../dist/application/PermissionAuthority.js";
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
import { createEvidence } from "../dist/domain/evidence.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 applicationGraphLimits = {
max_types: 100,
max_methods: 100,
max_fields: 100,
max_pinvoke_imports: 100,
max_native_implementations: 100,
};
const appManifestInspectionLimits = {
...inspectionLimits,
maxMetadataBytes: 64 * 1024 * 1024,
maxTableRows: 250_000,
maxHeapItemBytes: 16 * 1024 * 1024,
};
const appManifestMemberLimits = {
...memberLimits,
typeLimit: 1,
methodLimit: 1,
fieldLimit: 1,
memberRefLimit: 1,
edgeLimit: 1,
maxMetadataBytes: 64 * 1024 * 1024,
maxTableRows: 250_000,
maxHeapItemBytes: 16 * 1024 * 1024,
maxMethodBodyBytes: 4 * 1024 * 1024,
maxMethodInstructions: 20_000,
};
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-"));
const execFileAsync = promisify(execFile);
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 frameworkMembers = inspectManagedMembersBytes(
framework.bytes,
framework.target,
memberLimits,
);
const frameworkArtifactEvidence = createEvidence(
framework.target,
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_artifact",
parameters: inspectionLimits,
result: frameworkArtifact,
rawResult: null,
limitations: frameworkArtifact.limitations,
locations: [{ kind: "artifact-path", path: framework.target.path }],
},
);
const frameworkMemberEvidence = createEvidence(
framework.target,
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_members",
parameters: memberLimits,
result: frameworkMembers,
rawResult: null,
limitations: frameworkMembers.limitations,
locations: [{ kind: "artifact-path", path: framework.target.path }],
},
);
const frameworkBoundaryEvidence = createEvidence(
framework.target,
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_native_boundaries",
parameters: nativeBoundaryLimits,
result: frameworkBoundaries,
rawResult: null,
limitations: frameworkBoundaries.limitations,
locations: [{ kind: "artifact-path", path: framework.target.path }],
},
);
const nativeFunctionEvidence = createEvidence(
{
path: "/system/user32.dll",
sha256: "9".repeat(64),
format: "pe",
architecture: "x86",
},
{
id: "ghidra",
name: "Ghidra",
version: "12.1.2",
},
{
operation: "analyze_function",
parameters: { procedure: "MessageBoxW" },
result: functionDossier("MessageBoxW"),
rawResult: null,
limitations: [],
locations: [{ kind: "address", address: "0x401000" }],
},
);
const nativeVerification = verifyManagedNativeBoundariesEvidence({
managed_boundaries: frameworkBoundaryEvidence,
native_observations: [nativeFunctionEvidence],
limits: {
max_native_observations: 20,
max_candidates_per_import: 25,
},
});
assert.equal(nativeVerification.ok, true);
assert.equal(nativeVerification.value.normalized_result.summary.verified, 1);
const applicationGraph = projectManagedApplicationGraphEvidence({
managed_artifact: frameworkArtifactEvidence,
managed_members: frameworkMemberEvidence,
managed_native_boundaries: frameworkBoundaryEvidence,
limits: {
max_types: 100,
max_methods: 100,
max_fields: 100,
max_pinvoke_imports: 100,
max_native_implementations: 100,
},
});
assert.equal(applicationGraph.ok, true);
assert.equal(
applicationGraph.value.normalized_result.summary.pinvoke_imports,
1,
);
assert.equal(
applicationGraph.value.normalized_result.graph.nodes.some(
({ kind }) => kind === "managed-pinvoke-import",
),
true,
);
const applicationTrace = traceApplicationFeatureEvidence({
application: applicationGraph.value,
native_observations: [],
seed: {
kind: "string",
value: "MessageBoxW",
match: "exact",
case_sensitive: true,
},
direction: "incoming",
limits: {
max_seed_matches: 5,
max_depth: 4,
max_nodes: 50,
max_edges: 100,
max_paths: 10,
},
});
assert.equal(applicationTrace.ok, true);
assert.equal(
applicationTrace.value.normalized_result.summary.matched_seeds,
1,
);
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 runtimeExecutable = join(workspace, "dotnet");
await writeFile(runtimeExecutable, "#!/bin/sh\n");
const runtimeCeiling = {
capability: "managed_runtime",
roots: [workspace],
executables: [runtimeExecutable],
environment_names: [],
network: "none",
mount: false,
};
const runtimeAuthority = await createPermissionAuthority(
[runtimeCeiling],
[
{
...runtimeCeiling,
grant_id: "administrator:managed_runtime",
lifetime: "administrator",
operation_identity: null,
expires_at: null,
},
],
);
assert.equal(runtimeAuthority.ok, true);
const runtimeMethod = obfuscatedMembers.methods.items[0];
assert.ok(runtimeMethod);
const obfuscatedMembersEvidence = createEvidence(
undefined,
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_members",
parameters: { path: obfuscated.path },
result: obfuscatedMembers,
rawResult: null,
limitations: obfuscatedMembers.limitations,
},
);
const reconstructionImport = importManagedReconstructionEvidence({
static_members: obfuscatedMembersEvidence,
decompiler: {
name: "ilspycmd",
version: "9.1.0.7988",
family: "ilspy",
executable_sha256: null,
options: ["--type", "Fixture.ꙮType"],
},
methods: [
{
token: runtimeMethod.token,
signature_sha256: runtimeMethod.signature.raw_sha256,
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
reconstruction: {
kind: "decompiled-csharp",
language: "csharp",
text: "internal static void λ⛧() { /* synthetic fixture */ }",
},
},
],
notes: ["source-owned synthetic decompiler reconstruction"],
});
assert.equal(reconstructionImport.ok, true);
assert.equal(
reconstructionImport.value.normalized_result.methods[0].validation
.canonical_observation,
false,
);
assert.equal(reconstructionImport.value.confidence, "inferred");
const runtimePlan = await planManagedRuntimeCorrelationEvidence(
{
policy: {
enabled: true,
roots: [workspace],
executablePath: runtimeExecutable,
},
authority: runtimeAuthority.value,
},
{
static_members: obfuscatedMembersEvidence,
method: {
token: runtimeMethod.token,
signature_sha256: runtimeMethod.signature.raw_sha256,
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
},
requested_effect: "debugger",
host: {
os: "linux",
clr_family: "dotnet",
architecture: "x86_64",
},
bounds: {
timeout_ms: 5_000,
max_threads: 32,
max_output_bytes: 65_536,
allow_network: false,
allow_ui: false,
},
},
);
assert.equal(runtimePlan.ok, true);
assert.equal(runtimePlan.value.normalized_result.executed, false);
assert.equal(
runtimePlan.value.normalized_result.authority_model.capability,
"managed_runtime",
);
assert.equal(
runtimePlan.value.normalized_result.effect_taxonomy.uses_debugger,
true,
);
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");
const manifestSelfTest = await runManagedAppManifestSelfTest();
const operatorManifest = await runOptionalManagedAppManifest();
const ilspyOracle = await runOptionalIlspyOracle();
process.stdout.write(
`${JSON.stringify({
verified:
12 +
(operatorManifest === null ? 0 : 1) +
(ilspyOracle === null ? 0 : 1),
managedSurfaces: [
"inspect_managed_artifact",
"inspect_managed_members",
"inspect_managed_native_boundaries",
"compare_managed_members",
"verify_managed_native_boundaries",
"import_managed_reconstruction",
"plan_managed_runtime_correlation",
"project_managed_application_graph",
],
coverage: [
"modern-dotnet-anycpu",
"dotnet-framework-x86-pinvoke",
"x64-ready-to-run-native-body",
"managed-native-verification",
"managed-application-graph-projection",
"unicode-obfuscated-identifiers",
"decompiler-reconstruction-import",
"runtime-correlation-admission-plan",
"mvid-and-token-drift",
"not-managed",
"malformed-metadata",
"operator-local-managed-manifest",
...(ilspyOracle === null ? [] : ["ilspy-reconstruction-oracle"]),
],
managedAppManifest: {
env: "REA_MANAGED_APP_MANIFEST_PATH",
selfTest: manifestSelfTest,
operator: operatorManifest ?? { configured: false },
},
ilspyOracle: ilspyOracle ?? {
env: "REA_ILSPY_CMD_PATH",
configured: false,
},
})}\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");
}
async function runManagedAppManifestSelfTest() {
const sample = await fixture("manifest-self-test.exe", {
methodName: "PinnedSemanticSlice",
ilBody: defaultIlBody,
});
const artifact = inspectManagedArtifactBytes(
sample.bytes,
sample.target,
inspectionLimits,
);
const members = inspectManagedMembersBytes(
sample.bytes,
sample.target,
memberLimits,
);
const method = members.methods.items[0];
assert.ok(method);
const summary = await verifyManagedAppManifest(
{
schema_version: 1,
label: "source-owned manifest verifier self-test",
target: {
path: sample.path,
sha256: sample.target.sha256,
mvid: artifact.module?.mvid,
assembly_name: artifact.assembly?.name,
runtime_family: artifact.classification.runtime_family,
managed_architecture: artifact.classification.managed_architecture,
},
methods: [
{
label: "pinned-semantic-slice",
token: method.token,
signature_sha256: method.signature.raw_sha256,
il_size: method.body.il_size,
il_sha256: method.body.il_sha256,
normalized_il_sha256: method.body.normalized_il_sha256,
},
],
application_graph: {
expected_node_kinds: [
"artifact",
"managed-assembly",
"managed-module",
"managed-type",
"managed-method",
],
feature_traces: [
{
label: "trace pinned method by name",
method_token: method.token,
seed: "PinnedSemanticSlice",
match: "exact",
min_matched_seeds: 1,
},
],
},
},
join(workspace, "source-owned-managed-app-manifest.json"),
);
return {
verified: true,
assertions: summary.assertions,
methods: summary.methods.length,
applicationGraph: summary.application_graph ?? null,
target: summary.target,
};
}
async function runOptionalManagedAppManifest() {
const manifestPath = process.env.REA_MANAGED_APP_MANIFEST_PATH;
if (manifestPath === undefined) return null;
if (manifestPath.trim().length === 0)
throw new Error("REA_MANAGED_APP_MANIFEST_PATH is set but empty");
const resolvedManifestPath = resolve(process.cwd(), manifestPath);
const text = await readFile(resolvedManifestPath, "utf8");
let manifest;
try {
manifest = JSON.parse(text);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`managed app manifest ${resolvedManifestPath} is not valid JSON: ${message}`,
);
}
return {
configured: true,
...(await verifyManagedAppManifest(manifest, resolvedManifestPath)),
};
}
async function runOptionalIlspyOracle() {
const ilspyPath = process.env.REA_ILSPY_CMD_PATH;
if (ilspyPath === undefined) return null;
ensureIlspy(
ilspyPath.trim().length > 0,
"REA_ILSPY_CMD_PATH is set but empty",
);
ensureIlspy(
isAbsolute(ilspyPath),
"REA_ILSPY_CMD_PATH must be an absolute path",
);
await ensureExecutable(ilspyPath);
const versionOutput = await runIlspy(ilspyPath, ["--version"], 64 * 1024);
const version = ilspyVersion(versionOutput);
const executableSha256 = sha256(await readFile(ilspyPath));
const sample = await fixture("ilspy-oracle.exe", {
methodName: "PinnedSemanticSlice",
});
const artifact = inspectManagedArtifactBytes(
sample.bytes,
sample.target,
inspectionLimits,
);
const members = inspectManagedMembersBytes(
sample.bytes,
sample.target,
memberLimits,
);
const method = members.methods.items[0];
ensureIlspy(method !== undefined, "source-owned fixture has no method");
const listOutput = await runIlspy(
ilspyPath,
["--disable-updatecheck", "-l", "c", sample.path],
256 * 1024,
);
ensureIlspy(
listOutput.includes("Class Fixture.Program"),
"ilspycmd class listing did not include Fixture.Program",
);
const csharp = await runIlspy(
ilspyPath,
["--disable-updatecheck", "--type", "Fixture.Program", sample.path],
256 * 1024,
);
ensureIlspy(
csharp.includes("PinnedSemanticSlice"),
"ilspycmd C# output did not include the pinned method name",
);
ensureIlspy(
csharp.length <= 65_536,
"ilspycmd C# output exceeded the managed reconstruction import bound",
);
const memberEvidence = createEvidence(
sample.target,
MANAGED_STATIC_PROVIDER,
{
operation: "inspect_managed_members",
parameters: memberLimits,
result: members,
rawResult: null,
limitations: members.limitations,
locations: [{ kind: "artifact-path", path: sample.target.path }],
},
);
const reconstructionImport = importManagedReconstructionEvidence({
static_members: memberEvidence,
decompiler: {
name: "ilspycmd",
version,
family: "ilspy",
executable_sha256: executableSha256,
options: ["--disable-updatecheck", "--type", "Fixture.Program"],
},
methods: [
{
token: method.token,
signature_sha256: method.signature.raw_sha256,
normalized_il_sha256: method.body.normalized_il_sha256,
reconstruction: {
kind: "decompiled-csharp",
language: "csharp",
text: csharp,
},
},
],
notes: [
"source-owned real ilspycmd oracle; output is imported as reconstruction inference",
],
});
ensureIlspy(reconstructionImport.ok, "managed reconstruction import failed");
const imported = reconstructionImport.value.normalized_result;
ensureIlspy(
imported.summary.decompiled_csharp_methods === 1,
"managed reconstruction import did not record one decompiled C# method",
);
return {
env: "REA_ILSPY_CMD_PATH",
configured: true,
version,
executable_sha256: executableSha256,
fixture: {
file_name: basename(sample.path),
sha256: sample.target.sha256,
mvid: artifact.module?.mvid ?? null,
assembly_name: artifact.assembly?.name ?? null,
method: {
token: method.token,
declaring_type: method.declaring_type,
name: method.name,
signature_sha256: method.signature.raw_sha256,
il_size: method.body.il_size,
normalized_il_sha256: method.body.normalized_il_sha256,
},
},
output: {
class_listing_sha256: sha256(Buffer.from(listOutput)),
decompiled_csharp_sha256: sha256(Buffer.from(csharp)),
decompiled_csharp_bytes: Buffer.byteLength(csharp),
imported_evidence_id: reconstructionImport.value.evidence_id,
reconstruction_id: imported.reconstruction_id,
canonical_observation:
imported.methods[0]?.validation.canonical_observation ?? null,
confidence_floor:
imported.methods[0]?.validation.confidence_floor ?? null,
},
};
}
async function ensureExecutable(path) {
try {
await access(path, constants.X_OK);
} catch {
throw new Error(`managed ILSpy oracle failed: ${path} is not executable`);
}
}
async function runIlspy(path, args, maxBuffer) {
try {
const { stdout } = await execFileAsync(path, args, {
timeout: 30_000,
maxBuffer,
env: {
...process.env,
DOTNET_CLI_TELEMETRY_OPTOUT: "1",
},
});
return stdout;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`managed ILSpy oracle failed: ${message}`);
}
}
function ilspyVersion(output) {
const version = output
.split(/\r?\n/u)
.map((line) => line.trim())
.find((line) => line.startsWith("ilspycmd:"));
ensureIlspy(
version !== undefined,
"ilspycmd --version did not identify ilspycmd",
);
return version;
}
function ensureIlspy(condition, message) {
if (!condition) throw new Error(`managed ILSpy oracle failed: ${message}`);
}
async function verifyManagedAppManifest(rawManifest, manifestPath) {
const manifest = parseManagedAppManifest(rawManifest);
const targetPath = isAbsolute(manifest.target.path)
? manifest.target.path
: resolve(dirname(manifestPath), manifest.target.path);
const parsedTarget = await parseBinaryTarget(targetPath);
ensure(
parsedTarget.ok,
`target ${targetPath} could not be parsed as a binary target`,
);
const target = parsedTarget.value;
ensure(
target.sha256 === manifest.target.sha256,
`target sha256 mismatch: expected ${manifest.target.sha256}, observed ${target.sha256}`,
);
const bytes = await readFile(targetPath);
const artifact = inspectManagedArtifactBytes(
bytes,
target,
appManifestInspectionLimits,
);
ensure(
artifact.classification.status === "managed",
`target is ${artifact.classification.status}, expected managed`,
);
if (manifest.target.mvid !== undefined)
ensure(
artifact.module?.mvid === manifest.target.mvid,
`target MVID mismatch: expected ${manifest.target.mvid}, observed ${artifact.module?.mvid ?? "null"}`,
);
if (manifest.target.assembly_name !== undefined)
ensure(
artifact.assembly?.name === manifest.target.assembly_name,
`assembly name mismatch: expected ${manifest.target.assembly_name}, observed ${artifact.assembly?.name ?? "null"}`,
);
if (manifest.target.runtime_family !== undefined)
ensure(
artifact.classification.runtime_family === manifest.target.runtime_family,
`runtime family mismatch: expected ${manifest.target.runtime_family}, observed ${artifact.classification.runtime_family}`,
);
if (manifest.target.managed_architecture !== undefined)
ensure(
artifact.classification.managed_architecture ===
manifest.target.managed_architecture,
`managed architecture mismatch: expected ${manifest.target.managed_architecture}, observed ${artifact.classification.managed_architecture}`,
);
const methods = [];
let assertions = 1;
const inspectedMethods = new Map();
for (const expectedMethod of manifest.methods) {
const method = inspectManagedMethodByToken(bytes, target, expectedMethod);
ensure(
method.signature.raw_sha256 === expectedMethod.signature_sha256,
`method ${expectedMethod.token} signature sha256 mismatch: expected ${expectedMethod.signature_sha256}, observed ${method.signature.raw_sha256}`,
);
ensure(
method.body.il_size === expectedMethod.il_size,
`method ${expectedMethod.token} IL size mismatch: expected ${String(expectedMethod.il_size)}, observed ${String(method.body.il_size)}`,
);
if (expectedMethod.il_sha256 !== undefined)
ensure(
method.body.il_sha256 === expectedMethod.il_sha256,
`method ${expectedMethod.token} IL sha256 mismatch: expected ${expectedMethod.il_sha256}, observed ${method.body.il_sha256 ?? "null"}`,
);
ensure(
method.body.normalized_il_sha256 === expectedMethod.normalized_il_sha256,
`method ${expectedMethod.token} normalized IL sha256 mismatch: expected ${expectedMethod.normalized_il_sha256}, observed ${method.body.normalized_il_sha256 ?? "null"}`,
);
assertions += expectedMethod.il_sha256 === undefined ? 4 : 5;
inspectedMethods.set(expectedMethod.token, method);
methods.push({
label: expectedMethod.label ?? null,
token: method.token,
declaring_type: method.declaring_type,
name: method.name,
signature_sha256: method.signature.raw_sha256,
il_size: method.body.il_size,
il_sha256: method.body.il_sha256,
normalized_il_sha256: method.body.normalized_il_sha256,
});
}
const applicationGraph =
manifest.application_graph === undefined
? undefined
: verifyManagedAppManifestApplicationGraph({
graphManifest: manifest.application_graph,
bytes,
target,
artifact,
manifestMethods: manifest.methods,
inspectedMethods,
});
if (applicationGraph !== undefined) assertions += applicationGraph.assertions;
return {
label: manifest.label ?? null,
assertions,
target: {
file_name: basename(targetPath),
sha256: target.sha256,
mvid: artifact.module?.mvid ?? null,
assembly_name: artifact.assembly?.name ?? null,
runtime_family: artifact.classification.runtime_family,
managed_architecture: artifact.classification.managed_architecture,
},
methods,
...(applicationGraph === undefined
? {}
: { application_graph: applicationGraph.summary }),
};
}
function verifyManagedAppManifestApplicationGraph({
graphManifest,
bytes,
target,
artifact,
manifestMethods,
inspectedMethods,
}) {
const graphByToken = new Map();
const projectionForToken = (token) => {
const existing = graphByToken.get(token);
if (existing !== undefined) return existing;
const method = inspectedMethods.get(token);
ensure(
method !== undefined,
`application graph method ${token} must also be declared in manifest.methods`,
);
const members = inspectManagedMethodPageByToken(bytes, target, token);
const artifactEvidence = createEvidence(target, MANAGED_STATIC_PROVIDER, {
operation: "inspect_managed_artifact",
parameters: appManifestInspectionLimits,
result: artifact,
rawResult: null,
limitations: artifact.limitations,
locations: [{ kind: "artifact-path", path: target.path }],
});
const memberEvidence = createEvidence(target, MANAGED_STATIC_PROVIDER, {
operation: "inspect_managed_members",
parameters: {
...appManifestMemberLimits,
methodOffset: methodTokenRow(token) - 1,
},
result: members,
rawResult: null,
limitations: members.limitations,
locations: [{ kind: "artifact-path", path: target.path }],
});
const projected = projectManagedApplicationGraphEvidence({
managed_artifact: artifactEvidence,
managed_members: memberEvidence,
limits: applicationGraphLimits,
});
ensure(
projected.ok,
`application graph projection failed for method ${token}`,
);
const projection = {
method,
evidence: projected.value,
nodeKinds: [
...new Set(
projected.value.normalized_result.graph.nodes.map(({ kind }) => kind),
),
].sort(),
summary: projected.value.normalized_result.summary,
};
graphByToken.set(token, projection);
return projection;
};
const firstToken = manifestMethods[0]?.token;
ensure(
firstToken !== undefined,
"application graph verification requires at least one manifest method",
);
const baseline = projectionForToken(firstToken);
let assertions = 1;
for (const expectedKind of graphManifest.expected_node_kinds) {
ensure(
baseline.nodeKinds.includes(expectedKind),
`application graph is missing expected node kind ${expectedKind}`,
);
assertions += 1;
}
const traces = [];
for (const expectedTrace of graphManifest.feature_traces) {
const projection = projectionForToken(expectedTrace.method_token);
const traced = traceApplicationFeatureEvidence({
application: projection.evidence,
native_observations: [],
seed: {
kind: "string",
value: expectedTrace.seed,
match: expectedTrace.match,
case_sensitive: expectedTrace.case_sensitive,
},
direction: "incoming",
limits: {
max_seed_matches: 10,
max_depth: 4,
max_nodes: 100,
max_edges: 200,
max_paths: 20,
},
});
ensure(
traced.ok,
`application graph trace failed for method ${expectedTrace.method_token}`,
);
const matched = traced.value.normalized_result.summary.matched_seeds;
ensure(
matched >= expectedTrace.min_matched_seeds,
`application graph trace ${expectedTrace.seed} matched ${String(matched)} seeds, expected at least ${String(expectedTrace.min_matched_seeds)}`,
);
assertions += 1;
traces.push({
label: expectedTrace.label ?? null,
method_token: expectedTrace.method_token,
seed: expectedTrace.seed,
matched_seeds: matched,
trace_evidence_id: traced.value.evidence_id,
});
}
return {
assertions,
summary: {
projections: [...graphByToken.values()].map((projection) => ({
method_token: projection.method.token,
graph_evidence_id: projection.evidence.evidence_id,
node_kinds: projection.nodeKinds,
summary: projection.summary,
})),
expected_node_kinds: graphManifest.expected_node_kinds,
feature_traces: traces,
},
};
}
function inspectManagedMethodByToken(bytes, target, expectedMethod) {
const members = inspectManagedMethodPageByToken(
bytes,
target,
expectedMethod.token,
);
const method = members.methods.items.find(
({ token }) => token === expectedMethod.token,
);
ensure(
method !== undefined,
`method ${expectedMethod.token} was not returned by the member inspector`,
);
ensure(
method.body.status === "present",
`method ${expectedMethod.token} body status is ${method.body.status}, expected present`,
);
return method;
}
function inspectManagedMethodPageByToken(bytes, target, token) {
const row = methodTokenRow(token);
return inspectManagedMembersBytes(bytes, target, {
...appManifestMemberLimits,
methodOffset: row - 1,
});
}
function parseManagedAppManifest(rawManifest) {
const manifest = object(rawManifest, "manifest");
ensure(
manifest.schema_version === 1,
"manifest.schema_version must be exactly 1",
);
const target = object(manifest.target, "manifest.target");
const methods = array(manifest.methods, "manifest.methods");
ensure(manifest.methods.length > 0, "manifest.methods must not be empty");
return {
schema_version: 1,
label: optionalString(manifest.label, "manifest.label"),
target: {
path: string(target.path, "manifest.target.path"),
sha256: digest(target.sha256, "manifest.target.sha256"),
mvid: optionalCliMetadataGuid(target.mvid, "manifest.target.mvid"),
assembly_name: optionalString(
target.assembly_name,
"manifest.target.assembly_name",
),
runtime_family: optionalString(
target.runtime_family,
"manifest.target.runtime_family",
),
managed_architecture: optionalString(
target.managed_architecture,
"manifest.target.managed_architecture",
),
},
methods: methods.map((method, index) =>
parseManagedAppManifestMethod(method, index),
),
application_graph: parseManagedAppManifestGraph(manifest.application_graph),
};
}
function parseManagedAppManifestGraph(rawGraph) {
if (rawGraph === undefined) return undefined;
const graph = object(rawGraph, "manifest.application_graph");
const expectedKinds =
graph.expected_node_kinds === undefined
? []
: array(
graph.expected_node_kinds,
"manifest.application_graph.expected_node_kinds",
).map((kind, index) =>
string(
kind,
`manifest.application_graph.expected_node_kinds[${String(index)}]`,
),
);
const traces =
graph.feature_traces === undefined
? []
: array(
graph.feature_traces,
"manifest.application_graph.feature_traces",
).map((trace, index) => parseManagedAppManifestTrace(trace, index));
ensure(
expectedKinds.length > 0 || traces.length > 0,
"manifest.application_graph must declare expected_node_kinds or feature_traces",
);
return {
expected_node_kinds: expectedKinds,
feature_traces: traces,
};
}
function parseManagedAppManifestTrace(rawTrace, index) {
const prefix = `manifest.application_graph.feature_traces[${String(index)}]`;
const trace = object(rawTrace, prefix);
const match = trace.match ?? "exact";
ensure(
match === "exact" || match === "contains",
`${prefix}.match must be exact or contains`,
);
const caseSensitive = trace.case_sensitive ?? true;
ensure(
typeof caseSensitive === "boolean",
`${prefix}.case_sensitive must be a boolean`,
);
const minMatchedSeeds = trace.min_matched_seeds ?? 1;
ensure(
Number.isInteger(minMatchedSeeds) && minMatchedSeeds > 0,
`${prefix}.min_matched_seeds must be a positive integer`,
);
return {
label: optionalString(trace.label, `${prefix}.label`),
method_token: methodToken(trace.method_token, `${prefix}.method_token`),
seed: string(trace.seed, `${prefix}.seed`),
match,
case_sensitive: caseSensitive,
min_matched_seeds: minMatchedSeeds,
};
}
function parseManagedAppManifestMethod(rawMethod, index) {
const prefix = `manifest.methods[${String(index)}]`;
const method = object(rawMethod, prefix);
const token = methodToken(method.token, `${prefix}.token`);
return {
label: optionalString(method.label, `${prefix}.label`),
token,
signature_sha256: digest(
method.signature_sha256,
`${prefix}.signature_sha256`,
),
il_size: ilSize(method, prefix),
il_sha256:
method.il_sha256 === undefined
? undefined
: digest(method.il_sha256, `${prefix}.il_sha256`),
normalized_il_sha256: digest(
method.normalized_il_sha256,
`${prefix}.normalized_il_sha256`,
),
};
}
function ilSize(method, prefix) {
const hasIlSize = method.il_size !== undefined;
const hasIlLength = method.il_length !== undefined;
ensure(
hasIlSize || hasIlLength,
`${prefix}.il_size is required; il_length is accepted as a legacy alias`,
);
if (hasIlSize && hasIlLength)
ensure(
method.il_size === method.il_length,
`${prefix}.il_size and ${prefix}.il_length disagree`,
);
const value = hasIlSize ? method.il_size : method.il_length;
ensure(
Number.isInteger(value) && value >= 0,
`${prefix}.il_size must be a non-negative integer`,
);
return value;
}
function object(value, name) {
ensure(
value !== null && typeof value === "object" && !Array.isArray(value),
`${name} must be an object`,
);
return value;
}
function array(value, name) {
ensure(Array.isArray(value), `${name} must be an array`);
return value;
}
function string(value, name) {
ensure(
typeof value === "string" && value.length > 0,
`${name} must be a non-empty string`,
);
return value;
}
function optionalString(value, name) {
if (value === undefined) return undefined;
return string(value, name);
}
function digest(value, name) {
const text = string(value, name);
ensure(/^[a-f0-9]{64}$/u.test(text), `${name} must be a lowercase sha256`);
return text;
}
function optionalCliMetadataGuid(value, name) {
if (value === undefined) return undefined;
if (value === null) return null;
const text = string(value, name);
ensure(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.test(
text,
),
`${name} must be a lowercase GUID`,
);
return text;
}
function metadataToken(value, name) {
const text = string(value, name);
ensure(/^0x[0-9a-f]{8}$/u.test(text), `${name} must be a metadata token`);
return text;
}
function methodToken(value, name) {
const token = metadataToken(value, name);
ensure(
token.startsWith("0x06"),
`${name} must be a MethodDef token beginning with 0x06`,
);
return token;
}
function methodTokenRow(token) {
const row = Number.parseInt(token.slice(4), 16);
ensure(row > 0, `method token ${token} has an invalid row id`);
return row;
}
function ensure(condition, message) {
if (!condition)
throw new Error(`managed app manifest verification failed: ${message}`);
}
function functionDossier(name) {
const emptyPage = {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
};
return {
procedure: {
address: "0x401000",
name,
classification: {
external: false,
thunk: false,
thunk_target: null,
provenance: "synthetic-provider",
},
signature: null,
locals: [],
},
pseudocode: {
text: "",
total_chars: 0,
returned_chars: 0,
truncated: false,
next_offset: null,
},
assembly: emptyPage,
comments: emptyPage,
callers: emptyPage,
callees: emptyPage,
incoming_references: emptyPage,
outgoing_references: emptyPage,
referenced_strings: emptyPage,
referenced_names: emptyPage,
basic_blocks: emptyPage,
instruction_scan: { scanned: 0, truncated: false },
limitations: [],
};
}