refactor(managed): split metadata analysis

This commit is contained in:
morluto
2026-07-18 05:49:14 +00:00
parent e2c868c4bd
commit 20c88dd567
17 changed files with 3687 additions and 2983 deletions
+10 -512
View File
@@ -1,16 +1,13 @@
import { createHash } from "node:crypto";
import { basename } from "node:path";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js";
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
javascriptApplicationGraphSchema,
type ApplicationGraphEvidence,
type ApplicationEdge,
type ApplicationNode,
} from "./javascriptApplicationGraph.js";
import {
@@ -24,13 +21,16 @@ import {
import type { JsonValue } from "./jsonValue.js";
import {
assessManagedGraphOmissions,
completeManagedGraphCoverage,
managedGraphEvidenceCoverage,
managedGraphResultCoverage,
managedSourceCoverage,
managedSourcePageCoverage,
totalManagedGraphOmitted,
} from "./managedApplicationGraphCoverage.js";
import {
addArtifactNode,
addArtifactIdentityNodes,
addBoundaryNodes,
addMemberNodes,
} from "./managedApplicationGraphNodes.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
@@ -117,15 +117,7 @@ export type ManagedApplicationGraphResult = z.infer<
typeof managedApplicationGraphResultSchema
>;
type ManagedMethod = ManagedMemberInspection["methods"]["items"][number];
type ManagedField = ManagedMemberInspection["fields"]["items"][number];
type ManagedType = ManagedMemberInspection["types"]["items"][number];
type PinvokeImport =
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
type NativeImplementation =
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
interface ParsedManagedGraphInput {
export interface ParsedManagedGraphInput {
readonly artifact: {
readonly evidence: Evidence;
readonly result: ManagedArtifactInspection;
@@ -140,9 +132,9 @@ interface ParsedManagedGraphInput {
} | null;
}
interface GraphBuildState {
export interface GraphBuildState {
readonly nodes: ApplicationNode[];
readonly edges: ReturnType<typeof createJavaScriptApplicationEdge>[];
readonly edges: ApplicationEdge[];
readonly artifactSha256: string;
readonly artifactPath: string;
readonly evidenceLinks: string[];
@@ -151,11 +143,6 @@ interface GraphBuildState {
readonly fieldNodes: Map<string, ApplicationNode>;
}
interface ContainsEdgeOptions {
readonly kind: string;
readonly coverage: ApplicationGraphEvidence["coverage"];
}
const sha256 = (value: JsonValue): string => {
const serialized = canonicalize(value);
if (serialized === undefined)
@@ -306,495 +293,6 @@ const assertSameArtifact = (
);
};
const addArtifactNode = (state: GraphBuildState): ApplicationNode => {
const node = createJavaScriptApplicationNode({
kind: "artifact",
identity: {
strategy: "content-digest",
stability: "global-exact",
sha256: state.artifactSha256,
},
observations: [
{
label: basename(state.artifactPath),
properties: {
format: "pe",
source: "managed-application-graph",
},
evidence: managedEvidence(state, "project-managed-artifact"),
},
],
});
state.nodes.push(node);
return node;
};
const addArtifactIdentityNodes = (
state: GraphBuildState,
artifactNode: ApplicationNode,
parsed: ParsedManagedGraphInput,
): void => {
const artifact = parsed.artifact?.result;
const artifactCoverage = managedSourceCoverage(
parsed.artifact?.result.coverage.state ?? "complete",
);
if (artifact?.assembly !== undefined && artifact.assembly !== null) {
const assembly = createJavaScriptApplicationNode({
kind: "managed-assembly",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-assembly",
artifact.assembly.token,
),
observations: [
{
label: artifact.assembly.name,
properties: {
name: artifact.assembly.name,
version: artifact.assembly.version,
culture: artifact.assembly.culture,
public_key_kind: artifact.assembly.public_key.kind,
},
evidence: managedEvidence(
state,
"inspect_managed_artifact",
artifactCoverage,
),
},
],
});
state.nodes.push(assembly);
addContainsEdge(state, artifactNode, assembly, {
kind: "declares-managed-assembly",
coverage: artifactCoverage,
});
}
const module =
artifact?.module ??
parsed.members?.result.module ??
parsed.boundaries?.result.module;
if (module !== undefined && module !== null) {
const moduleCoverage = managedSourceCoverage(moduleCoverageState(parsed));
const moduleNode = createJavaScriptApplicationNode({
kind: "managed-module",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-module",
module.mvid ?? module.token,
),
observations: [
{
label: module.name,
properties: {
name: module.name,
mvid: module.mvid,
generation: module.generation,
token: module.token,
},
evidence: managedEvidence(
state,
"inspect_managed_artifact",
moduleCoverage,
),
},
],
});
state.nodes.push(moduleNode);
addContainsEdge(state, artifactNode, moduleNode, {
kind: "declares-managed-module",
coverage: moduleCoverage,
});
}
};
const addMemberNodes = (
state: GraphBuildState,
artifactNode: ApplicationNode,
parsed: ParsedManagedGraphInput,
limits: ProjectManagedApplicationGraphInput["limits"],
) => {
const members = parsed.members?.result;
if (members === undefined) return { types: 0, methods: 0, fields: 0 };
const types = members.types.items.slice(0, limits.max_types);
const methods = members.methods.items.slice(0, limits.max_methods);
const fields = members.fields.items.slice(0, limits.max_fields);
const typeCoverage = managedSourcePageCoverage(
members.coverage.state,
members.types,
"types",
);
const methodCoverage = managedSourcePageCoverage(
members.coverage.state,
members.methods,
"methods",
);
const fieldCoverage = managedSourcePageCoverage(
members.coverage.state,
members.fields,
"fields",
);
for (const type of types) {
const node = typeNode(state, type, typeCoverage);
state.nodes.push(node);
state.typeNodes.set(type.token, node);
addContainsEdge(state, artifactNode, node, {
kind: "declares-managed-type",
coverage: typeCoverage,
});
}
for (const method of methods) {
const node = methodNode(state, method, methodCoverage);
state.nodes.push(node);
state.methodNodes.set(method.token, node);
const owner =
method.declaring_type_token === null
? artifactNode
: (state.typeNodes.get(method.declaring_type_token) ?? artifactNode);
addContainsEdge(state, owner, node, {
kind: "declares-managed-method",
coverage: methodCoverage,
});
}
for (const field of fields) {
const node = fieldNode(state, field, fieldCoverage);
state.nodes.push(node);
state.fieldNodes.set(field.token, node);
const owner =
field.declaring_type_token === null
? artifactNode
: (state.typeNodes.get(field.declaring_type_token) ?? artifactNode);
addContainsEdge(state, owner, node, {
kind: "declares-managed-field",
coverage: fieldCoverage,
});
}
return {
types: types.length,
methods: methods.length,
fields: fields.length,
};
};
const typeNode = (
state: GraphBuildState,
type: ManagedType,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-type",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-type-token",
type.token,
),
observations: [
{
label: type.full_name,
properties: {
token: type.token,
namespace: type.namespace,
name: type.name,
full_name: type.full_name,
flags: type.flags,
extends_token: type.extends_token,
},
evidence: managedEvidence(state, "inspect_managed_members", coverage),
},
],
});
const methodNode = (
state: GraphBuildState,
method: ManagedMethod,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-method",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-method-token",
method.token,
),
observations: [
{
label:
method.declaring_type === null
? method.name
: `${method.declaring_type}.${method.name}`,
properties: {
token: method.token,
declaring_type_token: method.declaring_type_token,
declaring_type: method.declaring_type,
name: method.name,
rva: method.rva,
signature_sha256: method.signature.raw_sha256,
signature_status: method.signature.parse_status,
body_status: method.body.status,
normalized_il_sha256: method.body.normalized_il_sha256,
il_size: method.body.il_size,
},
evidence: managedEvidence(state, "inspect_managed_members", coverage),
},
],
});
const fieldNode = (
state: GraphBuildState,
field: ManagedField,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-field",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-field-token",
field.token,
),
observations: [
{
label:
field.declaring_type === null
? field.name
: `${field.declaring_type}.${field.name}`,
properties: {
token: field.token,
declaring_type_token: field.declaring_type_token,
declaring_type: field.declaring_type,
name: field.name,
flags: field.flags,
signature_sha256: field.signature.raw_sha256,
signature_status: field.signature.parse_status,
},
evidence: managedEvidence(state, "inspect_managed_members", coverage),
},
],
});
const addBoundaryNodes = (
state: GraphBuildState,
artifactNode: ApplicationNode,
parsed: ParsedManagedGraphInput,
limits: ProjectManagedApplicationGraphInput["limits"],
) => {
const boundaries = parsed.boundaries?.result;
if (boundaries === undefined)
return { pinvoke_imports: 0, native_implementations: 0 };
const pinvokes = boundaries.pinvoke_imports.items.slice(
0,
limits.max_pinvoke_imports,
);
const implementations = boundaries.native_implementations.items.slice(
0,
limits.max_native_implementations,
);
const pinvokeCoverage = managedSourcePageCoverage(
boundaries.coverage.state,
boundaries.pinvoke_imports,
"pinvoke_imports",
);
const implementationCoverage = managedSourcePageCoverage(
boundaries.coverage.state,
boundaries.native_implementations,
"native_implementations",
);
for (const pinvoke of pinvokes) {
const node = pinvokeNode(state, pinvoke, pinvokeCoverage);
state.nodes.push(node);
const owner =
pinvoke.member_token === null
? artifactNode
: (state.methodNodes.get(pinvoke.member_token) ?? artifactNode);
state.edges.push(
createJavaScriptApplicationEdge({
source_node_id: owner.node_id,
target_node_id: node.node_id,
relation: "imports",
properties: {
kind: "managed-pinvoke",
import_name: pinvoke.import_name,
import_scope_name: pinvoke.import_scope_name,
},
evidence: managedEvidence(
state,
"inspect_managed_native_boundaries",
pinvokeCoverage,
),
}),
);
}
for (const implementation of implementations) {
const node = nativeImplementationNode(
state,
implementation,
implementationCoverage,
);
state.nodes.push(node);
const owner = state.methodNodes.get(implementation.token) ?? artifactNode;
addContainsEdge(state, owner, node, {
kind: "declares-managed-native-implementation",
coverage: implementationCoverage,
});
}
return {
pinvoke_imports: pinvokes.length,
native_implementations: implementations.length,
};
};
const pinvokeNode = (
state: GraphBuildState,
pinvoke: PinvokeImport,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-pinvoke-import",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-pinvoke-token",
pinvoke.token,
),
observations: [
{
label: pinvoke.import_name,
properties: {
token: pinvoke.token,
member_token: pinvoke.member_token,
member_name: pinvoke.member_name,
import_name: pinvoke.import_name,
import_scope_name: pinvoke.import_scope_name,
char_set: pinvoke.char_set,
call_convention: pinvoke.call_convention,
verification: pinvoke.verification,
},
evidence: managedEvidence(
state,
"inspect_managed_native_boundaries",
coverage,
),
},
],
});
const nativeImplementationNode = (
state: GraphBuildState,
implementation: NativeImplementation,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-native-implementation",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-native-implementation-token",
implementation.token,
),
observations: [
{
label: implementation.name,
properties: {
token: implementation.token,
name: implementation.name,
rva: implementation.rva,
code_type: implementation.code_type,
managed_kind: implementation.managed_kind,
pinvoke_declared: implementation.pinvoke_declared,
boundary_kind: implementation.boundary_kind,
body_interpretation: implementation.body_interpretation,
},
evidence: managedEvidence(
state,
"inspect_managed_native_boundaries",
coverage,
),
},
],
});
const addContainsEdge = (
state: GraphBuildState,
source: ApplicationNode,
target: ApplicationNode,
options: ContainsEdgeOptions,
): void => {
state.edges.push(
createJavaScriptApplicationEdge({
source_node_id: source.node_id,
target_node_id: target.node_id,
relation: "contains",
properties: { kind: options.kind },
evidence: managedEvidence(
state,
"project-managed-contains",
options.coverage,
),
}),
);
};
const artifactLocalIdentity = (
artifactSha256: string,
namespace: string,
key: string,
) => ({
strategy: "artifact-local-key" as const,
stability: "artifact-version" as const,
artifact_sha256: artifactSha256,
namespace,
key,
});
const managedEvidence = (
state: GraphBuildState,
operation: string,
coverage: ApplicationGraphEvidence["coverage"] = completeManagedGraphCoverage(),
): ApplicationGraphEvidence => ({
authority: "managed-static-analysis",
state: "observed",
confidence: "exact",
artifact: {
available: true,
artifact_id: `art_${state.artifactSha256}`,
sha256: state.artifactSha256,
},
location: {
available: true,
value: {
kind: "artifact-path",
path: graphArtifactPath(state.artifactPath),
},
},
extractor: {
name: "rea-dotnet-static",
version: "1",
operation,
executable_sha256: null,
},
coverage,
limitations:
coverage.status === "complete"
? []
: [
"The source managed Evidence slice is incomplete; unreturned or unavailable items remain unobserved.",
],
evidence_ids: state.evidenceLinks,
});
const graphArtifactPath = (path: string): string => {
const name = basename(path).replaceAll(/[^A-Za-z0-9._-]/gu, "_");
return `managed/${name.length === 0 ? "artifact.pe" : name}`;
};
const moduleCoverageState = (
parsed: ParsedManagedGraphInput,
): "complete" | "partial" | "unavailable" => {
const artifact = parsed.artifact;
if (artifact !== null && artifact.result.module !== null)
return artifact.result.coverage.state;
const members = parsed.members;
if (members !== null && members.result.module !== null)
return members.result.coverage.state;
return parsed.boundaries?.result.coverage.state ?? "complete";
};
const omittedCounts = (
parsed: ParsedManagedGraphInput,
limits: ProjectManagedApplicationGraphInput["limits"],
+532
View File
@@ -0,0 +1,532 @@
import { basename } from "node:path";
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationNode,
type ApplicationGraphEvidence,
type ApplicationNode,
} from "./javascriptApplicationGraph.js";
import {
managedSourceCoverage,
managedSourcePageCoverage,
type ManagedGraphProjectionLimits,
} from "./managedApplicationGraphCoverage.js";
import {
type ManagedMemberInspection,
type ManagedNativeBoundaryInspection,
} from "./managedArtifact.js";
import type { GraphBuildState, ParsedManagedGraphInput } from "./managedApplicationGraph.js";
type ManagedMethod = ManagedMemberInspection["methods"]["items"][number];
type ManagedField = ManagedMemberInspection["fields"]["items"][number];
type ManagedType = ManagedMemberInspection["types"]["items"][number];
type PinvokeImport =
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
type NativeImplementation =
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
/** Options for a managed contains relationship edge. */
export interface ContainsEdgeOptions {
readonly kind: string;
readonly coverage: ApplicationGraphEvidence["coverage"];
}
/** Add the root managed artifact node to the graph state. */
export const addArtifactNode = (state: GraphBuildState): ApplicationNode => {
const node = createJavaScriptApplicationNode({
kind: "artifact",
identity: {
strategy: "content-digest",
stability: "global-exact",
sha256: state.artifactSha256,
},
observations: [
{
label: basename(state.artifactPath),
properties: {
format: "pe",
source: "managed-application-graph",
},
evidence: managedEvidence(state, "project-managed-artifact"),
},
],
});
state.nodes.push(node);
return node;
};
/** Add managed assembly and module identity nodes for the artifact. */
export const addArtifactIdentityNodes = (
state: GraphBuildState,
artifactNode: ApplicationNode,
parsed: ParsedManagedGraphInput,
): void => {
const artifact = parsed.artifact?.result;
const artifactCoverage = managedSourceCoverage(
parsed.artifact?.result.coverage.state ?? "complete",
);
if (artifact?.assembly !== undefined && artifact.assembly !== null) {
const assembly = createJavaScriptApplicationNode({
kind: "managed-assembly",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-assembly",
artifact.assembly.token,
),
observations: [
{
label: artifact.assembly.name,
properties: {
name: artifact.assembly.name,
version: artifact.assembly.version,
culture: artifact.assembly.culture,
public_key_kind: artifact.assembly.public_key.kind,
},
evidence: managedEvidence(
state,
"inspect_managed_artifact",
artifactCoverage,
),
},
],
});
state.nodes.push(assembly);
addContainsEdge(state, artifactNode, assembly, {
kind: "declares-managed-assembly",
coverage: artifactCoverage,
});
}
const module =
artifact?.module ??
parsed.members?.result.module ??
parsed.boundaries?.result.module;
if (module !== undefined && module !== null) {
const moduleCoverage = managedSourceCoverage(moduleCoverageState(parsed));
const moduleNode = createJavaScriptApplicationNode({
kind: "managed-module",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-module",
module.mvid ?? module.token,
),
observations: [
{
label: module.name,
properties: {
name: module.name,
mvid: module.mvid,
generation: module.generation,
token: module.token,
},
evidence: managedEvidence(
state,
"inspect_managed_artifact",
moduleCoverage,
),
},
],
});
state.nodes.push(moduleNode);
addContainsEdge(state, artifactNode, moduleNode, {
kind: "declares-managed-module",
coverage: moduleCoverage,
});
}
};
/** Add managed type, method, and field nodes from member evidence. */
export const addMemberNodes = (
state: GraphBuildState,
artifactNode: ApplicationNode,
parsed: ParsedManagedGraphInput,
limits: ManagedGraphProjectionLimits,
) => {
const members = parsed.members?.result;
if (members === undefined) return { types: 0, methods: 0, fields: 0 };
const types = members.types.items.slice(0, limits.max_types);
const methods = members.methods.items.slice(0, limits.max_methods);
const fields = members.fields.items.slice(0, limits.max_fields);
const typeCoverage = managedSourcePageCoverage(
members.coverage.state,
members.types,
"types",
);
const methodCoverage = managedSourcePageCoverage(
members.coverage.state,
members.methods,
"methods",
);
const fieldCoverage = managedSourcePageCoverage(
members.coverage.state,
members.fields,
"fields",
);
for (const type of types) {
const node = typeNode(state, type, typeCoverage);
state.nodes.push(node);
state.typeNodes.set(type.token, node);
addContainsEdge(state, artifactNode, node, {
kind: "declares-managed-type",
coverage: typeCoverage,
});
}
for (const method of methods) {
const node = methodNode(state, method, methodCoverage);
state.nodes.push(node);
state.methodNodes.set(method.token, node);
const owner =
method.declaring_type_token === null
? artifactNode
: (state.typeNodes.get(method.declaring_type_token) ?? artifactNode);
addContainsEdge(state, owner, node, {
kind: "declares-managed-method",
coverage: methodCoverage,
});
}
for (const field of fields) {
const node = fieldNode(state, field, fieldCoverage);
state.nodes.push(node);
state.fieldNodes.set(field.token, node);
const owner =
field.declaring_type_token === null
? artifactNode
: (state.typeNodes.get(field.declaring_type_token) ?? artifactNode);
addContainsEdge(state, owner, node, {
kind: "declares-managed-field",
coverage: fieldCoverage,
});
}
return {
types: types.length,
methods: methods.length,
fields: fields.length,
};
};
/** Build a managed type application node. */
export const typeNode = (
state: GraphBuildState,
type: ManagedType,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-type",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-type-token",
type.token,
),
observations: [
{
label: type.full_name,
properties: {
token: type.token,
namespace: type.namespace,
name: type.name,
full_name: type.full_name,
flags: type.flags,
extends_token: type.extends_token,
},
evidence: managedEvidence(state, "inspect_managed_members", coverage),
},
],
});
/** Build a managed method application node. */
export const methodNode = (
state: GraphBuildState,
method: ManagedMethod,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-method",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-method-token",
method.token,
),
observations: [
{
label:
method.declaring_type === null
? method.name
: `${method.declaring_type}.${method.name}`,
properties: {
token: method.token,
declaring_type_token: method.declaring_type_token,
declaring_type: method.declaring_type,
name: method.name,
rva: method.rva,
signature_sha256: method.signature.raw_sha256,
signature_status: method.signature.parse_status,
body_status: method.body.status,
normalized_il_sha256: method.body.normalized_il_sha256,
il_size: method.body.il_size,
},
evidence: managedEvidence(state, "inspect_managed_members", coverage),
},
],
});
/** Build a managed field application node. */
export const fieldNode = (
state: GraphBuildState,
field: ManagedField,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-field",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-field-token",
field.token,
),
observations: [
{
label:
field.declaring_type === null
? field.name
: `${field.declaring_type}.${field.name}`,
properties: {
token: field.token,
declaring_type_token: field.declaring_type_token,
declaring_type: field.declaring_type,
name: field.name,
flags: field.flags,
signature_sha256: field.signature.raw_sha256,
signature_status: field.signature.parse_status,
},
evidence: managedEvidence(state, "inspect_managed_members", coverage),
},
],
});
/** Add P/Invoke import and native implementation boundary nodes. */
export const addBoundaryNodes = (
state: GraphBuildState,
artifactNode: ApplicationNode,
parsed: ParsedManagedGraphInput,
limits: ManagedGraphProjectionLimits,
) => {
const boundaries = parsed.boundaries?.result;
if (boundaries === undefined)
return { pinvoke_imports: 0, native_implementations: 0 };
const pinvokes = boundaries.pinvoke_imports.items.slice(
0,
limits.max_pinvoke_imports,
);
const implementations = boundaries.native_implementations.items.slice(
0,
limits.max_native_implementations,
);
const pinvokeCoverage = managedSourcePageCoverage(
boundaries.coverage.state,
boundaries.pinvoke_imports,
"pinvoke_imports",
);
const implementationCoverage = managedSourcePageCoverage(
boundaries.coverage.state,
boundaries.native_implementations,
"native_implementations",
);
for (const pinvoke of pinvokes) {
const node = pinvokeNode(state, pinvoke, pinvokeCoverage);
state.nodes.push(node);
const owner =
pinvoke.member_token === null
? artifactNode
: (state.methodNodes.get(pinvoke.member_token) ?? artifactNode);
state.edges.push(
createJavaScriptApplicationEdge({
source_node_id: owner.node_id,
target_node_id: node.node_id,
relation: "imports",
properties: {
kind: "managed-pinvoke",
import_name: pinvoke.import_name,
import_scope_name: pinvoke.import_scope_name,
},
evidence: managedEvidence(
state,
"inspect_managed_native_boundaries",
pinvokeCoverage,
),
}),
);
}
for (const implementation of implementations) {
const node = nativeImplementationNode(
state,
implementation,
implementationCoverage,
);
state.nodes.push(node);
const owner = state.methodNodes.get(implementation.token) ?? artifactNode;
addContainsEdge(state, owner, node, {
kind: "declares-managed-native-implementation",
coverage: implementationCoverage,
});
}
return {
pinvoke_imports: pinvokes.length,
native_implementations: implementations.length,
};
};
/** Build a managed P/Invoke import application node. */
export const pinvokeNode = (
state: GraphBuildState,
pinvoke: PinvokeImport,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-pinvoke-import",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-pinvoke-token",
pinvoke.token,
),
observations: [
{
label: pinvoke.import_name,
properties: {
token: pinvoke.token,
member_token: pinvoke.member_token,
member_name: pinvoke.member_name,
import_name: pinvoke.import_name,
import_scope_name: pinvoke.import_scope_name,
char_set: pinvoke.char_set,
call_convention: pinvoke.call_convention,
verification: pinvoke.verification,
},
evidence: managedEvidence(
state,
"inspect_managed_native_boundaries",
coverage,
),
},
],
});
/** Build a managed native implementation application node. */
export const nativeImplementationNode = (
state: GraphBuildState,
implementation: NativeImplementation,
coverage: ApplicationGraphEvidence["coverage"],
): ApplicationNode =>
createJavaScriptApplicationNode({
kind: "managed-native-implementation",
identity: artifactLocalIdentity(
state.artifactSha256,
"managed-native-implementation-token",
implementation.token,
),
observations: [
{
label: implementation.name,
properties: {
token: implementation.token,
name: implementation.name,
rva: implementation.rva,
code_type: implementation.code_type,
managed_kind: implementation.managed_kind,
pinvoke_declared: implementation.pinvoke_declared,
boundary_kind: implementation.boundary_kind,
body_interpretation: implementation.body_interpretation,
},
evidence: managedEvidence(
state,
"inspect_managed_native_boundaries",
coverage,
),
},
],
});
/** Add a contains relationship edge between two nodes. */
export const addContainsEdge = (
state: GraphBuildState,
source: ApplicationNode,
target: ApplicationNode,
options: ContainsEdgeOptions,
): void => {
state.edges.push(
createJavaScriptApplicationEdge({
source_node_id: source.node_id,
target_node_id: target.node_id,
relation: "contains",
properties: { kind: options.kind },
evidence: managedEvidence(
state,
"project-managed-contains",
options.coverage,
),
}),
);
};
/** Build an artifact-local key identity for a managed graph entity. */
export const artifactLocalIdentity = (
artifactSha256: string,
namespace: string,
key: string,
) => ({
strategy: "artifact-local-key" as const,
stability: "artifact-version" as const,
artifact_sha256: artifactSha256,
namespace,
key,
});
/** Build managed static-analysis evidence for a graph observation. */
export const managedEvidence = (
state: GraphBuildState,
operation: string,
coverage: ApplicationGraphEvidence["coverage"] = managedSourceCoverage("complete"),
): ApplicationGraphEvidence => ({
authority: "managed-static-analysis",
state: "observed",
confidence: "exact",
artifact: {
available: true,
artifact_id: `art_${state.artifactSha256}`,
sha256: state.artifactSha256,
},
location: {
available: true,
value: {
kind: "artifact-path",
path: graphArtifactPath(state.artifactPath),
},
},
extractor: {
name: "rea-dotnet-static",
version: "1",
operation,
executable_sha256: null,
},
coverage,
limitations:
coverage.status === "complete"
? []
: [
"The source managed Evidence slice is incomplete; unreturned or unavailable items remain unobserved.",
],
evidence_ids: state.evidenceLinks,
});
/** Sanitize an artifact path into a managed graph location. */
export const graphArtifactPath = (path: string): string => {
const name = basename(path).replaceAll(/[^A-Za-z0-9._-]/gu, "_");
return `managed/${name.length === 0 ? "artifact.pe" : name}`;
};
/** Derive the module coverage state from the best available evidence source. */
export const moduleCoverageState = (
parsed: ParsedManagedGraphInput,
): "complete" | "partial" | "unavailable" =>
parsed.artifact !== null && parsed.artifact.result.module !== null
? parsed.artifact.result.coverage.state
: parsed.members !== null && parsed.members.result.module !== null
? parsed.members.result.coverage.state
: (parsed.boundaries?.result.coverage.state ?? "complete");
+32 -623
View File
@@ -1,16 +1,24 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceSchema, parseEvidence } from "./evidence.js";
import { evidenceSchema } from "./evidence.js";
import {
cliMetadataGuidSchema,
managedMemberInspectionSchema,
type ManagedMemberInspection,
} from "./managedArtifact.js";
import type { JsonValue } from "./jsonValue.js";
import { assessKnownPageCoverage } from "./knownPageCoverage.js";
import {
buildComparisonCoverage,
buildComparisonMatching,
buildComparisonSummary,
comparisonLimitations,
sideManifest,
} from "./managedMemberComparisonCoverage.js";
import {
buildFieldItems,
buildMethodItems,
keyMembers,
sha256,
} from "./managedMemberComparisonMatch.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
@@ -176,44 +184,12 @@ export type ManagedMemberComparisonResult = z.infer<
typeof managedMemberComparisonResultSchema
>;
type Method = ManagedMemberInspection["methods"]["items"][number];
type Field = ManagedMemberInspection["fields"]["items"][number];
type MethodItem = ManagedMemberComparisonResult["methods"][number];
type FieldItem = ManagedMemberComparisonResult["fields"][number];
type MatchBasis = z.infer<typeof matchBasisSchema>;
/** One authenticated side of a managed member comparison. */
export interface ManagedMemberComparisonSide {
readonly evidenceId: string;
readonly result: ManagedMemberInspection;
}
interface Keyed<Item> {
readonly item: Item;
readonly exactKey: string | null;
readonly structuralKey: string | null;
}
interface MatchedPair<Item> {
readonly left: Keyed<Item>;
readonly right: Keyed<Item>;
readonly basis: MatchBasis;
readonly confidence: "exact" | "high";
}
interface Ambiguous<Item> {
readonly left: readonly Keyed<Item>[];
readonly right: readonly Keyed<Item>[];
readonly basis: MatchBasis;
}
const sha256 = (value: JsonValue): string => {
const serialized = canonicalize(value);
if (serialized === undefined)
throw new TypeError("Managed comparison canonicalization failed");
return createHash("sha256").update(serialized).digest("hex");
};
/** Compare two parsed managed member observations without name-based matching. */
export const compareManagedMembers = (
left: ManagedMemberComparisonSide,
@@ -224,56 +200,24 @@ export const compareManagedMembers = (
const rightMethodPage = assessKnownPageCoverage(right.result.methods);
const leftFieldPage = assessKnownPageCoverage(left.result.fields);
const rightFieldPage = assessKnownPageCoverage(right.result.fields);
const methodMatches = matchMethods(
left.result.methods.items.map(keyMethod),
right.result.methods.items.map(keyMethod),
const { methodMatches, fieldMatches } = keyMembers(
left,
right,
limits.max_candidates,
);
const fieldMatches = matchFields(
left.result.fields.items.map(keyField),
right.result.fields.items.map(keyField),
limits.max_candidates,
);
const methodItems = buildMethodItems(methodMatches, {
const itemContext = {
leftEvidenceId: left.evidenceId,
rightEvidenceId: right.evidenceId,
leftComplete: leftMethodPage.sourceComplete,
rightComplete: rightMethodPage.sourceComplete,
leftComplete: leftMethodPage.sourceComplete && leftFieldPage.sourceComplete,
rightComplete:
rightMethodPage.sourceComplete && rightFieldPage.sourceComplete,
limits,
});
const fieldItems = buildFieldItems(fieldMatches, {
leftEvidenceId: left.evidenceId,
rightEvidenceId: right.evidenceId,
leftComplete: leftFieldPage.sourceComplete,
rightComplete: rightFieldPage.sourceComplete,
limits,
});
};
const methodItems = buildMethodItems(methodMatches, itemContext);
const fieldItems = buildFieldItems(fieldMatches, itemContext);
const allItems = [...methodItems.items, ...fieldItems.items];
const summary = {
unchanged: allItems.filter(({ status }) => status === "unchanged").length,
changed: allItems.filter(({ status }) => status === "changed").length,
added: allItems.filter(({ status }) => status === "added").length,
removed: allItems.filter(({ status }) => status === "removed").length,
unknown: allItems.filter(({ status }) => status === "unknown").length,
};
const matching = {
exact_il_signature: methodItems.items.filter(
({ match }) => match.basis === "exact-il-signature",
).length,
structural_method_shape: methodItems.items.filter(
({ match }) => match.basis === "structural-method-shape",
).length,
field_signature: fieldItems.items.filter(
({ match }) => match.basis === "field-signature",
).length,
ambiguous:
methodItems.items.filter(({ match }) => match.status === "ambiguous")
.length +
fieldItems.items.filter(({ match }) => match.status === "ambiguous")
.length,
unmatched: allItems.filter(({ match }) => match.status === "unmatched")
.length,
};
const summary = buildComparisonSummary(allItems);
const matching = buildComparisonMatching(methodItems.items, fieldItems.items);
const coverage = buildComparisonCoverage({
left: left.result,
right: right.result,
@@ -293,7 +237,7 @@ export const compareManagedMembers = (
coverage.omitted_candidates,
);
const result = {
schema_version: 1,
schema_version: 1 as const,
comparison_id: `mmc_${sha256({
left: left.evidenceId,
right: right.evidenceId,
@@ -302,10 +246,10 @@ export const compareManagedMembers = (
limits,
})}`,
algorithm: {
name: "rea-managed-member-comparison",
version: 1,
token_identity: "build-local",
name_matching: "not-used",
name: "rea-managed-member-comparison" as const,
version: 1 as const,
token_identity: "build-local" as const,
name_matching: "not-used" as const,
},
left: sideManifest(left),
right: sideManifest(right),
@@ -320,539 +264,4 @@ export const compareManagedMembers = (
return managedMemberComparisonResultSchema.parse(result);
};
type PageAssessment = ReturnType<typeof assessKnownPageCoverage>;
interface ComparisonCoverageInput {
readonly left: ManagedMemberInspection;
readonly right: ManagedMemberInspection;
readonly leftMethodPage: PageAssessment;
readonly rightMethodPage: PageAssessment;
readonly leftFieldPage: PageAssessment;
readonly rightFieldPage: PageAssessment;
readonly omittedMethodItems: number;
readonly omittedFieldItems: number;
readonly omittedCandidates: number;
}
const buildComparisonCoverage = ({
left,
right,
leftMethodPage,
rightMethodPage,
leftFieldPage,
rightFieldPage,
omittedMethodItems,
omittedFieldItems,
omittedCandidates,
}: ComparisonCoverageInput): ManagedMemberComparisonResult["coverage"] => {
const omittedMethods =
omittedMethodItems +
leftMethodPage.sourceOmittedCount +
rightMethodPage.sourceOmittedCount;
const omittedFields =
omittedFieldItems +
leftFieldPage.sourceOmittedCount +
rightFieldPage.sourceOmittedCount;
const leftStatus = sideCoverageStatus(
left,
leftMethodPage.sourceComplete && leftFieldPage.sourceComplete,
);
const rightStatus = sideCoverageStatus(
right,
rightMethodPage.sourceComplete && rightFieldPage.sourceComplete,
);
const unknownInput =
left.coverage.state !== "complete" || right.coverage.state !== "complete";
const truncated = omittedMethods + omittedFields + omittedCandidates > 0;
return {
status: unknownInput
? "partial"
: truncated
? "truncated"
: leftStatus === "complete" && rightStatus === "complete"
? "complete-within-inputs"
: "partial",
left_status: leftStatus,
right_status: rightStatus,
omitted_methods: omittedMethods,
omitted_fields: omittedFields,
omitted_candidates: omittedCandidates,
};
};
const sideCoverageStatus = (
result: ManagedMemberInspection,
pagesComplete: boolean,
): ManagedMemberComparisonResult["coverage"]["left_status"] =>
result.coverage.state === "unavailable"
? "unavailable"
: result.coverage.state === "complete" && pagesComplete
? "complete"
: "partial";
const sideManifest = (
side: ManagedMemberComparisonSide,
): ManagedMemberComparisonResult["left"] => ({
evidence_id: side.evidenceId,
artifact_sha256: side.result.artifact.sha256,
mvid: side.result.module?.mvid ?? null,
module_name: side.result.module?.name ?? null,
metadata_status: side.result.metadata.status,
methods_total: side.result.methods.total,
fields_total: side.result.fields.total,
});
const keyMethod = (item: Method): Keyed<Method> => ({
item,
exactKey:
item.signature.parse_status === "decoded" &&
item.body.status === "present" &&
item.body.normalized_il_sha256 !== null
? stableKey([
"method-exact",
item.signature.raw_sha256,
item.body.normalized_il_sha256,
])
: null,
structuralKey:
item.body.status === "present"
? stableKey([
"method-structural",
item.signature.kind,
item.signature.calling_convention,
item.signature.generic_parameter_count,
item.signature.parameter_count,
item.signature.return_type,
item.signature.parameter_types,
item.body.status,
item.body.header_format,
item.body.il_size,
item.body.max_stack,
item.body.init_locals,
item.body.opcode_counts,
item.body.anchors.map(({ opcode, operand_kind }) => [
opcode,
operand_kind,
]),
item.body.exception_regions.map((region) => [
region.flags,
region.try_length,
region.handler_length,
region.class_token === null ? null : "type-token",
region.filter_offset === null ? null : "filter",
]),
])
: null,
});
const keyField = (item: Field): Keyed<Field> => ({
item,
exactKey:
item.signature.parse_status === "decoded"
? stableKey(["field-exact", item.signature.raw_sha256])
: null,
structuralKey: stableKey([
"field-structural",
item.signature.kind,
item.signature.field_type,
item.flags,
]),
});
const stableKey = (value: JsonValue): string => sha256(value);
const matchMethods = (
left: readonly Keyed<Method>[],
right: readonly Keyed<Method>[],
maxCandidates: number,
) =>
matchByKeys(left, right, maxCandidates, "exact-il-signature", [
"structural-method-shape",
]);
const matchFields = (
left: readonly Keyed<Field>[],
right: readonly Keyed<Field>[],
maxCandidates: number,
) => matchByKeys(left, right, maxCandidates, "field-signature", []);
const matchByKeys = <Item>(
left: readonly Keyed<Item>[],
right: readonly Keyed<Item>[],
maxCandidates: number,
exactBasis: MatchBasis,
fallbackBases: readonly MatchBasis[],
): {
readonly pairs: readonly MatchedPair<Item>[];
readonly ambiguous: readonly Ambiguous<Item>[];
readonly leftOnly: readonly Keyed<Item>[];
readonly rightOnly: readonly Keyed<Item>[];
readonly omittedCandidates: number;
} => {
const usedLeft = new Set<Keyed<Item>>();
const usedRight = new Set<Keyed<Item>>();
const pairs: MatchedPair<Item>[] = [];
const ambiguous: Ambiguous<Item>[] = [];
let omittedCandidates = 0;
const rounds: readonly {
readonly basis: MatchBasis;
readonly key: (item: Keyed<Item>) => string | null;
}[] = [
{ basis: exactBasis, key: ({ exactKey }) => exactKey },
...fallbackBases.map((basis) => ({
basis,
key: ({ structuralKey }: Keyed<Item>) => structuralKey,
})),
];
for (const round of rounds) {
const leftGroups = groupBy(
left.filter((item) => !usedLeft.has(item)),
round.key,
);
const rightGroups = groupBy(
right.filter((item) => !usedRight.has(item)),
round.key,
);
for (const [key, leftItems] of leftGroups) {
const rightItems = rightGroups.get(key);
if (rightItems === undefined) continue;
if (leftItems.length === 1 && rightItems.length === 1) {
const [leftItem] = leftItems;
const [rightItem] = rightItems;
if (leftItem === undefined || rightItem === undefined) continue;
usedLeft.add(leftItem);
usedRight.add(rightItem);
pairs.push({
left: leftItem,
right: rightItem,
basis: round.basis,
confidence:
round.basis === "exact-il-signature" ||
round.basis === "field-signature"
? "exact"
: "high",
});
} else {
for (const item of leftItems) usedLeft.add(item);
for (const item of rightItems) usedRight.add(item);
omittedCandidates += Math.max(
0,
leftItems.length + rightItems.length - maxCandidates * 2,
);
ambiguous.push({
left: leftItems.slice(0, maxCandidates),
right: rightItems.slice(0, maxCandidates),
basis: round.basis,
});
}
}
}
return {
pairs,
ambiguous,
leftOnly: left.filter((item) => !usedLeft.has(item)),
rightOnly: right.filter((item) => !usedRight.has(item)),
omittedCandidates,
};
};
const groupBy = <Item>(
items: readonly Item[],
keyOf: (item: Item) => string | null,
): Map<string, readonly Item[]> => {
const grouped = new Map<string, Item[]>();
for (const item of items) {
const key = keyOf(item);
if (key === null) continue;
const bucket = grouped.get(key) ?? [];
bucket.push(item);
grouped.set(key, bucket);
}
return grouped;
};
const buildMethodItems = (
matches: ReturnType<typeof matchMethods>,
context: ComparisonItemContext,
): { readonly items: MethodItem[]; readonly omitted: number } => {
const items: MethodItem[] = [];
for (const pair of matches.pairs) {
const dimensions = changedMethodDimensions(pair.left.item, pair.right.item);
items.push({
item_id: `mmc_method_${sha256({
left: pair.left.item.token,
right: pair.right.item.token,
basis: pair.basis,
})}`,
status: dimensions.length === 0 ? "unchanged" : "changed",
left: methodIdentity(pair.left.item),
right: methodIdentity(pair.right.item),
match: {
status: "matched",
basis: pair.basis,
confidence: pair.confidence,
candidate_left_tokens: [],
candidate_right_tokens: [],
},
dimensions,
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [],
});
}
for (const ambiguous of matches.ambiguous) {
items.push({
item_id: `mmc_method_${sha256({
left: ambiguous.left.map(({ item }) => item.token),
right: ambiguous.right.map(({ item }) => item.token),
basis: ambiguous.basis,
})}`,
status: "unknown",
left: null,
right: null,
match: {
status: "ambiguous",
basis: ambiguous.basis,
confidence: "unknown",
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
},
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
"Multiple managed methods share the same non-name identity key; REA did not guess a token remap.",
],
});
}
for (const item of matches.leftOnly)
items.push(methodOnlyItem(item.item, context, true));
for (const item of matches.rightOnly)
items.push(methodOnlyItem(item.item, context, false));
return limitItems(items, context.limits.max_method_matches);
};
const buildFieldItems = (
matches: ReturnType<typeof matchFields>,
context: ComparisonItemContext,
): { readonly items: FieldItem[]; readonly omitted: number } => {
const items: FieldItem[] = [];
for (const pair of matches.pairs) {
const changed =
pair.left.item.signature.raw_sha256 !==
pair.right.item.signature.raw_sha256 ||
pair.left.item.flags !== pair.right.item.flags;
items.push({
item_id: `mmc_field_${sha256({
left: pair.left.item.token,
right: pair.right.item.token,
})}`,
status: changed ? "changed" : "unchanged",
left: fieldIdentity(pair.left.item),
right: fieldIdentity(pair.right.item),
match: {
status: "matched",
basis: pair.basis,
confidence: pair.confidence,
candidate_left_tokens: [],
candidate_right_tokens: [],
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [],
});
}
for (const ambiguous of matches.ambiguous)
items.push({
item_id: `mmc_field_${sha256({
left: ambiguous.left.map(({ item }) => item.token),
right: ambiguous.right.map(({ item }) => item.token),
})}`,
status: "unknown",
left: null,
right: null,
match: {
status: "ambiguous",
basis: ambiguous.basis,
confidence: "unknown",
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
"Multiple managed fields share the same signature; REA did not guess a token remap from names.",
],
});
for (const item of matches.leftOnly)
items.push(fieldOnlyItem(item.item, context, true));
for (const item of matches.rightOnly)
items.push(fieldOnlyItem(item.item, context, false));
return limitItems(items, context.limits.max_field_matches);
};
interface ComparisonItemContext {
readonly leftEvidenceId: string;
readonly rightEvidenceId: string;
readonly leftComplete: boolean;
readonly rightComplete: boolean;
readonly limits: CompareManagedMembersInput["limits"];
}
const limitItems = <Item>(
items: readonly Item[],
limit: number,
): { readonly items: Item[]; readonly omitted: number } => ({
items: items.slice(0, limit),
omitted: Math.max(0, items.length - limit),
});
const methodOnlyItem = (
item: Method,
context: ComparisonItemContext,
left: boolean,
): MethodItem => {
const absenceObserved = left ? context.rightComplete : context.leftComplete;
return {
item_id: `mmc_method_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
left: left ? methodIdentity(item) : null,
right: left ? null : methodIdentity(item),
match: {
status: "unmatched",
basis: "none",
confidence: "unknown",
candidate_left_tokens: [],
candidate_right_tokens: [],
},
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: absenceObserved
? []
: [
`unknown-within-unobserved-page: The ${left ? "right" : "left"} method page is incomplete, so absence was not observed.`,
],
};
};
const fieldOnlyItem = (
item: Field,
context: ComparisonItemContext,
left: boolean,
): FieldItem => {
const absenceObserved = left ? context.rightComplete : context.leftComplete;
return {
item_id: `mmc_field_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
left: left ? fieldIdentity(item) : null,
right: left ? null : fieldIdentity(item),
match: {
status: "unmatched",
basis: "none",
confidence: "unknown",
candidate_left_tokens: [],
candidate_right_tokens: [],
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: absenceObserved
? []
: [
`unknown-within-unobserved-page: The ${left ? "right" : "left"} field page is incomplete, so absence was not observed.`,
],
};
};
const methodIdentity = (item: Method): NonNullable<MethodItem["left"]> => ({
token: item.token,
declaring_type: item.declaring_type,
name: item.name,
signature_sha256: item.signature.raw_sha256,
normalized_il_sha256: item.body.normalized_il_sha256,
});
const fieldIdentity = (item: Field): NonNullable<FieldItem["left"]> => ({
token: item.token,
declaring_type: item.declaring_type,
name: item.name,
signature_sha256: item.signature.raw_sha256,
});
const changedMethodDimensions = (
left: Method,
right: Method,
): MethodItem["dimensions"] => {
const dimensions: MethodItem["dimensions"] = [];
if (left.signature.raw_sha256 !== right.signature.raw_sha256)
dimensions.push("signature");
if (left.body.normalized_il_sha256 !== right.body.normalized_il_sha256)
dimensions.push("cil");
if (
canonicalize(left.body.opcode_counts) !==
canonicalize(right.body.opcode_counts)
)
dimensions.push("opcode-shape");
if (callShape(left.body.anchors) !== callShape(right.body.anchors))
dimensions.push("call-shape");
if (fieldShape(left.body.anchors) !== fieldShape(right.body.anchors))
dimensions.push("field-shape");
if (
canonicalize(left.body.exception_regions) !==
canonicalize(right.body.exception_regions)
)
dimensions.push("exception-shape");
if (left.body.status !== right.body.status) dimensions.push("availability");
return dimensions;
};
const callShape = (anchors: Method["body"]["anchors"]): string =>
JSON.stringify(
anchors
.filter(({ operand_kind }) => operand_kind === "method")
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
);
const fieldShape = (anchors: Method["body"]["anchors"]): string =>
JSON.stringify(
anchors
.filter(({ operand_kind }) => operand_kind === "field")
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
);
const comparisonLimitations = (
left: ManagedMemberInspection,
right: ManagedMemberInspection,
omittedItems: number,
omittedCandidates: number,
): string[] => {
const limitations: string[] = [
"Metadata tokens are build-local coordinates; matched pairs are remaps, not persistent identities.",
"Names are reported as observations but are not used as a matching basis.",
];
if (!left.methods.complete || !right.methods.complete)
limitations.push("At least one method page is incomplete.");
if (!left.fields.complete || !right.fields.complete)
limitations.push("At least one field page is incomplete.");
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
limitations.push("At least one managed member observation is partial.");
if (omittedItems > 0)
limitations.push(
`${String(omittedItems)} comparison items were omitted by output limits.`,
);
if (omittedCandidates > 0)
limitations.push(
`${String(omittedCandidates)} ambiguous candidates were omitted by candidate limits.`,
);
return limitations;
};
/** Extract and authenticate a managed member inspection Evidence record. */
export const parseManagedMemberEvidence = (
evidence: unknown,
): {
readonly evidenceId: string;
readonly result: ManagedMemberInspection;
} => {
const parsed = parseEvidence(evidence);
if (parsed.operation !== "inspect_managed_members")
throw new TypeError("Evidence operation is not inspect_managed_members");
return {
evidenceId: parsed.evidence_id,
result: managedMemberInspectionSchema.parse(parsed.normalized_result),
};
};
export { parseManagedMemberEvidence } from "./managedMemberComparisonMatch.js";
@@ -0,0 +1,151 @@
import type {
ManagedMemberComparisonResult,
ManagedMemberComparisonSide,
} from "./managedMemberComparison.js";
import type { ManagedMemberInspection } from "./managedArtifact.js";
export interface PageAssessment {
readonly sourceComplete: boolean;
readonly sourceOmittedCount: number;
}
interface ComparisonCoverageInput {
readonly left: ManagedMemberInspection;
readonly right: ManagedMemberInspection;
readonly leftMethodPage: PageAssessment;
readonly rightMethodPage: PageAssessment;
readonly leftFieldPage: PageAssessment;
readonly rightFieldPage: PageAssessment;
readonly omittedMethodItems: number;
readonly omittedFieldItems: number;
readonly omittedCandidates: number;
}
export const buildComparisonCoverage = ({
left,
right,
leftMethodPage,
rightMethodPage,
leftFieldPage,
rightFieldPage,
omittedMethodItems,
omittedFieldItems,
omittedCandidates,
}: ComparisonCoverageInput): ManagedMemberComparisonResult["coverage"] => {
const omittedMethods =
omittedMethodItems +
leftMethodPage.sourceOmittedCount +
rightMethodPage.sourceOmittedCount;
const omittedFields =
omittedFieldItems +
leftFieldPage.sourceOmittedCount +
rightFieldPage.sourceOmittedCount;
const leftStatus = sideCoverageStatus(
left,
leftMethodPage.sourceComplete && leftFieldPage.sourceComplete,
);
const rightStatus = sideCoverageStatus(
right,
rightMethodPage.sourceComplete && rightFieldPage.sourceComplete,
);
const unknownInput =
left.coverage.state !== "complete" || right.coverage.state !== "complete";
const truncated = omittedMethods + omittedFields + omittedCandidates > 0;
return {
status: unknownInput
? "partial"
: truncated
? "truncated"
: leftStatus === "complete" && rightStatus === "complete"
? "complete-within-inputs"
: "partial",
left_status: leftStatus,
right_status: rightStatus,
omitted_methods: omittedMethods,
omitted_fields: omittedFields,
omitted_candidates: omittedCandidates,
};
};
const sideCoverageStatus = (
result: ManagedMemberInspection,
pagesComplete: boolean,
): ManagedMemberComparisonResult["coverage"]["left_status"] =>
result.coverage.state === "unavailable"
? "unavailable"
: result.coverage.state === "complete" && pagesComplete
? "complete"
: "partial";
export const sideManifest = (
side: ManagedMemberComparisonSide,
): ManagedMemberComparisonResult["left"] => ({
evidence_id: side.evidenceId,
artifact_sha256: side.result.artifact.sha256,
mvid: side.result.module?.mvid ?? null,
module_name: side.result.module?.name ?? null,
metadata_status: side.result.metadata.status,
methods_total: side.result.methods.total,
fields_total: side.result.fields.total,
});
export const buildComparisonSummary = (
items: readonly (
| ManagedMemberComparisonResult["methods"][number]
| ManagedMemberComparisonResult["fields"][number]
)[],
): ManagedMemberComparisonResult["summary"] => ({
unchanged: items.filter(({ status }) => status === "unchanged").length,
changed: items.filter(({ status }) => status === "changed").length,
added: items.filter(({ status }) => status === "added").length,
removed: items.filter(({ status }) => status === "removed").length,
unknown: items.filter(({ status }) => status === "unknown").length,
});
export const buildComparisonMatching = (
methodItems: readonly ManagedMemberComparisonResult["methods"][number][],
fieldItems: readonly ManagedMemberComparisonResult["fields"][number][],
): ManagedMemberComparisonResult["matching"] => ({
exact_il_signature: methodItems.filter(
({ match }) => match.basis === "exact-il-signature",
).length,
structural_method_shape: methodItems.filter(
({ match }) => match.basis === "structural-method-shape",
).length,
field_signature: fieldItems.filter(
({ match }) => match.basis === "field-signature",
).length,
ambiguous:
methodItems.filter(({ match }) => match.status === "ambiguous").length +
fieldItems.filter(({ match }) => match.status === "ambiguous").length,
unmatched: [...methodItems, ...fieldItems].filter(
({ match }) => match.status === "unmatched",
).length,
});
export const comparisonLimitations = (
left: ManagedMemberInspection,
right: ManagedMemberInspection,
omittedItems: number,
omittedCandidates: number,
): string[] => {
const limitations: string[] = [
"Metadata tokens are build-local coordinates; matched pairs are remaps, not persistent identities.",
"Names are reported as observations but are not used as a matching basis.",
];
if (!left.methods.complete || !right.methods.complete)
limitations.push("At least one method page is incomplete.");
if (!left.fields.complete || !right.fields.complete)
limitations.push("At least one field page is incomplete.");
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
limitations.push("At least one managed member observation is partial.");
if (omittedItems > 0)
limitations.push(
`${String(omittedItems)} comparison items were omitted by output limits.`,
);
if (omittedCandidates > 0)
limitations.push(
`${String(omittedCandidates)} ambiguous candidates were omitted by candidate limits.`,
);
return limitations;
};
+513
View File
@@ -0,0 +1,513 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { parseEvidence } from "./evidence.js";
import {
managedMemberInspectionSchema,
type ManagedMemberInspection,
} from "./managedArtifact.js";
import type {
CompareManagedMembersInput,
ManagedMemberComparisonResult,
ManagedMemberComparisonSide,
} from "./managedMemberComparison.js";
import type { JsonValue } from "./jsonValue.js";
export const sha256 = (value: JsonValue): string => {
const serialized = canonicalize(value);
if (serialized === undefined)
throw new TypeError("Managed comparison canonicalization failed");
return createHash("sha256").update(serialized).digest("hex");
};
type Method = ManagedMemberInspection["methods"]["items"][number];
type Field = ManagedMemberInspection["fields"]["items"][number];
type MethodItem = ManagedMemberComparisonResult["methods"][number];
type FieldItem = ManagedMemberComparisonResult["fields"][number];
type MatchBasis = MethodItem["match"]["basis"];
interface Keyed<Item> {
readonly item: Item;
readonly exactKey: string | null;
readonly structuralKey: string | null;
}
interface MatchedPair<Item> {
readonly left: Keyed<Item>;
readonly right: Keyed<Item>;
readonly basis: MatchBasis;
readonly confidence: "exact" | "high";
}
interface Ambiguous<Item> {
readonly left: readonly Keyed<Item>[];
readonly right: readonly Keyed<Item>[];
readonly basis: MatchBasis;
}
export interface ComparisonItemContext {
readonly leftEvidenceId: string;
readonly rightEvidenceId: string;
readonly leftComplete: boolean;
readonly rightComplete: boolean;
readonly limits: CompareManagedMembersInput["limits"];
}
const keyMethod = (item: Method): Keyed<Method> => ({
item,
exactKey:
item.signature.parse_status === "decoded" &&
item.body.status === "present" &&
item.body.normalized_il_sha256 !== null
? stableKey([
"method-exact",
item.signature.raw_sha256,
item.body.normalized_il_sha256,
])
: null,
structuralKey:
item.body.status === "present"
? stableKey([
"method-structural",
item.signature.kind,
item.signature.calling_convention,
item.signature.generic_parameter_count,
item.signature.parameter_count,
item.signature.return_type,
item.signature.parameter_types,
item.body.status,
item.body.header_format,
item.body.il_size,
item.body.max_stack,
item.body.init_locals,
item.body.opcode_counts,
item.body.anchors.map(({ opcode, operand_kind }) => [
opcode,
operand_kind,
]),
item.body.exception_regions.map((region) => [
region.flags,
region.try_length,
region.handler_length,
region.class_token === null ? null : "type-token",
region.filter_offset === null ? null : "filter",
]),
])
: null,
});
const keyField = (item: Field): Keyed<Field> => ({
item,
exactKey:
item.signature.parse_status === "decoded"
? stableKey(["field-exact", item.signature.raw_sha256])
: null,
structuralKey: stableKey([
"field-structural",
item.signature.kind,
item.signature.field_type,
item.flags,
]),
});
const stableKey = (value: JsonValue): string => sha256(value);
const matchMethods = (
left: readonly Keyed<Method>[],
right: readonly Keyed<Method>[],
maxCandidates: number,
) =>
matchByKeys({
left,
right,
maxCandidates,
exactBasis: "exact-il-signature",
fallbackBases: ["structural-method-shape"],
});
const matchFields = (
left: readonly Keyed<Field>[],
right: readonly Keyed<Field>[],
maxCandidates: number,
) =>
matchByKeys({
left,
right,
maxCandidates,
exactBasis: "field-signature",
fallbackBases: [],
});
interface MatchByKeysInput<Item> {
readonly left: readonly Keyed<Item>[];
readonly right: readonly Keyed<Item>[];
readonly maxCandidates: number;
readonly exactBasis: MatchBasis;
readonly fallbackBases: readonly MatchBasis[];
}
const matchByKeys = <Item>({
left,
right,
maxCandidates,
exactBasis,
fallbackBases,
}: MatchByKeysInput<Item>): {
readonly pairs: readonly MatchedPair<Item>[];
readonly ambiguous: readonly Ambiguous<Item>[];
readonly leftOnly: readonly Keyed<Item>[];
readonly rightOnly: readonly Keyed<Item>[];
readonly omittedCandidates: number;
} => {
const usedLeft = new Set<Keyed<Item>>();
const usedRight = new Set<Keyed<Item>>();
const pairs: MatchedPair<Item>[] = [];
const ambiguous: Ambiguous<Item>[] = [];
let omittedCandidates = 0;
const rounds: readonly {
readonly basis: MatchBasis;
readonly key: (item: Keyed<Item>) => string | null;
}[] = [
{ basis: exactBasis, key: ({ exactKey }) => exactKey },
...fallbackBases.map((basis) => ({
basis,
key: ({ structuralKey }: Keyed<Item>) => structuralKey,
})),
];
for (const round of rounds) {
const leftGroups = groupBy(
left.filter((item) => !usedLeft.has(item)),
round.key,
);
const rightGroups = groupBy(
right.filter((item) => !usedRight.has(item)),
round.key,
);
for (const [key, leftItems] of leftGroups) {
const rightItems = rightGroups.get(key);
if (rightItems === undefined) continue;
if (leftItems.length === 1 && rightItems.length === 1) {
const [leftItem] = leftItems;
const [rightItem] = rightItems;
if (leftItem === undefined || rightItem === undefined) continue;
usedLeft.add(leftItem);
usedRight.add(rightItem);
pairs.push({
left: leftItem,
right: rightItem,
basis: round.basis,
confidence:
round.basis === "exact-il-signature" ||
round.basis === "field-signature"
? "exact"
: "high",
});
} else {
for (const item of leftItems) usedLeft.add(item);
for (const item of rightItems) usedRight.add(item);
omittedCandidates += Math.max(
0,
leftItems.length + rightItems.length - maxCandidates * 2,
);
ambiguous.push({
left: leftItems.slice(0, maxCandidates),
right: rightItems.slice(0, maxCandidates),
basis: round.basis,
});
}
}
}
return {
pairs,
ambiguous,
leftOnly: left.filter((item) => !usedLeft.has(item)),
rightOnly: right.filter((item) => !usedRight.has(item)),
omittedCandidates,
};
};
const groupBy = <Item>(
items: readonly Item[],
keyOf: (item: Item) => string | null,
): Map<string, readonly Item[]> => {
const grouped = new Map<string, Item[]>();
for (const item of items) {
const key = keyOf(item);
if (key === null) continue;
const bucket = grouped.get(key) ?? [];
bucket.push(item);
grouped.set(key, bucket);
}
return grouped;
};
const changedMethodDimensions = (
left: Method,
right: Method,
): MethodItem["dimensions"] => {
const dimensions: MethodItem["dimensions"] = [];
if (left.signature.raw_sha256 !== right.signature.raw_sha256)
dimensions.push("signature");
if (left.body.normalized_il_sha256 !== right.body.normalized_il_sha256)
dimensions.push("cil");
if (
canonicalize(left.body.opcode_counts) !==
canonicalize(right.body.opcode_counts)
)
dimensions.push("opcode-shape");
if (callShape(left.body.anchors) !== callShape(right.body.anchors))
dimensions.push("call-shape");
if (fieldShape(left.body.anchors) !== fieldShape(right.body.anchors))
dimensions.push("field-shape");
if (
canonicalize(left.body.exception_regions) !==
canonicalize(right.body.exception_regions)
)
dimensions.push("exception-shape");
if (left.body.status !== right.body.status) dimensions.push("availability");
return dimensions;
};
const callShape = (anchors: Method["body"]["anchors"]): string =>
JSON.stringify(
anchors
.filter(({ operand_kind }) => operand_kind === "method")
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
);
const fieldShape = (anchors: Method["body"]["anchors"]): string =>
JSON.stringify(
anchors
.filter(({ operand_kind }) => operand_kind === "field")
.map(({ opcode, operand_kind }) => [opcode, operand_kind]),
);
const methodIdentity = (item: Method): NonNullable<MethodItem["left"]> => ({
token: item.token,
declaring_type: item.declaring_type,
name: item.name,
signature_sha256: item.signature.raw_sha256,
normalized_il_sha256: item.body.normalized_il_sha256,
});
const fieldIdentity = (item: Field): NonNullable<FieldItem["left"]> => ({
token: item.token,
declaring_type: item.declaring_type,
name: item.name,
signature_sha256: item.signature.raw_sha256,
});
const methodOnlyItem = (
item: Method,
context: ComparisonItemContext,
left: boolean,
): MethodItem => {
const absenceObserved = left ? context.rightComplete : context.leftComplete;
return {
item_id: `mmc_method_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
left: left ? methodIdentity(item) : null,
right: left ? null : methodIdentity(item),
match: {
status: "unmatched",
basis: "none",
confidence: "unknown",
candidate_left_tokens: [],
candidate_right_tokens: [],
},
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: absenceObserved
? []
: [
`unknown-within-unobserved-page: The ${left ? "right" : "left"} method page is incomplete, so absence was not observed.`,
],
};
};
const fieldOnlyItem = (
item: Field,
context: ComparisonItemContext,
left: boolean,
): FieldItem => {
const absenceObserved = left ? context.rightComplete : context.leftComplete;
return {
item_id: `mmc_field_${sha256({ token: item.token, side: left ? "left" : "right" })}`,
status: absenceObserved ? (left ? "removed" : "added") : "unknown",
left: left ? fieldIdentity(item) : null,
right: left ? null : fieldIdentity(item),
match: {
status: "unmatched",
basis: "none",
confidence: "unknown",
candidate_left_tokens: [],
candidate_right_tokens: [],
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: absenceObserved
? []
: [
`unknown-within-unobserved-page: The ${left ? "right" : "left"} field page is incomplete, so absence was not observed.`,
],
};
};
export const buildMethodItems = (
matches: ReturnType<typeof matchMethods>,
context: ComparisonItemContext,
): { readonly items: MethodItem[]; readonly omitted: number } => {
const items: MethodItem[] = [];
for (const pair of matches.pairs) {
const dimensions = changedMethodDimensions(pair.left.item, pair.right.item);
items.push({
item_id: `mmc_method_${sha256({
left: pair.left.item.token,
right: pair.right.item.token,
basis: pair.basis,
})}`,
status: dimensions.length === 0 ? "unchanged" : "changed",
left: methodIdentity(pair.left.item),
right: methodIdentity(pair.right.item),
match: {
status: "matched",
basis: pair.basis,
confidence: pair.confidence,
candidate_left_tokens: [],
candidate_right_tokens: [],
},
dimensions,
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [],
});
}
for (const ambiguous of matches.ambiguous) {
items.push({
item_id: `mmc_method_${sha256({
left: ambiguous.left.map(({ item }) => item.token),
right: ambiguous.right.map(({ item }) => item.token),
basis: ambiguous.basis,
})}`,
status: "unknown",
left: null,
right: null,
match: {
status: "ambiguous",
basis: ambiguous.basis,
confidence: "unknown",
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
},
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
"Multiple managed methods share the same non-name identity key; REA did not guess a token remap.",
],
});
}
for (const item of matches.leftOnly)
items.push(methodOnlyItem(item.item, context, true));
for (const item of matches.rightOnly)
items.push(methodOnlyItem(item.item, context, false));
return limitItems(items, context.limits.max_method_matches);
};
export const buildFieldItems = (
matches: ReturnType<typeof matchFields>,
context: ComparisonItemContext,
): { readonly items: FieldItem[]; readonly omitted: number } => {
const items: FieldItem[] = [];
for (const pair of matches.pairs) {
const changed =
pair.left.item.signature.raw_sha256 !==
pair.right.item.signature.raw_sha256 ||
pair.left.item.flags !== pair.right.item.flags;
items.push({
item_id: `mmc_field_${sha256({
left: pair.left.item.token,
right: pair.right.item.token,
})}`,
status: changed ? "changed" : "unchanged",
left: fieldIdentity(pair.left.item),
right: fieldIdentity(pair.right.item),
match: {
status: "matched",
basis: pair.basis,
confidence: pair.confidence,
candidate_left_tokens: [],
candidate_right_tokens: [],
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [],
});
}
for (const ambiguous of matches.ambiguous)
items.push({
item_id: `mmc_field_${sha256({
left: ambiguous.left.map(({ item }) => item.token),
right: ambiguous.right.map(({ item }) => item.token),
})}`,
status: "unknown",
left: null,
right: null,
match: {
status: "ambiguous",
basis: ambiguous.basis,
confidence: "unknown",
candidate_left_tokens: ambiguous.left.map(({ item }) => item.token),
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
"Multiple managed fields share the same signature; REA did not guess a token remap from names.",
],
});
for (const item of matches.leftOnly)
items.push(fieldOnlyItem(item.item, context, true));
for (const item of matches.rightOnly)
items.push(fieldOnlyItem(item.item, context, false));
return limitItems(items, context.limits.max_field_matches);
};
const limitItems = <Item>(
items: readonly Item[],
limit: number,
): { readonly items: Item[]; readonly omitted: number } => ({
items: items.slice(0, limit),
omitted: Math.max(0, items.length - limit),
});
export const keyMembers = (
left: ManagedMemberComparisonSide,
right: ManagedMemberComparisonSide,
maxCandidates: number,
): {
readonly methodMatches: ReturnType<typeof matchMethods>;
readonly fieldMatches: ReturnType<typeof matchFields>;
} => ({
methodMatches: matchMethods(
left.result.methods.items.map(keyMethod),
right.result.methods.items.map(keyMethod),
maxCandidates,
),
fieldMatches: matchFields(
left.result.fields.items.map(keyField),
right.result.fields.items.map(keyField),
maxCandidates,
),
});
export const parseManagedMemberEvidence = (
evidence: unknown,
): {
readonly evidenceId: string;
readonly result: ManagedMemberInspection;
} => {
const parsed = parseEvidence(evidence);
if (parsed.operation !== "inspect_managed_members")
throw new TypeError("Evidence operation is not inspect_managed_members");
return {
evidenceId: parsed.evidence_id,
result: managedMemberInspectionSchema.parse(parsed.normalized_result),
};
};
+41 -488
View File
@@ -1,181 +1,30 @@
import { createHash } from "node:crypto";
import { basename } from "node:path";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceSchema, parseEvidence } from "./evidence.js";
import { functionDossierSchema } from "./hopperValues.js";
import { parseEvidence } from "./evidence.js";
import {
cliMetadataGuidSchema,
managedNativeBoundaryInspectionSchema,
type ManagedNativeBoundaryInspection,
} from "./managedArtifact.js";
import { inspectMachoSchema } from "./nativeInspection.js";
import type { JsonValue } from "./jsonValue.js";
import {
managedNativeVerificationResultSchema,
type ManagedNativeVerificationInput,
type ManagedNativeVerificationResult,
} from "./managedNativeVerificationSchemas.js";
import {
buildVerificationResult,
collectNativeSymbols,
verifyPinvoke,
} from "./managedNativeVerificationMatch.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
const boundedTextSchema = z.string().min(1).max(4_096);
const nativeVerificationLimitsSchema = z.strictObject({
max_native_observations: z.number().int().min(1).max(50).default(20),
max_candidates_per_import: z.number().int().min(1).max(100).default(25),
});
/** Authenticated managed boundary Evidence plus native observations. */
export const managedNativeVerificationInputSchema = z
.strictObject({
managed_boundaries: evidenceSchema,
native_observations: z.array(evidenceSchema).min(1).max(50),
limits: nativeVerificationLimitsSchema.default({
max_native_observations: 20,
max_candidates_per_import: 25,
}),
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine((input, context) => {
const ids = new Set<string>();
for (const [index, evidence] of input.native_observations.entries()) {
if (evidence.evidence_id === input.managed_boundaries.evidence_id) {
context.addIssue({
code: "custom",
path: ["native_observations", index],
message:
"Native observation Evidence must be distinct from managed boundary Evidence",
});
}
if (ids.has(evidence.evidence_id)) {
context.addIssue({
code: "custom",
path: ["native_observations", index],
message: "Native observation Evidence IDs must be unique",
});
}
ids.add(evidence.evidence_id);
}
});
const nativeSymbolSchema = z.strictObject({
evidence_id: evidenceIdSchema,
operation: z.string().min(1),
name: z.string().min(1),
address: z.string().nullable(),
module_name: z.string().nullable(),
module_path: z.string().nullable(),
source: z.enum(["macho-export", "function-dossier"]),
});
const matchStatusSchema = z.enum([
"verified",
"inferred",
"unresolved",
"contradicted",
]);
const matchBasisSchema = z.enum([
"exact-export-name",
"exact-function-name",
"decorated-name-candidate",
"module-mismatch",
"no-native-candidate",
"unsupported-native-evidence",
]);
const pinvokeVerificationSchema = z.strictObject({
item_id: z.string().regex(/^mnv_pinvoke_[a-f0-9]{64}$/u),
managed: z.strictObject({
token: tokenSchema,
member_token: tokenSchema.nullable(),
member_name: z.string().nullable(),
import_name: z.string().min(1),
import_scope_name: z.string().nullable(),
no_mangle: z.boolean(),
char_set: z.enum(["not-specified", "ansi", "unicode", "auto", "unknown"]),
call_convention: z.enum([
"not-specified",
"winapi",
"cdecl",
"stdcall",
"thiscall",
"fastcall",
"unknown",
]),
declaration_verification: z.literal("managed-declaration-only"),
}),
status: matchStatusSchema,
basis: matchBasisSchema,
confidence: z.enum(["observed", "inferred", "unknown"]),
matched_native: nativeSymbolSchema.nullable(),
candidates: z.array(nativeSymbolSchema).max(100),
evidence_links: z.array(evidenceIdSchema).min(1).max(51),
limitations: z.array(boundedTextSchema).max(100),
});
/** Provider-neutral managed/native verification result. */
export const managedNativeVerificationResultSchema = z.strictObject({
schema_version: z.literal(1),
verification_id: z.string().regex(/^mnv_[a-f0-9]{64}$/u),
algorithm: z.strictObject({
name: z.literal("rea-managed-native-verification"),
version: z.literal(1),
token_identity: z.literal("build-local"),
token_to_address_mapping: z.literal("not-inferred"),
}),
managed_boundary: z.strictObject({
evidence_id: evidenceIdSchema,
artifact_sha256: digestSchema,
artifact_path: z.string().min(1),
mvid: cliMetadataGuidSchema.nullable(),
metadata_status: z.enum(["absent", "complete", "partial", "malformed"]),
pinvoke_imports_total: z.number().int().min(0),
native_implementations_total: z.number().int().min(0),
coverage_state: z.enum(["complete", "partial", "unavailable"]),
}),
native_observations: z.strictObject({
total: z.number().int().min(1),
accepted: z.number().int().min(0),
unsupported: z.number().int().min(0),
symbols: z.number().int().min(0),
truncated: z.boolean(),
}),
summary: z.strictObject({
verified: z.number().int().min(0),
inferred: z.number().int().min(0),
unresolved: z.number().int().min(0),
contradicted: z.number().int().min(0),
native_body_unresolved: z.number().int().min(0),
}),
pinvoke_imports: z.array(pinvokeVerificationSchema).max(50_000),
native_implementations: z.strictObject({
unresolved: z.number().int().min(0),
reason: boundedTextSchema,
}),
coverage: z.strictObject({
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
omitted_native_observations: z.number().int().min(0),
omitted_candidates: z.number().int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(2).max(51),
limitations: z.array(boundedTextSchema).max(1_000),
});
export type ManagedNativeVerificationInput = z.infer<
typeof managedNativeVerificationInputSchema
>;
export type ManagedNativeVerificationResult = z.infer<
typeof managedNativeVerificationResultSchema
>;
type NativeSymbol = z.infer<typeof nativeSymbolSchema>;
type PinvokeImport =
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
type PinvokeVerification = z.infer<typeof pinvokeVerificationSchema>;
type VerifiedPinvoke = {
readonly verification: PinvokeVerification;
readonly omittedCandidates: number;
};
export {
managedNativeVerificationInputSchema,
type ManagedNativeVerificationInput,
type ManagedNativeVerificationResult,
} from "./managedNativeVerificationSchemas.js";
export { managedNativeVerificationResultSchema };
const sha256 = (value: JsonValue): string => {
const serialized = canonicalize(value);
@@ -184,346 +33,50 @@ const sha256 = (value: JsonValue): string => {
return createHash("sha256").update(serialized).digest("hex");
};
/** Verify managed P/Invoke declarations against authenticated native Evidence. */
export const verifyManagedNativeBoundaries = (
const managedBoundaryFromInput = (
input: ManagedNativeVerificationInput,
): ManagedNativeVerificationResult => {
): ManagedNativeBoundaryInspection => {
const managedEvidence = parseEvidence(input.managed_boundaries);
if (managedEvidence.operation !== "inspect_managed_native_boundaries")
throw new TypeError(
"Evidence operation is not inspect_managed_native_boundaries",
);
const managed = managedNativeBoundaryInspectionSchema.parse(
return managedNativeBoundaryInspectionSchema.parse(
managedEvidence.normalized_result,
);
};
/** Verify managed P/Invoke declarations against authenticated native Evidence. */
export const verifyManagedNativeBoundaries = (
input: ManagedNativeVerificationInput,
): ManagedNativeVerificationResult => {
const managed = managedBoundaryFromInput(input);
const managedEvidence = parseEvidence(input.managed_boundaries);
const native = collectNativeSymbols(
input.native_observations.slice(0, input.limits.max_native_observations),
);
const verifiedPinvokes = managed.pinvoke_imports.items.map((item) =>
verifyPinvoke(
verifyPinvoke({
item,
managedEvidence.evidence_id,
native.symbols,
native.accepted > 0,
managedEvidenceId: managedEvidence.evidence_id,
symbols: native.symbols,
hasSupportedNativeEvidence: native.accepted > 0,
input,
),
}),
);
const pinvokeImports = verifiedPinvokes.map(
({ verification }) => verification,
);
const counts = countStatuses(pinvokeImports);
const omittedNativeObservations = Math.max(
0,
input.native_observations.length - input.limits.max_native_observations,
);
const omittedCandidates = verifiedPinvokes.reduce(
(sum, item) => sum + item.omittedCandidates,
0,
);
const coverageStatus =
omittedNativeObservations > 0 || omittedCandidates > 0
? "truncated"
: managed.coverage.state === "complete" && native.unsupported === 0
? "complete-within-inputs"
: "partial";
const withoutId = {
schema_version: 1 as const,
algorithm: {
name: "rea-managed-native-verification" as const,
version: 1 as const,
token_identity: "build-local" as const,
token_to_address_mapping: "not-inferred" as const,
},
managed_boundary: {
evidence_id: managedEvidence.evidence_id,
artifact_sha256: managed.artifact.sha256,
artifact_path: managed.artifact.path,
mvid: managed.module?.mvid ?? null,
metadata_status: managed.metadata.status,
pinvoke_imports_total: managed.pinvoke_imports.total,
native_implementations_total: managed.native_implementations.total,
coverage_state: managed.coverage.state,
},
native_observations: {
total: input.native_observations.length,
accepted: native.accepted,
unsupported: native.unsupported,
symbols: native.symbols.length,
truncated: omittedNativeObservations > 0,
},
summary: {
...counts,
native_body_unresolved: managed.native_implementations.items.filter(
({ boundary_kind: kind }) => kind !== "pinvoke",
).length,
},
pinvoke_imports: pinvokeImports,
native_implementations: {
unresolved: managed.native_implementations.items.filter(
({ boundary_kind: kind }) => kind !== "pinvoke",
).length,
reason:
"Managed metadata tokens and RVAs are not translated to native provider addresses by this workflow; C++/CLI, ReadyToRun, and native-body mappings require explicit provider-supported bridge evidence.",
},
coverage: {
status: coverageStatus,
omitted_native_observations: omittedNativeObservations,
omitted_candidates: omittedCandidates,
},
evidence_links: [
managedEvidence.evidence_id,
...input.native_observations
.slice(0, input.limits.max_native_observations)
.map(({ evidence_id: id }) => id),
],
limitations: [
"P/Invoke verification checks declared import names against supplied native export or function-name Evidence only.",
"A matching native symbol verifies that a candidate export/function was observed; it does not prove CLR binding, marshaling behavior, call reachability, or runtime loading.",
"Missing supplied native symbols are reported as unresolved, not proof that a dependency cannot exist.",
"Managed metadata tokens and method RVAs are not interpreted as native addresses.",
...(managed.coverage.state === "complete"
? []
: ["Managed boundary input is partial or unavailable."]),
...(native.unsupported === 0
? []
: [
"Some native Evidence operations were unsupported by this workflow.",
]),
],
};
const withoutId = buildVerificationResult({
managedEvidence,
managed,
native,
pinvokeImports,
verifiedPinvokes,
input,
});
return managedNativeVerificationResultSchema.parse({
...withoutId,
verification_id: `mnv_${sha256(withoutId)}`,
});
};
const collectNativeSymbols = (
observations: readonly ManagedNativeVerificationInput["native_observations"][number][],
): {
readonly symbols: readonly NativeSymbol[];
readonly accepted: number;
readonly unsupported: number;
} => {
const symbols: NativeSymbol[] = [];
let accepted = 0;
let unsupported = 0;
for (const raw of observations) {
const evidence = parseEvidence(raw);
const extracted = symbolsForEvidence(evidence);
if (!extracted.supported) {
unsupported += 1;
} else {
accepted += 1;
symbols.push(...extracted.symbols);
}
}
return { symbols, accepted, unsupported };
};
const symbolsForEvidence = (
evidence: ReturnType<typeof parseEvidence>,
): {
readonly supported: boolean;
readonly symbols: readonly NativeSymbol[];
} => {
if (evidence.operation === "inspect_macho") {
const result = inspectMachoSchema.parse(evidence.normalized_result);
return {
supported: true,
symbols: result.exports.items.map((symbol) => ({
evidence_id: evidence.evidence_id,
operation: evidence.operation,
name: symbol.name,
address: symbol.address,
module_name: moduleName(evidence),
module_path: modulePath(evidence),
source: "macho-export" as const,
})),
};
}
if (evidence.operation === "analyze_function") {
const dossier = functionDossierSchema.parse(evidence.normalized_result);
return {
supported: true,
symbols: [
{
evidence_id: evidence.evidence_id,
operation: evidence.operation,
name: dossier.procedure.name,
address: dossier.procedure.address,
module_name: moduleName(evidence),
module_path: modulePath(evidence),
source: "function-dossier" as const,
},
],
};
}
return { supported: false, symbols: [] };
};
const verifyPinvoke = (
item: PinvokeImport,
managedEvidenceId: string,
symbols: readonly NativeSymbol[],
hasSupportedNativeEvidence: boolean,
input: ManagedNativeVerificationInput,
): VerifiedPinvoke => {
const managed = {
token: item.token,
member_token: item.member_token,
member_name: item.member_name,
import_name: item.import_name,
import_scope_name: item.import_scope_name,
no_mangle: item.no_mangle,
char_set: item.char_set,
call_convention: item.call_convention,
declaration_verification: item.verification,
};
const names = candidateNames(managed);
const allCandidates = symbols.filter((symbol) =>
names.some((name) => sameSymbolName(name, symbol.name)),
);
const candidates = allCandidates.slice(
0,
input.limits.max_candidates_per_import,
);
const exact = candidates.find(
(symbol) =>
sameSymbolName(item.import_name, symbol.name) &&
moduleCompatible(item.import_scope_name, symbol),
);
const decorated = candidates.find((symbol) =>
moduleCompatible(item.import_scope_name, symbol),
);
const selected = exact ?? decorated ?? candidates[0] ?? null;
const basis =
selected === null
? hasSupportedNativeEvidence
? "no-native-candidate"
: "unsupported-native-evidence"
: !moduleCompatible(item.import_scope_name, selected)
? "module-mismatch"
: exact !== undefined
? selected.source === "macho-export"
? "exact-export-name"
: "exact-function-name"
: "decorated-name-candidate";
const status =
selected === null
? "unresolved"
: basis === "module-mismatch"
? "contradicted"
: basis === "decorated-name-candidate"
? "inferred"
: "verified";
const limitations = [
...(item.import_scope_name === null
? ["Managed declaration does not name an import scope/module."]
: []),
...(status === "inferred"
? [
"Native symbol matched a decorated candidate rather than the exact declared import name.",
]
: []),
...(status === "contradicted"
? [
"A symbol name candidate exists, but the supplied native Evidence module identity does not match the managed import scope.",
]
: []),
...(status === "unresolved"
? [
"No supplied native export/function Evidence matched this declared import.",
]
: []),
];
return {
verification: pinvokeVerificationSchema.parse({
item_id: `mnv_pinvoke_${sha256({
managedEvidenceId,
token: item.token,
importName: item.import_name,
importScope: item.import_scope_name,
candidates,
})}`,
managed,
status,
basis,
confidence:
status === "verified"
? "observed"
: status === "inferred"
? "inferred"
: "unknown",
matched_native: selected,
candidates,
evidence_links: [
managedEvidenceId,
...new Set(candidates.map(({ evidence_id: id }) => id)),
],
limitations,
}),
omittedCandidates: Math.max(0, allCandidates.length - candidates.length),
};
};
const candidateNames = (
managed: Pick<
PinvokeImport,
"import_name" | "char_set" | "call_convention" | "no_mangle"
>,
): readonly string[] => {
const names = new Set<string>([managed.import_name]);
if (!managed.no_mangle) {
if (managed.char_set === "unicode") names.add(`${managed.import_name}W`);
if (managed.char_set === "ansi") names.add(`${managed.import_name}A`);
if (managed.call_convention === "stdcall") {
names.add(`_${managed.import_name}`);
names.add(`_${managed.import_name}@0`);
}
}
return [...names];
};
const sameSymbolName = (left: string, right: string): boolean =>
normalizeSymbol(left) === normalizeSymbol(right);
const normalizeSymbol = (value: string): string =>
value.replace(/^_+/u, "").replace(/@\d+$/u, "").toLowerCase();
const moduleCompatible = (
scope: string | null,
symbol: NativeSymbol,
): boolean => {
if (scope === null) return true;
const expected = normalizeModule(scope);
return [symbol.module_name, symbol.module_path]
.filter((value): value is string => value !== null)
.map(normalizeModule)
.some((value) => value === expected);
};
const normalizeModule = (value: string): string =>
basename(value)
.toLowerCase()
.replace(/^lib/u, "")
.replace(/\.(dll|dylib|so|node|exe)$/u, "");
const moduleName = (
evidence: ReturnType<typeof parseEvidence>,
): string | null => evidence.subject?.name ?? null;
const modulePath = (
evidence: ReturnType<typeof parseEvidence>,
): string | null => evidence.subject?.local_path ?? null;
const countStatuses = (
items: readonly z.infer<typeof pinvokeVerificationSchema>[],
): Pick<
ManagedNativeVerificationResult["summary"],
"verified" | "inferred" | "unresolved" | "contradicted"
> => ({
verified: items.filter(({ status }) => status === "verified").length,
inferred: items.filter(({ status }) => status === "inferred").length,
unresolved: items.filter(({ status }) => status === "unresolved").length,
contradicted: items.filter(({ status }) => status === "contradicted").length,
});
@@ -0,0 +1,423 @@
import { createHash } from "node:crypto";
import { basename } from "node:path";
import canonicalize from "canonicalize";
import { parseEvidence } from "./evidence.js";
import { functionDossierSchema } from "./hopperValues.js";
import { inspectMachoSchema } from "./nativeInspection.js";
import type { ManagedNativeBoundaryInspection } from "./managedArtifact.js";
import type { JsonValue } from "./jsonValue.js";
import {
pinvokeVerificationSchema,
type ManagedNativeVerificationInput,
type ManagedNativeVerificationResult,
type NativeSymbol,
type PinvokeVerification,
} from "./managedNativeVerificationSchemas.js";
const digest = (value: JsonValue): string => {
const serialized = canonicalize(value);
if (serialized === undefined)
throw new TypeError("Managed/native verification canonicalization failed");
return createHash("sha256").update(serialized).digest("hex");
};
type PinvokeImport =
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
type VerifiedPinvoke = {
readonly verification: PinvokeVerification;
readonly omittedCandidates: number;
};
const moduleName = (
evidence: ReturnType<typeof parseEvidence>,
): string | null => evidence.subject?.name ?? null;
const modulePath = (
evidence: ReturnType<typeof parseEvidence>,
): string | null => evidence.subject?.local_path ?? null;
export const symbolsForEvidence = (
evidence: ReturnType<typeof parseEvidence>,
): {
readonly supported: boolean;
readonly symbols: readonly NativeSymbol[];
} => {
if (evidence.operation === "inspect_macho") {
const result = inspectMachoSchema.parse(evidence.normalized_result);
return {
supported: true,
symbols: result.exports.items.map((symbol) => ({
evidence_id: evidence.evidence_id,
operation: evidence.operation,
name: symbol.name,
address: symbol.address,
module_name: moduleName(evidence),
module_path: modulePath(evidence),
source: "macho-export" as const,
})),
};
}
if (evidence.operation === "analyze_function") {
const dossier = functionDossierSchema.parse(evidence.normalized_result);
return {
supported: true,
symbols: [
{
evidence_id: evidence.evidence_id,
operation: evidence.operation,
name: dossier.procedure.name,
address: dossier.procedure.address,
module_name: moduleName(evidence),
module_path: modulePath(evidence),
source: "function-dossier" as const,
},
],
};
}
return { supported: false, symbols: [] };
};
export const collectNativeSymbols = (
observations: readonly ManagedNativeVerificationInput["native_observations"][number][],
): {
readonly symbols: readonly NativeSymbol[];
readonly accepted: number;
readonly unsupported: number;
} => {
const symbols: NativeSymbol[] = [];
let accepted = 0;
let unsupported = 0;
for (const raw of observations) {
const evidence = parseEvidence(raw);
const extracted = symbolsForEvidence(evidence);
if (!extracted.supported) {
unsupported += 1;
} else {
accepted += 1;
symbols.push(...extracted.symbols);
}
}
return { symbols, accepted, unsupported };
};
const normalizeSymbol = (value: string): string =>
value.replace(/^_+/u, "").replace(/@\d+$/u, "").toLowerCase();
const sameSymbolName = (left: string, right: string): boolean =>
normalizeSymbol(left) === normalizeSymbol(right);
const normalizeModule = (value: string): string =>
basename(value)
.toLowerCase()
.replace(/^lib/u, "")
.replace(/\.(dll|dylib|so|node|exe)$/u, "");
const moduleCompatible = (
scope: string | null,
symbol: NativeSymbol,
): boolean => {
if (scope === null) return true;
const expected = normalizeModule(scope);
return [symbol.module_name, symbol.module_path]
.filter((value): value is string => value !== null)
.map(normalizeModule)
.some((value) => value === expected);
};
const candidateNames = (
managed: Pick<
PinvokeImport,
"import_name" | "char_set" | "call_convention" | "no_mangle"
>,
): readonly string[] => {
const names = new Set<string>([managed.import_name]);
if (!managed.no_mangle) {
if (managed.char_set === "unicode") names.add(`${managed.import_name}W`);
if (managed.char_set === "ansi") names.add(`${managed.import_name}A`);
if (managed.call_convention === "stdcall") {
names.add(`_${managed.import_name}`);
names.add(`_${managed.import_name}@0`);
}
}
return [...names];
};
interface CandidateSelection {
readonly allCandidates: readonly NativeSymbol[];
readonly candidates: readonly NativeSymbol[];
readonly selected: NativeSymbol | null;
readonly exact: NativeSymbol | undefined;
readonly decorated: NativeSymbol | undefined;
}
const selectCandidates = (
managed: PinvokeImport,
symbols: readonly NativeSymbol[],
maxCandidates: number,
): CandidateSelection => {
const names = candidateNames(managed);
const allCandidates = symbols.filter((symbol) =>
names.some((name) => sameSymbolName(name, symbol.name)),
);
const candidates = allCandidates.slice(0, maxCandidates);
const exact = candidates.find(
(symbol) =>
sameSymbolName(managed.import_name, symbol.name) &&
moduleCompatible(managed.import_scope_name, symbol),
);
const decorated = candidates.find((symbol) =>
moduleCompatible(managed.import_scope_name, symbol),
);
const selected = exact ?? decorated ?? candidates[0] ?? null;
return { allCandidates, candidates, selected, exact, decorated };
};
interface BasisAndStatus {
readonly basis: PinvokeVerification["basis"];
readonly status: PinvokeVerification["status"];
readonly confidence: PinvokeVerification["confidence"];
}
const basisAndStatus = (
managed: PinvokeImport,
selection: CandidateSelection,
hasSupportedNativeEvidence: boolean,
): BasisAndStatus => {
const { selected, exact } = selection;
if (selected === null)
return {
basis: hasSupportedNativeEvidence
? "no-native-candidate"
: "unsupported-native-evidence",
status: "unresolved",
confidence: "unknown",
};
if (!moduleCompatible(managed.import_scope_name, selected))
return {
basis: "module-mismatch",
status: "contradicted",
confidence: "unknown",
};
if (exact !== undefined)
return {
basis:
selected.source === "macho-export"
? "exact-export-name"
: "exact-function-name",
status: "verified",
confidence: "observed",
};
return {
basis: "decorated-name-candidate",
status: "inferred",
confidence: "inferred",
};
};
const pinvokeLimitations = (
status: PinvokeVerification["status"],
managed: PinvokeImport,
): readonly string[] => [
...(managed.import_scope_name === null
? ["Managed declaration does not name an import scope/module."]
: []),
...(status === "inferred"
? [
"Native symbol matched a decorated candidate rather than the exact declared import name.",
]
: []),
...(status === "contradicted"
? [
"A symbol name candidate exists, but the supplied native Evidence module identity does not match the managed import scope.",
]
: []),
...(status === "unresolved"
? [
"No supplied native export/function Evidence matched this declared import.",
]
: []),
];
interface VerifyPinvokeContext {
readonly item: PinvokeImport;
readonly managedEvidenceId: string;
readonly symbols: readonly NativeSymbol[];
readonly hasSupportedNativeEvidence: boolean;
readonly input: ManagedNativeVerificationInput;
}
export const verifyPinvoke = ({
item,
managedEvidenceId,
symbols,
hasSupportedNativeEvidence,
input,
}: VerifyPinvokeContext): VerifiedPinvoke => {
const managed = {
token: item.token,
member_token: item.member_token,
member_name: item.member_name,
import_name: item.import_name,
import_scope_name: item.import_scope_name,
no_mangle: item.no_mangle,
char_set: item.char_set,
call_convention: item.call_convention,
declaration_verification: item.verification,
};
const selection = selectCandidates(
item,
symbols,
input.limits.max_candidates_per_import,
);
const { status, basis, confidence } = basisAndStatus(
item,
selection,
hasSupportedNativeEvidence,
);
const limitations = pinvokeLimitations(status, item);
return {
verification: pinvokeVerificationSchema.parse({
item_id: `mnv_pinvoke_${digest({
managedEvidenceId,
token: item.token,
importName: item.import_name,
importScope: item.import_scope_name,
candidates: [...selection.candidates],
})}`,
managed,
status,
basis,
confidence,
matched_native: selection.selected,
candidates: selection.candidates,
evidence_links: [
managedEvidenceId,
...new Set(selection.candidates.map(({ evidence_id: id }) => id)),
],
limitations,
}),
omittedCandidates: Math.max(
0,
selection.allCandidates.length - selection.candidates.length,
),
};
};
const countStatuses = (
items: readonly PinvokeVerification[],
): Pick<
ManagedNativeVerificationResult["summary"],
"verified" | "inferred" | "unresolved" | "contradicted"
> => ({
verified: items.filter(({ status }) => status === "verified").length,
inferred: items.filter(({ status }) => status === "inferred").length,
unresolved: items.filter(({ status }) => status === "unresolved").length,
contradicted: items.filter(({ status }) => status === "contradicted").length,
});
interface VerificationResultInput {
readonly managedEvidence: ReturnType<typeof parseEvidence>;
readonly managed: ManagedNativeBoundaryInspection;
readonly native: {
readonly symbols: readonly NativeSymbol[];
readonly accepted: number;
readonly unsupported: number;
};
readonly pinvokeImports: readonly PinvokeVerification[];
readonly verifiedPinvokes: readonly VerifiedPinvoke[];
readonly input: ManagedNativeVerificationInput;
}
export const buildVerificationResult = ({
managedEvidence,
managed,
native,
pinvokeImports,
verifiedPinvokes,
input,
}: VerificationResultInput): Omit<
ManagedNativeVerificationResult,
"verification_id"
> => {
const counts = countStatuses(pinvokeImports);
const omittedNativeObservations = Math.max(
0,
input.native_observations.length - input.limits.max_native_observations,
);
const omittedCandidates = verifiedPinvokes.reduce(
(sum, item) => sum + item.omittedCandidates,
0,
);
const nativeBodyUnresolved = managed.native_implementations.items.filter(
({ boundary_kind: kind }) => kind !== "pinvoke",
).length;
const coverageStatus =
omittedNativeObservations > 0 || omittedCandidates > 0
? "truncated"
: managed.coverage.state === "complete" && native.unsupported === 0
? "complete-within-inputs"
: "partial";
return {
schema_version: 1 as const,
algorithm: {
name: "rea-managed-native-verification" as const,
version: 1 as const,
token_identity: "build-local" as const,
token_to_address_mapping: "not-inferred" as const,
},
managed_boundary: {
evidence_id: managedEvidence.evidence_id,
artifact_sha256: managed.artifact.sha256,
artifact_path: managed.artifact.path,
mvid: managed.module?.mvid ?? null,
metadata_status: managed.metadata.status,
pinvoke_imports_total: managed.pinvoke_imports.total,
native_implementations_total: managed.native_implementations.total,
coverage_state: managed.coverage.state,
},
native_observations: {
total: input.native_observations.length,
accepted: native.accepted,
unsupported: native.unsupported,
symbols: native.symbols.length,
truncated: omittedNativeObservations > 0,
},
summary: {
...counts,
native_body_unresolved: nativeBodyUnresolved,
},
pinvoke_imports: [...pinvokeImports],
native_implementations: {
unresolved: nativeBodyUnresolved,
reason:
"Managed metadata tokens and RVAs are not translated to native provider addresses by this workflow; C++/CLI, ReadyToRun, and native-body mappings require explicit provider-supported bridge evidence.",
},
coverage: {
status: coverageStatus,
omitted_native_observations: omittedNativeObservations,
omitted_candidates: omittedCandidates,
},
evidence_links: [
managedEvidence.evidence_id,
...input.native_observations
.slice(0, input.limits.max_native_observations)
.map(({ evidence_id: id }) => id),
],
limitations: [
"P/Invoke verification checks declared import names against supplied native export or function-name Evidence only.",
"A matching native symbol verifies that a candidate export/function was observed; it does not prove CLR binding, marshaling behavior, call reachability, or runtime loading.",
"Missing supplied native symbols are reported as unresolved, not proof that a dependency cannot exist.",
"Managed metadata tokens and method RVAs are not interpreted as native addresses.",
...(managed.coverage.state === "complete"
? []
: ["Managed boundary input is partial or unavailable."]),
...(native.unsupported === 0
? []
: [
"Some native Evidence operations were unsupported by this workflow.",
]),
],
};
};
@@ -0,0 +1,163 @@
import { z } from "zod";
import { evidenceSchema } from "./evidence.js";
import { cliMetadataGuidSchema } from "./managedArtifact.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
const boundedTextSchema = z.string().min(1).max(4_096);
const matchStatusSchema = z.enum([
"verified",
"inferred",
"unresolved",
"contradicted",
]);
const matchBasisSchema = z.enum([
"exact-export-name",
"exact-function-name",
"decorated-name-candidate",
"module-mismatch",
"no-native-candidate",
"unsupported-native-evidence",
]);
export const nativeVerificationLimitsSchema = z.strictObject({
max_native_observations: z.number().int().min(1).max(50).default(20),
max_candidates_per_import: z.number().int().min(1).max(100).default(25),
});
export const nativeSymbolSchema = z.strictObject({
evidence_id: evidenceIdSchema,
operation: z.string().min(1),
name: z.string().min(1),
address: z.string().nullable(),
module_name: z.string().nullable(),
module_path: z.string().nullable(),
source: z.enum(["macho-export", "function-dossier"]),
});
export type NativeSymbol = z.infer<typeof nativeSymbolSchema>;
export const pinvokeVerificationSchema = z.strictObject({
item_id: z.string().regex(/^mnv_pinvoke_[a-f0-9]{64}$/u),
managed: z.strictObject({
token: tokenSchema,
member_token: tokenSchema.nullable(),
member_name: z.string().nullable(),
import_name: z.string().min(1),
import_scope_name: z.string().nullable(),
no_mangle: z.boolean(),
char_set: z.enum(["not-specified", "ansi", "unicode", "auto", "unknown"]),
call_convention: z.enum([
"not-specified",
"winapi",
"cdecl",
"stdcall",
"thiscall",
"fastcall",
"unknown",
]),
declaration_verification: z.literal("managed-declaration-only"),
}),
status: matchStatusSchema,
basis: matchBasisSchema,
confidence: z.enum(["observed", "inferred", "unknown"]),
matched_native: nativeSymbolSchema.nullable(),
candidates: z.array(nativeSymbolSchema).max(100),
evidence_links: z.array(evidenceIdSchema).min(1).max(51),
limitations: z.array(boundedTextSchema).max(100),
});
export type PinvokeVerification = z.infer<typeof pinvokeVerificationSchema>;
/** Authenticated managed boundary Evidence plus native observations. */
export const managedNativeVerificationInputSchema = z
.strictObject({
managed_boundaries: evidenceSchema,
native_observations: z.array(evidenceSchema).min(1).max(50),
limits: nativeVerificationLimitsSchema.default({
max_native_observations: 20,
max_candidates_per_import: 25,
}),
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine((input, context) => {
const ids = new Set<string>();
for (const [index, evidence] of input.native_observations.entries()) {
if (evidence.evidence_id === input.managed_boundaries.evidence_id) {
context.addIssue({
code: "custom",
path: ["native_observations", index],
message:
"Native observation Evidence must be distinct from managed boundary Evidence",
});
}
if (ids.has(evidence.evidence_id)) {
context.addIssue({
code: "custom",
path: ["native_observations", index],
message: "Native observation Evidence IDs must be unique",
});
}
ids.add(evidence.evidence_id);
}
});
export type ManagedNativeVerificationInput = z.infer<
typeof managedNativeVerificationInputSchema
>;
/** Provider-neutral managed/native verification result. */
export const managedNativeVerificationResultSchema = z.strictObject({
schema_version: z.literal(1),
verification_id: z.string().regex(/^mnv_[a-f0-9]{64}$/u),
algorithm: z.strictObject({
name: z.literal("rea-managed-native-verification"),
version: z.literal(1),
token_identity: z.literal("build-local"),
token_to_address_mapping: z.literal("not-inferred"),
}),
managed_boundary: z.strictObject({
evidence_id: evidenceIdSchema,
artifact_sha256: digestSchema,
artifact_path: z.string().min(1),
mvid: cliMetadataGuidSchema.nullable(),
metadata_status: z.enum(["absent", "complete", "partial", "malformed"]),
pinvoke_imports_total: z.number().int().min(0),
native_implementations_total: z.number().int().min(0),
coverage_state: z.enum(["complete", "partial", "unavailable"]),
}),
native_observations: z.strictObject({
total: z.number().int().min(1),
accepted: z.number().int().min(0),
unsupported: z.number().int().min(0),
symbols: z.number().int().min(0),
truncated: z.boolean(),
}),
summary: z.strictObject({
verified: z.number().int().min(0),
inferred: z.number().int().min(0),
unresolved: z.number().int().min(0),
contradicted: z.number().int().min(0),
native_body_unresolved: z.number().int().min(0),
}),
pinvoke_imports: z.array(pinvokeVerificationSchema).max(50_000),
native_implementations: z.strictObject({
unresolved: z.number().int().min(0),
reason: boundedTextSchema,
}),
coverage: z.strictObject({
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
omitted_native_observations: z.number().int().min(0),
omitted_candidates: z.number().int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(2).max(51),
limitations: z.array(boundedTextSchema).max(1_000),
});
export type ManagedNativeVerificationResult = z.infer<
typeof managedNativeVerificationResultSchema
>;
+143 -87
View File
@@ -7,6 +7,7 @@ import { evidenceSchema, parseEvidence } from "./evidence.js";
import {
cliMetadataGuidSchema,
managedMemberInspectionSchema,
type ManagedMemberInspection,
} from "./managedArtifact.js";
import type { JsonValue } from "./jsonValue.js";
@@ -39,6 +40,10 @@ const managedReconstructionMethodInputSchema = z.strictObject({
}),
});
type ManagedReconstructionMethodInput = z.infer<
typeof managedReconstructionMethodInputSchema
>;
export const managedReconstructionImportInputSchema = z.strictObject({
static_members: evidenceSchema,
decompiler: decompilerSchema,
@@ -111,6 +116,11 @@ export type ManagedReconstructionImportResult = z.infer<
typeof managedReconstructionImportResultSchema
>;
type Member = ManagedMemberInspection["methods"]["items"][number];
type ImportedMethod = ManagedReconstructionImportResult["methods"][number];
type StaticObservation =
ManagedReconstructionImportResult["static_observation"];
const sha256Text = (value: string): string =>
createHash("sha256").update(value).digest("hex");
@@ -121,6 +131,131 @@ const sha256Json = (value: JsonValue): string => {
return createHash("sha256").update(serialized).digest("hex");
};
const countLines = (text: string): number =>
text.length === 0 ? 0 : text.split(/\r\n|\r|\n/u).length;
const findObservedMethod = (
members: ManagedMemberInspection,
candidate: ManagedReconstructionMethodInput,
): Member | undefined =>
members.methods.items.find(
(item) =>
item.token === candidate.token &&
item.signature.raw_sha256 === candidate.signature_sha256 &&
item.body.status === "present" &&
item.body.normalized_il_sha256 !== null &&
candidate.normalized_il_sha256 !== null &&
item.body.normalized_il_sha256 === candidate.normalized_il_sha256,
);
const verifyTextSha256 = (
candidate: ManagedReconstructionMethodInput,
textSha256: string,
): void => {
if (
candidate.reconstruction.text_sha256 !== undefined &&
candidate.reconstruction.text_sha256 !== textSha256
)
throw new TypeError(
`Reconstruction text hash mismatch for ${candidate.token}`,
);
};
const buildImportedMethod = (
observed: Member,
candidate: ManagedReconstructionMethodInput,
textSha256: string,
): ImportedMethod => ({
token: observed.token,
declaring_type: observed.declaring_type,
name: observed.name,
signature_sha256: observed.signature.raw_sha256,
normalized_il_sha256: observed.body.normalized_il_sha256,
body_sha256: observed.body.il_sha256,
il_size: observed.body.il_size,
reconstruction: {
kind: candidate.reconstruction.kind,
language: candidate.reconstruction.language,
text: candidate.reconstruction.text,
text_sha256: textSha256,
text_length: candidate.reconstruction.text.length,
line_count: countLines(candidate.reconstruction.text),
source_path: candidate.reconstruction.source_path ?? null,
start_line: candidate.reconstruction.start_line ?? null,
end_line: candidate.reconstruction.end_line ?? null,
},
validation: {
matched_static_member: true as const,
exact_build_required: true as const,
canonical_observation: false as const,
confidence_floor: "inference" as const,
},
});
const buildReconstructionSummary = (
methods: readonly ImportedMethod[],
): ManagedReconstructionImportResult["summary"] => ({
imported_methods: methods.length,
decompiled_csharp_methods: methods.filter(
({ reconstruction }) => reconstruction.kind === "decompiled-csharp",
).length,
decompiled_il_methods: methods.filter(
({ reconstruction }) => reconstruction.kind === "decompiled-il",
).length,
pseudocode_methods: methods.filter(
({ reconstruction }) => reconstruction.kind === "semantic-pseudocode",
).length,
total_text_bytes: methods.reduce(
(total, { reconstruction }) => total + reconstruction.text_length,
0,
),
});
const buildStaticObservation = (
staticEvidence: ReturnType<typeof parseEvidence>,
members: ManagedMemberInspection,
): StaticObservation => ({
evidence_id: staticEvidence.evidence_id,
artifact_sha256: members.artifact.sha256,
artifact_path: members.artifact.path,
byte_length: members.artifact.byte_length,
module_name: members.module?.name ?? null,
mvid: members.module?.mvid ?? null,
metadata_status: members.metadata.status,
});
const buildDecompilerRecord = (
decompiler: ManagedReconstructionImportInput["decompiler"],
): ManagedReconstructionImportResult["decompiler"] => ({
...decompiler,
options_sha256: sha256Json(decompiler.options),
});
const RECONSTRUCTION_LIMITATIONS = [
"Decompiler output is imported as reconstruction evidence and analyst inference; metadata and IL observations remain canonical.",
"This operation did not execute, load, reflect over, debug, instrument, patch, or decompile the target artifact.",
"The imported reconstruction is valid only for the exact artifact SHA-256, MVID, method signature, and normalized IL shape recorded here.",
"Obfuscated names, decompiler variables, and control-flow restructuring are not treated as proof without separate validation.",
];
const buildReconstructionWithoutId = (
input: ManagedReconstructionImportInput,
staticEvidence: ReturnType<typeof parseEvidence>,
members: ManagedMemberInspection,
methods: readonly ImportedMethod[],
): Omit<ManagedReconstructionImportResult, "reconstruction_id"> => ({
schema_version: 1 as const,
phase: "reconstruction-import" as const,
executed: false as const,
static_observation: buildStaticObservation(staticEvidence, members),
decompiler: buildDecompilerRecord(input.decompiler),
summary: buildReconstructionSummary(methods),
methods: [...methods],
notes: input.notes,
evidence_links: [staticEvidence.evidence_id],
limitations: RECONSTRUCTION_LIMITATIONS,
});
/** Authenticate decompiler reconstruction against static IL Evidence. */
export const importManagedReconstruction = (
input: ManagedReconstructionImportInput,
@@ -132,102 +267,23 @@ export const importManagedReconstruction = (
staticEvidence.normalized_result,
);
const methods = input.methods.map((candidate) => {
const observed = members.methods.items.find(
(item) =>
item.token === candidate.token &&
item.signature.raw_sha256 === candidate.signature_sha256 &&
item.body.status === "present" &&
item.body.normalized_il_sha256 !== null &&
candidate.normalized_il_sha256 !== null &&
item.body.normalized_il_sha256 === candidate.normalized_il_sha256,
);
const observed = findObservedMethod(members, candidate);
if (observed === undefined)
throw new TypeError(
`Reconstruction method lock ${candidate.token} does not match a static managed member observation`,
);
const textSha256 = sha256Text(candidate.reconstruction.text);
if (
candidate.reconstruction.text_sha256 !== undefined &&
candidate.reconstruction.text_sha256 !== textSha256
)
throw new TypeError(
`Reconstruction text hash mismatch for ${candidate.token}`,
);
return {
token: observed.token,
declaring_type: observed.declaring_type,
name: observed.name,
signature_sha256: observed.signature.raw_sha256,
normalized_il_sha256: observed.body.normalized_il_sha256,
body_sha256: observed.body.il_sha256,
il_size: observed.body.il_size,
reconstruction: {
kind: candidate.reconstruction.kind,
language: candidate.reconstruction.language,
text: candidate.reconstruction.text,
text_sha256: textSha256,
text_length: candidate.reconstruction.text.length,
line_count: countLines(candidate.reconstruction.text),
source_path: candidate.reconstruction.source_path ?? null,
start_line: candidate.reconstruction.start_line ?? null,
end_line: candidate.reconstruction.end_line ?? null,
},
validation: {
matched_static_member: true as const,
exact_build_required: true as const,
canonical_observation: false as const,
confidence_floor: "inference" as const,
},
};
verifyTextSha256(candidate, textSha256);
return buildImportedMethod(observed, candidate, textSha256);
});
const withoutId = {
schema_version: 1 as const,
phase: "reconstruction-import" as const,
executed: false as const,
static_observation: {
evidence_id: staticEvidence.evidence_id,
artifact_sha256: members.artifact.sha256,
artifact_path: members.artifact.path,
byte_length: members.artifact.byte_length,
module_name: members.module?.name ?? null,
mvid: members.module?.mvid ?? null,
metadata_status: members.metadata.status,
},
decompiler: {
...input.decompiler,
options_sha256: sha256Json(input.decompiler.options),
},
summary: {
imported_methods: methods.length,
decompiled_csharp_methods: methods.filter(
({ reconstruction }) => reconstruction.kind === "decompiled-csharp",
).length,
decompiled_il_methods: methods.filter(
({ reconstruction }) => reconstruction.kind === "decompiled-il",
).length,
pseudocode_methods: methods.filter(
({ reconstruction }) => reconstruction.kind === "semantic-pseudocode",
).length,
total_text_bytes: methods.reduce(
(total, { reconstruction }) => total + reconstruction.text_length,
0,
),
},
const withoutId = buildReconstructionWithoutId(
input,
staticEvidence,
members,
methods,
notes: input.notes,
evidence_links: [staticEvidence.evidence_id],
limitations: [
"Decompiler output is imported as reconstruction evidence and analyst inference; metadata and IL observations remain canonical.",
"This operation did not execute, load, reflect over, debug, instrument, patch, or decompile the target artifact.",
"The imported reconstruction is valid only for the exact artifact SHA-256, MVID, method signature, and normalized IL shape recorded here.",
"Obfuscated names, decompiler variables, and control-flow restructuring are not treated as proof without separate validation.",
],
};
);
return managedReconstructionImportResultSchema.parse({
...withoutId,
reconstruction_id: `mre_${sha256Json(withoutId)}`,
});
};
const countLines = (text: string): number =>
text.length === 0 ? 0 : text.split(/\r\n|\r|\n/u).length;
+80 -36
View File
@@ -161,16 +161,21 @@ const unavailableResult = (
});
};
const runtimeClassification = (
layout: ManagedPeLayout,
metadata: ManagedMetadataLayout,
interface RuntimeClassificationInput {
readonly layout: ManagedPeLayout;
readonly metadata: ManagedMetadataLayout;
readonly inventory: ManagedMetadataInventory;
}
const runtimeFamilyFor = (
cli: ManagedCliHeader,
inventory: ManagedMetadataInventory,
metadata: ManagedMetadataLayout,
layout: ManagedPeLayout,
): Pick<
ManagedArtifactInspection["classification"],
"runtime_family" | "implementation" | "managed_architecture" | "evidence"
"runtime_family" | "evidence"
> => {
const cli = layout.cli;
if (cli === null) throw new TypeError("Managed classification requires CLI");
const evidence: ManagedArtifactInspection["classification"]["evidence"] = [
{
code: "cli-header-observed",
@@ -232,22 +237,47 @@ const runtimeClassification = (
: (metadata.table(12)?.offset ?? null),
});
}
const implementation =
(cli.flags & 1) === 0
? ("cpp-cli-mixed" as const)
: cli.readyToRunSignature
? ("cil-and-ready-to-run" as const)
: (metadata.rowCounts[6] ?? 0) === 0
? ("metadata-only" as const)
: ("cil" as const);
const managedArchitecture =
layout.architecture === "x86" && (cli.flags & 1) !== 0
? (cli.flags & 0x0000_0002) !== 0
? "x86"
: (cli.flags & 0x0002_0000) !== 0
? "anycpu-prefer-32"
: "anycpu"
: layout.architecture;
return { runtime_family: runtimeFamily, evidence };
};
const implementationFor = (
cli: ManagedCliHeader,
metadata: ManagedMetadataLayout,
): ManagedArtifactInspection["classification"]["implementation"] => {
if ((cli.flags & 1) === 0) return "cpp-cli-mixed";
if (cli.readyToRunSignature) return "cil-and-ready-to-run";
return (metadata.rowCounts[6] ?? 0) === 0 ? "metadata-only" : "cil";
};
const managedArchitectureFor = (
layout: ManagedPeLayout,
cli: ManagedCliHeader,
): ManagedArtifactInspection["classification"]["managed_architecture"] => {
if (layout.architecture !== "x86" || (cli.flags & 1) === 0)
return layout.architecture;
if ((cli.flags & 0x0000_0002) !== 0) return "x86";
if ((cli.flags & 0x0002_0000) !== 0) return "anycpu-prefer-32";
return "anycpu";
};
const runtimeClassification = ({
layout,
metadata,
inventory,
}: RuntimeClassificationInput): Pick<
ManagedArtifactInspection["classification"],
"runtime_family" | "implementation" | "managed_architecture" | "evidence"
> => {
const cli = layout.cli;
if (cli === null) throw new TypeError("Managed classification requires CLI");
const { runtime_family: runtimeFamily, evidence } = runtimeFamilyFor(
cli,
inventory,
metadata,
layout,
);
const implementation = implementationFor(cli, metadata);
const managedArchitecture = managedArchitectureFor(layout, cli);
evidence.push({
code: "managed-architecture-derived",
detail: `Managed architecture ${managedArchitecture} is derived from PE machine and CLI flags.`,
@@ -261,13 +291,21 @@ const runtimeClassification = (
};
};
const partialMetadataResult = (
target: BinaryTarget,
bytes: Buffer,
layout: ManagedPeLayout,
limits: ManagedInspectionLimits,
issue: ManagedParseIssue,
): ManagedArtifactInspection => ({
interface PartialMetadataContext {
readonly target: BinaryTarget;
readonly bytes: Buffer;
readonly layout: ManagedPeLayout;
readonly limits: ManagedInspectionLimits;
readonly issue: ManagedParseIssue;
}
const partialMetadataResult = ({
target,
bytes,
layout,
limits,
issue,
}: PartialMetadataContext): ManagedArtifactInspection => ({
...base(target, bytes, layout),
classification: {
status: "malformed",
@@ -338,11 +376,17 @@ export const inspectManagedArtifactBytes = (
const cli = layout.cli;
if (cli.metadata.size > limits.maxMetadataBytes)
return managedArtifactInspectionSchema.parse(
partialMetadataResult(target, bytes, layout, limits, {
code: "limit-exceeded",
scope: "cli.metadata",
offset: cli.headerOffset + 8,
detail: `CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
partialMetadataResult({
target,
bytes,
layout,
limits,
issue: {
code: "limit-exceeded",
scope: "cli.metadata",
offset: cli.headerOffset + 8,
detail: `CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
},
}),
);
let metadata: ManagedMetadataLayout;
@@ -361,7 +405,7 @@ export const inspectManagedArtifactBytes = (
} catch (cause: unknown) {
if (!(cause instanceof ManagedReaderFailure)) throw cause;
return managedArtifactInspectionSchema.parse(
partialMetadataResult(target, bytes, layout, limits, cause.issue),
partialMetadataResult({ target, bytes, layout, limits, issue: cause.issue }),
);
}
const resourceIssues: ManagedParseIssue[] = [];
@@ -395,7 +439,7 @@ export const inspectManagedArtifactBytes = (
classification: {
status: "managed",
container: "pe",
...runtimeClassification(layout, metadata, inventory),
...runtimeClassification({ layout, metadata, inventory }),
},
metadata: {
status: issues.length === 0 ? "complete" : "partial",
+133 -538
View File
@@ -4,23 +4,18 @@ import type {
} from "../domain/managedArtifact.js";
import {
METADATA_TABLE_NAMES,
metadataRowOffset,
type ManagedMetadataLayout,
type MetadataTableLayout,
} from "./ManagedMetadataLayout.js";
import {
MetadataRowCursor,
metadataToken,
readMetadataBlob,
readMetadataGuid,
readMetadataString,
sha256Bytes,
strongNameToken,
} from "./ManagedMetadataHeaps.js";
import {
ManagedReaderFailure,
managedFailure,
} from "./ManagedReaderFailure.js";
readAssembly,
readAssemblyReference,
readCustomAttribute,
readModule,
readResource,
} from "./ManagedMetadataInventoryRows.js";
import { metadataToken } from "./ManagedMetadataHeaps.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
type ModuleIdentity = NonNullable<ManagedArtifactInspection["module"]>;
type AssemblyIdentity = NonNullable<ManagedArtifactInspection["assembly"]>;
@@ -64,469 +59,6 @@ export interface ManagedMetadataInventory {
readonly issues: readonly ManagedParseIssue[];
}
const rowCursor = (
bytes: Buffer,
layout: ManagedMetadataLayout,
table: number,
row: number,
): MetadataRowCursor => {
const descriptor = layout.table(table);
const start = metadataRowOffset(layout, table, row);
if (descriptor === undefined)
throw managedFailure(
"invalid-row",
`metadata.${String(table)}`,
"Metadata table is absent",
start,
);
return new MetadataRowCursor(
bytes,
start,
start + descriptor.rowSize,
`metadata.${descriptor.name}`,
);
};
const publicKeyIdentity = (
bytes: Buffer,
kind: "public-key" | "public-key-token",
): AssemblyIdentity["public_key"] => ({
kind: bytes.length === 0 ? "none" : kind,
byte_length: bytes.length,
sha256: bytes.length === 0 ? null : sha256Bytes(bytes),
token:
bytes.length === 0
? null
: kind === "public-key"
? strongNameToken(bytes)
: bytes.length === 8
? bytes.toString("hex")
: null,
});
const readModule = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): ModuleIdentity | null => {
const table = layout.table(0);
if (table === undefined || table.rowCount === 0) return null;
const cursor = rowCursor(bytes, layout, 0, 1);
const generation = cursor.readUInt16();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const mvid = readMetadataGuid(
bytes,
layout,
cursor.readIndex(layout.guidIndexSize),
);
const encId = readMetadataGuid(
bytes,
layout,
cursor.readIndex(layout.guidIndexSize),
);
const encBaseId = readMetadataGuid(
bytes,
layout,
cursor.readIndex(layout.guidIndexSize),
);
return {
name,
generation,
mvid,
enc_id: encId,
enc_base_id: encBaseId,
token: metadataToken(0, 1),
row_offset: table.offset,
};
};
const readAssembly = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): AssemblyIdentity | null => {
const table = layout.table(32);
if (table === undefined || table.rowCount === 0) return null;
const cursor = rowCursor(bytes, layout, 32, 1);
const hashAlgorithm = cursor.readUInt32();
const version = [
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
].join(".");
const flags = cursor.readUInt32();
const key = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const culture = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
return {
name,
version,
culture: culture.length === 0 ? null : culture,
flags,
hash_algorithm: hashAlgorithm,
public_key: publicKeyIdentity(
key,
(flags & 1) === 0 ? "public-key-token" : "public-key",
),
token: metadataToken(32, 1),
row_offset: table.offset,
};
};
const readAssemblyReference = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
): AssemblyReference => {
const cursor = rowCursor(bytes, layout, 35, row);
const version = [
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
].join(".");
const flags = cursor.readUInt32();
const keyOrToken = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const culture = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const hashValue = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
return {
name,
version,
culture: culture.length === 0 ? null : culture,
flags,
public_key_or_token: publicKeyIdentity(
keyOrToken,
(flags & 1) === 0 ? "public-key-token" : "public-key",
),
hash_value_sha256: hashValue.length === 0 ? null : sha256Bytes(hashValue),
hash_value_length: hashValue.length,
token: metadataToken(35, row),
row_offset: metadataRowOffset(layout, 35, row),
};
};
const codedToken = (
raw: number,
bits: number,
tables: readonly (number | undefined)[],
): string | null => {
if (raw === 0) return null;
const tag = raw & (2 ** bits - 1);
const row = Math.floor(raw / 2 ** bits);
const table = tables[tag];
return table === undefined || row === 0 ? null : metadataToken(table, row);
};
const readTypeName = (
bytes: Buffer,
layout: ManagedMetadataLayout,
table: 1 | 2,
row: number,
maxBytes: number,
): string | null => {
const descriptor = layout.table(table);
if (descriptor === undefined || row < 1 || row > descriptor.rowCount)
return null;
const cursor = rowCursor(bytes, layout, table, row);
if (table === 1) cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
else cursor.readUInt32();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const namespace = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
return namespace.length === 0 ? name : `${namespace}.${name}`;
};
const declaringTypeForMethod = (
bytes: Buffer,
layout: ManagedMetadataLayout,
methodRow: number,
maxBytes: number,
): string | null => {
const types = layout.table(2);
const methods = layout.table(6);
if (types === undefined || methods === undefined) return null;
for (let row = 1; row <= types.rowCount; row += 1) {
const cursor = rowCursor(bytes, layout, 2, row);
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
cursor.readIndex(layout.tableIndexSize(4));
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
const methodEnd =
row === types.rowCount
? methods.rowCount + 1
: methodListForType(bytes, layout, row + 1);
if (methodRow < methodStart || methodRow >= methodEnd) continue;
const name = readMetadataString(bytes, layout, nameIndex, maxBytes);
const namespace = readMetadataString(
bytes,
layout,
namespaceIndex,
maxBytes,
);
return namespace.length === 0 ? name : `${namespace}.${name}`;
}
return null;
};
const methodListForType = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
): number => {
const cursor = rowCursor(bytes, layout, 2, row);
cursor.readUInt32();
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
cursor.readIndex(layout.tableIndexSize(4));
return cursor.readIndex(layout.tableIndexSize(6));
};
const attributeTypeName = (
bytes: Buffer,
layout: ManagedMetadataLayout,
rawType: number,
maxBytes: number,
): string | null => {
const tag = rawType & 7;
const row = Math.floor(rawType / 8);
if (row === 0) return null;
if (tag === 2) return declaringTypeForMethod(bytes, layout, row, maxBytes);
if (tag !== 3) return null;
const memberRefs = layout.table(10);
if (memberRefs === undefined || row > memberRefs.rowCount) return null;
const cursor = rowCursor(bytes, layout, 10, row);
const parent = cursor.readIndex(layout.codedIndexSize("MemberRefParent"));
const parentTag = parent & 7;
const parentRow = Math.floor(parent / 8);
if (parentTag === 0)
return readTypeName(bytes, layout, 2, parentRow, maxBytes);
if (parentTag === 1)
return readTypeName(bytes, layout, 1, parentRow, maxBytes);
if (parentTag === 3)
return declaringTypeForMethod(bytes, layout, parentRow, maxBytes);
return null;
};
const decodeCompressedLength = (
blob: Buffer,
offset: number,
): { readonly length: number; readonly prefix: number } | undefined => {
const first = blob[offset];
if (first === undefined) return undefined;
if ((first & 0x80) === 0) return { length: first, prefix: 1 };
if ((first & 0xc0) === 0x80 && blob[offset + 1] !== undefined)
return {
length: ((first & 0x3f) << 8) | (blob[offset + 1] ?? 0),
prefix: 2,
};
if (
(first & 0xe0) === 0xc0 &&
blob[offset + 1] !== undefined &&
blob[offset + 2] !== undefined &&
blob[offset + 3] !== undefined
)
return {
length:
(first & 0x1f) * 0x01_00_00_00 +
(blob[offset + 1] ?? 0) * 0x01_00_00 +
(blob[offset + 2] ?? 0) * 0x0100 +
(blob[offset + 3] ?? 0),
prefix: 4,
};
return undefined;
};
const decodeFixedString = (blob: Buffer): string | null => {
if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff)
return null;
const length = decodeCompressedLength(blob, 2);
if (length === undefined) return null;
const start = 2 + length.prefix;
if (start > blob.length - length.length) return null;
try {
return new TextDecoder("utf-8", { fatal: true }).decode(
blob.subarray(start, start + length.length),
);
} catch {
return null;
}
};
const readCustomAttribute = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
): CustomAttribute => {
const cursor = rowCursor(bytes, layout, 12, row);
const parentRaw = cursor.readIndex(
layout.codedIndexSize("HasCustomAttribute"),
);
const typeRaw = cursor.readIndex(
layout.codedIndexSize("CustomAttributeType"),
);
const value = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
const parent = codedToken(
parentRaw,
5,
[
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
44, 43,
],
);
if (parent === null)
throw managedFailure(
"invalid-row",
"metadata.CustomAttribute",
"CustomAttribute parent coded index is invalid",
cursor.start,
);
const typeName = attributeTypeName(bytes, layout, typeRaw, maxBytes);
return {
parent_token: parent,
constructor_token: codedToken(typeRaw, 3, [undefined, undefined, 6, 10]),
type_name: typeName,
value_length: value.length,
value_sha256: sha256Bytes(value),
decoded_fixed_string:
typeName === "System.Runtime.Versioning.TargetFrameworkAttribute"
? decodeFixedString(value)
: null,
token: metadataToken(12, row),
row_offset: cursor.start,
};
};
const readResource = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
directory: ManagedResourceDirectory | null,
issues: ManagedParseIssue[],
): ManagedResource => {
const cursor = rowCursor(bytes, layout, 40, row);
const declaredOffset = cursor.readUInt32();
const flags = cursor.readUInt32();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const implementationRaw = cursor.readIndex(
layout.codedIndexSize("Implementation"),
);
const implementationToken = codedToken(implementationRaw, 2, [38, 35, 39]);
let dataLength: number | null = null;
let dataSha256: string | null = null;
if (implementationRaw === 0) {
if (directory === null || declaredOffset > directory.size - 4) {
issues.push({
code: "invalid-resource",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: directory?.offset ?? null,
detail: "Embedded resource offset leaves the CLI resources directory",
});
} else {
const start = directory.offset + declaredOffset;
dataLength = bytes.readUInt32LE(start);
if (dataLength > directory.size - declaredOffset - 4) {
issues.push({
code: "invalid-resource",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: start,
detail: "Embedded resource length leaves the CLI resources directory",
});
} else if (dataLength > maxBytes) {
issues.push({
code: "limit-exceeded",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: start + 4,
detail: `Embedded resource exceeds max_heap_item_bytes ${String(maxBytes)}; digest omitted`,
});
} else {
dataSha256 = sha256Bytes(
bytes.subarray(start + 4, start + 4 + dataLength),
);
}
}
}
return {
name,
flags,
visibility:
(flags & 7) === 1 ? "public" : (flags & 7) === 2 ? "private" : "unknown",
implementation_token: implementationToken,
embedded: implementationRaw === 0,
declared_offset: declaredOffset,
data_length: dataLength,
data_sha256: dataSha256,
token: metadataToken(40, row),
row_offset: cursor.start,
};
};
const safeRead = <Value>(
operation: () => Value,
issues: ManagedParseIssue[],
@@ -570,6 +102,111 @@ const uniqueIssues = (
...new Map(issues.map((issue) => [JSON.stringify(issue), issue])).values(),
];
const readPagedReferences = (
bytes: Buffer,
layout: ManagedMetadataLayout,
input: ManagedInventoryInput,
issues: ManagedParseIssue[],
): ManagedPage<AssemblyReference> =>
pageRows(
layout.table(35),
input.referenceOffset,
input.referenceLimit,
(row) =>
safeRead(
() => readAssemblyReference(bytes, layout, row, input.maxHeapItemBytes),
issues,
),
);
const readPagedResources = ({
bytes,
layout,
input,
resourceDirectory,
issues,
}: {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly input: ManagedInventoryInput;
readonly resourceDirectory: ManagedResourceDirectory | null;
readonly issues: ManagedParseIssue[];
}): ManagedPage<ManagedResource> =>
pageRows(layout.table(40), input.resourceOffset, input.resourceLimit, (row) =>
safeRead(
() =>
readResource({
bytes,
layout,
row,
maxBytes: input.maxHeapItemBytes,
directory: resourceDirectory,
issues,
}),
issues,
),
);
const readPagedAttributes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
input: ManagedInventoryInput,
issues: ManagedParseIssue[],
): ManagedPage<CustomAttribute> =>
pageRows(
layout.table(12),
input.attributeOffset,
input.attributeLimit,
(row) =>
safeRead(
() => readCustomAttribute(bytes, layout, row, input.maxHeapItemBytes),
issues,
),
);
const collectReferenceNames = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
issues: ManagedParseIssue[],
): readonly string[] => {
const names: string[] = [];
const referenceTable = layout.table(35);
for (let row = 1; row <= (referenceTable?.rowCount ?? 0); row += 1) {
const reference = safeRead(
() => readAssemblyReference(bytes, layout, row, maxBytes),
issues,
);
if (reference !== undefined) names.push(reference.name);
}
return names;
};
const collectTargetFrameworks = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
issues: ManagedParseIssue[],
): readonly string[] => {
const targetFrameworks: string[] = [];
const attributeTable = layout.table(12);
for (let row = 1; row <= (attributeTable?.rowCount ?? 0); row += 1) {
const attribute = safeRead(
() => readCustomAttribute(bytes, layout, row, maxBytes),
issues,
);
if (
attribute?.parent_token === metadataToken(32, 1) &&
attribute.type_name ===
"System.Runtime.Versioning.TargetFrameworkAttribute" &&
attribute.decoded_fixed_string !== null &&
!targetFrameworks.includes(attribute.decoded_fixed_string)
)
targetFrameworks.push(attribute.decoded_fixed_string);
}
return targetFrameworks;
};
/** Inventory bounded identity tables without CLR reflection or execution. */
export const readManagedMetadataInventory = (
bytes: Buffer,
@@ -586,73 +223,31 @@ export const readManagedMetadataInventory = (
() => readAssembly(bytes, layout, input.maxHeapItemBytes),
issues,
) ?? null;
const references = pageRows(
layout.table(35),
input.referenceOffset,
input.referenceLimit,
(row) =>
safeRead(
() => readAssemblyReference(bytes, layout, row, input.maxHeapItemBytes),
issues,
),
const references = readPagedReferences(bytes, layout, input, issues);
const resources = readPagedResources({
bytes,
layout,
input,
resourceDirectory,
issues,
});
const attributes = readPagedAttributes(bytes, layout, input, issues);
const referenceNames = collectReferenceNames(
bytes,
layout,
input.maxHeapItemBytes,
issues,
);
const resources = pageRows(
layout.table(40),
input.resourceOffset,
input.resourceLimit,
(row) =>
safeRead(
() =>
readResource(
bytes,
layout,
row,
input.maxHeapItemBytes,
resourceDirectory,
issues,
),
issues,
),
const targetFrameworks = collectTargetFrameworks(
bytes,
layout,
input.maxHeapItemBytes,
issues,
);
const attributes = pageRows(
layout.table(12),
input.attributeOffset,
input.attributeLimit,
(row) =>
safeRead(
() => readCustomAttribute(bytes, layout, row, input.maxHeapItemBytes),
issues,
),
);
const referenceNames: string[] = [];
const referenceTable = layout.table(35);
for (let row = 1; row <= (referenceTable?.rowCount ?? 0); row += 1) {
const reference = safeRead(
() => readAssemblyReference(bytes, layout, row, input.maxHeapItemBytes),
issues,
);
if (reference !== undefined) referenceNames.push(reference.name);
}
const targetFrameworks: string[] = [];
const attributeTable = layout.table(12);
for (let row = 1; row <= (attributeTable?.rowCount ?? 0); row += 1) {
const attribute = safeRead(
() => readCustomAttribute(bytes, layout, row, input.maxHeapItemBytes),
issues,
);
if (
attribute?.parent_token === metadataToken(32, 1) &&
attribute.type_name ===
"System.Runtime.Versioning.TargetFrameworkAttribute" &&
attribute.decoded_fixed_string !== null &&
!targetFrameworks.includes(attribute.decoded_fixed_string)
)
targetFrameworks.push(attribute.decoded_fixed_string);
}
return {
module,
assembly,
targetFrameworks: targetFrameworks.sort(),
targetFrameworks: [...targetFrameworks].sort(),
referenceNames: [...new Set(referenceNames)].sort(),
references,
resources,
+513
View File
@@ -0,0 +1,513 @@
import type {
ManagedArtifactInspection,
ManagedParseIssue,
} from "../domain/managedArtifact.js";
import {
metadataRowOffset,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
MetadataRowCursor,
metadataToken,
readMetadataBlob,
readMetadataGuid,
readMetadataString,
sha256Bytes,
strongNameToken,
} from "./ManagedMetadataHeaps.js";
import { managedFailure } from "./ManagedReaderFailure.js";
import type { ManagedResourceDirectory } from "./ManagedMetadataInventory.js";
type ModuleIdentity = NonNullable<ManagedArtifactInspection["module"]>;
type AssemblyIdentity = NonNullable<ManagedArtifactInspection["assembly"]>;
type AssemblyReference =
ManagedArtifactInspection["references"]["items"][number];
type ManagedResource = ManagedArtifactInspection["resources"]["items"][number];
type CustomAttribute = ManagedArtifactInspection["attributes"]["items"][number];
interface RowCursorContext {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly table: number;
readonly row: number;
}
const rowCursor = ({
bytes,
layout,
table,
row,
}: RowCursorContext): MetadataRowCursor => {
const descriptor = layout.table(table);
const start = metadataRowOffset(layout, table, row);
if (descriptor === undefined)
throw managedFailure(
"invalid-row",
`metadata.${String(table)}`,
"Metadata table is absent",
start,
);
return new MetadataRowCursor(
bytes,
start,
start + descriptor.rowSize,
`metadata.${descriptor.name}`,
);
};
const publicKeyIdentity = (
bytes: Buffer,
kind: "public-key" | "public-key-token",
): AssemblyIdentity["public_key"] => ({
kind: bytes.length === 0 ? "none" : kind,
byte_length: bytes.length,
sha256: bytes.length === 0 ? null : sha256Bytes(bytes),
token:
bytes.length === 0
? null
: kind === "public-key"
? strongNameToken(bytes)
: bytes.length === 8
? bytes.toString("hex")
: null,
});
export const readModule = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): ModuleIdentity | null => {
const table = layout.table(0);
if (table === undefined || table.rowCount === 0) return null;
const cursor = rowCursor({ bytes, layout, table: 0, row: 1 });
const generation = cursor.readUInt16();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const mvid = readMetadataGuid(
bytes,
layout,
cursor.readIndex(layout.guidIndexSize),
);
const encId = readMetadataGuid(
bytes,
layout,
cursor.readIndex(layout.guidIndexSize),
);
const encBaseId = readMetadataGuid(
bytes,
layout,
cursor.readIndex(layout.guidIndexSize),
);
return {
name,
generation,
mvid,
enc_id: encId,
enc_base_id: encBaseId,
token: metadataToken(0, 1),
row_offset: table.offset,
};
};
export const readAssembly = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): AssemblyIdentity | null => {
const table = layout.table(32);
if (table === undefined || table.rowCount === 0) return null;
const cursor = rowCursor({ bytes, layout, table: 32, row: 1 });
const hashAlgorithm = cursor.readUInt32();
const version = [
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
].join(".");
const flags = cursor.readUInt32();
const key = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const culture = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
return {
name,
version,
culture: culture.length === 0 ? null : culture,
flags,
hash_algorithm: hashAlgorithm,
public_key: publicKeyIdentity(
key,
(flags & 1) === 0 ? "public-key-token" : "public-key",
),
token: metadataToken(32, 1),
row_offset: table.offset,
};
};
export const readAssemblyReference = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
): AssemblyReference => {
const cursor = rowCursor({ bytes, layout, table: 35, row });
const version = [
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
cursor.readUInt16(),
].join(".");
const flags = cursor.readUInt32();
const keyOrToken = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const culture = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const hashValue = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
return {
name,
version,
culture: culture.length === 0 ? null : culture,
flags,
public_key_or_token: publicKeyIdentity(
keyOrToken,
(flags & 1) === 0 ? "public-key-token" : "public-key",
),
hash_value_sha256: hashValue.length === 0 ? null : sha256Bytes(hashValue),
hash_value_length: hashValue.length,
token: metadataToken(35, row),
row_offset: metadataRowOffset(layout, 35, row),
};
};
const codedToken = (
raw: number,
bits: number,
tables: readonly (number | undefined)[],
): string | null => {
if (raw === 0) return null;
const tag = raw & (2 ** bits - 1);
const row = Math.floor(raw / 2 ** bits);
const table = tables[tag];
return table === undefined || row === 0 ? null : metadataToken(table, row);
};
interface ReadTypeNameContext {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly table: 1 | 2;
readonly row: number;
readonly maxBytes: number;
}
const readTypeName = ({
bytes,
layout,
table,
row,
maxBytes,
}: ReadTypeNameContext): string | null => {
const descriptor = layout.table(table);
if (descriptor === undefined || row < 1 || row > descriptor.rowCount)
return null;
const cursor = rowCursor({ bytes, layout, table, row });
if (table === 1) cursor.readIndex(layout.codedIndexSize("ResolutionScope"));
else cursor.readUInt32();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const namespace = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
return namespace.length === 0 ? name : `${namespace}.${name}`;
};
const declaringTypeForMethod = (
bytes: Buffer,
layout: ManagedMetadataLayout,
methodRow: number,
maxBytes: number,
): string | null => {
const types = layout.table(2);
const methods = layout.table(6);
if (types === undefined || methods === undefined) return null;
for (let row = 1; row <= types.rowCount; row += 1) {
const cursor = rowCursor({ bytes, layout, table: 2, row });
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
const namespaceIndex = cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
cursor.readIndex(layout.tableIndexSize(4));
const methodStart = cursor.readIndex(layout.tableIndexSize(6));
const methodEnd =
row === types.rowCount
? methods.rowCount + 1
: methodListForType(bytes, layout, row + 1);
if (methodRow < methodStart || methodRow >= methodEnd) continue;
const name = readMetadataString(bytes, layout, nameIndex, maxBytes);
const namespace = readMetadataString(
bytes,
layout,
namespaceIndex,
maxBytes,
);
return namespace.length === 0 ? name : `${namespace}.${name}`;
}
return null;
};
const methodListForType = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
): number => {
const cursor = rowCursor({ bytes, layout, table: 2, row });
cursor.readUInt32();
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
cursor.readIndex(layout.tableIndexSize(4));
return cursor.readIndex(layout.tableIndexSize(6));
};
const attributeTypeName = (
bytes: Buffer,
layout: ManagedMetadataLayout,
rawType: number,
maxBytes: number,
): string | null => {
const tag = rawType & 7;
const row = Math.floor(rawType / 8);
if (row === 0) return null;
if (tag === 2) return declaringTypeForMethod(bytes, layout, row, maxBytes);
if (tag !== 3) return null;
const memberRefs = layout.table(10);
if (memberRefs === undefined || row > memberRefs.rowCount) return null;
const cursor = rowCursor({ bytes, layout, table: 10, row });
const parent = cursor.readIndex(layout.codedIndexSize("MemberRefParent"));
const parentTag = parent & 7;
const parentRow = Math.floor(parent / 8);
if (parentTag === 0)
return readTypeName({ bytes, layout, table: 2, row: parentRow, maxBytes });
if (parentTag === 1)
return readTypeName({ bytes, layout, table: 1, row: parentRow, maxBytes });
if (parentTag === 3)
return declaringTypeForMethod(bytes, layout, parentRow, maxBytes);
return null;
};
const decodeCompressedLength = (
blob: Buffer,
offset: number,
): { readonly length: number; readonly prefix: number } | undefined => {
const first = blob[offset];
if (first === undefined) return undefined;
if ((first & 0x80) === 0) return { length: first, prefix: 1 };
if ((first & 0xc0) === 0x80 && blob[offset + 1] !== undefined)
return {
length: ((first & 0x3f) << 8) | (blob[offset + 1] ?? 0),
prefix: 2,
};
if (
(first & 0xe0) === 0xc0 &&
blob[offset + 1] !== undefined &&
blob[offset + 2] !== undefined &&
blob[offset + 3] !== undefined
)
return {
length:
(first & 0x1f) * 0x01_00_00_00 +
(blob[offset + 1] ?? 0) * 0x01_00_00 +
(blob[offset + 2] ?? 0) * 0x0100 +
(blob[offset + 3] ?? 0),
prefix: 4,
};
return undefined;
};
const decodeFixedString = (blob: Buffer): string | null => {
if (blob.length < 3 || blob.readUInt16LE(0) !== 1 || blob[2] === 0xff)
return null;
const length = decodeCompressedLength(blob, 2);
if (length === undefined) return null;
const start = 2 + length.prefix;
if (start > blob.length - length.length) return null;
try {
return new TextDecoder("utf-8", { fatal: true }).decode(
blob.subarray(start, start + length.length),
);
} catch {
return null;
}
};
export const readCustomAttribute = (
bytes: Buffer,
layout: ManagedMetadataLayout,
row: number,
maxBytes: number,
): CustomAttribute => {
const cursor = rowCursor({ bytes, layout, table: 12, row });
const parentRaw = cursor.readIndex(
layout.codedIndexSize("HasCustomAttribute"),
);
const typeRaw = cursor.readIndex(
layout.codedIndexSize("CustomAttributeType"),
);
const value = readMetadataBlob(
bytes,
layout,
cursor.readIndex(layout.blobIndexSize),
maxBytes,
);
const parent = codedToken(
parentRaw,
5,
[
6, 4, 1, 2, 8, 9, 10, 0, 14, 23, 20, 17, 26, 27, 32, 35, 38, 39, 40, 42,
44, 43,
],
);
if (parent === null)
throw managedFailure(
"invalid-row",
"metadata.CustomAttribute",
"CustomAttribute parent coded index is invalid",
cursor.start,
);
const typeName = attributeTypeName(bytes, layout, typeRaw, maxBytes);
return {
parent_token: parent,
constructor_token: codedToken(typeRaw, 3, [undefined, undefined, 6, 10]),
type_name: typeName,
value_length: value.length,
value_sha256: sha256Bytes(value),
decoded_fixed_string:
typeName === "System.Runtime.Versioning.TargetFrameworkAttribute"
? decodeFixedString(value)
: null,
token: metadataToken(12, row),
row_offset: cursor.start,
};
};
interface ReadResourceContext {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly row: number;
readonly maxBytes: number;
readonly directory: ManagedResourceDirectory | null;
readonly issues: ManagedParseIssue[];
}
export const readResource = ({
bytes,
layout,
row,
maxBytes,
directory,
issues,
}: ReadResourceContext): ManagedResource => {
const cursor = rowCursor({ bytes, layout, table: 40, row });
const declaredOffset = cursor.readUInt32();
const flags = cursor.readUInt32();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const implementationRaw = cursor.readIndex(
layout.codedIndexSize("Implementation"),
);
const implementationToken = codedToken(implementationRaw, 2, [38, 35, 39]);
let dataLength: number | null = null;
let dataSha256: string | null = null;
if (implementationRaw === 0) {
if (directory === null || declaredOffset > directory.size - 4) {
issues.push({
code: "invalid-resource",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: directory?.offset ?? null,
detail: "Embedded resource offset leaves the CLI resources directory",
});
} else {
const start = directory.offset + declaredOffset;
dataLength = bytes.readUInt32LE(start);
if (dataLength > directory.size - declaredOffset - 4) {
issues.push({
code: "invalid-resource",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: start,
detail: "Embedded resource length leaves the CLI resources directory",
});
} else if (dataLength > maxBytes) {
issues.push({
code: "limit-exceeded",
scope: `metadata.ManifestResource:${metadataToken(40, row)}`,
offset: start + 4,
detail: `Embedded resource exceeds max_heap_item_bytes ${String(maxBytes)}; digest omitted`,
});
} else {
dataSha256 = sha256Bytes(
bytes.subarray(start + 4, start + 4 + dataLength),
);
}
}
}
return {
name,
flags,
visibility:
(flags & 7) === 1 ? "public" : (flags & 7) === 2 ? "private" : "unknown",
implementation_token: implementationToken,
embedded: implementationRaw === 0,
declared_offset: declaredOffset,
data_length: dataLength,
data_sha256: dataSha256,
token: metadataToken(40, row),
row_offset: cursor.start,
};
};
+116 -129
View File
@@ -1,4 +1,8 @@
import { managedFailure } from "./ManagedReaderFailure.js";
import {
computeRowSize,
type RowSizeContext,
} from "./ManagedMetadataRowSizes.js";
export const METADATA_TABLE_NAMES = [
"Module",
@@ -80,7 +84,7 @@ export interface ManagedMetadataLayout {
codedIndexSize(name: CodedIndexName): 2 | 4;
}
type CodedIndexName = keyof typeof CODED_INDEXES;
export type CodedIndexName = keyof typeof CODED_INDEXES;
const CODED_INDEXES = {
TypeDefOrRef: { bits: 2, tables: [2, 1, 27] },
@@ -196,129 +200,15 @@ const codedSize = (
const tableSize = (index: number, rows: readonly number[]): 2 | 4 =>
(rows[index] ?? 0) < 0x1_0000 ? 2 : 4;
const rowSize = (
table: number,
rowCounts: readonly number[],
heaps: { readonly string: 2 | 4; readonly guid: 2 | 4; readonly blob: 2 | 4 },
): number => {
const t = (index: number): number => tableSize(index, rowCounts);
const c = (name: CodedIndexName): number => codedSize(name, rowCounts);
const { string: s, guid: g, blob: b } = heaps;
switch (table) {
case 0:
return 2 + s + g * 3;
case 1:
return c("ResolutionScope") + s * 2;
case 2:
return 4 + s * 2 + c("TypeDefOrRef") + t(4) + t(6);
case 3:
return t(4);
case 4:
return 2 + s + b;
case 5:
return t(6);
case 6:
return 8 + s + b + t(8);
case 7:
return t(8);
case 8:
return 4 + s;
case 9:
return t(2) + c("TypeDefOrRef");
case 10:
return c("MemberRefParent") + s + b;
case 11:
return 2 + c("HasConstant") + b;
case 12:
return c("HasCustomAttribute") + c("CustomAttributeType") + b;
case 13:
return c("HasFieldMarshal") + b;
case 14:
return 2 + c("HasDeclSecurity") + b;
case 15:
return 6 + t(2);
case 16:
return 4 + t(4);
case 17:
return b;
case 18:
return t(2) + t(20);
case 19:
return t(20);
case 20:
return 2 + s + c("TypeDefOrRef");
case 21:
return t(2) + t(23);
case 22:
return t(23);
case 23:
return 2 + s + b;
case 24:
return 2 + t(6) + c("HasSemantics");
case 25:
return t(2) + c("MethodDefOrRef") * 2;
case 26:
return s;
case 27:
return b;
case 28:
return 2 + c("MemberForwarded") + s + t(26);
case 29:
return 4 + t(4);
case 30:
return 8;
case 31:
return 4;
case 32:
return 16 + b + s * 2;
case 33:
return 4;
case 34:
return 12;
case 35:
return 12 + b * 2 + s * 2;
case 36:
return 4 + t(35);
case 37:
return 12 + t(35);
case 38:
return 4 + s + b;
case 39:
return 8 + s * 2 + c("Implementation");
case 40:
return 8 + s + c("Implementation");
case 41:
return t(2) * 2;
case 42:
return 4 + c("TypeOrMethodDef") + s;
case 43:
return c("MethodDefOrRef") + b;
case 44:
return t(42) + c("TypeDefOrRef");
default:
throw managedFailure(
"invalid-tables",
"metadata.tables",
`Metadata table ${String(table)} is unsupported`,
);
}
};
interface ParsedStreams {
readonly streams: ReadonlyMap<string, MetadataStream>;
readonly version: string;
}
/** Parse ECMA-335 stream and table layout with checked byte extents. */
export const readManagedMetadataLayout = (
artifact: Buffer,
const readMetadataStreams = (
bytes: Buffer,
rootOffset: number,
size: number,
maxTableRows: number,
): ManagedMetadataLayout => {
if (size < 20 || rootOffset < 0 || rootOffset > artifact.length - size)
throw managedFailure(
"invalid-metadata-root",
"metadata.root",
"CLI metadata directory leaves the artifact byte range",
rootOffset,
);
const bytes = artifact.subarray(rootOffset, rootOffset + size);
): ParsedStreams => {
if (bytes.readUInt32LE(0) !== 0x424a_5342)
throw managedFailure(
"invalid-metadata-root",
@@ -365,6 +255,26 @@ export const readManagedMetadataLayout = (
});
cursor = named.next;
}
return { streams, version };
};
interface HeapSizes {
readonly string: 2 | 4;
readonly guid: 2 | 4;
readonly blob: 2 | 4;
}
interface ParsedTablesHeader {
readonly tableBytes: Buffer;
readonly tablesOffset: number;
readonly heaps: HeapSizes;
readonly valid: bigint;
}
const readTablesHeader = (
bytes: Buffer,
streams: ReadonlyMap<string, MetadataStream>,
): ParsedTablesHeader => {
const tablesStream = streamByName(streams, ["#~", "#-"]);
if (tablesStream === undefined)
throw managedFailure(
@@ -372,7 +282,7 @@ export const readManagedMetadataLayout = (
"metadata.tables",
"Required metadata table stream #~ or #- is absent",
);
const tableBytes = artifact.subarray(
const tableBytes = bytes.subarray(
tablesStream.offset,
tablesStream.offset + tablesStream.size,
);
@@ -391,6 +301,15 @@ export const readManagedMetadataLayout = (
"Metadata valid mask names unsupported tables outside the admitted ECMA-335 range",
tablesStream.offset + 8,
);
return { tableBytes, tablesOffset: tablesStream.offset, heaps, valid };
};
const readRowCounts = (
tableBytes: Buffer,
valid: bigint,
maxTableRows: number,
tablesOffset: number,
): readonly number[] => {
const rowCounts = Array<number>(METADATA_TABLE_NAMES.length).fill(0);
let tableCursor = 24;
for (let index = 0; index < METADATA_TABLE_NAMES.length; index += 1) {
@@ -402,23 +321,56 @@ export const readManagedMetadataLayout = (
"limit-exceeded",
`metadata.${METADATA_TABLE_NAMES[index] ?? String(index)}`,
`Metadata table row count ${String(count)} exceeds max_table_rows ${String(maxTableRows)}`,
tablesStream.offset + tableCursor,
tablesOffset + tableCursor,
);
rowCounts[index] = count;
tableCursor += 4;
}
return rowCounts;
};
const buildRowSizeContext = (
rowCounts: readonly number[],
heaps: HeapSizes,
): RowSizeContext => ({
tableSize: (index) => tableSize(index, rowCounts),
codedSize: (name) => codedSize(name, rowCounts),
stringSize: heaps.string,
guidSize: heaps.guid,
blobSize: heaps.blob,
});
const buildMetadataTables = ({
tableBytes,
rowCounts,
heaps,
valid,
tablesOffset,
}: {
readonly tableBytes: Buffer;
readonly rowCounts: readonly number[];
readonly heaps: HeapSizes;
readonly valid: bigint;
readonly tablesOffset: number;
}): ReadonlyMap<number, MetadataTableLayout> => {
const tables = new Map<number, MetadataTableLayout>();
const context = buildRowSizeContext(rowCounts, heaps);
let tableCursor = 24;
for (let index = 0; index < METADATA_TABLE_NAMES.length; index += 1) {
if ((valid & (1n << BigInt(index))) === 0n) continue;
tableCursor += 4;
}
for (let index = 0; index < METADATA_TABLE_NAMES.length; index += 1) {
const count = rowCounts[index] ?? 0;
if ((valid & (1n << BigInt(index))) === 0n) continue;
const sizeOfRow = rowSize(index, rowCounts, heaps);
const sizeOfRow = computeRowSize(index, context);
const byteLength = count * sizeOfRow;
if (!Number.isSafeInteger(byteLength))
throw managedFailure(
"invalid-tables",
"metadata.tables",
"Metadata table byte size overflowed",
tablesStream.offset + tableCursor,
tablesOffset + tableCursor,
);
requireRange(tableBytes, tableCursor, byteLength, "metadata.table-data");
tables.set(index, {
@@ -426,11 +378,47 @@ export const readManagedMetadataLayout = (
name: METADATA_TABLE_NAMES[index] ?? `Table${String(index)}`,
rowCount: count,
rowSize: sizeOfRow,
offset: tablesStream.offset + tableCursor,
offset: tablesOffset + tableCursor,
});
tableCursor += byteLength;
}
const layout: ManagedMetadataLayout = {
return tables;
};
/** Parse ECMA-335 stream and table layout with checked byte extents. */
export const readManagedMetadataLayout = (
artifact: Buffer,
rootOffset: number,
size: number,
maxTableRows: number,
): ManagedMetadataLayout => {
if (size < 20 || rootOffset < 0 || rootOffset > artifact.length - size)
throw managedFailure(
"invalid-metadata-root",
"metadata.root",
"CLI metadata directory leaves the artifact byte range",
rootOffset,
);
const bytes = artifact.subarray(rootOffset, rootOffset + size);
const { streams, version } = readMetadataStreams(bytes, rootOffset);
const { tableBytes, tablesOffset, heaps, valid } = readTablesHeader(
artifact,
streams,
);
const rowCounts = readRowCounts(
tableBytes,
valid,
maxTableRows,
tablesOffset,
);
const tables = buildMetadataTables({
tableBytes,
rowCounts,
heaps,
valid,
tablesOffset,
});
return {
rootOffset,
size,
version,
@@ -447,7 +435,6 @@ export const readManagedMetadataLayout = (
tableIndexSize: (index) => tableSize(index, rowCounts),
codedIndexSize: (name) => codedSize(name, rowCounts),
};
return layout;
};
/** Resolve a one-based metadata row to its exact file offset. */
+78
View File
@@ -0,0 +1,78 @@
import { managedFailure } from "./ManagedReaderFailure.js";
import type { CodedIndexName } from "./ManagedMetadataLayout.js";
export interface RowSizeContext {
readonly tableSize: (index: number) => number;
readonly codedSize: (name: CodedIndexName) => number;
readonly stringSize: 2 | 4;
readonly guidSize: 2 | 4;
readonly blobSize: 2 | 4;
}
type RowSizeFactory = (context: RowSizeContext) => number;
const ROW_SIZE_FACTORIES: readonly RowSizeFactory[] = [
({ stringSize: s, guidSize: g }) => 2 + s + g * 3,
({ codedSize: c, stringSize: s }) => c("ResolutionScope") + s * 2,
({ tableSize: t, codedSize: c, stringSize: s }) =>
4 + s * 2 + c("TypeDefOrRef") + t(4) + t(6),
({ tableSize: t }) => t(4),
({ stringSize: s, blobSize: b }) => 2 + s + b,
({ tableSize: t }) => t(6),
({ tableSize: t, stringSize: s, blobSize: b }) => 8 + s + b + t(8),
({ tableSize: t }) => t(8),
({ stringSize: s }) => 4 + s,
({ tableSize: t, codedSize: c }) => t(2) + c("TypeDefOrRef"),
({ codedSize: c, stringSize: s, blobSize: b }) =>
c("MemberRefParent") + s + b,
({ codedSize: c, blobSize: b }) => 2 + c("HasConstant") + b,
({ codedSize: c, blobSize: b }) =>
c("HasCustomAttribute") + c("CustomAttributeType") + b,
({ codedSize: c, blobSize: b }) => c("HasFieldMarshal") + b,
({ codedSize: c, blobSize: b }) => 2 + c("HasDeclSecurity") + b,
({ tableSize: t }) => 6 + t(2),
({ tableSize: t }) => 4 + t(4),
({ blobSize: b }) => b,
({ tableSize: t }) => t(2) + t(20),
({ tableSize: t }) => t(20),
({ stringSize: s, codedSize: c }) => 2 + s + c("TypeDefOrRef"),
({ tableSize: t }) => t(2) + t(23),
({ tableSize: t }) => t(23),
({ stringSize: s, blobSize: b }) => 2 + s + b,
({ tableSize: t, codedSize: c }) => 2 + t(6) + c("HasSemantics"),
({ tableSize: t, codedSize: c }) => t(2) + c("MethodDefOrRef") * 2,
({ stringSize: s }) => s,
({ blobSize: b }) => b,
({ tableSize: t, codedSize: c, stringSize: s }) =>
2 + c("MemberForwarded") + s + t(26),
({ tableSize: t }) => 4 + t(4),
() => 8,
() => 4,
({ blobSize: b, stringSize: s }) => 16 + b + s * 2,
() => 4,
() => 12,
({ blobSize: b, stringSize: s }) => 12 + b * 2 + s * 2,
({ tableSize: t }) => 4 + t(35),
({ tableSize: t }) => 12 + t(35),
({ stringSize: s, blobSize: b }) => 4 + s + b,
({ stringSize: s, codedSize: c }) => 8 + s * 2 + c("Implementation"),
({ stringSize: s, codedSize: c }) => 8 + s + c("Implementation"),
({ tableSize: t }) => t(2) * 2,
({ stringSize: s, codedSize: c }) => 4 + c("TypeOrMethodDef") + s,
({ codedSize: c, blobSize: b }) => c("MethodDefOrRef") + b,
({ tableSize: t, codedSize: c }) => t(42) + c("TypeDefOrRef"),
];
export const computeRowSize = (
table: number,
context: RowSizeContext,
): number => {
const factory = ROW_SIZE_FACTORIES[table];
if (factory === undefined)
throw managedFailure(
"invalid-tables",
"metadata.tables",
`Metadata table ${String(table)} is unsupported`,
);
return factory(context);
};
+499
View File
@@ -0,0 +1,499 @@
import type { BinaryTarget } from "../domain/binaryTarget.js";
import {
managedNativeBoundaryInspectionSchema,
type ManagedNativeBoundaryInspection,
type ManagedParseIssue,
} from "../domain/managedArtifact.js";
import {
metadataRowOffset,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
MetadataRowCursor,
metadataToken,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
import { managedTableRowCounts } from "./ManagedMetadataInventory.js";
import type { ManagedMetadataInventory } from "./ManagedMetadataInventory.js";
import { managedFailure } from "./ManagedReaderFailure.js";
import type { ManagedNativeBoundaryInspectionLimits } from "./ManagedNativeBoundaryInspector.js";
import type { ManagedPeLayout } from "./ManagedPeReader.js";
type Page<Item> = {
readonly items: readonly Item[];
readonly offset: number;
readonly limit: number;
readonly total: number;
readonly returned: number;
readonly dropped: number;
readonly complete: boolean;
};
type ModuleRef =
ManagedNativeBoundaryInspection["module_refs"]["items"][number];
type NativeImport =
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
type NativeImplementation =
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
interface MemberCore {
readonly token: string;
readonly rowOffset: number;
readonly kind: NativeImport["member_kind"];
readonly name: string;
readonly flags: number;
readonly implFlags: number | null;
readonly rva: number | null;
}
interface RowCursorContext {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly table: number;
readonly row: number;
}
const rowCursor = ({
bytes,
layout,
table,
row,
}: RowCursorContext): MetadataRowCursor => {
const descriptor = layout.table(table);
const start = metadataRowOffset(layout, table, row);
if (descriptor === undefined)
throw managedFailure(
"invalid-row",
`metadata.${String(table)}`,
"Metadata table is absent",
start,
);
return new MetadataRowCursor(
bytes,
start,
start + descriptor.rowSize,
`metadata.${descriptor.name}`,
);
};
const codedToken = (
raw: number,
tagBits: number,
tables: readonly number[],
): string | null => {
const tagMask = (1 << tagBits) - 1;
const table = tables[raw & tagMask];
const row = raw >> tagBits;
if (table === undefined || row === 0) return null;
return metadataToken(table, row);
};
const flagsHex = (value: number): string =>
`0x${value.toString(16).padStart(4, "0")}`;
const page = <Item>(
items: readonly Item[],
offset: number,
limit: number,
): Page<Item> => {
const safeOffset = Math.min(offset, items.length);
const selected = items.slice(safeOffset, safeOffset + limit);
return {
items: selected,
offset,
limit,
total: items.length,
returned: selected.length,
dropped: Math.max(0, items.length - safeOffset - selected.length),
complete: safeOffset === 0 && selected.length === items.length,
};
};
export const parseModuleRefs = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): readonly ModuleRef[] => {
const refs: ModuleRef[] = [];
const table = layout.table(26);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor({ bytes, layout, table: 26, row });
refs.push({
token: metadataToken(26, row),
row_offset: cursor.start,
name: readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
),
});
}
return refs;
};
export const parseFields = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): ReadonlyMap<string, MemberCore> => {
const fields = new Map<string, MemberCore>();
const table = layout.table(4);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor({ bytes, layout, table: 4, row });
const flags = cursor.readUInt16();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
cursor.readIndex(layout.blobIndexSize);
const token = metadataToken(4, row);
fields.set(token, {
token,
rowOffset: cursor.start,
kind: "field",
name,
flags,
implFlags: null,
rva: null,
});
}
return fields;
};
export const parseMethods = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): ReadonlyMap<string, MemberCore> => {
const methods = new Map<string, MemberCore>();
const table = layout.table(6);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor({ bytes, layout, table: 6, row });
const rva = cursor.readUInt32();
const implFlags = cursor.readUInt16();
const flags = cursor.readUInt16();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
cursor.readIndex(layout.blobIndexSize);
cursor.readIndex(layout.tableIndexSize(8));
const token = metadataToken(6, row);
methods.set(token, {
token,
rowOffset: cursor.start,
kind: "method",
name,
flags,
implFlags,
rva,
});
}
return methods;
};
interface ParseImplMapsContext {
readonly bytes: Buffer;
readonly layout: ManagedMetadataLayout;
readonly maxBytes: number;
readonly modules: readonly ModuleRef[];
readonly members: ReadonlyMap<string, MemberCore>;
}
export const parseImplMaps = ({
bytes,
layout,
maxBytes,
modules,
members,
}: ParseImplMapsContext): readonly NativeImport[] => {
const moduleNames = new Map(modules.map((module) => [module.token, module]));
const imports: NativeImport[] = [];
const table = layout.table(28);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor({ bytes, layout, table: 28, row });
const mappingFlags = cursor.readUInt16();
const memberToken = codedToken(
cursor.readIndex(layout.codedIndexSize("MemberForwarded")),
1,
[4, 6],
);
const importName = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const importScopeRow = cursor.readIndex(layout.tableIndexSize(26));
const importScopeToken =
importScopeRow === 0 ? null : metadataToken(26, importScopeRow);
const member = memberToken === null ? undefined : members.get(memberToken);
imports.push({
token: metadataToken(28, row),
row_offset: cursor.start,
mapping_flags: mappingFlags,
mapping_flags_hex: flagsHex(mappingFlags),
member_token: memberToken,
member_kind: member?.kind ?? "unknown",
member_name: member?.name ?? null,
import_name: importName,
import_scope_token: importScopeToken,
import_scope_name:
importScopeToken === null
? null
: (moduleNames.get(importScopeToken)?.name ?? null),
no_mangle: (mappingFlags & 0x0001) !== 0,
char_set: charSet(mappingFlags),
call_convention: callConvention(mappingFlags),
supports_last_error: (mappingFlags & 0x0040) !== 0,
best_fit: bestFit(mappingFlags),
throw_on_unmappable_char: throwOnUnmappable(mappingFlags),
verification: "managed-declaration-only",
});
}
return imports;
};
const charSet = (flags: number): NativeImport["char_set"] => {
switch (flags & 0x0006) {
case 0x0000:
return "not-specified";
case 0x0002:
return "ansi";
case 0x0004:
return "unicode";
case 0x0006:
return "auto";
default:
return "unknown";
}
};
const callConvention = (flags: number): NativeImport["call_convention"] => {
switch (flags & 0x0700) {
case 0x0000:
return "not-specified";
case 0x0100:
return "winapi";
case 0x0200:
return "cdecl";
case 0x0300:
return "stdcall";
case 0x0400:
return "thiscall";
case 0x0500:
return "fastcall";
default:
return "unknown";
}
};
const bestFit = (flags: number): NativeImport["best_fit"] => {
switch (flags & 0x0030) {
case 0x0000:
return "assembly-default";
case 0x0010:
return "enabled";
case 0x0020:
return "disabled";
default:
return "unknown";
}
};
const throwOnUnmappable = (
flags: number,
): NativeImport["throw_on_unmappable_char"] => {
switch (flags & 0x3000) {
case 0x0000:
return "assembly-default";
case 0x1000:
return "enabled";
case 0x2000:
return "disabled";
default:
return "unknown";
}
};
export const nativeImplementations = (
methods: Iterable<MemberCore>,
pinvokeTokens: ReadonlySet<string>,
): readonly NativeImplementation[] => {
const implementations: NativeImplementation[] = [];
for (const method of methods) {
const implFlags = method.implFlags ?? 0;
const codeType = codeTypeFor(implFlags);
const managedKind = managedKindFor(implFlags);
const pinvokeDeclared =
pinvokeTokens.has(method.token) || (method.flags & 0x2000) !== 0;
if (!pinvokeDeclared && codeType === "il" && managedKind === "managed")
continue;
implementations.push({
token: method.token,
row_offset: method.rowOffset,
name: method.name,
rva: method.rva ?? 0,
flags: method.flags,
impl_flags: implFlags,
code_type: codeType,
managed_kind: managedKind,
pinvoke_declared: pinvokeDeclared,
boundary_kind: boundaryKind(pinvokeDeclared, codeType, managedKind),
body_interpretation:
codeType === "il" && managedKind === "managed" && !pinvokeDeclared
? "managed-cil"
: method.rva === 0
? "not-file-backed"
: "native-or-runtime",
});
}
return implementations;
};
const codeTypeFor = (implFlags: number): NativeImplementation["code_type"] => {
switch (implFlags & 0x0003) {
case 0x0000:
return "il";
case 0x0001:
return "native";
case 0x0002:
return "optil";
case 0x0003:
return "runtime";
default:
return "unknown";
}
};
const managedKindFor = (
implFlags: number,
): NativeImplementation["managed_kind"] => {
switch (implFlags & 0x0004) {
case 0x0000:
return "managed";
case 0x0004:
return "unmanaged";
default:
return "unknown";
}
};
const boundaryKind = (
pinvokeDeclared: boolean,
codeType: NativeImplementation["code_type"],
managedKind: NativeImplementation["managed_kind"],
): NativeImplementation["boundary_kind"] => {
if (pinvokeDeclared) return "pinvoke";
if (codeType === "native") return "native-body";
if (codeType === "runtime") return "runtime-provided";
if (managedKind === "unmanaged") return "unmanaged-method";
return "mixed-or-unknown";
};
export const cliNative = (
pe: ManagedPeLayout,
): ManagedNativeBoundaryInspection["cli_native"] => {
if (pe.cli === null)
return {
il_only: false,
requires_32bit: false,
strong_name_signed: false,
native_entry_point: false,
ready_to_run_signature: false,
managed_native_header_rva: 0,
managed_native_header_size: 0,
};
return {
il_only: (pe.cli.flags & 0x0000_0001) !== 0,
requires_32bit: (pe.cli.flags & 0x0000_0002) !== 0,
strong_name_signed: (pe.cli.flags & 0x0000_0008) !== 0,
native_entry_point: (pe.cli.flags & 0x0000_0010) !== 0,
ready_to_run_signature: pe.cli.readyToRunSignature,
managed_native_header_rva: pe.cli.managedNativeHeader.rva,
managed_native_header_size: pe.cli.managedNativeHeader.size,
};
};
interface BoundaryInspectionContext {
readonly target: BinaryTarget;
readonly bytes: Buffer;
readonly limits: ManagedNativeBoundaryInspectionLimits;
readonly pe: ManagedPeLayout;
readonly layout: ManagedMetadataLayout;
readonly inventory: ManagedMetadataInventory;
readonly moduleRefs: readonly ModuleRef[];
readonly imports: readonly NativeImport[];
readonly implementations: readonly NativeImplementation[];
readonly native: ManagedNativeBoundaryInspection["cli_native"];
readonly issues: readonly ManagedParseIssue[];
}
export const buildNativeBoundaryInspection = ({
target,
bytes,
limits,
layout,
inventory,
moduleRefs,
imports,
implementations,
native,
issues,
}: BoundaryInspectionContext): ManagedNativeBoundaryInspection =>
managedNativeBoundaryInspectionSchema.parse({
schema_version: 1,
artifact: {
path: target.path,
sha256: target.sha256,
byte_length: bytes.length,
format: "pe",
},
module: inventory.module,
metadata: {
status: issues.length === 0 ? "complete" : "partial",
version: layout.version,
table_row_counts: managedTableRowCounts(layout),
},
identity_scope: {
token_identity: "build-local",
requires_artifact_sha256: target.sha256,
requires_mvid: inventory.module?.mvid ?? null,
},
cli_native: native,
module_refs: page(
moduleRefs,
limits.moduleRefOffset,
limits.moduleRefLimit,
),
pinvoke_imports: page(imports, limits.importOffset, limits.importLimit),
native_implementations: page(
implementations,
limits.implementationOffset,
limits.implementationLimit,
),
summary: {
module_ref_count: moduleRefs.length,
pinvoke_import_count: imports.length,
native_implementation_count: implementations.length,
ready_to_run: native.ready_to_run_signature,
mixed_mode_or_native_header:
native.managed_native_header_rva !== 0 ||
native.managed_native_header_size !== 0 ||
native.native_entry_point,
},
coverage: {
state: issues.length === 0 ? "complete" : "partial",
issues,
},
limitations: [
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
"Managed metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
"ReadyToRun, NativeAOT, C++/CLI, and IL2CPP native semantics require separately selected native-provider evidence; this tool does not translate managed tokens into native addresses.",
],
});
+156 -525
View File
@@ -4,30 +4,29 @@ import {
type ManagedNativeBoundaryInspection,
type ManagedParseIssue,
} from "../domain/managedArtifact.js";
import {
metadataRowOffset,
type ManagedMetadataLayout,
readManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
MetadataRowCursor,
metadataToken,
readMetadataString,
} from "./ManagedMetadataHeaps.js";
import {
readManagedMetadataInventory,
managedTableRowCounts,
type ManagedResourceDirectory,
} from "./ManagedMetadataInventory.js";
import {
readManagedMetadataLayout,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import {
readManagedPeLayout,
type ManagedPeLayout,
} from "./ManagedPeReader.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import {
ManagedReaderFailure,
managedFailure,
} from "./ManagedReaderFailure.js";
buildNativeBoundaryInspection,
cliNative,
nativeImplementations,
parseFields,
parseImplMaps,
parseMethods,
parseModuleRefs,
} from "./ManagedNativeBoundaryHelpers.js";
/** Bounds for execution-free managed/native boundary inspection. */
export interface ManagedNativeBoundaryInspectionLimits {
readonly moduleRefOffset: number;
readonly moduleRefLimit: number;
@@ -40,396 +39,25 @@ export interface ManagedNativeBoundaryInspectionLimits {
readonly maxHeapItemBytes: number;
}
type Page<Item> = {
readonly items: readonly Item[];
readonly offset: number;
readonly limit: number;
readonly total: number;
readonly returned: number;
readonly dropped: number;
readonly complete: boolean;
};
type ModuleRef =
ManagedNativeBoundaryInspection["module_refs"]["items"][number];
type NativeImport =
ManagedNativeBoundaryInspection["pinvoke_imports"]["items"][number];
type NativeImplementation =
ManagedNativeBoundaryInspection["native_implementations"]["items"][number];
type Inventory = ReturnType<typeof readManagedMetadataInventory>;
interface MemberCore {
readonly token: string;
readonly rowOffset: number;
readonly kind: NativeImport["member_kind"];
readonly name: string;
readonly flags: number;
readonly implFlags: number | null;
readonly rva: number | null;
}
const emptyPage = <Item>(): ManagedNativeBoundaryInspection["module_refs"] & {
items: readonly Item[];
} => ({
items: [],
offset: 0,
limit: 0,
total: 0,
returned: 0,
dropped: 0,
complete: true,
});
const rowCursor = (
bytes: Buffer,
layout: ManagedMetadataLayout,
table: number,
row: number,
): MetadataRowCursor => {
const descriptor = layout.table(table);
const start = metadataRowOffset(layout, table, row);
if (descriptor === undefined)
throw managedFailure(
"invalid-row",
`metadata.${String(table)}`,
"Metadata table is absent",
start,
);
return new MetadataRowCursor(
bytes,
start,
start + descriptor.rowSize,
`metadata.${descriptor.name}`,
);
};
const codedToken = (
raw: number,
tagBits: number,
tables: readonly number[],
): string | null => {
const tagMask = (1 << tagBits) - 1;
const table = tables[raw & tagMask];
const row = raw >> tagBits;
if (table === undefined || row === 0) return null;
return metadataToken(table, row);
};
const flagsHex = (value: number): string =>
`0x${value.toString(16).padStart(4, "0")}`;
const page = <Item>(
items: readonly Item[],
offset: number,
limit: number,
): Page<Item> => {
const safeOffset = Math.min(offset, items.length);
const selected = items.slice(safeOffset, safeOffset + limit);
return {
items: selected,
offset,
limit,
total: items.length,
returned: selected.length,
dropped: Math.max(0, items.length - safeOffset - selected.length),
complete: safeOffset === 0 && selected.length === items.length,
};
};
const parseModuleRefs = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): readonly ModuleRef[] => {
const refs: ModuleRef[] = [];
const table = layout.table(26);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor(bytes, layout, 26, row);
refs.push({
token: metadataToken(26, row),
row_offset: cursor.start,
name: readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
),
});
}
return refs;
};
const parseFields = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): ReadonlyMap<string, MemberCore> => {
const fields = new Map<string, MemberCore>();
const table = layout.table(4);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor(bytes, layout, 4, row);
const flags = cursor.readUInt16();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
cursor.readIndex(layout.blobIndexSize);
const token = metadataToken(4, row);
fields.set(token, {
token,
rowOffset: cursor.start,
kind: "field",
name,
flags,
implFlags: null,
rva: null,
});
}
return fields;
};
const parseMethods = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
): ReadonlyMap<string, MemberCore> => {
const methods = new Map<string, MemberCore>();
const table = layout.table(6);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor(bytes, layout, 6, row);
const rva = cursor.readUInt32();
const implFlags = cursor.readUInt16();
const flags = cursor.readUInt16();
const name = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
cursor.readIndex(layout.blobIndexSize);
cursor.readIndex(layout.tableIndexSize(8));
const token = metadataToken(6, row);
methods.set(token, {
token,
rowOffset: cursor.start,
kind: "method",
name,
flags,
implFlags,
rva,
});
}
return methods;
};
const parseImplMaps = (
bytes: Buffer,
layout: ManagedMetadataLayout,
maxBytes: number,
modules: readonly ModuleRef[],
members: ReadonlyMap<string, MemberCore>,
): readonly NativeImport[] => {
const moduleNames = new Map(modules.map((module) => [module.token, module]));
const imports: NativeImport[] = [];
const table = layout.table(28);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
const cursor = rowCursor(bytes, layout, 28, row);
const mappingFlags = cursor.readUInt16();
const memberToken = codedToken(
cursor.readIndex(layout.codedIndexSize("MemberForwarded")),
1,
[4, 6],
);
const importName = readMetadataString(
bytes,
layout,
cursor.readIndex(layout.stringIndexSize),
maxBytes,
);
const importScopeRow = cursor.readIndex(layout.tableIndexSize(26));
const importScopeToken =
importScopeRow === 0 ? null : metadataToken(26, importScopeRow);
const member = memberToken === null ? undefined : members.get(memberToken);
imports.push({
token: metadataToken(28, row),
row_offset: cursor.start,
mapping_flags: mappingFlags,
mapping_flags_hex: flagsHex(mappingFlags),
member_token: memberToken,
member_kind: member?.kind ?? "unknown",
member_name: member?.name ?? null,
import_name: importName,
import_scope_token: importScopeToken,
import_scope_name:
importScopeToken === null
? null
: (moduleNames.get(importScopeToken)?.name ?? null),
no_mangle: (mappingFlags & 0x0001) !== 0,
char_set: charSet(mappingFlags),
call_convention: callConvention(mappingFlags),
supports_last_error: (mappingFlags & 0x0040) !== 0,
best_fit: bestFit(mappingFlags),
throw_on_unmappable_char: throwOnUnmappable(mappingFlags),
verification: "managed-declaration-only",
});
}
return imports;
};
const charSet = (flags: number): NativeImport["char_set"] => {
switch (flags & 0x0006) {
case 0x0000:
return "not-specified";
case 0x0002:
return "ansi";
case 0x0004:
return "unicode";
case 0x0006:
return "auto";
default:
return "unknown";
}
};
const callConvention = (flags: number): NativeImport["call_convention"] => {
switch (flags & 0x0700) {
case 0x0000:
return "not-specified";
case 0x0100:
return "winapi";
case 0x0200:
return "cdecl";
case 0x0300:
return "stdcall";
case 0x0400:
return "thiscall";
case 0x0500:
return "fastcall";
default:
return "unknown";
}
};
const bestFit = (flags: number): NativeImport["best_fit"] => {
switch (flags & 0x0030) {
case 0x0000:
return "assembly-default";
case 0x0010:
return "enabled";
case 0x0020:
return "disabled";
default:
return "unknown";
}
};
const throwOnUnmappable = (
flags: number,
): NativeImport["throw_on_unmappable_char"] => {
switch (flags & 0x3000) {
case 0x0000:
return "assembly-default";
case 0x1000:
return "enabled";
case 0x2000:
return "disabled";
default:
return "unknown";
}
};
const nativeImplementations = (
methods: Iterable<MemberCore>,
pinvokeTokens: ReadonlySet<string>,
): readonly NativeImplementation[] => {
const implementations: NativeImplementation[] = [];
for (const method of methods) {
const implFlags = method.implFlags ?? 0;
const codeType = codeTypeFor(implFlags);
const managedKind = managedKindFor(implFlags);
const pinvokeDeclared =
pinvokeTokens.has(method.token) || (method.flags & 0x2000) !== 0;
if (!pinvokeDeclared && codeType === "il" && managedKind === "managed")
continue;
implementations.push({
token: method.token,
row_offset: method.rowOffset,
name: method.name,
rva: method.rva ?? 0,
flags: method.flags,
impl_flags: implFlags,
code_type: codeType,
managed_kind: managedKind,
pinvoke_declared: pinvokeDeclared,
boundary_kind: boundaryKind(pinvokeDeclared, codeType, managedKind),
body_interpretation:
codeType === "il" && managedKind === "managed" && !pinvokeDeclared
? "managed-cil"
: method.rva === 0
? "not-file-backed"
: "native-or-runtime",
});
}
return implementations;
};
const codeTypeFor = (implFlags: number): NativeImplementation["code_type"] => {
switch (implFlags & 0x0003) {
case 0x0000:
return "il";
case 0x0001:
return "native";
case 0x0002:
return "optil";
case 0x0003:
return "runtime";
default:
return "unknown";
}
};
const managedKindFor = (
implFlags: number,
): NativeImplementation["managed_kind"] => {
switch (implFlags & 0x0004) {
case 0x0000:
return "managed";
case 0x0004:
return "unmanaged";
default:
return "unknown";
}
};
const boundaryKind = (
pinvokeDeclared: boolean,
codeType: NativeImplementation["code_type"],
managedKind: NativeImplementation["managed_kind"],
): NativeImplementation["boundary_kind"] => {
if (pinvokeDeclared) return "pinvoke";
if (codeType === "native") return "native-body";
if (codeType === "runtime") return "runtime-provided";
if (managedKind === "unmanaged") return "unmanaged-method";
return "mixed-or-unknown";
};
const cliNative = (
pe: ManagedPeLayout,
): ManagedNativeBoundaryInspection["cli_native"] => {
if (pe.cli === null)
return {
il_only: false,
requires_32bit: false,
strong_name_signed: false,
native_entry_point: false,
ready_to_run_signature: false,
managed_native_header_rva: 0,
managed_native_header_size: 0,
};
return {
il_only: (pe.cli.flags & 0x0000_0001) !== 0,
requires_32bit: (pe.cli.flags & 0x0000_0002) !== 0,
strong_name_signed: (pe.cli.flags & 0x0000_0008) !== 0,
native_entry_point: (pe.cli.flags & 0x0000_0010) !== 0,
ready_to_run_signature: pe.cli.readyToRunSignature,
managed_native_header_rva: pe.cli.managedNativeHeader.rva,
managed_native_header_size: pe.cli.managedNativeHeader.size,
};
};
const unavailable = (
const emptyInspection = (
target: BinaryTarget,
bytes: Buffer,
limits: ManagedNativeBoundaryInspectionLimits,
issue: ManagedParseIssue | null,
classification: "not-managed" | "malformed" | "not-available" | "managed",
issues: readonly ManagedParseIssue[] = [],
): ManagedNativeBoundaryInspection =>
managedNativeBoundaryInspectionSchema.parse({
schema_version: 1,
@@ -440,7 +68,14 @@ const unavailable = (
format: "pe",
},
module: null,
metadata: { status: "absent", version: null, table_row_counts: {} },
metadata: {
status:
classification === "malformed" || classification === "not-available"
? "malformed"
: "absent",
version: null,
table_row_counts: {},
},
identity_scope: {
token_identity: "build-local",
requires_artifact_sha256: target.sha256,
@@ -455,13 +90,9 @@ const unavailable = (
managed_native_header_rva: 0,
managed_native_header_size: 0,
},
module_refs: page([], limits.moduleRefOffset, limits.moduleRefLimit),
pinvoke_imports: page([], limits.importOffset, limits.importLimit),
native_implementations: page(
[],
limits.implementationOffset,
limits.implementationLimit,
),
module_refs: emptyPage(),
pinvoke_imports: emptyPage(),
native_implementations: emptyPage(),
summary: {
module_ref_count: 0,
pinvoke_import_count: 0,
@@ -469,132 +100,132 @@ const unavailable = (
ready_to_run: false,
mixed_mode_or_native_header: false,
},
coverage: {
state: "unavailable",
issues: issue === null ? [] : [issue],
},
coverage: { state: classification, issues },
limitations: [
"The PE has no admitted CLI metadata; managed/native boundary inspection is unavailable.",
"No CLI data was admitted; native boundary declarations are unavailable.",
"Static inspection does not load or execute target code, so native export resolution is not performed.",
],
});
/** Inspect PE/CLI managed/native boundary declarations without executing code. */
const readBoundaryInventory = (
bytes: Buffer,
pe: ManagedPeLayout,
limits: ManagedNativeBoundaryInspectionLimits,
): {
readonly layout: ManagedMetadataLayout;
readonly inventory: Inventory;
} => {
const cli = pe.cli!;
const metadataOffset = pe.rvaToOffset(
cli.metadata.rva,
cli.metadata.size,
"cli.metadata",
);
const layout = readManagedMetadataLayout(
bytes,
metadataOffset,
cli.metadata.size,
limits.maxTableRows,
);
const resourceDirectory: ManagedResourceDirectory = {
offset: pe.rvaToOffset(
cli.resources.rva,
cli.resources.size,
"cli.resources",
),
size: cli.resources.size,
};
const inventory = readManagedMetadataInventory(
bytes,
layout,
{
referenceOffset: 0,
referenceLimit: 0,
resourceOffset: 0,
resourceLimit: 0,
attributeOffset: 0,
attributeLimit: 0,
maxHeapItemBytes: limits.maxHeapItemBytes,
},
resourceDirectory,
);
return { layout, inventory };
};
const metadataLimitIssue = (
cli: NonNullable<ManagedPeLayout["cli"]>,
maxBytes: number,
): ManagedParseIssue => ({
code: "limit-exceeded",
scope: "cli.metadata",
offset: cli.headerOffset + 8,
detail: `CLI metadata size exceeds max_metadata_bytes ${String(maxBytes)}`,
});
/** Inspect managed/native boundary declarations from PE metadata without execution. */
export const inspectManagedNativeBoundariesBytes = (
bytes: Buffer,
target: BinaryTarget,
limits: ManagedNativeBoundaryInspectionLimits,
): ManagedNativeBoundaryInspection => {
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) return unavailable(target, bytes, limits, pe.cliIssue);
const issues: ManagedParseIssue[] = [];
let pe: ManagedPeLayout;
try {
const rootOffset = pe.rvaToOffset(
pe.cli.metadata.rva,
pe.cli.metadata.size,
"cli.metadata",
);
if (pe.cli.metadata.size > limits.maxMetadataBytes)
throw managedFailure(
"limit-exceeded",
"metadata.root",
`CLI metadata size exceeds max_metadata_bytes ${String(limits.maxMetadataBytes)}`,
rootOffset,
);
const layout = readManagedMetadataLayout(
bytes,
rootOffset,
pe.cli.metadata.size,
limits.maxTableRows,
);
const inventory = readManagedMetadataInventory(
bytes,
layout,
{
referenceOffset: 0,
referenceLimit: 1,
resourceOffset: 0,
resourceLimit: 1,
attributeOffset: 0,
attributeLimit: 1,
maxHeapItemBytes: limits.maxHeapItemBytes,
},
null,
);
issues.push(...inventory.issues);
const moduleRefs = parseModuleRefs(bytes, layout, limits.maxHeapItemBytes);
const fields = parseFields(bytes, layout, limits.maxHeapItemBytes);
const methods = parseMethods(bytes, layout, limits.maxHeapItemBytes);
const members = new Map([...fields, ...methods]);
const imports = parseImplMaps(
bytes,
layout,
limits.maxHeapItemBytes,
moduleRefs,
members,
);
const implementations = nativeImplementations(
methods.values(),
new Set(
imports
.map(({ member_token: token }) => token)
.filter((token): token is string => token !== null),
),
);
const native = cliNative(pe);
return managedNativeBoundaryInspectionSchema.parse({
schema_version: 1,
artifact: {
path: target.path,
sha256: target.sha256,
byte_length: bytes.length,
format: "pe",
},
module: inventory.module,
metadata: {
status: issues.length === 0 ? "complete" : "partial",
version: layout.version,
table_row_counts: managedTableRowCounts(layout),
},
identity_scope: {
token_identity: "build-local",
requires_artifact_sha256: target.sha256,
requires_mvid: inventory.module?.mvid ?? null,
},
cli_native: native,
module_refs: page(
moduleRefs,
limits.moduleRefOffset,
limits.moduleRefLimit,
),
pinvoke_imports: page(imports, limits.importOffset, limits.importLimit),
native_implementations: page(
implementations,
limits.implementationOffset,
limits.implementationLimit,
),
summary: {
module_ref_count: moduleRefs.length,
pinvoke_import_count: imports.length,
native_implementation_count: implementations.length,
ready_to_run: native.ready_to_run_signature,
mixed_mode_or_native_header:
native.managed_native_header_rva !== 0 ||
native.managed_native_header_size !== 0 ||
native.native_entry_point,
},
coverage: {
state: issues.length === 0 ? "complete" : "partial",
issues,
},
limitations: [
"P/Invoke rows prove managed import declarations only; this inspection does not verify that a native library, export, thunk, or provider-qualified function exists.",
"Managed metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
"ReadyToRun, NativeAOT, C++/CLI, and IL2CPP native semantics require separately selected native-provider evidence; this tool does not translate managed tokens into native addresses.",
],
});
pe = readManagedPeLayout(bytes);
} catch (cause: unknown) {
if (cause instanceof ManagedReaderFailure)
return unavailable(target, bytes, limits, cause.issue);
throw cause;
if (!(cause instanceof ManagedReaderFailure)) throw cause;
return emptyInspection(target, bytes, "malformed", [cause.issue]);
}
if (pe.cli === null)
return emptyInspection(
target,
bytes,
pe.cliDirectoryPresent ? "malformed" : "not-managed",
pe.cliIssue === null ? [] : [pe.cliIssue],
);
if (pe.cli.metadata.size > limits.maxMetadataBytes)
return emptyInspection(target, bytes, "malformed", [
metadataLimitIssue(pe.cli, limits.maxMetadataBytes),
]);
let layout: ManagedMetadataLayout;
let inventory: Inventory;
try {
({ layout, inventory } = readBoundaryInventory(bytes, pe, limits));
} catch (cause: unknown) {
if (!(cause instanceof ManagedReaderFailure)) throw cause;
return emptyInspection(target, bytes, "malformed", [cause.issue]);
}
const moduleRefs = parseModuleRefs(bytes, layout, limits.maxHeapItemBytes);
const members = new Map([
...parseFields(bytes, layout, limits.maxHeapItemBytes),
...parseMethods(bytes, layout, limits.maxHeapItemBytes),
]);
const imports = parseImplMaps({
bytes,
layout,
maxBytes: limits.maxHeapItemBytes,
modules: moduleRefs,
members,
});
const pinvokeTokens = new Set(
imports
.map(({ member_token }) => member_token)
.filter((token): token is string => token !== null),
);
const implementations = nativeImplementations(
members.values(),
pinvokeTokens,
);
return buildNativeBoundaryInspection({
target,
bytes,
limits,
pe,
layout,
inventory,
moduleRefs,
imports,
implementations,
native: cliNative(pe),
issues: [],
});
};
+104 -45
View File
@@ -43,6 +43,29 @@ export interface ManagedPeLayout {
rvaToOffset(rva: number, size: number, scope: string): number;
}
interface RvaMapping {
readonly bytes: Buffer;
readonly sections: readonly PeSection[];
readonly rva: number;
readonly size: number;
readonly scope: string;
}
interface CoffHeader {
readonly coff: number;
readonly machine: number;
readonly sectionCount: number;
readonly optionalSize: number;
readonly characteristics: number;
}
interface OptionalHeader {
readonly optionalOffset: number;
readonly optionalHeader: "pe32" | "pe32-plus";
readonly directoryCountOffset: number;
readonly directoryOffset: number;
}
const requireRange = (
bytes: Buffer,
offset: number,
@@ -95,13 +118,8 @@ const architectureFor = (machine: number): ManagedPeLayout["architecture"] => {
}
};
const mapRva = (
bytes: Buffer,
sections: readonly PeSection[],
rva: number,
size: number,
scope: string,
): number => {
const mapRva = (mapping: RvaMapping): number => {
const { bytes, sections, rva, size, scope } = mapping;
if (rva === 0 && size === 0) return 0;
for (const section of sections) {
const mappedSize = Math.max(section.virtualSize, section.rawSize);
@@ -144,7 +162,13 @@ const readCliHeader = (
"cli.header",
"CLI header is shorter than the required 72 bytes",
);
const offset = mapRva(bytes, sections, directory.rva, 72, "cli.header");
const offset = mapRva({
bytes,
sections,
rva: directory.rva,
size: 72,
scope: "cli.header",
});
const headerSize = bytes.readUInt32LE(offset);
if (headerSize < 72 || headerSize > directory.size)
throw managedFailure(
@@ -165,13 +189,13 @@ const readCliHeader = (
const managedNativeHeader = readDirectory(bytes, offset + 64);
let readyToRunSignature = false;
if (managedNativeHeader.rva !== 0 && managedNativeHeader.size >= 4) {
const nativeOffset = mapRva(
const nativeOffset = mapRva({
bytes,
sections,
managedNativeHeader.rva,
4,
"cli.managed-native-header",
);
rva: managedNativeHeader.rva,
size: 4,
scope: "cli.managed-native-header",
});
readyToRunSignature = bytes.readUInt32LE(nativeOffset) === 0x0052_5452;
}
return {
@@ -189,16 +213,7 @@ const readCliHeader = (
};
};
/** Parse the file-backed PE and CLI layout without loading target code. */
export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
requireRange(bytes, 0, 64, "pe.dos-header");
if (bytes[0] !== 0x4d || bytes[1] !== 0x5a)
throw managedFailure(
"invalid-directory",
"pe.dos-header",
"Artifact does not start with an MZ header",
0,
);
const readCoffHeader = (bytes: Buffer): CoffHeader => {
const peOffset = bytes.readUInt32LE(0x3c);
requireRange(bytes, peOffset, 24, "pe.coff-header");
if (bytes.readUInt32LE(peOffset) !== 0x0000_4550)
@@ -220,9 +235,13 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
);
const optionalSize = bytes.readUInt16LE(coff + 16);
const characteristics = bytes.readUInt16LE(coff + 18);
const optionalOffset = coff + 20;
requireRange(bytes, optionalOffset, optionalSize, "pe.optional-header");
if (optionalSize < 96)
return { coff, machine, sectionCount, optionalSize, characteristics };
};
const readOptionalHeader = (bytes: Buffer, coff: CoffHeader): OptionalHeader => {
const optionalOffset = coff.coff + 20;
requireRange(bytes, optionalOffset, coff.optionalSize, "pe.optional-header");
if (coff.optionalSize < 96)
throw managedFailure(
"invalid-directory",
"pe.optional-header",
@@ -239,16 +258,29 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
"PE optional header magic is unsupported",
optionalOffset,
);
const directoryCountOffset =
optionalOffset + (optionalHeader === "pe32" ? 92 : 108);
const directoryOffset =
optionalOffset + (optionalHeader === "pe32" ? 96 : 112);
requireRange(bytes, directoryCountOffset, 4, "pe.data-directories");
const directoryCount = bytes.readUInt32LE(directoryCountOffset);
const sectionOffset = add(optionalOffset, optionalSize, "pe.sections");
requireRange(bytes, sectionOffset, sectionCount * 40, "pe.sections");
return {
optionalOffset,
optionalHeader,
directoryCountOffset:
optionalOffset + (optionalHeader === "pe32" ? 92 : 108),
directoryOffset: optionalOffset + (optionalHeader === "pe32" ? 96 : 112),
};
};
const readSections = (
bytes: Buffer,
coff: CoffHeader,
optional: OptionalHeader,
): readonly PeSection[] => {
const sectionOffset = add(optional.optionalOffset, coff.optionalSize, "pe.sections");
requireRange(
bytes,
sectionOffset,
coff.sectionCount * 40,
"pe.sections",
);
const sections: PeSection[] = [];
for (let index = 0; index < sectionCount; index += 1) {
for (let index = 0; index < coff.sectionCount; index += 1) {
const offset = sectionOffset + index * 40;
const section = {
virtualSize: bytes.readUInt32LE(offset + 8),
@@ -265,11 +297,38 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
);
sections.push(section);
}
const cliDirectory =
return sections;
};
const readCliDirectory = (
bytes: Buffer,
coff: CoffHeader,
optional: OptionalHeader,
): PeDataDirectory => {
requireRange(bytes, optional.directoryCountOffset, 4, "pe.data-directories");
const directoryCount = bytes.readUInt32LE(optional.directoryCountOffset);
if (
directoryCount <= 14 ||
optionalSize < directoryOffset - optionalOffset + 120
? { rva: 0, size: 0 }
: readDirectory(bytes, directoryOffset + 14 * 8);
coff.optionalSize < optional.directoryOffset - optional.optionalOffset + 120
)
return { rva: 0, size: 0 };
return readDirectory(bytes, optional.directoryOffset + 14 * 8);
};
/** Parse the file-backed PE and CLI layout without loading target code. */
export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
requireRange(bytes, 0, 64, "pe.dos-header");
if (bytes[0] !== 0x4d || bytes[1] !== 0x5a)
throw managedFailure(
"invalid-directory",
"pe.dos-header",
"Artifact does not start with an MZ header",
0,
);
const coff = readCoffHeader(bytes);
const optional = readOptionalHeader(bytes, coff);
const sections = readSections(bytes, coff, optional);
const cliDirectory = readCliDirectory(bytes, coff, optional);
const cliDirectoryPresent = cliDirectory.rva !== 0 || cliDirectory.size !== 0;
let cli: ManagedCliHeader | null = null;
let cliIssue: ManagedParseIssue | null = null;
@@ -281,16 +340,16 @@ export const readManagedPeLayout = (bytes: Buffer): ManagedPeLayout => {
cliIssue = cause.issue;
}
return {
machine,
architecture: architectureFor(machine),
optionalHeader,
sectionCount,
characteristics,
machine: coff.machine,
architecture: architectureFor(coff.machine),
optionalHeader: optional.optionalHeader,
sectionCount: coff.sectionCount,
characteristics: coff.characteristics,
sections,
cli,
cliDirectoryPresent,
cliIssue,
rvaToOffset: (rva, size, scope) =>
mapRva(bytes, sections, rva, size, scope),
mapRva({ bytes, sections, rva, size, scope }),
};
};