mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
refactor(tools): remove arbitrary workflow ceilings
This commit is contained in:
@@ -683,10 +683,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
facts.addProperty("external", symbol.isExternal());
|
||||
facts.addProperty("type", normalizedSymbolType(symbol));
|
||||
facts.addProperty("source", normalizedSource(symbol.getSource()));
|
||||
addBounded(
|
||||
result,
|
||||
new InventoryItem(address, symbol.getName(true), facts)
|
||||
);
|
||||
result.add(new InventoryItem(address, symbol.getName(true), facts));
|
||||
}
|
||||
result.sort(INVENTORY_ORDER);
|
||||
nameInventory = List.copyOf(result);
|
||||
@@ -761,7 +758,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
instance.isMissingNullTerminator() ? "missing" : "present_or_not_required"
|
||||
);
|
||||
facts.addProperty("byte_length", Math.max(0, data.getLength()));
|
||||
addBounded(result, new InventoryItem(data.getAddress(), value, facts));
|
||||
result.add(new InventoryItem(data.getAddress(), value, facts));
|
||||
}
|
||||
result.sort(INVENTORY_ORDER);
|
||||
stringInventory = List.copyOf(result);
|
||||
@@ -1687,10 +1684,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return source.name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static void addBounded(List<InventoryItem> destination, InventoryItem item) {
|
||||
destination.add(item);
|
||||
}
|
||||
|
||||
private static ValueMatcher literalMatcher(String pattern, boolean caseSensitive) {
|
||||
String needle = caseSensitive ? pattern : pattern.toLowerCase(Locale.ROOT);
|
||||
return value -> {
|
||||
|
||||
@@ -159,12 +159,6 @@ const registerTraceCommand = (cli: CliInstance, logger: Logger): void => {
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe("Match the query with exact letter case"),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.default(10_000)
|
||||
.describe("Maximum matching results to return"),
|
||||
snapshot: z
|
||||
.string()
|
||||
.min(1)
|
||||
@@ -183,7 +177,6 @@ const registerTraceCommand = (cli: CliInstance, logger: Logger): void => {
|
||||
{
|
||||
query: args.query,
|
||||
case_sensitive: options.caseSensitive,
|
||||
limit: options.limit,
|
||||
},
|
||||
directAnalysisOptions(logger, options.snapshot, options.provider),
|
||||
),
|
||||
|
||||
@@ -173,7 +173,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
official(
|
||||
"list_strings",
|
||||
"List every provider-defined string, or filter to one address, as address/value pairs. Ghidra reports charset, missing-terminator status, byte length, and explicit value truncation.",
|
||||
"List every provider-defined string, or filter to one address, as address/value pairs. Ghidra also reports encoding, terminator status, and byte length.",
|
||||
z.object({ document, address: optionalAddress }),
|
||||
),
|
||||
official(
|
||||
@@ -293,7 +293,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
/** Closed provider operation names exposed by direct analysis adapters. */
|
||||
export type OfficialToolName = (typeof OFFICIAL_TOOL_CONTRACTS)[number]["name"];
|
||||
|
||||
/** Bounded workflows composed from one or more bridge operations. */
|
||||
/** Workflow tools composed from one or more provider operations. */
|
||||
export const ENHANCED_TOOL_CONTRACTS = [
|
||||
enhanced(
|
||||
"get_objc_classes",
|
||||
|
||||
@@ -76,6 +76,18 @@ const bookmarkFacetSchema = z.discriminatedUnion("state", [
|
||||
}),
|
||||
]);
|
||||
|
||||
const addressedString = z.object({
|
||||
address: z.string(),
|
||||
value: z.string(),
|
||||
string: z
|
||||
.object({
|
||||
encoding: z.string().min(1),
|
||||
termination: z.enum(["missing", "present_or_not_required"]),
|
||||
byte_length: z.number().int().min(0),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/** Exact structured-content schemas shared by direct analysis providers. */
|
||||
export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
address_name: resultOf(nullableText),
|
||||
@@ -90,7 +102,7 @@ export const officialOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
list_names: resultOf(z.array(addressedValue)),
|
||||
list_procedures: resultOf(z.array(addressedValue)),
|
||||
list_segments: segmentOutput,
|
||||
list_strings: resultOf(z.array(addressedValue)),
|
||||
list_strings: resultOf(z.array(addressedString)),
|
||||
next_address: resultOf(z.string()),
|
||||
prev_address: resultOf(z.string()),
|
||||
procedure_address: resultOf(z.string()),
|
||||
|
||||
@@ -31,7 +31,7 @@ const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
||||
result: "Primary structured result returned by this operation.",
|
||||
left: "Left-hand input used for comparison or differential execution.",
|
||||
limits:
|
||||
"Bounded resource-consumption and result-size limits for this operation.",
|
||||
"Resource budgets that bound execution and retained output for this operation.",
|
||||
mode: "Operation mode that selects the requested behavior.",
|
||||
overwrite: "Whether an existing destination may be replaced.",
|
||||
path: "Local filesystem path used by this operation.",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
analysisProfilesEqual,
|
||||
createAnalysisProfile,
|
||||
} from "./analysisProfile.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
const PROVIDER = {
|
||||
id: "fixture",
|
||||
@@ -58,15 +59,25 @@ describe("analysis profile commitments", () => {
|
||||
expect(analysisProfileSchema.parse(profile)).toEqual(profile);
|
||||
});
|
||||
|
||||
it("rejects digest tampering and structurally unsafe parameters", () => {
|
||||
it("accepts deep, large, and long-string profile parameters", () => {
|
||||
let nested: JsonValue = "leaf";
|
||||
for (let index = 0; index < 24; index += 1)
|
||||
nested = { [`level-${String(index)}`]: nested };
|
||||
const parameters = {
|
||||
nested,
|
||||
entries: Array.from({ length: 5_000 }, (_, index) => index),
|
||||
long_value: "x".repeat(20 * 1024),
|
||||
};
|
||||
|
||||
const profile = createAnalysisProfile(PROVIDER, parameters);
|
||||
|
||||
expect(analysisProfileSchema.parse(profile)).toEqual(profile);
|
||||
});
|
||||
|
||||
it("rejects digest tampering", () => {
|
||||
const profile = createAnalysisProfile(PROVIDER, { loader: "default" });
|
||||
expect(() =>
|
||||
analysisProfileSchema.parse({ ...profile, digest: "0".repeat(64) }),
|
||||
).toThrow(/digest/u);
|
||||
expect(() =>
|
||||
createAnalysisProfile(PROVIDER, {
|
||||
oversized: "x".repeat(65 * 1024),
|
||||
}),
|
||||
).toThrow(/structural limits/u);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,14 +3,8 @@ import { createHash } from "node:crypto";
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { isJsonWithinLimits } from "./jsonLimits.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "./jsonValue.js";
|
||||
|
||||
const PROFILE_JSON_LIMITS = Object.freeze({
|
||||
maxDepth: 16,
|
||||
maxStringLength: 16 * 1024,
|
||||
maxNodes: 4_096,
|
||||
});
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
|
||||
/** Concrete provider identity committed by an analysis profile. */
|
||||
@@ -20,14 +14,9 @@ export const committedProviderSchema = z.object({
|
||||
version: z.string().min(1),
|
||||
});
|
||||
|
||||
const boundedParametersSchema = jsonObjectSchema.refine(
|
||||
(parameters) => isJsonWithinLimits(parameters, PROFILE_JSON_LIMITS),
|
||||
{ message: "Analysis profile parameters exceed structural limits" },
|
||||
);
|
||||
|
||||
const unsignedAnalysisProfileSchema = z.object({
|
||||
provider: committedProviderSchema,
|
||||
parameters: boundedParametersSchema,
|
||||
parameters: jsonObjectSchema,
|
||||
});
|
||||
|
||||
/** Canonical, provider-neutral commitment to analysis-affecting semantics. */
|
||||
|
||||
+47
@@ -79,6 +79,53 @@ const createLargeNativeExportGraph = (): JavaScriptApplicationGraph => {
|
||||
});
|
||||
};
|
||||
|
||||
it("accepts deeply nested canonical application properties", () => {
|
||||
let nested: unknown = "leaf";
|
||||
for (let index = 0; index < 12; index += 1)
|
||||
nested = { [`level-${String(index)}`]: nested };
|
||||
const node = createJavaScriptApplicationNode({
|
||||
kind: "native-export",
|
||||
identity: {
|
||||
strategy: "canonical-path",
|
||||
stability: "artifact-version",
|
||||
artifact_sha256: APPLICATION_GRAPH_DIGESTS.nativeAddon,
|
||||
path: "native/deep-export",
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
label: "deep export",
|
||||
properties: { nested },
|
||||
evidence: artifactEvidence(
|
||||
APPLICATION_GRAPH_DIGESTS.nativeAddon,
|
||||
"native/deep-export",
|
||||
"native-analysis-provider",
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [node.node_id],
|
||||
nodes: [node],
|
||||
edges: [],
|
||||
coverage: {
|
||||
status: "complete",
|
||||
truncated: false,
|
||||
omitted_count: 0,
|
||||
limits: [],
|
||||
},
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
expect(graph.nodes[0]?.observations[0]?.properties).toEqual({ nested });
|
||||
expect(
|
||||
parseJavaScriptApplicationGraph(
|
||||
JSON.parse(serializeJavaScriptApplicationGraph(graph)),
|
||||
),
|
||||
).toEqual(graph);
|
||||
});
|
||||
|
||||
describe("JavaScript Application Graph", () => {
|
||||
it("defines the complete provider-neutral node and relation vocabulary", () => {
|
||||
expect(JAVASCRIPT_APPLICATION_NODE_KINDS).toEqual([
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
applicationGraphEvidenceSchema,
|
||||
applicationNodeIdentitySchema,
|
||||
} from "./javascriptApplicationEvidenceSchemas.js";
|
||||
import { isJsonWithinDepth } from "./jsonLimits.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
|
||||
const boundedTextSchema = z.string().min(1);
|
||||
@@ -70,15 +69,10 @@ export const JAVASCRIPT_APPLICATION_RELATIONS = [
|
||||
/** Provider-neutral relation categories. */
|
||||
const applicationRelationSchema = z.enum(JAVASCRIPT_APPLICATION_RELATIONS);
|
||||
|
||||
const applicationPropertiesSchema = z
|
||||
.record(z.string().min(1), jsonValueSchema)
|
||||
.superRefine((properties, context) => {
|
||||
if (!isJsonWithinDepth(properties, 6))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Application graph properties exceed the supported depth",
|
||||
});
|
||||
});
|
||||
const applicationPropertiesSchema = z.record(
|
||||
z.string().min(1),
|
||||
jsonValueSchema,
|
||||
);
|
||||
|
||||
/** One node observation before its semantic identifier is derived. */
|
||||
const applicationNodeObservationInputSchema = z.strictObject({
|
||||
|
||||
@@ -9,7 +9,7 @@ const nodeIdSchema = z.string().regex(/^jag_node_[a-f0-9]{64}$/u);
|
||||
const edgeIdSchema = z.string().regex(/^jag_edge_[a-f0-9]{64}$/u);
|
||||
const boundedTextSchema = z.string().min(1);
|
||||
|
||||
/** Literal starting point for one bounded application trace. */
|
||||
/** Literal starting point for one application feature trace. */
|
||||
const applicationFeatureSeedSchema = z.strictObject({
|
||||
kind: z.enum([
|
||||
"node-id",
|
||||
|
||||
@@ -96,21 +96,23 @@ const node = (
|
||||
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
|
||||
role: string,
|
||||
properties: Record<string, string | number | boolean | null> = {},
|
||||
functionNodeId: string | null = null,
|
||||
modulePath = "bundle.js",
|
||||
options: {
|
||||
readonly functionNodeId?: string | null;
|
||||
readonly modulePath?: string;
|
||||
} = {},
|
||||
): JavaScriptSemanticGraphNode =>
|
||||
createJavaScriptSemanticGraphNode({
|
||||
kind,
|
||||
identity: {
|
||||
artifact_sha256: SHA,
|
||||
module_path: modulePath,
|
||||
module_path: options.modulePath ?? "bundle.js",
|
||||
source_range: {
|
||||
start: { line: 1, column: role.length },
|
||||
end: { line: 1, column: role.length + 1 },
|
||||
},
|
||||
role_key: role,
|
||||
},
|
||||
function_node_id: functionNodeId,
|
||||
function_node_id: options.functionNodeId ?? null,
|
||||
application_node_ids: [],
|
||||
label: role,
|
||||
properties,
|
||||
@@ -120,7 +122,7 @@ const node = (
|
||||
it("accepts semantic module paths longer than the former schema ceiling", () => {
|
||||
const modulePath = `${"segment/".repeat(600)}entry.js`;
|
||||
expect(
|
||||
node("module", "long-path", {}, null, modulePath).identity.module_path,
|
||||
node("module", "long-path", {}, { modulePath }).identity.module_path,
|
||||
).toBe(modulePath);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { applicationGraphEvidenceSchema } from "./javascriptApplicationEvidenceSchemas.js";
|
||||
import { isJsonWithinDepth } from "./jsonLimits.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
@@ -190,15 +189,7 @@ const sourceRangeSchema = z
|
||||
});
|
||||
});
|
||||
|
||||
const semanticPropertiesSchema = z
|
||||
.record(z.string().min(1), jsonValueSchema)
|
||||
.superRefine((properties, context) => {
|
||||
if (!isJsonWithinDepth(properties, 6))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Semantic properties exceed the supported depth",
|
||||
});
|
||||
});
|
||||
const semanticPropertiesSchema = z.record(z.string().min(1), jsonValueSchema);
|
||||
|
||||
const semanticNodeIdentitySchema = z.strictObject({
|
||||
artifact_sha256: digestSchema,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type JavaScriptSemanticGraphNode,
|
||||
} from "./javascriptSemanticGraph.js";
|
||||
import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
const SHA = "a".repeat(64);
|
||||
const JAG_ID = `jag_${"b".repeat(64)}`;
|
||||
@@ -91,7 +92,7 @@ const unknownEvidence = (): ApplicationGraphEvidence => ({
|
||||
const node = (
|
||||
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
|
||||
role: string,
|
||||
properties: Record<string, string | number | boolean | null> = {},
|
||||
properties: Record<string, JsonValue> = {},
|
||||
functionNodeId: string | null = null,
|
||||
): JavaScriptSemanticGraphNode =>
|
||||
createJavaScriptSemanticGraphNode({
|
||||
@@ -319,6 +320,16 @@ it("traverses paths beyond the former fixed depth ceiling", () => {
|
||||
expect(result.nodes).toHaveLength(74);
|
||||
});
|
||||
|
||||
it("accepts deeply nested semantic properties", () => {
|
||||
let nested: JsonValue = "leaf";
|
||||
for (let index = 0; index < 12; index += 1)
|
||||
nested = { [`level-${String(index)}`]: nested };
|
||||
|
||||
const result = node("binding", "deep-properties", { nested });
|
||||
|
||||
expect(result.properties).toEqual({ nested });
|
||||
});
|
||||
|
||||
it("keeps relevant dynamic frontiers unknown", () => {
|
||||
const result = queryJavaScriptSemanticGraph(fixtureGraph(true), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { isJsonWithinDepth, isJsonWithinLimits } from "./jsonLimits.js";
|
||||
|
||||
const limits = {
|
||||
maxDepth: 3,
|
||||
maxStringLength: 8,
|
||||
maxNodes: 6,
|
||||
};
|
||||
|
||||
describe("JSON limits", () => {
|
||||
it("checks depth without rejecting large keys, collections, or strings", () => {
|
||||
const properties = Object.fromEntries(
|
||||
Array.from({ length: 1_001 }, (_, index) => [
|
||||
`${"k".repeat(128)}${index}`,
|
||||
"v".repeat(4_097),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(isJsonWithinDepth(properties, 6)).toBe(true);
|
||||
expect(isJsonWithinDepth({ nested: { value: true } }, 1)).toBe(false);
|
||||
});
|
||||
|
||||
it("traverses nested object and array values", () => {
|
||||
expect(isJsonWithinLimits({ one: ["two", { three: 3 }] }, limits)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
isJsonWithinLimits({ one: ["two", { three: "too-long-value" }] }, limits),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("enforces depth, node, and object-key limits", () => {
|
||||
expect(
|
||||
isJsonWithinLimits(
|
||||
{ one: { two: { three: null } } },
|
||||
{
|
||||
...limits,
|
||||
maxDepth: 2,
|
||||
},
|
||||
),
|
||||
).toBe(false);
|
||||
expect(isJsonWithinLimits([1, 2, 3], { ...limits, maxNodes: 3 })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isJsonWithinLimits({ "too-long-key": true }, limits)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
/** Structural limits applied while traversing an unknown JSON value. */
|
||||
export interface JsonLimits {
|
||||
readonly maxDepth: number;
|
||||
readonly maxStringLength: number;
|
||||
readonly maxNodes: number;
|
||||
}
|
||||
|
||||
/** Return whether a JSON value fits a depth constraint without size quotas. */
|
||||
export const isJsonWithinDepth = (root: unknown, maxDepth: number): boolean => {
|
||||
const pending: Array<{ readonly value: unknown; readonly depth: number }> = [
|
||||
{ value: root, depth: 0 },
|
||||
];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
if (current === undefined) break;
|
||||
if (current.depth > maxDepth) return false;
|
||||
if (typeof current.value !== "object" || current.value === null) continue;
|
||||
for (const value of Object.values(current.value))
|
||||
pending.push({ value, depth: current.depth + 1 });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/** Return whether every node, key, and string fits within the supplied limits. */
|
||||
export const isJsonWithinLimits = (
|
||||
root: unknown,
|
||||
limits: JsonLimits,
|
||||
): boolean => {
|
||||
const pending: Array<{ readonly value: unknown; readonly depth: number }> = [
|
||||
{ value: root, depth: 0 },
|
||||
];
|
||||
let nodes = 0;
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
if (current === undefined) break;
|
||||
nodes += 1;
|
||||
if (nodes > limits.maxNodes || current.depth > limits.maxDepth)
|
||||
return false;
|
||||
if (
|
||||
typeof current.value === "string" &&
|
||||
current.value.length > limits.maxStringLength
|
||||
)
|
||||
return false;
|
||||
if (typeof current.value !== "object" || current.value === null) continue;
|
||||
for (const [key, value] of Object.entries(current.value)) {
|
||||
if (key.length > limits.maxStringLength) return false;
|
||||
pending.push({ value, depth: current.depth + 1 });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ import { AnalysisInputError, AnalysisOutputError } from "../domain/errors.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
/** Read-only direct operations admitted by the Ghidra v1 adapter. */
|
||||
/** Read-only direct inventory operations admitted by the Ghidra adapter. */
|
||||
export const GHIDRA_INVENTORY_OPERATIONS = [
|
||||
"address_name",
|
||||
"list_documents",
|
||||
@@ -170,7 +170,7 @@ const resultSchemas = {
|
||||
search_strings: z.array(searchItem),
|
||||
} satisfies Readonly<Record<GhidraInventoryOperation, z.ZodType>>;
|
||||
|
||||
/** Require exact, bounded Java-bridge output before creating Evidence. */
|
||||
/** Validate exact Java-bridge output before creating Evidence. */
|
||||
export const parseGhidraInventoryResult = (
|
||||
operation: GhidraInventoryOperation,
|
||||
value: JsonValue,
|
||||
|
||||
@@ -14,11 +14,11 @@ import { err, ok } from "../domain/result.js";
|
||||
import { GhidraSessionError } from "./GhidraSessionError.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
||||
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
||||
const installationHost = (): GhidraInstallationHost => ({
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.2\n",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
@@ -85,7 +85,7 @@ describe("Ghidra provider", () => {
|
||||
status: "available",
|
||||
diagnostics: {
|
||||
install_dir: INSTALL,
|
||||
provider_version: "12.1.2",
|
||||
provider_version: "12.1.4",
|
||||
java_version: "21.0.11",
|
||||
},
|
||||
});
|
||||
@@ -255,7 +255,7 @@ describe("Ghidra client projection", () => {
|
||||
expect(resolved.ok).toBe(true);
|
||||
if (!resolved.ok || resolved.value.profile === null) return;
|
||||
expect(resolved.value.profile).toMatchObject({
|
||||
provider: { id: "ghidra", name: "Ghidra", version: "12.1.2" },
|
||||
provider: { id: "ghidra", name: "Ghidra", version: "12.1.4" },
|
||||
parameters: {
|
||||
import_mode: "ephemeral-read-only",
|
||||
analyzer_preset: "ghidra-default",
|
||||
@@ -289,7 +289,7 @@ describe("Ghidra client projection", () => {
|
||||
provider: {
|
||||
id: "ghidra",
|
||||
name: "Ghidra",
|
||||
version: "12.1.2",
|
||||
version: "12.1.4",
|
||||
},
|
||||
analysisProfile: resolved.value.profile,
|
||||
result: [{ address: "0x1000", value: "fixture_main" }],
|
||||
@@ -411,7 +411,7 @@ const sessionInfo = () => ({
|
||||
name: "REA Ghidra bridge" as const,
|
||||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
profile_digest: "a".repeat(64),
|
||||
provider: { id: "ghidra" as const, version: "12.1.2" },
|
||||
provider: { id: "ghidra" as const, version: "12.1.4" },
|
||||
read_only: true as const,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
|
||||
@@ -24,10 +24,7 @@ export const workerRequest = (prepared: PreparedReplayPlan) => ({
|
||||
limits: {
|
||||
resultDepth: prepared.publicPlan.limits.result_depth,
|
||||
resultNodes: prepared.publicPlan.limits.result_nodes,
|
||||
exceptionBytes: Math.min(
|
||||
64 * 1024,
|
||||
prepared.publicPlan.limits.output_bytes,
|
||||
),
|
||||
exceptionBytes: prepared.publicPlan.limits.output_bytes,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -186,6 +186,10 @@ describe("canonical CLI catalog", () => {
|
||||
expect(cliCommandOptionNames(cli, name)).toContain("provider");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not impose a private result limit on feature tracing", () => {
|
||||
expect(cliCommandOptionNames(createCli(), "trace")).not.toContain("limit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("canonical product catalog drift", () => {
|
||||
|
||||
+12
-7
@@ -27,7 +27,7 @@ it("reads local source maps as part of static artifact analysis", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not follow symlinks and analyzes text beyond the former file-size cap", async () => {
|
||||
it("does not follow symlinks and analyzes each local source file", async () => {
|
||||
const root = await fixtureDirectory();
|
||||
const outside = await createTestTempDirectory("rea-javascript-outside-");
|
||||
const outsideFile = join(outside, "secret.js");
|
||||
@@ -37,22 +37,27 @@ it("does not follow symlinks and analyzes text beyond the former file-size cap",
|
||||
);
|
||||
await symlink(outsideFile, join(root, "escape.js"));
|
||||
const baseline = await reconstructJavaScriptArtifact({ input_path: root });
|
||||
const oversizedSource = `// ${"private-marker-".repeat(600_000)}\nfetch("https://inside.invalid");\n`;
|
||||
await writeFile(join(root, "oversized.js"), oversizedSource);
|
||||
const additionalSource = Array.from(
|
||||
{ length: 1_000 },
|
||||
(_value, index) => `export const value${String(index)} = ${String(index)};`,
|
||||
).join("\n");
|
||||
await writeFile(join(root, "additional.js"), additionalSource);
|
||||
|
||||
const result = await reconstructJavaScriptArtifact({ input_path: root });
|
||||
const encoded = JSON.stringify(result);
|
||||
|
||||
expect(result.statistics.text_bytes_read).toBeGreaterThan(8 * 1024 * 1024);
|
||||
expect(result.graph.coverage).toMatchObject({
|
||||
status: "complete",
|
||||
truncated: false,
|
||||
});
|
||||
expect(result.statistics.parsed_javascript_files).toBe(
|
||||
baseline.statistics.parsed_javascript_files + 1,
|
||||
);
|
||||
expect(result.statistics.text_bytes_read).toBe(
|
||||
baseline.statistics.text_bytes_read + Buffer.byteLength(additionalSource),
|
||||
);
|
||||
expect(encoded).not.toContain(outsideFile);
|
||||
expect(encoded).not.toContain("outside-secret.invalid");
|
||||
expect(result.statistics.visited_ast_nodes).toBeGreaterThan(
|
||||
baseline.statistics.visited_ast_nodes,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps malformed JavaScript, package metadata, and source maps as explicit unknowns", async () => {
|
||||
@@ -18,9 +18,9 @@ describe("Ghidra MCP evidence parity", () => {
|
||||
);
|
||||
expect(listed).toMatchObject({
|
||||
operation: "list_procedures",
|
||||
provider: { id: "ghidra", name: "Ghidra", version: "12.1.2" },
|
||||
provider: { id: "ghidra", name: "Ghidra", version: "12.1.4" },
|
||||
analysis_profile: {
|
||||
provider: { id: "ghidra", version: "12.1.2" },
|
||||
provider: { id: "ghidra", version: "12.1.4" },
|
||||
parameters: {
|
||||
import_mode: "ephemeral-read-only",
|
||||
analyzer_preset: "ghidra-default",
|
||||
@@ -78,7 +78,7 @@ describe("Ghidra MCP evidence parity", () => {
|
||||
);
|
||||
expect(pseudocode).toMatchObject({
|
||||
operation: "procedure_pseudo_code",
|
||||
provider: { id: "ghidra", version: "12.1.2" },
|
||||
provider: { id: "ghidra", version: "12.1.4" },
|
||||
normalized_result: expect.stringContaining("return 42"),
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining("not text-equivalent to Hopper"),
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
ghidraReferenceEdge,
|
||||
} from "../../../src/domain/hopperValues.fixture.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
||||
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
||||
|
||||
export const connectGhidraMcp = async (name: string) => {
|
||||
const calls: GhidraOperation[] = [];
|
||||
@@ -98,7 +98,7 @@ export const sessionEvidence = (session: BinarySession, value: unknown) => {
|
||||
const installationHost = (): GhidraInstallationHost => ({
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.2\n",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
@@ -113,7 +113,7 @@ const sessionInfo = (profileDigest: string, targetSha256: string) => ({
|
||||
name: "REA Ghidra bridge" as const,
|
||||
run_id: "11111111-1111-4111-8111-111111111111",
|
||||
profile_digest: profileDigest,
|
||||
provider: { id: "ghidra" as const, version: "12.1.2" },
|
||||
provider: { id: "ghidra" as const, version: "12.1.4" },
|
||||
read_only: true as const,
|
||||
analysis_complete: true,
|
||||
analysis_timed_out: false,
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("Ghidra MCP capability routing", () => {
|
||||
result: {
|
||||
open: true,
|
||||
analysis_provider_binding: {
|
||||
provider: { id: "ghidra", version: "12.1.2" },
|
||||
provider: { id: "ghidra", version: "12.1.4" },
|
||||
},
|
||||
tool_availability: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -82,6 +82,28 @@ describe("read_bytes contract", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("list_strings contract", () => {
|
||||
it("describes optional provider string metadata inline", () => {
|
||||
const contract = OFFICIAL_TOOL_CONTRACTS.find(
|
||||
({ name }) => name === "list_strings",
|
||||
);
|
||||
expect(
|
||||
contract?.outputSchema.shape.result.safeParse([
|
||||
{
|
||||
address: "0x1000",
|
||||
value: "coffee",
|
||||
string: {
|
||||
encoding: "UTF-8",
|
||||
termination: "present_or_not_required",
|
||||
byte_length: 6,
|
||||
},
|
||||
},
|
||||
{ address: "0x1008", value: "beans" },
|
||||
]).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
resources.splice(0).map(async (resource) => resource.close()),
|
||||
|
||||
@@ -23,7 +23,7 @@ const fixturePath = fileURLToPath(
|
||||
new URL("../../../fixtures/fakeGhidra.mjs", import.meta.url),
|
||||
);
|
||||
const PROFILE_DIGEST = "a".repeat(64);
|
||||
const PROVIDER_VERSION = "12.1.2";
|
||||
const PROVIDER_VERSION = "12.1.4";
|
||||
const TARGET_SHA256 = createHash("sha256")
|
||||
.update(readFileSync(fixturePath))
|
||||
.digest("hex");
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("Ghidra headless launcher", () => {
|
||||
const command = ghidraHeadlessCommand({
|
||||
platform: "win32",
|
||||
analyzeHeadlessPath:
|
||||
"C:\\Program Files\\Ghidra 12.1.2\\support\\analyzeHeadless.bat",
|
||||
"C:\\Program Files\\Ghidra 12.1.4\\support\\analyzeHeadless.bat",
|
||||
arguments: [
|
||||
"C:\\REA Runtime\\project",
|
||||
"rea-project",
|
||||
@@ -116,7 +116,7 @@ describe("Ghidra headless launcher", () => {
|
||||
"/v:off",
|
||||
"/s",
|
||||
"/c",
|
||||
'""C:\\Program Files\\Ghidra 12.1.2\\support\\analyzeHeadless.bat" "C:\\REA Runtime\\project" "rea-project" "-import" "C:\\REA Runtime\\target.exe""',
|
||||
'""C:\\Program Files\\Ghidra 12.1.4\\support\\analyzeHeadless.bat" "C:\\REA Runtime\\project" "rea-project" "-import" "C:\\REA Runtime\\target.exe""',
|
||||
],
|
||||
});
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe("Ghidra headless launcher", () => {
|
||||
runId: "d6fcbb66-e829-4ff6-a535-0035aec63139",
|
||||
targetPath: "/tmp/fixture",
|
||||
targetSha256: "b".repeat(64),
|
||||
providerVersion: "12.1.2",
|
||||
providerVersion: "12.1.4",
|
||||
profileDigest: "a".repeat(64),
|
||||
});
|
||||
expect(launched.ok).toBe(true);
|
||||
|
||||
@@ -64,6 +64,23 @@ const request = async (
|
||||
limits: { resultDepth: 16, resultNodes: 10_000, exceptionBytes: 4096 },
|
||||
});
|
||||
|
||||
it("uses the caller's full output budget for exception text", async () => {
|
||||
const message = "x".repeat(70_000);
|
||||
const workerRequest = await request("exception.mjs", "esm", "default", [
|
||||
message,
|
||||
]);
|
||||
workerRequest.limits.exceptionBytes = 80_000;
|
||||
const result = await runWorker(workerRequest);
|
||||
|
||||
expect(result.code).toBe(0);
|
||||
const response = JSON.parse(result.stdout) as {
|
||||
readonly left: readonly {
|
||||
readonly exception: { readonly message: string };
|
||||
}[];
|
||||
};
|
||||
expect(response.left[0]?.exception.message).toBe(`fixture:${message}`);
|
||||
});
|
||||
|
||||
describe("disposable JavaScript replay worker", () => {
|
||||
it("rejects malformed runtime-hop requests before loading modules", async () => {
|
||||
const result = await runWorker({
|
||||
|
||||
Reference in New Issue
Block a user