mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
feat(ghidra): apply validated analyzer overrides before analysis
Apply caller-selected analyzer opt-outs before Ghidra analysis, reject unknown or ineffective names, and commit raw/header imports to the same analyzer profile. Use supported x86 raw compiler specs and cover the real analyzer/readback and raw workflows. Adopts the original implementation from #1800 with validated corrections. Refs #1783, #1836. Co-authored-by: akushonkamen <[email protected]> Co-authored-by: N0zoM1z0 <[email protected]> Co-authored-by: Kaoru0822 <[email protected]>
This commit is contained in:
co-authored by
akushonkamen
Kaoru0822
parent
12ee785e5e
commit
9878bb7296
@@ -0,0 +1,38 @@
|
||||
import java.util.Map;
|
||||
|
||||
import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
|
||||
import ghidra.app.util.headless.HeadlessScript;
|
||||
|
||||
/** Disable the named auto-analyzers before default auto-analysis. */
|
||||
public final class ReaGhidraAnalysisOptions extends HeadlessScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] arguments = getScriptArgs();
|
||||
if (arguments.length != 1 || arguments[0].isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"ReaGhidraAnalysisOptions requires one comma-separated analyzer-name argument");
|
||||
}
|
||||
String[] names = arguments[0].split(",");
|
||||
AutoAnalysisManager analysis = AutoAnalysisManager.getAnalysisManager(currentProgram);
|
||||
Map<String, String> options = getCurrentAnalysisOptionsAndValues(currentProgram);
|
||||
for (String requestedName : names) {
|
||||
monitor.checkCancelled();
|
||||
String name = requestedName.trim();
|
||||
if (analysis.getAnalyzer(name) == null || !options.containsKey(name)) {
|
||||
throw new IllegalArgumentException("Unknown Ghidra analyzer for this program: " + name);
|
||||
}
|
||||
}
|
||||
for (String requestedName : names) {
|
||||
monitor.checkCancelled();
|
||||
setAnalysisOption(currentProgram, requestedName.trim(), "false");
|
||||
}
|
||||
options = getCurrentAnalysisOptionsAndValues(currentProgram);
|
||||
for (String requestedName : names) {
|
||||
monitor.checkCancelled();
|
||||
String name = requestedName.trim();
|
||||
if (!"false".equals(options.get(name))) {
|
||||
throw new IllegalStateException("Ghidra analyzer remained enabled: " + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -800,6 +800,27 @@ Each line is `address<TAB>kind[<TAB>name]`, where kind is `function`, `code` or
|
||||
|
||||
Label seeds can name mapped uninitialized storage such as `.bss`; function and code seeds require initialized bytes.
|
||||
|
||||
To skip a named Ghidra auto-analyzer (for example, when default auto-analysis
|
||||
stalls in one analyzer on a Swift/Objective-C arm64 Mach-O):
|
||||
|
||||
```bash
|
||||
export REA_GHIDRA_DISABLED_ANALYZERS="Objective-C Message Analyzer"
|
||||
```
|
||||
|
||||
The value is a comma-separated list of exact Ghidra analyzer names. A pre-script
|
||||
disables each name before auto-analysis, the analysis profile records the
|
||||
override in `analyzer_preset` (for example
|
||||
`ghidra-default+disabled:Objective-C Message Analyzer`), and a session fails to
|
||||
open when its committed profile and the setting differ. A name starts with a
|
||||
letter or digit and contains only letters, digits, spaces, periods, underscores,
|
||||
and hyphens; at most 32 names are admitted, each at most 120 characters.
|
||||
Names must identify analyzers registered for the selected program. Unknown names
|
||||
or options that remain enabled abort the headless import before auto-analysis
|
||||
and the bridge, so an unsuccessful override cannot produce a session claiming
|
||||
that the analyzer was disabled. For Windows PE resource analysis, the exact name
|
||||
is `WindowsResourceReference`. Disabling an analyzer skips the facts it would
|
||||
have produced.
|
||||
|
||||
### Ghidra heap and CPU controls
|
||||
|
||||
Set resource controls in the environment that launches the CLI or MCP server,
|
||||
|
||||
+11
-7
@@ -35,14 +35,18 @@ Omitted `format` keeps header-based detection; architecture and entry without
|
||||
`format=raw-binary` are rejected.
|
||||
|
||||
Ghidra imports a private snapshot with `BinaryLoader` and these little-endian
|
||||
generic languages:
|
||||
generic languages and compiler specifications:
|
||||
|
||||
| architecture | language_id |
|
||||
| ------------ | ------------------- |
|
||||
| x86 | `x86:LE:32:default` |
|
||||
| x86_64 | `x86:LE:64:default` |
|
||||
| arm | `ARM:LE:32:v7` |
|
||||
| arm64 | `AARCH64:LE:64:v8A` |
|
||||
| architecture | language_id | compiler_spec_id |
|
||||
| ------------ | ------------------- | ---------------- |
|
||||
| x86 | `x86:LE:32:default` | `gcc` |
|
||||
| x86_64 | `x86:LE:64:default` | `gcc` |
|
||||
| arm | `ARM:LE:32:v7` | `default` |
|
||||
| arm64 | `AARCH64:LE:64:v8A` | `default` |
|
||||
|
||||
Compiler specifications select analysis conventions; they do not identify the
|
||||
original compiler or ABI of a headerless target. Ghidra's x86 languages do not
|
||||
provide a compiler specification named `default`.
|
||||
|
||||
Those settings, the caller-supplied entry, and `language_selection:
|
||||
caller-supplied-v1` are committed in the analysis profile. A different entry
|
||||
|
||||
+5
-3
@@ -463,7 +463,7 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
|
||||
| Ghidra lane | Supported runner/target | Additional local tools |
|
||||
| ------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:seeds` | POSIX host; source-owned 32-bit x86 raw fixture; function order and BSS labels | Ghidra 12.1.4 and full JDK 21 |
|
||||
| `npm run verify:ghidra:seeds` | POSIX; raw x86 fixture; seed ordering, BSS labels, analyzer readback and unknown-name rejection | Ghidra 12.1.4 and full JDK 21 |
|
||||
| `npm run verify:ghidra:swift` | macOS x64/arm64 Mach-O | Host Swift compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; signed/unsigned byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
|
||||
@@ -803,8 +803,10 @@ through the compiled CLI and stdio MCP server. With the same Ghidra/JDK
|
||||
prerequisites, it checks preparation and bridge startup, verified load mappings,
|
||||
instruction and byte readback, canonical-entry session reuse, replacement when
|
||||
the entry changes without `provider_id`, unchanged source bytes, and owned
|
||||
cleanup. Other raw CPU families remain covered by deterministic tests rather
|
||||
than this real-provider lane. See [raw binary analysis](raw-binary.md).
|
||||
cleanup. Six-byte x86 and x86_64 controls also verify supported compiler
|
||||
specifications, independent mappings, unchanged bytes, and decompilation of a
|
||||
known return value. AArch64 remains covered by deterministic tests rather than
|
||||
this real-provider lane. See [raw binary analysis](raw-binary.md).
|
||||
|
||||
## Apple Interface Builder archives
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"bridge/ghidra/ReaGhidraExtensions.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareCom.java",
|
||||
"bridge/ghidra/ReaGhidraApplySeeds.java",
|
||||
"bridge/ghidra/ReaGhidraAnalysisOptions.java",
|
||||
"bridge/ghidra/ReaGhidraNoReturnFix.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareRaw.java",
|
||||
"bridge/android/ReaJadxBridge.java",
|
||||
|
||||
@@ -172,11 +172,13 @@ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const runtimeParent = join(workspace, "runtime with spaces");
|
||||
await mkdir(runtimeParent);
|
||||
const callerDirectory = join(workspace, "caller cwd with spaces");
|
||||
// Keep sorted for comparison with the final caller-directory listing.
|
||||
const scriptCollisions = [
|
||||
"ReaGhidraBridge.java",
|
||||
"ReaGhidraPrepareCom.java",
|
||||
"ReaGhidraAnalysisOptions.java",
|
||||
"ReaGhidraApplySeeds.java",
|
||||
"ReaGhidraBridge.java",
|
||||
"ReaGhidraNoReturnFix.java",
|
||||
"ReaGhidraPrepareCom.java",
|
||||
"ReaGhidraPrepareRaw.java",
|
||||
];
|
||||
await mkdir(callerDirectory);
|
||||
|
||||
@@ -53,6 +53,15 @@ const env = {
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
|
||||
? {}
|
||||
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
|
||||
...(process.env.REA_GHIDRA_DISABLED_ANALYZERS === undefined
|
||||
? {}
|
||||
: {
|
||||
REA_GHIDRA_DISABLED_ANALYZERS:
|
||||
process.env.REA_GHIDRA_DISABLED_ANALYZERS,
|
||||
}),
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
@@ -160,6 +169,48 @@ try {
|
||||
assert.equal(cli.normalized_result.base_address, "0x8000000");
|
||||
assert.equal(cli.analysis_profile.digest, profiles[0]);
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
const x86Targets = [];
|
||||
for (const architecture of ["x86", "x86_64"]) {
|
||||
const x86Bytes = Buffer.from("b82a000000c3", "hex"); // mov eax, 42; ret.
|
||||
const x86Sha256 = createHash("sha256").update(x86Bytes).digest("hex");
|
||||
const x86Path = join(workspace, `${architecture}.bin`);
|
||||
await writeFile(x86Path, x86Bytes);
|
||||
await call("open_binary", {
|
||||
path: x86Path,
|
||||
format: "raw-binary",
|
||||
architecture,
|
||||
entry_point: "0x1000",
|
||||
});
|
||||
const image = await call("inspect_native_load_image", {}, x86Sha256);
|
||||
assert.equal(
|
||||
image.normalized_result.status,
|
||||
"verified",
|
||||
JSON.stringify(image),
|
||||
);
|
||||
assert.equal(image.normalized_result.base_address, "0x1000");
|
||||
assert.equal(image.normalized_result.observations.compiler_spec_id, "gcc");
|
||||
assert.equal(image.analysis_profile.parameters.compiler_spec_id, "gcc");
|
||||
const memory = await call(
|
||||
"read_bytes",
|
||||
{ address: "0x1000", length: x86Bytes.length },
|
||||
x86Sha256,
|
||||
);
|
||||
assert.equal(memory.normalized_result.bytes_hex, x86Bytes.toString("hex"));
|
||||
const pseudo = await call(
|
||||
"procedure_pseudo_code",
|
||||
{ procedure: "0x1000" },
|
||||
x86Sha256,
|
||||
);
|
||||
assert.match(pseudo.normalized_result, /\b(?:42|0x2a)\b/iu);
|
||||
await call("close_binary");
|
||||
assert.deepEqual(await readFile(x86Path), x86Bytes);
|
||||
x86Targets.push({
|
||||
architecture,
|
||||
sha256: x86Sha256,
|
||||
compiler_spec_id: "gcc",
|
||||
return_42: true,
|
||||
});
|
||||
}
|
||||
report = {
|
||||
ok: true,
|
||||
mocked: false,
|
||||
@@ -179,6 +230,7 @@ try {
|
||||
invalid_entry_preserves_active_session: true,
|
||||
load_images_verified: true,
|
||||
original_unchanged: true,
|
||||
x86_targets: x86Targets,
|
||||
};
|
||||
} catch (error) {
|
||||
if (stderr.trim()) process.stderr.write(stderr);
|
||||
@@ -199,7 +251,7 @@ process.stdout.write(
|
||||
}) + "\n",
|
||||
);
|
||||
|
||||
async function call(name, args = {}) {
|
||||
async function call(name, args = {}, targetSha256 = sha256) {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000 },
|
||||
@@ -213,7 +265,7 @@ async function call(name, args = {}) {
|
||||
)
|
||||
return projected;
|
||||
const evidence = parseEvidence(result.structuredContent);
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.equal(evidence.subject.digest.sha256, targetSha256);
|
||||
assert.equal(evidence.subject.format, "raw-binary");
|
||||
return evidence;
|
||||
}
|
||||
|
||||
@@ -42,11 +42,66 @@ try {
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const output = await runHeadless(target, "seed-regression", [
|
||||
"-postScript",
|
||||
"ReaSeedRegressionProbe.java",
|
||||
seeds,
|
||||
]);
|
||||
// Headless can exit zero after a script fails, so require the probe's marker.
|
||||
assert.ok(output.includes("REA_SEED_REGRESSION_OK "), output);
|
||||
|
||||
const analyzer = "Decompiler Switch Analysis";
|
||||
const disabled = await runHeadless(target, "analyzer-regression-valid", [
|
||||
"-preScript",
|
||||
"ReaGhidraAnalysisOptions.java",
|
||||
analyzer,
|
||||
"-postScript",
|
||||
"ReaAnalyzerOptionsRegressionProbe.java",
|
||||
analyzer,
|
||||
]);
|
||||
assert.ok(
|
||||
disabled.includes(`REA_ANALYZER_OPTIONS_DISABLED_OK ${analyzer}`),
|
||||
disabled,
|
||||
);
|
||||
|
||||
const unknownAnalyzer = "REA Unknown Analyzer";
|
||||
const rejected = await runHeadless(target, "analyzer-regression-unknown", [
|
||||
"-preScript",
|
||||
"ReaGhidraAnalysisOptions.java",
|
||||
`${analyzer},${unknownAnalyzer}`,
|
||||
"-postScript",
|
||||
"ReaAnalyzerOptionsRegressionProbe.java",
|
||||
analyzer,
|
||||
]);
|
||||
assert.ok(
|
||||
rejected.includes(
|
||||
`Unknown Ghidra analyzer for this program: ${unknownAnalyzer}`,
|
||||
),
|
||||
rejected,
|
||||
);
|
||||
assert.ok(
|
||||
rejected.includes("Processing aborted as a result of pre-script."),
|
||||
rejected,
|
||||
);
|
||||
assert.ok(!rejected.includes("REA_ANALYZER_OPTIONS_DISABLED_OK"), rejected);
|
||||
} finally {
|
||||
if (supervisor !== undefined) {
|
||||
const stopped = await supervisor.stop();
|
||||
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
|
||||
supervisor.dispose();
|
||||
}
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
console.log(
|
||||
"Real Ghidra pre-script regression passed: descending function order, out-of-range seed, BSS label, BSS code rejection, analyzer option readback, unknown-analyzer abort before post-scripts, and owned-process cleanup.",
|
||||
);
|
||||
|
||||
async function runHeadless(target, project, scriptArguments) {
|
||||
const launch = await spawnOwnedProviderProcess({
|
||||
command: join(installation, "support", "analyzeHeadless"),
|
||||
arguments: [
|
||||
workspace,
|
||||
"seed-regression",
|
||||
project,
|
||||
"-import",
|
||||
target,
|
||||
"-loader",
|
||||
@@ -63,9 +118,7 @@ try {
|
||||
new URL("../../../tests/conformance/ghidra", import.meta.url),
|
||||
),
|
||||
].join(";"),
|
||||
"-postScript",
|
||||
"ReaSeedRegressionProbe.java",
|
||||
seeds,
|
||||
...scriptArguments,
|
||||
],
|
||||
runId: run.run_id,
|
||||
expectedCommand: null,
|
||||
@@ -82,16 +135,9 @@ try {
|
||||
const snapshot = supervisor.snapshot();
|
||||
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
|
||||
assert.equal(snapshot.exitCode, 0, output);
|
||||
// Headless can exit zero after a script fails, so require the probe's marker.
|
||||
assert.ok(output.includes("REA_SEED_REGRESSION_OK "), output);
|
||||
} finally {
|
||||
if (supervisor !== undefined) {
|
||||
const stopped = await supervisor.stop();
|
||||
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
|
||||
supervisor.dispose();
|
||||
}
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
const stopped = await supervisor.stop();
|
||||
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
|
||||
supervisor.dispose();
|
||||
supervisor = undefined;
|
||||
return output;
|
||||
}
|
||||
console.log(
|
||||
"Real Ghidra seed regression passed: descending function order, out-of-range seed, BSS label, BSS code rejection, and owned-process cleanup.",
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ghidraCompilerSpecIdSchema,
|
||||
ghidraLanguageIdSchema,
|
||||
} from "./ghidraLanguageOverride.js";
|
||||
import { ghidraDisabledAnalyzersSchema } from "./ghidraDisabledAnalyzers.js";
|
||||
import {
|
||||
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
@@ -48,6 +49,7 @@ const environmentSchema = z.object({
|
||||
),
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const MAX_GHIDRA_DISABLED_ANALYZER_NAMES = 32;
|
||||
export const MAX_GHIDRA_DISABLED_ANALYZER_NAME_LENGTH = 120;
|
||||
|
||||
// Ghidra's headless parser consumes `-`-prefixed arguments after a script path
|
||||
// as its own options, and REA joins the names with commas into one pre-script
|
||||
// argument, so a name can neither start with `-` nor contain a comma. The
|
||||
// remaining admitted characters keep the argument a single quotable token on
|
||||
// every supported launcher, including the Windows batch invocation.
|
||||
const ghidraAnalyzerNamePattern = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/u;
|
||||
|
||||
/**
|
||||
* Parse `REA_GHIDRA_DISABLED_ANALYZERS` into the Ghidra auto-analyzer names to
|
||||
* disable before default auto-analysis, such as
|
||||
* `Objective-C Message Analyzer`. Empty and blank entries are dropped; a list
|
||||
* without names selects no override.
|
||||
*/
|
||||
export const ghidraDisabledAnalyzersSchema = z
|
||||
.string()
|
||||
.transform((value) =>
|
||||
value
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.filter((name) => name.length > 0),
|
||||
)
|
||||
.refine(
|
||||
(names) => names.length <= MAX_GHIDRA_DISABLED_ANALYZER_NAMES,
|
||||
`must list at most ${MAX_GHIDRA_DISABLED_ANALYZER_NAMES} analyzer names`,
|
||||
)
|
||||
.refine(
|
||||
(names) =>
|
||||
names.every(
|
||||
(name) => name.length <= MAX_GHIDRA_DISABLED_ANALYZER_NAME_LENGTH,
|
||||
),
|
||||
`must keep every analyzer name at most ${MAX_GHIDRA_DISABLED_ANALYZER_NAME_LENGTH} characters`,
|
||||
)
|
||||
.refine(
|
||||
(names) => names.every((name) => ghidraAnalyzerNamePattern.test(name)),
|
||||
"must be Ghidra analyzer names starting with a letter or digit and containing only letters, digits, spaces, periods, underscores, and hyphens",
|
||||
)
|
||||
.transform((names) => (names.length === 0 ? undefined : names));
|
||||
@@ -88,6 +88,9 @@ export const parseConfig = (
|
||||
...(env.REA_GHIDRA_SEED_FILE === undefined
|
||||
? {}
|
||||
: { ghidraSeedFile: env.REA_GHIDRA_SEED_FILE }),
|
||||
...(env.REA_GHIDRA_DISABLED_ANALYZERS === undefined
|
||||
? {}
|
||||
: { ghidraDisabledAnalyzers: env.REA_GHIDRA_DISABLED_ANALYZERS }),
|
||||
...(env.REA_GHIDRA_NATIVEAOT_JAR === undefined
|
||||
? {}
|
||||
: { ghidraNativeAotJar: env.REA_GHIDRA_NATIVEAOT_JAR }),
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface AppConfig {
|
||||
readonly ghidraJavaHome: string | undefined;
|
||||
readonly ghidraStartupTimeoutMs: number;
|
||||
readonly ghidraLanguageOverride?: GhidraLanguageOverride;
|
||||
readonly ghidraDisabledAnalyzers?: readonly string[];
|
||||
readonly ghidraSeedFile?: string;
|
||||
readonly ghidraNativeAotJar?: string;
|
||||
readonly ilspyCmdPath: string | undefined;
|
||||
|
||||
@@ -36,6 +36,7 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
installation: GhidraInstallationInspection,
|
||||
signal?: AbortSignal,
|
||||
languageOverride?: GhidraLanguageOverride,
|
||||
disabledAnalyzers?: readonly string[],
|
||||
): Promise<Result<AnalysisProfileResolution, AnalysisError>> => {
|
||||
if (signal?.aborted === true)
|
||||
return Promise.resolve(err(new AnalysisCancelledError("open_binary")));
|
||||
@@ -70,6 +71,13 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
: null;
|
||||
if (pspExtension !== null && !pspExtension.ok) return pspExtension;
|
||||
const provider = { ...identity, version: installation.providerVersion };
|
||||
const analyzerParameters =
|
||||
disabledAnalyzers === undefined || disabledAnalyzers.length === 0
|
||||
? { analyzer_preset: "ghidra-default" }
|
||||
: {
|
||||
analyzer_preset: `ghidra-default+disabled:${disabledAnalyzers.join(",")}`,
|
||||
analysis_options: { disabled_analyzers: [...disabledAnalyzers] },
|
||||
};
|
||||
if (target.format === "raw-binary") {
|
||||
if (languageOverride !== undefined)
|
||||
return Promise.resolve(
|
||||
@@ -116,7 +124,7 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
language_selection: "caller-supplied-v1",
|
||||
entry_point: target.entryPoint,
|
||||
entry_seed: "external-entry-and-function-before-analysis-v1",
|
||||
analyzer_preset: "ghidra-default",
|
||||
...analyzerParameters,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -218,7 +226,7 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
entry_seed: "external-entry-and-function-before-analysis-v1",
|
||||
}
|
||||
: {}),
|
||||
analyzer_preset: "ghidra-default",
|
||||
...analyzerParameters,
|
||||
...ghidraMipsProfileParameters(target),
|
||||
...(pspExtension?.ok === true
|
||||
? {
|
||||
@@ -255,3 +263,26 @@ export const sameGhidraLanguageOverride = (
|
||||
): boolean =>
|
||||
left?.languageId === right?.languageId &&
|
||||
left?.compilerSpecId === right?.compilerSpecId;
|
||||
|
||||
/** Read the analyzer-option commitment back from a resolved profile. */
|
||||
export const ghidraProfileDisabledAnalyzers = (
|
||||
parameters: Readonly<Record<string, unknown>>,
|
||||
): readonly string[] | undefined => {
|
||||
const options = parameters.analysis_options;
|
||||
if (typeof options !== "object" || options === null) return undefined;
|
||||
const disabled = (options as Record<string, unknown>).disabled_analyzers;
|
||||
if (!Array.isArray(disabled)) return undefined;
|
||||
return disabled.every((name) => typeof name === "string")
|
||||
? (disabled as readonly string[])
|
||||
: undefined;
|
||||
};
|
||||
|
||||
/** Whether a committed profile and the active configuration disable the same analyzers. */
|
||||
export const sameGhidraDisabledAnalyzers = (
|
||||
left: readonly string[] | undefined,
|
||||
right: readonly string[] | undefined,
|
||||
): boolean =>
|
||||
left === undefined || right === undefined
|
||||
? left === right
|
||||
: left.length === right.length &&
|
||||
left.every((name, index) => name === right[index]);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
ghidraDisabledAnalyzersSchema,
|
||||
MAX_GHIDRA_DISABLED_ANALYZER_NAMES,
|
||||
} from "../config/ghidraDisabledAnalyzers.js";
|
||||
import { parseConfig } from "../config/parseConfig.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
||||
|
||||
const configured = (setting?: string) => {
|
||||
const config = parseConfig({
|
||||
GHIDRA_INSTALL_DIR: INSTALL,
|
||||
...(setting === undefined
|
||||
? {}
|
||||
: { REA_GHIDRA_DISABLED_ANALYZERS: setting }),
|
||||
});
|
||||
if (!config.ok) throw config.error;
|
||||
return config.value.ghidraDisabledAnalyzers;
|
||||
};
|
||||
|
||||
describe("REA_GHIDRA_DISABLED_ANALYZERS", () => {
|
||||
it("stays unset without the setting", () => {
|
||||
expect(configured(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("splits a comma-separated list and trims each name", () => {
|
||||
expect(
|
||||
configured(" Objective-C Message Analyzer,Decompiler Switch Analysis "),
|
||||
).toEqual(["Objective-C Message Analyzer", "Decompiler Switch Analysis"]);
|
||||
});
|
||||
|
||||
it("treats an empty or blank list as unset", () => {
|
||||
expect(configured("")).toBeUndefined();
|
||||
expect(configured(" , ")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps empty names out of the list instead of failing", () => {
|
||||
expect(
|
||||
configured("Objective-C Message Analyzer,,Decompiler Switch Analysis"),
|
||||
).toEqual(["Objective-C Message Analyzer", "Decompiler Switch Analysis"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a leading flag name", "-Decompiler Switch Analysis"],
|
||||
["a name with command metacharacters", "Analyzer(&calc)"],
|
||||
["a non-ASCII name", "分析器"],
|
||||
["an over-long name", "A".repeat(121)],
|
||||
])("rejects %s", (_label, setting) => {
|
||||
const config = parseConfig({
|
||||
GHIDRA_INSTALL_DIR: INSTALL,
|
||||
REA_GHIDRA_DISABLED_ANALYZERS: setting,
|
||||
});
|
||||
expect(config.ok).toBe(false);
|
||||
if (!config.ok)
|
||||
expect(config.error).toMatchObject({
|
||||
settings: [
|
||||
expect.objectContaining({
|
||||
setting: "REA_GHIDRA_DISABLED_ANALYZERS",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects more than the admitted analyzer count", () => {
|
||||
const setting = Array.from(
|
||||
{ length: MAX_GHIDRA_DISABLED_ANALYZER_NAMES + 1 },
|
||||
(_, index) => `Analyzer ${index}`,
|
||||
).join(",");
|
||||
const config = parseConfig({
|
||||
GHIDRA_INSTALL_DIR: INSTALL,
|
||||
REA_GHIDRA_DISABLED_ANALYZERS: setting,
|
||||
});
|
||||
expect(config.ok).toBe(false);
|
||||
if (!config.ok)
|
||||
expect(config.error).toMatchObject({
|
||||
settings: [
|
||||
expect.objectContaining({
|
||||
setting: "REA_GHIDRA_DISABLED_ANALYZERS",
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("admits the documented Objective-C analyzer name", () => {
|
||||
expect(
|
||||
ghidraDisabledAnalyzersSchema.parse("Objective-C Message Analyzer"),
|
||||
).toEqual(["Objective-C Message Analyzer"]);
|
||||
});
|
||||
});
|
||||
@@ -108,6 +108,8 @@ export interface GhidraHeadlessLauncherOptions {
|
||||
};
|
||||
/** Explicit import language committed by the analysis profile. */
|
||||
readonly languageOverride?: GhidraLanguageOverride;
|
||||
/** Auto-analyzers disabled before default auto-analysis, as configured. */
|
||||
readonly disabledAnalyzers?: readonly string[];
|
||||
/** Caller seeds committed by the analysis profile, applied before analysis. */
|
||||
readonly analysisSeeds?: GhidraSeedCommitment;
|
||||
readonly analysisExtensions?: readonly GhidraExtension[];
|
||||
@@ -213,6 +215,9 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
? {}
|
||||
: { languageOverride: this.options.languageOverride }),
|
||||
...(seedPath === undefined ? {} : { seedPath }),
|
||||
...(this.options.disabledAnalyzers === undefined
|
||||
? {}
|
||||
: { disabledAnalyzers: this.options.disabledAnalyzers }),
|
||||
});
|
||||
const scriptCommand = ghidraHeadlessCommand({
|
||||
environment: this.options.environment,
|
||||
@@ -425,6 +430,8 @@ export interface GhidraHeadlessArgumentOptions {
|
||||
readonly baseAddr: string;
|
||||
};
|
||||
readonly languageOverride?: GhidraLanguageOverride;
|
||||
/** Auto-analyzers disabled by the pre-analysis options script. */
|
||||
readonly disabledAnalyzers?: readonly string[];
|
||||
/** Canonical seed snapshot passed to the pre-analysis seed script. */
|
||||
readonly seedPath?: string;
|
||||
}
|
||||
@@ -555,6 +562,16 @@ export const ghidraHeadlessArguments = (
|
||||
scriptPath.join(bridgeDirectory, GHIDRA_SEED_SCRIPT),
|
||||
options.seedPath,
|
||||
]),
|
||||
// Analyzer options must apply before default auto-analysis and before the
|
||||
// post-analysis repair and bridge scripts start serving.
|
||||
...(options.disabledAnalyzers === undefined ||
|
||||
options.disabledAnalyzers.length === 0
|
||||
? []
|
||||
: [
|
||||
"-preScript",
|
||||
scriptPath.join(bridgeDirectory, "ReaGhidraAnalysisOptions.java"),
|
||||
options.disabledAnalyzers.join(","),
|
||||
]),
|
||||
...((options.platform ?? process.platform) === "win32"
|
||||
? []
|
||||
: [
|
||||
|
||||
@@ -23,6 +23,7 @@ import { parseExecutableHeader } from "../domain/binaryTarget.js";
|
||||
import { GhidraProvider } from "./GhidraProvider.js";
|
||||
import type { GhidraTargetSnapshot } from "./GhidraClient.js";
|
||||
import type { GhidraProviderClientFactory } from "./GhidraProviderClient.js";
|
||||
import type { GhidraHeadlessLauncher } from "./GhidraLauncher.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
@@ -1810,3 +1811,152 @@ describe("Ghidra analysis seeds and configured language", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra disabled analyzer options", () => {
|
||||
const configuredProvider = (
|
||||
environment: Record<string, string>,
|
||||
clientFactory?: GhidraProviderClientFactory,
|
||||
) => {
|
||||
const config = parseConfig({ GHIDRA_INSTALL_DIR: INSTALL, ...environment });
|
||||
if (!config.ok) throw config.error;
|
||||
return new GhidraProvider(
|
||||
config.value,
|
||||
silentLogger,
|
||||
{},
|
||||
installationHost(),
|
||||
clientFactory,
|
||||
);
|
||||
};
|
||||
|
||||
it("records the configured analyzer options in the analysis profile", async () => {
|
||||
const ghidra = configuredProvider({
|
||||
REA_GHIDRA_DISABLED_ANALYZERS:
|
||||
"Objective-C Message Analyzer,Decompiler Switch Analysis",
|
||||
});
|
||||
const resolved = await ghidra.resolveAnalysisProfile(
|
||||
executableTarget("elf", "x86_64"),
|
||||
);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected a bound Ghidra profile");
|
||||
expect(resolved.value.profile.parameters).toMatchObject({
|
||||
analyzer_preset:
|
||||
"ghidra-default+disabled:Objective-C Message Analyzer,Decompiler Switch Analysis",
|
||||
analysis_options: {
|
||||
disabled_analyzers: [
|
||||
"Objective-C Message Analyzer",
|
||||
"Decompiler Switch Analysis",
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("hands the configured analyzers to the launcher for a matching profile", async () => {
|
||||
const launchers: GhidraHeadlessLauncher[] = [];
|
||||
const ghidra = configuredProvider(
|
||||
{ REA_GHIDRA_DISABLED_ANALYZERS: "Objective-C Message Analyzer" },
|
||||
(options) => {
|
||||
launchers.push(options.launcher as GhidraHeadlessLauncher);
|
||||
return {
|
||||
start: () => Promise.resolve(ok(sessionInfo())),
|
||||
callTool: () => Promise.resolve(ok(null)),
|
||||
close: () => Promise.resolve(ok(null)),
|
||||
};
|
||||
},
|
||||
);
|
||||
const target = executableTarget("elf", "x86_64");
|
||||
const resolved = await ghidra.resolveAnalysisProfile(target);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected a bound Ghidra profile");
|
||||
const client = ghidra.createClient(target, resolved.value.profile);
|
||||
const health = await client.execute("health", {});
|
||||
expect(health.ok).toBe(true);
|
||||
expect(launchers).toHaveLength(1);
|
||||
expect(launchers[0]?.options.disabledAnalyzers).toEqual([
|
||||
"Objective-C Message Analyzer",
|
||||
]);
|
||||
});
|
||||
|
||||
it("binds analyzer overrides and raw import settings in one profile", async () => {
|
||||
const launchers: GhidraHeadlessLauncher[] = [];
|
||||
const ghidra = configuredProvider(
|
||||
{ REA_GHIDRA_DISABLED_ANALYZERS: "Decompiler Switch Analysis" },
|
||||
(options) => {
|
||||
launchers.push(options.launcher as GhidraHeadlessLauncher);
|
||||
return {
|
||||
start: () => Promise.resolve(ok(sessionInfo())),
|
||||
callTool: () => Promise.resolve(ok(null)),
|
||||
close: () => Promise.resolve(ok(null)),
|
||||
};
|
||||
},
|
||||
);
|
||||
const target: BinaryTarget = {
|
||||
path: "/tmp/entry.bin",
|
||||
sha256: "a".repeat(64),
|
||||
kind: "executable",
|
||||
format: "raw-binary",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
entryPoint: "0x1000",
|
||||
entryPointSource: "0x1000",
|
||||
};
|
||||
const resolved = await ghidra.resolveAnalysisProfile(target);
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("expected a bound raw-binary profile");
|
||||
expect(resolved.value.profile.parameters).toMatchObject({
|
||||
target_format: "raw-binary",
|
||||
entry_point: "0x1000",
|
||||
analyzer_preset: "ghidra-default+disabled:Decompiler Switch Analysis",
|
||||
analysis_options: { disabled_analyzers: ["Decompiler Switch Analysis"] },
|
||||
});
|
||||
const client = ghidra.createClient(target, resolved.value.profile);
|
||||
expect(launchers).toHaveLength(1);
|
||||
expect(launchers[0]?.options).toMatchObject({
|
||||
disabledAnalyzers: ["Decompiler Switch Analysis"],
|
||||
rawBinary: {
|
||||
languageId: "x86:LE:32:default",
|
||||
compilerSpecId: "gcc",
|
||||
baseAddr: "0x1000",
|
||||
},
|
||||
});
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it("refuses a profile resolved without or with other analyzer options", async () => {
|
||||
const target = executableTarget("elf", "x86_64");
|
||||
const plainProvider = configuredProvider({});
|
||||
const plain = await plainProvider.resolveAnalysisProfile(target);
|
||||
if (!plain.ok || plain.value.profile === null)
|
||||
throw new Error("expected profile");
|
||||
const configuredProvider2 = configuredProvider({
|
||||
REA_GHIDRA_DISABLED_ANALYZERS: "Objective-C Message Analyzer",
|
||||
});
|
||||
const configured = await configuredProvider2.resolveAnalysisProfile(target);
|
||||
if (!configured.ok || configured.value.profile === null)
|
||||
throw new Error("expected configured profile");
|
||||
for (const [profile, environment] of [
|
||||
[
|
||||
plain.value.profile,
|
||||
{ REA_GHIDRA_DISABLED_ANALYZERS: "Objective-C Message Analyzer" },
|
||||
],
|
||||
[configured.value.profile, {}],
|
||||
[
|
||||
configured.value.profile,
|
||||
{ REA_GHIDRA_DISABLED_ANALYZERS: "Decompiler Switch Analysis" },
|
||||
],
|
||||
] as const) {
|
||||
let starts = 0;
|
||||
const client = configuredProvider(environment, () => ({
|
||||
start: () => {
|
||||
starts++;
|
||||
return Promise.resolve(ok(sessionInfo()));
|
||||
},
|
||||
callTool: () => Promise.resolve(ok(null)),
|
||||
close: () => Promise.resolve(ok(null)),
|
||||
})).createClient(target, profile);
|
||||
const failed = await client.execute("health", {});
|
||||
expect(failed.ok).toBe(false);
|
||||
if (!failed.ok) expect(failed.error._tag).toBe("ProviderAdapterError");
|
||||
expect(starts).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,6 +200,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
installation,
|
||||
options?.signal,
|
||||
this.config.ghidraLanguageOverride,
|
||||
this.config.ghidraDisabledAnalyzers,
|
||||
);
|
||||
if (!resolved.ok || resolved.value.profile === null) return resolved;
|
||||
const override = this.config.ghidraLanguageOverride;
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
ghidraSeedLimitations,
|
||||
} from "./GhidraAnalysisSeeds.js";
|
||||
import {
|
||||
ghidraProfileDisabledAnalyzers,
|
||||
ghidraProfileLanguageOverride,
|
||||
sameGhidraDisabledAnalyzers,
|
||||
sameGhidraLanguageOverride,
|
||||
} from "./GhidraAnalysisProfile.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
@@ -431,6 +433,23 @@ export const createGhidraProviderClient = (input: {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const disabledAnalyzers = ghidraProfileDisabledAnalyzers(
|
||||
committedProfile.parameters,
|
||||
);
|
||||
if (
|
||||
!sameGhidraDisabledAnalyzers(
|
||||
disabledAnalyzers,
|
||||
config.ghidraDisabledAnalyzers,
|
||||
)
|
||||
)
|
||||
return unavailableClient(
|
||||
new ProviderAdapterError("ghidra", "open_binary", {
|
||||
diagnostics: {
|
||||
reason:
|
||||
"The committed analysis profile and REA_GHIDRA_DISABLED_ANALYZERS differ; reopen the target to resolve a new profile.",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const invalidProfile = validateGhidraExtensionProfile(
|
||||
extensions,
|
||||
config,
|
||||
@@ -501,6 +520,9 @@ export const createGhidraProviderClient = (input: {
|
||||
},
|
||||
}),
|
||||
...(languageOverride === undefined ? {} : { languageOverride }),
|
||||
...(config.ghidraDisabledAnalyzers === undefined
|
||||
? {}
|
||||
: { disabledAnalyzers: config.ghidraDisabledAnalyzers }),
|
||||
...(seeds === undefined ? {} : { analysisSeeds: seeds }),
|
||||
platform: installation.platform,
|
||||
...(extensions.length === 0 ? {} : { analysisExtensions: extensions }),
|
||||
|
||||
@@ -3,21 +3,23 @@ import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
|
||||
/** Bound Ghidra language for a caller-supplied raw-binary architecture. */
|
||||
export interface GhidraRawBinaryLanguage {
|
||||
readonly languageId: string;
|
||||
readonly compilerSpecId: "default";
|
||||
readonly compilerSpecId: "default" | "gcc";
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the four caller-visible CPU families to little-endian generic Ghidra
|
||||
* languages. Big-endian, Thumb-only, and Cortex-M variants are out of scope.
|
||||
* languages with a supported generic compiler specification. These are
|
||||
* analysis conventions, not evidence of the target's original compiler.
|
||||
* Big-endian, Thumb-only, and Cortex-M variants are out of scope.
|
||||
*/
|
||||
export const ghidraRawBinaryLanguage = (
|
||||
architecture: Exclude<BinaryArchitecture, "mips">,
|
||||
): GhidraRawBinaryLanguage => {
|
||||
switch (architecture) {
|
||||
case "x86":
|
||||
return { languageId: "x86:LE:32:default", compilerSpecId: "default" };
|
||||
return { languageId: "x86:LE:32:default", compilerSpecId: "gcc" };
|
||||
case "x86_64":
|
||||
return { languageId: "x86:LE:64:default", compilerSpecId: "default" };
|
||||
return { languageId: "x86:LE:64:default", compilerSpecId: "gcc" };
|
||||
case "arm":
|
||||
return { languageId: "ARM:LE:32:v7", compilerSpecId: "default" };
|
||||
case "arm64":
|
||||
|
||||
@@ -8,11 +8,11 @@ describe("raw-binary Ghidra language binding", () => {
|
||||
it("maps the four admitted families to little-endian generic languages", () => {
|
||||
expect(ghidraRawBinaryLanguage("x86")).toEqual({
|
||||
languageId: "x86:LE:32:default",
|
||||
compilerSpecId: "default",
|
||||
compilerSpecId: "gcc",
|
||||
});
|
||||
expect(ghidraRawBinaryLanguage("x86_64")).toEqual({
|
||||
languageId: "x86:LE:64:default",
|
||||
compilerSpecId: "default",
|
||||
compilerSpecId: "gcc",
|
||||
});
|
||||
expect(ghidraRawBinaryLanguage("arm")).toEqual({
|
||||
languageId: "ARM:LE:32:v7",
|
||||
|
||||
@@ -289,6 +289,99 @@ describe("Ghidra configured import language and seeds", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra disabled analyzers", () => {
|
||||
const base = {
|
||||
platform: "darwin" as const,
|
||||
projectRoot: "/tmp/project",
|
||||
targetPath: "/tmp/target",
|
||||
bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java",
|
||||
descriptorPath: "/tmp/session.json",
|
||||
ghidraLogPath: "/tmp/ghidra.log",
|
||||
scriptLogPath: "/tmp/script.log",
|
||||
};
|
||||
|
||||
it("applies the analyzer options after seeds and before analysis scripts", () => {
|
||||
const arguments_ = ghidraHeadlessArguments({
|
||||
...base,
|
||||
dosCom: true,
|
||||
seedPath: posix.join("/tmp/runtime/seeds", "seeds.tsv"),
|
||||
disabledAnalyzers: [
|
||||
"Objective-C Message Analyzer",
|
||||
"Decompiler Switch Analysis",
|
||||
],
|
||||
});
|
||||
const options = arguments_.indexOf(
|
||||
posix.join("/package/bridge", "ReaGhidraAnalysisOptions.java"),
|
||||
);
|
||||
expect(arguments_[options - 1]).toBe("-preScript");
|
||||
expect(arguments_[options + 1]).toBe(
|
||||
"Objective-C Message Analyzer,Decompiler Switch Analysis",
|
||||
);
|
||||
const prepare = arguments_.indexOf(
|
||||
posix.join("/package/bridge", "ReaGhidraPrepareCom.java"),
|
||||
);
|
||||
const seeds = arguments_.indexOf(
|
||||
posix.join("/package/bridge", "ReaGhidraApplySeeds.java"),
|
||||
);
|
||||
expect(prepare).toBeLessThan(seeds);
|
||||
expect(seeds).toBeLessThan(options);
|
||||
expect(options).toBeLessThan(arguments_.indexOf("-postScript"));
|
||||
});
|
||||
|
||||
it.each(["darwin", "win32"] as const)(
|
||||
"uses the target platform's script path for the options script on %s",
|
||||
(platform) => {
|
||||
const path = platform === "win32" ? win32 : posix;
|
||||
const arguments_ = ghidraHeadlessArguments({
|
||||
...base,
|
||||
platform,
|
||||
bridgeScriptPath: path.join(
|
||||
"/package",
|
||||
"bridge",
|
||||
"ReaGhidraBridge.java",
|
||||
),
|
||||
disabledAnalyzers: ["Objective-C Message Analyzer"],
|
||||
});
|
||||
const options = arguments_.indexOf(
|
||||
path.join("/package", "bridge", "ReaGhidraAnalysisOptions.java"),
|
||||
);
|
||||
expect(arguments_[options - 1]).toBe("-preScript");
|
||||
expect(arguments_[options + 1]).toBe("Objective-C Message Analyzer");
|
||||
expect(options).toBeLessThan(arguments_.indexOf("-postScript"));
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the invocation byte-identical without an override", () => {
|
||||
expect(ghidraHeadlessArguments(base)).toEqual([
|
||||
"/tmp/project",
|
||||
"rea-project",
|
||||
"-import",
|
||||
"/tmp/target",
|
||||
"-readOnly",
|
||||
"-deleteProject",
|
||||
"-log",
|
||||
"/tmp/ghidra.log",
|
||||
"-scriptlog",
|
||||
"/tmp/script.log",
|
||||
"-scriptPath",
|
||||
"/package/bridge",
|
||||
"-postScript",
|
||||
"/package/bridge/ReaGhidraNoReturnFix.java",
|
||||
"-postScript",
|
||||
"/package/bridge/ReaGhidraBridge.java",
|
||||
"/tmp/session.json",
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds no analyzer script for an empty override", () => {
|
||||
expect(
|
||||
ghidraHeadlessArguments({ ...base, disabledAnalyzers: [] }),
|
||||
).not.toContain(
|
||||
posix.join("/package/bridge", "ReaGhidraAnalysisOptions.java"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra Windows launcher", () => {
|
||||
it("keeps analysis repair outside Windows P0 authority", () => {
|
||||
const args = ghidraHeadlessArguments({
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import ghidra.app.util.headless.HeadlessScript;
|
||||
|
||||
/** Independent readback oracle for the production analyzer-options pre-script. */
|
||||
public final class ReaAnalyzerOptionsRegressionProbe extends HeadlessScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] arguments = getScriptArgs();
|
||||
if (arguments.length != 1 || arguments[0].isBlank()) {
|
||||
throw new IllegalArgumentException("Expected one analyzer name");
|
||||
}
|
||||
monitor.checkCancelled();
|
||||
String name = arguments[0];
|
||||
String enabled = getCurrentAnalysisOptionsAndValues(currentProgram).get(name);
|
||||
if (!"false".equals(enabled)) {
|
||||
throw new IllegalStateException("Analyzer option is not disabled: " + name);
|
||||
}
|
||||
println("REA_ANALYZER_OPTIONS_DISABLED_OK " + name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user