150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
import { z } from "zod";
|
|
import { isAbsolute } from "node:path";
|
|
|
|
import { ConfigurationError } from "../domain/configurationErrors.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import { analysisProviderSelectorSchema } from "../contracts/providerSelection.js";
|
|
import {
|
|
ghidraCompilerSpecIdSchema,
|
|
ghidraLanguageIdSchema,
|
|
} from "./ghidraLanguageOverride.js";
|
|
import { ghidraDisabledAnalyzersSchema } from "./ghidraDisabledAnalyzers.js";
|
|
import {
|
|
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
|
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
|
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
|
|
providerStartupTimeoutSchema,
|
|
} from "./providerStartupTimeout.js";
|
|
import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js";
|
|
import { mcpResponseBudgetSchema } from "./mcpResponseBudget.js";
|
|
import { parseLoaderArgs, parseStringArray } from "./parsers.js";
|
|
|
|
const environmentSchema = z.object({
|
|
REA_ANALYSIS_PROVIDER: analysisProviderSelectorSchema.default("auto"),
|
|
REA_MCP_MAX_RESPONSE_BYTES: mcpResponseBudgetSchema.optional(),
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: mcpInputSchemaProfileSchema.optional(),
|
|
REA_IDA_MCP_CONFIG: z
|
|
.string()
|
|
.min(1)
|
|
.refine(isAbsolute, "REA_IDA_MCP_CONFIG must be absolute")
|
|
.optional(),
|
|
GHIDRA_INSTALL_DIR: z
|
|
.string()
|
|
.min(1)
|
|
.refine(isAbsolute, "GHIDRA_INSTALL_DIR must be absolute")
|
|
.optional(),
|
|
JAVA_HOME: z
|
|
.string()
|
|
.min(1)
|
|
.refine(isAbsolute, "JAVA_HOME must be absolute")
|
|
.optional(),
|
|
REA_GHIDRA_NATIVEAOT_JAR: z
|
|
.string()
|
|
.min(1)
|
|
.refine(isAbsolute, "REA_GHIDRA_NATIVEAOT_JAR must be absolute")
|
|
.optional(),
|
|
REA_GHIDRA_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
|
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
|
),
|
|
REA_HOPPER_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
|
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
|
),
|
|
REA_SWIFTC_COMPILE_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
|
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
|
|
),
|
|
REA_GHIDRA_LANGUAGE_ID: ghidraLanguageIdSchema.optional(),
|
|
REA_GHIDRA_COMPILER_SPEC_ID: ghidraCompilerSpecIdSchema.optional(),
|
|
REA_GHIDRA_DISABLED_ANALYZERS: ghidraDisabledAnalyzersSchema.optional(),
|
|
REA_GHIDRA_SEED_FILE: z
|
|
.string()
|
|
.min(1)
|
|
.refine(isAbsolute, "REA_GHIDRA_SEED_FILE must be absolute")
|
|
.optional(),
|
|
REA_ILSPY_CMD_PATH: z
|
|
.string()
|
|
.min(1)
|
|
.refine(isAbsolute, "REA_ILSPY_CMD_PATH must be absolute")
|
|
.optional(),
|
|
HOPPER_LAUNCHER_PATH: z.string().min(1).optional(),
|
|
HOPPER_TARGET_PATH: z.string().min(1).optional(),
|
|
HOPPER_TARGET_KIND: z.enum(["executable", "database"]).default("executable"),
|
|
HOPPER_LOADER_ARGS_JSON: z.string().optional(),
|
|
REA_LOG_LEVEL: z
|
|
.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
|
|
.default("info"),
|
|
REA_REFERENCE_SECRET_PATTERNS_JSON: z.string().default("[]"),
|
|
});
|
|
|
|
type Environment = Omit<
|
|
z.infer<typeof environmentSchema>,
|
|
"HOPPER_LOADER_ARGS_JSON" | "REA_REFERENCE_SECRET_PATTERNS_JSON"
|
|
> & {
|
|
readonly loaderArgs: readonly string[];
|
|
readonly referenceSecretPatterns: readonly string[];
|
|
};
|
|
|
|
/** Parse individual settings before layered registration fragments are merged. */
|
|
export const parseEnvironmentValues = (
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): Result<Environment, ConfigurationError> => {
|
|
const parsed = environmentSchema.safeParse(environment);
|
|
if (!parsed.success) {
|
|
// Zod messages name the constraint, never the rejected value.
|
|
const settings = parsed.error.issues.map((issue) => ({
|
|
setting: String(issue.path[0] ?? "environment"),
|
|
constraint: issue.message,
|
|
}));
|
|
return err(
|
|
new ConfigurationError("Invalid REA environment configuration", {
|
|
cause: parsed.error,
|
|
settings,
|
|
}),
|
|
);
|
|
}
|
|
const {
|
|
HOPPER_LOADER_ARGS_JSON: encodedLoaderArgs,
|
|
REA_REFERENCE_SECRET_PATTERNS_JSON: encodedSecretPatterns,
|
|
...env
|
|
} = parsed.data;
|
|
const loaderArgs = parseLoaderArgs(encodedLoaderArgs);
|
|
if (!loaderArgs.ok) return loaderArgs;
|
|
const secretPatterns = parseStringArray(
|
|
encodedSecretPatterns,
|
|
"REA_REFERENCE_SECRET_PATTERNS_JSON",
|
|
);
|
|
if (!secretPatterns.ok) return secretPatterns;
|
|
return ok({
|
|
...env,
|
|
loaderArgs: loaderArgs.value,
|
|
referenceSecretPatterns: secretPatterns.value,
|
|
});
|
|
};
|
|
|
|
/** Validate complete runtime semantics without probing the host or persisting defaults. */
|
|
export const parseEnvironment = (
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
// Registration expressions are not evaluated; their absence is not proven.
|
|
unresolvedSettings: ReadonlySet<string> = new Set(),
|
|
): Result<Environment, ConfigurationError> => {
|
|
const parsed = parseEnvironmentValues(environment);
|
|
if (!parsed.ok) return parsed;
|
|
const env = parsed.value;
|
|
// Ghidra's analyzeHeadless accepts -cspec only together with -processor.
|
|
if (
|
|
env.REA_GHIDRA_COMPILER_SPEC_ID !== undefined &&
|
|
env.REA_GHIDRA_LANGUAGE_ID === undefined &&
|
|
!unresolvedSettings.has("REA_GHIDRA_LANGUAGE_ID")
|
|
)
|
|
return err(
|
|
new ConfigurationError("Invalid REA environment configuration", {
|
|
settings: [
|
|
{
|
|
setting: "REA_GHIDRA_COMPILER_SPEC_ID",
|
|
constraint: "requires REA_GHIDRA_LANGUAGE_ID",
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
return parsed;
|
|
};
|