Files
rea/src/cli/managedCommands.ts
T

501 lines
17 KiB
TypeScript

import { z } from "incur";
import {
compareManagedMemberPaths,
type ManagedMemberPathInspectionLimits,
} from "../application/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../application/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../application/ManagedReconstructionService.js";
import { planManagedRuntimeCorrelationEvidence } from "../application/ManagedRuntimeCorrelationService.js";
import { runProviderAnalysis } from "../application/DirectAnalysis.js";
import { parseConfig } from "../config.js";
import { loadConfiguredPermissionAuthority } from "../application/PermissionConfiguration.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { projectAnalysisError } from "../domain/errors.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "../logger.js";
import type { CliInstance } from "./types.js";
import { registerProjectManagedApplicationGraph } from "./managedProjectGraphCommand.js";
const managedOffset = (subject: string) =>
z.number().int().min(0).default(0).describe(`Zero-based ${subject} offset`);
const managedLimit = (subject: string, maximum: number, fallback: number) =>
z
.number()
.int()
.min(1)
.max(maximum)
.default(fallback)
.describe(`Maximum ${subject} to return`);
const managedInspectionLimits = {
maxFileBytes: z
.number()
.int()
.min(4_096)
.max(1_073_741_824)
.default(268_435_456)
.describe("Maximum managed artifact file size accepted for inspection"),
maxMetadataBytes: z
.number()
.int()
.min(256)
.max(268_435_456)
.default(67_108_864)
.describe("Maximum CLI metadata region size accepted for inspection"),
maxTableRows: z
.number()
.int()
.min(1)
.max(1_000_000)
.default(100_000)
.describe("Maximum rows accepted in any CLI metadata table"),
maxHeapItemBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576)
.describe("Maximum bytes accepted for one CLI metadata heap item"),
};
export const registerManagedCommands = (
cli: CliInstance,
logger: Logger,
): void => {
registerInspectManagedArtifact(cli, logger);
registerInspectManagedMembers(cli, logger);
registerInspectManagedNativeBoundaries(cli, logger);
registerCompareManagedMembers(cli, logger);
registerImportManagedReconstruction(cli, logger);
registerVerifyManagedNativeBoundaries(cli, logger);
registerPlanManagedRuntimeCorrelation(cli, logger);
registerProjectManagedApplicationGraph(cli, logger);
};
const registerInspectManagedArtifact = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectManagedArtifact, {
description: "Inspect PE/CLI identity without loading target code",
args: z.object({
path: z.string().describe("Managed PE executable or assembly path"),
}),
options: z.object({
referenceOffset: managedOffset("assembly-reference"),
referenceLimit: managedLimit("assembly references", 500, 100),
resourceOffset: managedOffset("manifest-resource"),
resourceLimit: managedLimit("manifest resources", 500, 100),
attributeOffset: managedOffset("assembly-attribute"),
attributeLimit: managedLimit("assembly attributes", 500, 100),
...managedInspectionLimits,
}),
alias: {
referenceOffset: "reference-offset",
referenceLimit: "reference-limit",
resourceOffset: "resource-offset",
resourceLimit: "resource-limit",
attributeOffset: "attribute-offset",
attributeLimit: "attribute-limit",
maxFileBytes: "max-file-bytes",
maxMetadataBytes: "max-metadata-bytes",
maxTableRows: "max-table-rows",
maxHeapItemBytes: "max-heap-item-bytes",
},
run: ({ args, options }) =>
logCliCommand(logger, "inspect-managed-artifact", () =>
runProviderAnalysis(
args.path,
"inspect_managed_artifact",
{
reference_offset: options.referenceOffset,
reference_limit: options.referenceLimit,
resource_offset: options.resourceOffset,
resource_limit: options.resourceLimit,
attribute_offset: options.attributeOffset,
attribute_limit: options.attributeLimit,
max_file_bytes: options.maxFileBytes,
max_metadata_bytes: options.maxMetadataBytes,
max_table_rows: options.maxTableRows,
max_heap_item_bytes: options.maxHeapItemBytes,
},
logger,
),
),
});
};
const registerInspectManagedMembers = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectManagedMembers, {
description:
"Inspect PE/CLI metadata members, signatures, and CIL anchors without loading target code",
args: z.object({
path: z.string().describe("Managed PE executable or assembly path"),
}),
options: z.object({
typeOffset: managedOffset("type definition"),
typeLimit: managedLimit("type definitions", 500, 100),
methodOffset: managedOffset("method definition"),
methodLimit: managedLimit("method definitions", 500, 100),
fieldOffset: managedOffset("field definition"),
fieldLimit: managedLimit("field definitions", 500, 100),
memberRefOffset: managedOffset("member reference"),
memberRefLimit: managedLimit("member references", 500, 100),
edgeOffset: managedOffset("member edge"),
edgeLimit: managedLimit("member edges", 1_000, 250),
instructionAnchorLimit: z
.number()
.int()
.min(0)
.max(500)
.default(100)
.describe("Maximum CIL instruction anchors per method"),
...managedInspectionLimits,
maxMethodBodyBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576)
.describe("Maximum CIL method body size accepted for inspection"),
maxMethodInstructions: z
.number()
.int()
.min(1)
.max(100_000)
.default(10_000)
.describe("Maximum decoded CIL instructions per method"),
}),
alias: {
typeOffset: "type-offset",
typeLimit: "type-limit",
methodOffset: "method-offset",
methodLimit: "method-limit",
fieldOffset: "field-offset",
fieldLimit: "field-limit",
memberRefOffset: "member-ref-offset",
memberRefLimit: "member-ref-limit",
edgeOffset: "edge-offset",
edgeLimit: "edge-limit",
instructionAnchorLimit: "instruction-anchor-limit",
maxFileBytes: "max-file-bytes",
maxMetadataBytes: "max-metadata-bytes",
maxTableRows: "max-table-rows",
maxHeapItemBytes: "max-heap-item-bytes",
maxMethodBodyBytes: "max-method-body-bytes",
maxMethodInstructions: "max-method-instructions",
},
run: ({ args, options }) =>
logCliCommand(logger, "inspect-managed-members", () =>
runProviderAnalysis(
args.path,
"inspect_managed_members",
{
type_offset: options.typeOffset,
type_limit: options.typeLimit,
method_offset: options.methodOffset,
method_limit: options.methodLimit,
field_offset: options.fieldOffset,
field_limit: options.fieldLimit,
member_ref_offset: options.memberRefOffset,
member_ref_limit: options.memberRefLimit,
edge_offset: options.edgeOffset,
edge_limit: options.edgeLimit,
instruction_anchor_limit: options.instructionAnchorLimit,
max_file_bytes: options.maxFileBytes,
max_metadata_bytes: options.maxMetadataBytes,
max_table_rows: options.maxTableRows,
max_heap_item_bytes: options.maxHeapItemBytes,
max_method_body_bytes: options.maxMethodBodyBytes,
max_method_instructions: options.maxMethodInstructions,
},
logger,
),
),
});
};
const registerInspectManagedNativeBoundaries = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.inspectManagedNativeBoundaries, {
description:
"Inspect PE/CLI PInvoke and native implementation boundary declarations without loading target code",
args: z.object({
path: z.string().describe("Managed PE executable or assembly path"),
}),
options: z.object({
moduleRefOffset: managedOffset("module reference"),
moduleRefLimit: managedLimit("module references", 500, 100),
importOffset: managedOffset("platform import"),
importLimit: managedLimit("platform imports", 500, 100),
implementationOffset: managedOffset("native implementation"),
implementationLimit: managedLimit("native implementations", 500, 100),
...managedInspectionLimits,
}),
alias: {
moduleRefOffset: "module-ref-offset",
moduleRefLimit: "module-ref-limit",
importOffset: "import-offset",
importLimit: "import-limit",
implementationOffset: "implementation-offset",
implementationLimit: "implementation-limit",
maxFileBytes: "max-file-bytes",
maxMetadataBytes: "max-metadata-bytes",
maxTableRows: "max-table-rows",
maxHeapItemBytes: "max-heap-item-bytes",
},
run: ({ args, options }) =>
logCliCommand(logger, "inspect-managed-native-boundaries", () =>
runProviderAnalysis(
args.path,
"inspect_managed_native_boundaries",
{
module_ref_offset: options.moduleRefOffset,
module_ref_limit: options.moduleRefLimit,
import_offset: options.importOffset,
import_limit: options.importLimit,
implementation_offset: options.implementationOffset,
implementation_limit: options.implementationLimit,
max_file_bytes: options.maxFileBytes,
max_metadata_bytes: options.maxMetadataBytes,
max_table_rows: options.maxTableRows,
max_heap_item_bytes: options.maxHeapItemBytes,
},
logger,
),
),
});
};
const compareManagedMemberArgs = z.object({
leftPath: z.string().describe("Baseline managed PE executable or assembly"),
rightPath: z.string().describe("Candidate managed PE executable or assembly"),
});
const registerCompareManagedMembers = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.compareManagedMembers, {
description:
"Compare two managed PE/CLI member inventories without name-based matching",
args: compareManagedMemberArgs,
options: z.object({
maxMethodMatches: z
.number()
.int()
.min(1)
.max(50_000)
.default(10_000)
.describe("Maximum exact method matches to return"),
maxFieldMatches: z
.number()
.int()
.min(0)
.max(50_000)
.default(5_000)
.describe("Maximum exact field matches to return"),
maxCandidates: z
.number()
.int()
.min(1)
.max(500)
.default(50)
.describe("Maximum ambiguous candidates per unmatched member"),
typeLimit: managedLimit("type definitions per artifact", 500, 500),
methodLimit: managedLimit("method definitions per artifact", 500, 500),
fieldLimit: managedLimit("field definitions per artifact", 500, 500),
memberRefLimit: managedLimit("member references per artifact", 500, 500),
edgeLimit: managedLimit("member edges per artifact", 1_000, 1_000),
instructionAnchorLimit: z
.number()
.int()
.min(0)
.max(500)
.default(500)
.describe("Maximum CIL instruction anchors per compared method"),
...managedInspectionLimits,
maxMethodBodyBytes: z
.number()
.int()
.min(1)
.max(16_777_216)
.default(1_048_576)
.describe("Maximum CIL method body size accepted for comparison"),
maxMethodInstructions: z
.number()
.int()
.min(1)
.max(100_000)
.default(100_000)
.describe("Maximum decoded CIL instructions per compared method"),
}),
alias: {
maxMethodMatches: "max-method-matches",
maxFieldMatches: "max-field-matches",
maxCandidates: "max-candidates",
typeLimit: "type-limit",
methodLimit: "method-limit",
fieldLimit: "field-limit",
memberRefLimit: "member-ref-limit",
edgeLimit: "edge-limit",
instructionAnchorLimit: "instruction-anchor-limit",
maxFileBytes: "max-file-bytes",
maxMetadataBytes: "max-metadata-bytes",
maxTableRows: "max-table-rows",
maxHeapItemBytes: "max-heap-item-bytes",
maxMethodBodyBytes: "max-method-body-bytes",
maxMethodInstructions: "max-method-instructions",
},
run: ({ args, options }) =>
logCliCommand(logger, "compare-managed-members", async () => {
const result = await compareManagedMemberPaths({
leftPath: args.leftPath,
rightPath: args.rightPath,
comparisonLimits: {
max_method_matches: options.maxMethodMatches,
max_field_matches: options.maxFieldMatches,
max_candidates: options.maxCandidates,
},
memberLimits: buildMemberLimits(options),
});
return result.ok
? result.value
: {
error: "Managed member comparison failed",
...projectAnalysisError(result.error),
};
}),
});
};
interface ManagedMemberLimitOptions {
readonly maxFileBytes: number;
readonly typeLimit: number;
readonly methodLimit: number;
readonly fieldLimit: number;
readonly memberRefLimit: number;
readonly edgeLimit: number;
readonly instructionAnchorLimit: number;
readonly maxMetadataBytes: number;
readonly maxTableRows: number;
readonly maxHeapItemBytes: number;
readonly maxMethodBodyBytes: number;
readonly maxMethodInstructions: number;
}
const buildMemberLimits = (
options: ManagedMemberLimitOptions,
): ManagedMemberPathInspectionLimits => ({
maxFileBytes: options.maxFileBytes,
typeOffset: 0,
typeLimit: options.typeLimit,
methodOffset: 0,
methodLimit: options.methodLimit,
fieldOffset: 0,
fieldLimit: options.fieldLimit,
memberRefOffset: 0,
memberRefLimit: options.memberRefLimit,
edgeOffset: 0,
edgeLimit: options.edgeLimit,
instructionAnchorLimit: options.instructionAnchorLimit,
maxMetadataBytes: options.maxMetadataBytes,
maxTableRows: options.maxTableRows,
maxHeapItemBytes: options.maxHeapItemBytes,
maxMethodBodyBytes: options.maxMethodBodyBytes,
maxMethodInstructions: options.maxMethodInstructions,
});
const registerImportManagedReconstruction = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.importManagedReconstruction, {
description:
"Import decompiler-produced managed reconstruction against exact static member evidence",
args: z.object({
inputJson: z
.string()
.describe("Inline managed reconstruction JSON or JSON file path"),
}),
run: ({ args }) =>
logCliCommand(logger, "import-managed-reconstruction", async () => {
const input = await parseCliJsonInput(
args.inputJson,
"import-managed-reconstruction",
);
if (!input.ok) return input.error;
const result = importManagedReconstructionEvidence(input.value);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
});
};
const registerVerifyManagedNativeBoundaries = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.verifyManagedNativeBoundaries, {
description:
"Verify managed P/Invoke declarations against authenticated native Evidence",
args: z.object({
inputJson: z
.string()
.describe("Inline managed/native verification JSON or JSON file path"),
}),
run: ({ args }) =>
logCliCommand(logger, "verify-managed-native-boundaries", async () => {
const input = await parseCliJsonInput(
args.inputJson,
"verify-managed-native-boundaries",
);
if (!input.ok) return input.error;
const result = verifyManagedNativeBoundariesEvidence(input.value);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
});
};
const registerPlanManagedRuntimeCorrelation = (
cli: CliInstance,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.planManagedRuntimeCorrelation, {
description:
"Plan a separately authorized managed runtime-correlation experiment without executing target code",
args: z.object({
inputJson: z
.string()
.describe("Inline managed runtime-correlation JSON or JSON file path"),
}),
run: ({ args }) =>
logCliCommand(logger, "plan-managed-runtime-correlation", async () => {
const input = await parseCliJsonInput(
args.inputJson,
"plan-managed-runtime-correlation",
);
if (!input.ok) return input.error;
const config = parseConfig(process.env);
if (!config.ok) return projectAnalysisError(config.error);
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) return projectAnalysisError(authority.error);
const result = await planManagedRuntimeCorrelationEvidence(
{
policy: config.value.managedRuntimePolicy,
authority: authority.value,
},
input.value,
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
});
};