1263 lines
41 KiB
TypeScript
1263 lines
41 KiB
TypeScript
import { Cli, z } from "incur";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { runDoctor } from "./application/Doctor.js";
|
|
import {
|
|
runDirectAnalysis,
|
|
runProviderAnalysis,
|
|
runSessionStatus,
|
|
} from "./application/DirectAnalysis.js";
|
|
import { compareManagedMemberPaths } from "./application/ManagedMemberComparisonService.js";
|
|
import { projectManagedApplicationGraphEvidence } from "./application/ManagedApplicationGraphService.js";
|
|
import { verifyManagedNativeBoundariesEvidence } from "./application/ManagedNativeVerificationService.js";
|
|
import { importManagedReconstructionEvidence } from "./application/ManagedReconstructionService.js";
|
|
import { planManagedRuntimeCorrelationEvidence } from "./application/ManagedRuntimeCorrelationService.js";
|
|
import { runSetup, systemSetupHost } from "./application/Setup.js";
|
|
import { runUninstall } from "./application/Uninstall.js";
|
|
import { runUpgrade, systemUpgradeHost } from "./application/Upgrade.js";
|
|
import { PRODUCT_IDENTITY } from "./identity.js";
|
|
import { createLogger, parseLogLevel, type Logger } from "./logger.js";
|
|
import { logCliCommand } from "./cliLogging.js";
|
|
import { parseConfig } from "./config.js";
|
|
import { importReferenceSource } from "./application/ReferenceSourceImport.js";
|
|
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
|
|
import { registerProcessCommands } from "./cliProcessCommands.js";
|
|
import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
|
|
import {
|
|
AnalysisProtocolError,
|
|
PermissionRequiredError,
|
|
projectAnalysisError,
|
|
} from "./domain/errors.js";
|
|
import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
|
|
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
|
|
import { registerPolicyCommands } from "./cliPolicyCommands.js";
|
|
import { registerBrowserCommands } from "./cliBrowserCommands.js";
|
|
import { registerAdvancedBrowserCommands } from "./cliBrowserAdvancedCommands.js";
|
|
import { registerElectronCommands } from "./cliElectronCommands.js";
|
|
import { registerApplicationCommands } from "./cliApplicationCommands.js";
|
|
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
|
import { parseCliJsonInput } from "./cliJsonInput.js";
|
|
import {
|
|
analysisProviderSelectorSchema,
|
|
type AnalysisProviderSelector,
|
|
} from "./contracts/providerSelection.js";
|
|
import { createSystemDoctorHost } from "./doctorRuntime.js";
|
|
import {
|
|
conciseSetupResult,
|
|
confirmInteractiveSetup,
|
|
renderInteractiveSetupResult,
|
|
renderSetupProgress,
|
|
} from "./cliSetup.js";
|
|
|
|
/**
|
|
* Build the one-shot Incur CLI without starting Hopper at import time.
|
|
* Analysis commands acquire and close their own sessions; bare `mcp` and
|
|
* `--mcp` are intercepted by the executable dispatcher before this module loads.
|
|
*/
|
|
export const createCli = (): ReturnType<typeof Cli.create> => {
|
|
const logger = createLogger(
|
|
"cli",
|
|
process.env.REA_LOG_LEVEL === undefined
|
|
? "silent"
|
|
: parseLogLevel(process.env.REA_LOG_LEVEL),
|
|
);
|
|
const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
|
|
version: PRODUCT_IDENTITY.packageVersion,
|
|
description: "Reverse engineer anything from your terminal or agent.",
|
|
mcp: {
|
|
command: PRODUCT_IDENTITY.mcpCommand,
|
|
instructions:
|
|
"Ask what software, artifact, protocol, or behavior the user wants to understand, then choose the available investigation capabilities that can produce evidence.",
|
|
},
|
|
sync: {
|
|
cwd: fileURLToPath(new URL("..", import.meta.url)),
|
|
include: ["skills/*"],
|
|
suggestions: [
|
|
"understand how a software feature works",
|
|
"investigate an artifact or observed behavior",
|
|
"check my REA setup",
|
|
],
|
|
},
|
|
});
|
|
|
|
registerSetupCommands(cli, logger);
|
|
registerCoreCommands(cli, logger);
|
|
registerFunctionCommand(cli, logger);
|
|
registerSearchCommand(cli, logger);
|
|
registerXrefsCommand(cli, logger);
|
|
registerTraceCommand(cli, logger);
|
|
registerCapabilityCommands(cli, logger);
|
|
registerNativeCommands(cli, logger);
|
|
registerArtifactCommands(cli, logger);
|
|
registerManagedCommands(cli, logger);
|
|
registerInvestigationCommands(cli, logger);
|
|
registerEvidenceCommands(cli, logger);
|
|
registerReferenceSourceCommand(cli, logger);
|
|
registerProcessCommands(cli, logger);
|
|
registerPolicyCommands(cli, logger);
|
|
registerBrowserCommands(cli, logger);
|
|
registerAdvancedBrowserCommands(cli, logger);
|
|
registerElectronCommands(cli, logger);
|
|
registerApplicationCommands(cli, logger);
|
|
return cli;
|
|
};
|
|
|
|
const registerCoreCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
const overviewOptions = z.object({
|
|
detail: z
|
|
.enum(["concise", "detailed"])
|
|
.default("concise")
|
|
.describe("Overview detail level"),
|
|
limit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(50)
|
|
.default(10)
|
|
.describe("Maximum overview items to return"),
|
|
snapshot: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Load and update a local analysis snapshot"),
|
|
provider: providerSelectionOption,
|
|
});
|
|
cli.command(CLI_COMMANDS.analyze, {
|
|
description: "Get an overview of an app",
|
|
args: z.object({
|
|
path: z.string().describe("App, program, or analysis database path"),
|
|
}),
|
|
options: overviewOptions,
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "analyze", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"binary_overview",
|
|
{ detail: options.detail, limit: options.limit },
|
|
directAnalysisOptions(logger, options.snapshot, options.provider),
|
|
),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.inspect, {
|
|
description: "Inspect an app overview with evidence",
|
|
args: z.object({
|
|
path: z.string().describe("App, program, or analysis database path"),
|
|
}),
|
|
options: overviewOptions,
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "inspect", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"binary_overview",
|
|
{ detail: options.detail, limit: options.limit },
|
|
directAnalysisOptions(logger, options.snapshot, options.provider),
|
|
),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.decompile, {
|
|
description: "Read one part of an app as code",
|
|
args: z.object({
|
|
path: z.string().describe("App or program path"),
|
|
address: z.string().describe("Procedure address"),
|
|
}),
|
|
options: z.object({
|
|
snapshot: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Load and update a local analysis snapshot"),
|
|
provider: providerSelectionOption,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "decompile", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"procedure_pseudo_code",
|
|
{ procedure: args.address },
|
|
directAnalysisOptions(logger, options.snapshot, options.provider),
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerSetupCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.setup, {
|
|
description: "Install requirements and configure agents",
|
|
outputPolicy: "agent-only",
|
|
options: z.object({
|
|
yes: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Approve user-owned setup actions without prompting"),
|
|
installHopper: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Also approve Hopper installation with --yes"),
|
|
client: z
|
|
.array(
|
|
z.enum([
|
|
"claude_code",
|
|
"claude_desktop",
|
|
"codex",
|
|
"cursor",
|
|
"gemini_cli",
|
|
"windsurf",
|
|
]),
|
|
)
|
|
.default([])
|
|
.describe("Agent integration to configure; repeat for multiple agents"),
|
|
allDetected: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Configure every detected supported agent"),
|
|
skill: z
|
|
.boolean()
|
|
.optional()
|
|
.describe("Install or skip the bundled REA skill"),
|
|
dryRun: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Print the resolved plan without applying it"),
|
|
accessible: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Use sequential accessible setup prompts"),
|
|
}),
|
|
alias: {
|
|
yes: "y",
|
|
},
|
|
run: ({ options, formatExplicit, agent }) =>
|
|
logCliCommand(logger, "setup", () =>
|
|
runSetupCommand({ options, formatExplicit, agent }),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.doctor, {
|
|
description: "Check whether REA is ready",
|
|
options: z.object({
|
|
target: z.string().optional().describe("Optional app path to check"),
|
|
}),
|
|
run: ({ options }) =>
|
|
logCliCommand(logger, "doctor", () =>
|
|
runDoctor(options.target, createSystemDoctorHost()),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.uninstall, {
|
|
description: "Remove REA-owned agent configuration and skill files",
|
|
options: z.object({
|
|
purgeData: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Also remove REA caches and state"),
|
|
}),
|
|
alias: { purgeData: "purge-data" },
|
|
run: ({ options }) =>
|
|
logCliCommand(logger, "uninstall", () => runUninstall(options.purgeData)),
|
|
});
|
|
cli.command(CLI_COMMANDS.upgrade, {
|
|
description: "Upgrade a global npm installation to the latest REA release",
|
|
run: ({ formatExplicit }) =>
|
|
logCliCommand(logger, "upgrade", () =>
|
|
runUpgrade(
|
|
PRODUCT_IDENTITY.packageVersion,
|
|
systemUpgradeHost(),
|
|
formatExplicit ? "structured" : "human",
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const runSetupCommand = async (input: {
|
|
readonly options: {
|
|
readonly yes: boolean;
|
|
readonly installHopper: boolean;
|
|
readonly client: readonly string[];
|
|
readonly allDetected: boolean;
|
|
readonly skill?: boolean | undefined;
|
|
readonly dryRun: boolean;
|
|
readonly accessible: boolean;
|
|
};
|
|
readonly formatExplicit: boolean;
|
|
readonly agent: boolean;
|
|
}) => {
|
|
const { options } = input;
|
|
const interactive =
|
|
!options.yes &&
|
|
!options.dryRun &&
|
|
!input.formatExplicit &&
|
|
process.stdin.isTTY === true &&
|
|
process.stderr.isTTY === true;
|
|
const hasExplicitScope =
|
|
options.allDetected ||
|
|
options.client.length > 0 ||
|
|
options.skill !== undefined ||
|
|
options.installHopper;
|
|
if (options.yes && !hasExplicitScope)
|
|
process.stderr.write(
|
|
"! Implicit `rea setup --yes` scope is deprecated; use `--all-detected --skill` to retain it.\n",
|
|
);
|
|
const result = await runSetup(
|
|
{
|
|
approved: options.yes && !options.dryRun,
|
|
installHopper: options.installHopper,
|
|
structured: input.formatExplicit || options.dryRun,
|
|
proposeHopper: !hasExplicitScope || options.installHopper,
|
|
...(!hasExplicitScope || options.allDetected
|
|
? {}
|
|
: { clientIds: options.client }),
|
|
...(!hasExplicitScope ? {} : { installSkill: options.skill ?? false }),
|
|
...(interactive ? { onProgress: renderSetupProgress } : {}),
|
|
},
|
|
systemSetupHost(createSystemDoctorHost()),
|
|
interactive
|
|
? (actions) => confirmInteractiveSetup(actions, options.accessible)
|
|
: undefined,
|
|
);
|
|
if (interactive) renderInteractiveSetupResult(result);
|
|
return input.formatExplicit ? result : conciseSetupResult(result);
|
|
};
|
|
|
|
const registerXrefsCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.xrefs, {
|
|
description: "List bounded references to an analyzed address",
|
|
args: z.object({
|
|
path: z.string().describe("App or program path"),
|
|
address: z.string().describe("Hexadecimal address"),
|
|
}),
|
|
options: z.object({
|
|
snapshot: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Load and update a local analysis snapshot"),
|
|
provider: providerSelectionOption,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "xrefs", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"xrefs",
|
|
{ address: args.address },
|
|
directAnalysisOptions(logger, options.snapshot, options.provider),
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerTraceCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.trace, {
|
|
description: "Trace a bounded literal feature through analyzed references",
|
|
args: z.object({
|
|
path: z.string().describe("App or program path"),
|
|
query: z.string().min(1).describe("Literal feature query"),
|
|
}),
|
|
options: z.object({
|
|
caseSensitive: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Match the query with exact letter case"),
|
|
limit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(100)
|
|
.default(20)
|
|
.describe("Maximum matching roots to examine"),
|
|
maxOperations: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(100)
|
|
.default(20)
|
|
.describe("Maximum analysis operations in the trace"),
|
|
snapshot: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Load and update a local analysis snapshot"),
|
|
provider: providerSelectionOption,
|
|
}),
|
|
alias: {
|
|
caseSensitive: "case-sensitive",
|
|
maxOperations: "max-operations",
|
|
},
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "trace", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"trace_feature",
|
|
{
|
|
query: args.query,
|
|
case_sensitive: options.caseSensitive,
|
|
limit: options.limit,
|
|
max_operations: options.maxOperations,
|
|
},
|
|
directAnalysisOptions(logger, options.snapshot, options.provider),
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerCapabilityCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
for (const command of [
|
|
CLI_COMMANDS.capabilities,
|
|
CLI_COMMANDS.providers,
|
|
] as const) {
|
|
cli.command(command, {
|
|
description:
|
|
command === "capabilities"
|
|
? "List provider capabilities and side effects"
|
|
: "List configured analysis providers",
|
|
run: () => logCliCommand(logger, command, () => runSessionStatus(logger)),
|
|
});
|
|
}
|
|
};
|
|
|
|
const registerFunctionCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.function, {
|
|
description: "Analyze one bounded function with evidence",
|
|
args: z.object({
|
|
path: z.string().describe("App or program path"),
|
|
address: z.string().describe("Procedure name or address"),
|
|
}),
|
|
options: z.object({
|
|
includeAssembly: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Include bounded assembly instructions"),
|
|
limit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(500)
|
|
.default(100)
|
|
.describe("Maximum referenced items to return"),
|
|
maxPseudocodeChars: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(100_000)
|
|
.default(20_000)
|
|
.describe("Maximum pseudocode characters to return"),
|
|
maxInstructions: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(5_000)
|
|
.default(500)
|
|
.describe("Maximum assembly instructions to return"),
|
|
snapshot: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Load and update a local analysis snapshot"),
|
|
provider: providerSelectionOption,
|
|
}),
|
|
alias: {
|
|
includeAssembly: "include-assembly",
|
|
maxPseudocodeChars: "max-pseudocode-chars",
|
|
maxInstructions: "max-instructions",
|
|
},
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "function", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"analyze_function",
|
|
{
|
|
procedure: args.address,
|
|
include_assembly: options.includeAssembly,
|
|
limit: options.limit,
|
|
max_pseudocode_chars: options.maxPseudocodeChars,
|
|
max_instructions: options.maxInstructions,
|
|
},
|
|
directAnalysisOptions(logger, options.snapshot, options.provider),
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerSearchCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.search, {
|
|
description: "Search bounded analyzed strings or procedure names",
|
|
args: z.object({
|
|
path: z.string().describe("App or program path"),
|
|
pattern: z.string().min(1).describe("Literal text or regex pattern"),
|
|
}),
|
|
options: z.object({
|
|
kind: z
|
|
.enum(["strings", "procedures"])
|
|
.default("strings")
|
|
.describe("Analyzed item kind to search"),
|
|
mode: z
|
|
.enum(["literal", "regex"])
|
|
.default("literal")
|
|
.describe("Interpret the pattern as literal text or a regex"),
|
|
caseSensitive: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Match the pattern with exact letter case"),
|
|
offset: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.default(0)
|
|
.describe("Zero-based matching-item offset"),
|
|
limit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(100)
|
|
.default(100)
|
|
.describe("Maximum matching items to return"),
|
|
snapshot: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Load and update a local analysis snapshot"),
|
|
provider: providerSelectionOption,
|
|
}),
|
|
alias: { caseSensitive: "case-sensitive" },
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "search", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
options.kind === "strings" ? "search_strings" : "search_procedures",
|
|
{
|
|
pattern: args.pattern,
|
|
mode: options.mode,
|
|
case_sensitive: options.caseSensitive,
|
|
offset: options.offset,
|
|
limit: options.limit,
|
|
},
|
|
directAnalysisOptions(logger, options.snapshot, options.provider),
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerReferenceSourceCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.importReferenceSource, {
|
|
description: "Import a bounded source tree as historical reference only",
|
|
args: z.object({
|
|
root: z
|
|
.string()
|
|
.describe("Source root allowed by REA_REFERENCE_ROOTS_JSON"),
|
|
}),
|
|
run: ({ args }) =>
|
|
logCliCommand(logger, "import-reference-source", async () => {
|
|
const config = parseConfig(process.env);
|
|
if (!config.ok)
|
|
return {
|
|
error: "Import failed",
|
|
...projectAnalysisError(config.error),
|
|
};
|
|
const authority = await loadConfiguredPermissionAuthority(config.value);
|
|
if (!authority.ok)
|
|
return {
|
|
error: "Import failed",
|
|
...projectAnalysisError(authority.error),
|
|
};
|
|
const authorized = await authority.value.authorize(
|
|
{
|
|
capability: "reference_read",
|
|
roots: [args.root],
|
|
executables: [],
|
|
environment_names: [],
|
|
network: "none",
|
|
mount: false,
|
|
operation_identity: `import_reference_source:${args.root}`,
|
|
},
|
|
"read",
|
|
);
|
|
if (!authorized.ok) {
|
|
const error =
|
|
authorized.error instanceof PermissionRequiredError
|
|
? authorized.error
|
|
: new AnalysisProtocolError(authorized.error.message, {
|
|
cause: authorized.error,
|
|
});
|
|
return {
|
|
error: "Import failed",
|
|
...projectAnalysisError(error),
|
|
};
|
|
}
|
|
const imported = await importReferenceSource({
|
|
root: args.root,
|
|
caller: "rea-cli",
|
|
policy: config.value.referenceSourcePolicy,
|
|
importer: PRODUCT_IDENTITY.packageName,
|
|
importerVersion: null,
|
|
});
|
|
return imported.ok
|
|
? imported.value
|
|
: {
|
|
error: "Import failed",
|
|
...projectReferenceSourceImportError(imported.error),
|
|
};
|
|
}),
|
|
});
|
|
};
|
|
|
|
const registerArtifactCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.inventoryArtifact, {
|
|
description: "Build a bounded deterministic artifact graph",
|
|
args: z.object({
|
|
path: z.string().describe("Application or package path"),
|
|
}),
|
|
options: z.object({
|
|
offset: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.default(0)
|
|
.describe("Zero-based offset for graph nodes, occurrences, and edges"),
|
|
limit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(500)
|
|
.default(100)
|
|
.describe("Maximum nodes, occurrences, and edges per result section"),
|
|
integrityPolicy: z
|
|
.enum(["fail", "record-and-continue"])
|
|
.default("fail")
|
|
.describe("Behavior when declared artifact integrity does not match"),
|
|
integrityContinueApproved: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Approve continuing after recorded integrity mismatches"),
|
|
maxIntegrityMismatches: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(100)
|
|
.default(10)
|
|
.describe("Maximum integrity mismatches to record before stopping"),
|
|
nativeMountApproved: z
|
|
.boolean()
|
|
.default(false)
|
|
.describe("Approve read-only native mounting when required"),
|
|
}),
|
|
alias: {
|
|
integrityPolicy: "integrity-policy",
|
|
integrityContinueApproved: "integrity-continue-approved",
|
|
maxIntegrityMismatches: "max-integrity-mismatches",
|
|
nativeMountApproved: "native-mount-approved",
|
|
},
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "inventory-artifact", () =>
|
|
runProviderAnalysis(
|
|
args.path,
|
|
"inventory_artifact",
|
|
{
|
|
node_offset: options.offset,
|
|
node_limit: options.limit,
|
|
occurrence_offset: options.offset,
|
|
occurrence_limit: options.limit,
|
|
edge_offset: options.offset,
|
|
edge_limit: options.limit,
|
|
integrity_policy: options.integrityPolicy,
|
|
integrity_continue_approved: options.integrityContinueApproved,
|
|
max_integrity_mismatches: options.maxIntegrityMismatches,
|
|
native_mount_approved: options.nativeMountApproved,
|
|
},
|
|
logger,
|
|
),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.extractArtifact, {
|
|
description: "Extract explicitly selected artifact occurrences safely",
|
|
args: z.object({
|
|
path: z.string().describe("Application or package path"),
|
|
outputRoot: z.string().describe("Absent absolute output root"),
|
|
occurrenceIds: z
|
|
.array(z.string().regex(/^occ_[a-f0-9]{64}$/u))
|
|
.min(1)
|
|
.max(500)
|
|
.describe("Exact artifact occurrence IDs selected for extraction"),
|
|
}),
|
|
alias: { outputRoot: "output-root", occurrenceIds: "occurrence-ids" },
|
|
run: ({ args }) =>
|
|
logCliCommand(logger, "extract-artifact", () =>
|
|
runProviderAnalysis(
|
|
args.path,
|
|
"extract_artifact",
|
|
{
|
|
approved: true,
|
|
output_root: args.outputRoot,
|
|
occurrence_ids: args.occurrenceIds,
|
|
},
|
|
logger,
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
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"),
|
|
};
|
|
|
|
const registerManagedCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
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: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.default(0)
|
|
.describe("Zero-based assembly-reference offset"),
|
|
referenceLimit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(500)
|
|
.default(100)
|
|
.describe("Maximum assembly references to return"),
|
|
resourceOffset: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.default(0)
|
|
.describe("Zero-based manifest-resource offset"),
|
|
resourceLimit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(500)
|
|
.default(100)
|
|
.describe("Maximum manifest resources to return"),
|
|
attributeOffset: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.default(0)
|
|
.describe("Zero-based assembly-attribute offset"),
|
|
attributeLimit: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(500)
|
|
.default(100)
|
|
.describe("Maximum assembly attributes to return"),
|
|
...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,
|
|
),
|
|
),
|
|
});
|
|
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,
|
|
),
|
|
),
|
|
});
|
|
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,
|
|
),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.compareManagedMembers, {
|
|
description:
|
|
"Compare two managed PE/CLI member inventories without name-based matching",
|
|
args: z.object({
|
|
leftPath: z
|
|
.string()
|
|
.describe("Baseline managed PE executable or assembly"),
|
|
rightPath: z
|
|
.string()
|
|
.describe("Candidate managed PE executable or assembly"),
|
|
}),
|
|
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: {
|
|
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,
|
|
},
|
|
});
|
|
return result.ok
|
|
? result.value
|
|
: {
|
|
error: "Managed member comparison failed",
|
|
...projectAnalysisError(result.error),
|
|
};
|
|
}),
|
|
});
|
|
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);
|
|
}),
|
|
});
|
|
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);
|
|
}),
|
|
});
|
|
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);
|
|
}),
|
|
});
|
|
cli.command(CLI_COMMANDS.projectManagedApplicationGraph, {
|
|
description:
|
|
"Project authenticated managed static Evidence into an application graph",
|
|
args: z.object({
|
|
inputJson: z
|
|
.string()
|
|
.describe("Inline managed application graph JSON or JSON file path"),
|
|
}),
|
|
run: ({ args }) =>
|
|
logCliCommand(logger, "project-managed-application-graph", async () => {
|
|
const input = await parseCliJsonInput(
|
|
args.inputJson,
|
|
"project-managed-application-graph",
|
|
);
|
|
if (!input.ok) return input.error;
|
|
const result = projectManagedApplicationGraphEvidence(input.value);
|
|
return result.ok ? result.value : projectAnalysisError(result.error);
|
|
}),
|
|
});
|
|
};
|
|
|
|
const registerNativeCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
for (const [command, tool] of [
|
|
[CLI_COMMANDS.inspectMacho, "inspect_macho"],
|
|
[CLI_COMMANDS.inspectSignature, "inspect_signature"],
|
|
[CLI_COMMANDS.listArchitectures, "list_architectures"],
|
|
] as const) {
|
|
cli.command(command, {
|
|
description: `Run ${tool} without launching Hopper`,
|
|
args: z.object({ path: z.string().describe("Mach-O or app path") }),
|
|
run: ({ args }) =>
|
|
logCliCommand(logger, command, () =>
|
|
runProviderAnalysis(args.path, tool, {}, logger),
|
|
),
|
|
});
|
|
}
|
|
cli.command(CLI_COMMANDS.inspectPlist, {
|
|
description: "Parse app plist metadata without launching Hopper",
|
|
args: z.object({ path: z.string().describe("App or Mach-O path") }),
|
|
options: z.object({
|
|
relativePath: z
|
|
.string()
|
|
.default("Contents/Info.plist")
|
|
.describe("Plist path relative to the app root"),
|
|
}),
|
|
alias: { relativePath: "relative-path" },
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "inspect-plist", () =>
|
|
runProviderAnalysis(
|
|
args.path,
|
|
"inspect_plist",
|
|
{ relative_path: options.relativePath },
|
|
logger,
|
|
),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.demangleSwift, {
|
|
description: "Demangle a bounded Swift symbol batch without Hopper",
|
|
args: z.object({
|
|
path: z.string().describe("Artifact path used for evidence identity"),
|
|
symbols: z
|
|
.array(z.string().min(1))
|
|
.min(1)
|
|
.max(500)
|
|
.describe("Swift mangled symbols to demangle"),
|
|
}),
|
|
run: ({ args }) =>
|
|
logCliCommand(logger, "demangle-swift", () =>
|
|
runProviderAnalysis(
|
|
args.path,
|
|
"demangle_swift",
|
|
{ symbols: args.symbols },
|
|
logger,
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const providerSelectionOption = analysisProviderSelectorSchema
|
|
.optional()
|
|
.describe(
|
|
"Bind deep analysis to a provider ID or use deterministic auto selection",
|
|
);
|
|
|
|
const directAnalysisOptions = (
|
|
logger: Logger,
|
|
snapshotPath: string | undefined,
|
|
providerId: AnalysisProviderSelector | undefined,
|
|
) => ({
|
|
logger,
|
|
snapshotPath,
|
|
...(providerId === undefined ? {} : { providerId }),
|
|
});
|