perf(ghidra): aggregate binary overview at the provider boundary (#4754)

Compute overview counts at the Ghidra inventory owner, preserve complete inventory access and coverage, and share CLI/MCP projection. Keep native aggregate Evidence separate from workflow snapshot identities.

Fixes #4746

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-17 10:44:24 +00:00
1 parent 5120eb4412
commit ae029fe2f8
24 files changed
+406 -52

No files matched your search

+18
View File
@@ -140,6 +140,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
"params"
);
private static final String[] CAPABILITIES = {
"read_binary_inventory_summary",
"annotate_native_function",
"set_address_name",
"set_addresses_names",
@@ -427,6 +428,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
);
}
return switch (request.method) {
case "read_binary_inventory_summary" -> binaryInventorySummary(request.params);
case "inspect_native_load_image" -> inspectNativeLoadImage(request.params);
case "read_bytes" -> readMemoryBytes(request.params);
case "search_bytes" -> searchMemoryBytes(request.params, descriptor);
@@ -1974,6 +1976,18 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
private JsonObject binaryInventorySummary(JsonObject params) throws Exception {
requireKeys(params, Set.of());
JsonObject result = new JsonObject();
result.addProperty("document", currentProgram.getName());
result.add("segments", segments());
// Count the owning inventories, preserving their exact discovery rules
// without serializing function identities or string values for counts.
result.addProperty("procedure_count", procedures().size());
result.addProperty("string_count", strings().size());
return result;
}
private JsonArray listNames(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
@@ -2000,6 +2014,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
private JsonArray listSegments(JsonObject params) {
requireKeys(params, Set.of("document"));
requireDocument(params);
return segments();
}
private JsonArray segments() {
JsonArray result = new JsonArray();
String imageBase = canonicalAddress(currentProgram.getImageBase());
MemoryBlock[] blocks = currentProgram.getMemory().getBlocks();
+8
View File
@@ -65,6 +65,14 @@ PDB, or change analyzer settings.
## Loaded addresses and file offsets
Ghidra computes overview procedure and string counts from the same inventories
used by `list_procedures` and `list_strings`, without transporting those rows
solely to count them. Counts include external functions and only Ghidra-defined
strings; they do not establish discovery of unidentified functions or undefined
string bytes. Complete inventories remain available through the list tools.
Overview reuse follows the target/profile snapshot lifetime and is invalidated
by session metadata edits.
`rea analyze <path> --provider ghidra --json` and MCP `binary_overview` report
the observed loaded `image_base` in `normalized_result`. They retain each
segment's reported `image_base` and `address_space`. A provider that does not
+10 -9
View File
@@ -442,15 +442,16 @@ try {
return evidence.normalized_result;
},
);
const { GHIDRA_MUTATING_OPERATIONS, GHIDRA_SESSION_CAPABILITIES } =
await import(
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
);
const expected = GHIDRA_SESSION_CAPABILITIES.filter(
(name) =>
name !== "ping" &&
name !== "shutdown" &&
!GHIDRA_MUTATING_OPERATIONS.has(name),
const { GHIDRA_MUTATING_OPERATIONS } = await import(
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
);
const { GHIDRA_OPERATIONS } = await import(
pathToFileURL(
join(packageRoot, "dist/ghidra/GhidraProviderCapabilities.js"),
)
);
const expected = GHIDRA_OPERATIONS.filter(
(name) => !GHIDRA_MUTATING_OPERATIONS.has(name),
);
const expectedOperations = new Set([
...expected,
@@ -90,6 +90,20 @@ try {
),
);
const procedures = parseEvidence(await call("list_procedures"));
const strings = parseEvidence(await call("list_strings"));
assert.equal(
overview.normalized_result.procedure_count,
procedures.normalized_result.length,
);
assert.equal(
overview.normalized_result.string_count,
strings.normalized_result.length,
);
assert.ok(
overview.limitations.some((text) =>
text.includes("Ghidra-defined string Data"),
),
);
const entry = procedures.normalized_result.find(
(item) => item.value === "rea_pie_entry",
);
+10
View File
@@ -166,6 +166,16 @@ try {
!Array.isArray(strings)
)
throw new Error("Windows Ghidra inventory proof was incomplete");
const overview = await inventory("read_binary_inventory_summary", {});
if (
overview.document !== documents[0] ||
overview.procedure_count !== procedures.length ||
overview.string_count !== strings.length ||
overview.segment_count !== segments.length
)
throw new Error(
"Windows Ghidra overview aggregates disagree with inventories",
);
const procedure = procedures.find(
(item) => item.procedure?.external === false,
);
+3
View File
@@ -40,6 +40,9 @@ export type AnalysisOperation =
| NativeToolName
| ArtifactAnalysisOperation
| ManagedToolName
// Provider aggregate and composed public overview have distinct Evidence and
// snapshot identities. This capability does not add a CLI/MCP tool.
| "read_binary_inventory_summary"
| "health";
/** Atomic successful provider observation with exact producing provenance. */
+61
View File
@@ -0,0 +1,61 @@
import { expect, it } from "vitest";
import { EnhancedTools } from "./EnhancedTools.js";
import { createAnalysisExecution } from "./AnalysisProvider.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { err, ok } from "../domain/result.js";
it("uses the provider-owned overview without requesting complete inventories", async () => {
const calls: string[] = [];
const result = {
document: "selected",
procedure_count: 12,
string_count: 34,
};
const tools = new EnhancedTools({
execute: async (operation) => {
calls.push(operation);
return ok(
createAnalysisExecution(result, {
id: "fixture",
name: "Fixture",
version: "1",
}),
);
},
});
expect(await tools.execute("binary_overview", {})).toEqual(ok(result));
expect(calls).toEqual(["read_binary_inventory_summary"]);
});
it.each([
new AnalysisOutputError("binary_overview", "Malformed aggregate"),
new AnalysisCancelledError("binary_overview"),
new AnalysisCapabilityUnavailableError(
"fixture",
"binary_overview",
"Cleanup pending",
{
cleanup: {
reason: "Owned process is still closing",
resources: ["session"],
},
},
),
])(
"preserves an aggregate failure rather than silently repeating analysis: %s",
async (failure) => {
const calls: string[] = [];
const tools = new EnhancedTools({
execute: async (operation) => {
calls.push(operation);
return err(failure);
},
});
expect(await tools.execute("binary_overview", {})).toEqual(err(failure));
expect(calls).toEqual(["read_binary_inventory_summary"]);
},
);
@@ -561,6 +561,24 @@ it("reports host denial before target mismatch but keeps composed workflow prere
});
describe("binary_overview capability requirements", () => {
it("admits a native overview without requiring full inventory operations", () => {
expect(
entry(
"binary_overview",
status({
open: true,
kind: "executable",
capabilities: [
{
operation: "read_binary_inventory_summary",
available: true,
reason: null,
},
],
}),
),
).toMatchObject({ available: true, reason: "available" });
});
it("admits a provider with current_document and other mandatory capabilities", () => {
const capabilities = [
"current_document",
+5
View File
@@ -679,6 +679,11 @@ const composedAvailability = (
name: string,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
if (
name === "binary_overview" &&
descriptors.get("read_binary_inventory_summary")?.available === true
)
return { reason: "available", remediation: null };
const requirements = ENHANCED_REQUIREMENTS[name];
if (requirements === undefined)
return {
+20 -29
View File
@@ -16,13 +16,12 @@ import {
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
addressDistance,
functionDossierSchema,
parseFunctionDossier,
parseListCount,
parseRelatedAddresses,
parseSegments,
parseSegmentImageBase,
projectBinaryOverview,
} from "../domain/hopperValues.js";
import { err, ok, type Result } from "../domain/result.js";
import {
@@ -617,6 +616,17 @@ export class EnhancedTools {
}
async #binaryOverview(signal?: AbortSignal): EnhancedResult {
const direct = await this.#call(
"read_binary_inventory_summary",
{},
signal,
);
if (
direct.ok ||
!(direct.error instanceof AnalysisCapabilityUnavailableError) ||
direct.error.cleanupIncomplete
)
return direct;
const document = await resolveAnalysisDocument(
this.analysis,
undefined,
@@ -647,33 +657,14 @@ export class EnhancedTools {
const stringCount = parseListCount(stringsResult.value, "strings");
if (!stringCount.ok) return stringCount;
const observedSegments = segments?.ok ? segments.value : undefined;
const imageBase =
observedSegments === undefined
? undefined
: parseSegmentImageBase(observedSegments);
return ok({
document: document.value,
...(imageBase === undefined ? {} : { image_base: imageBase }),
...(observedSegments === undefined
? {}
: {
segments: observedSegments.map(
({ name, start, end, image_base, address_space }) => ({
name,
start,
end,
length: addressDistance(start, end),
...(image_base === undefined ? {} : { image_base }),
...(address_space === undefined ? {} : { address_space }),
}),
),
segment_count: observedSegments.length,
}),
procedure_count: procedureCount.value,
string_count: stringCount.value,
});
return ok(
projectBinaryOverview({
document: document.value,
...(segments?.ok ? { segments: segments.value } : {}),
procedure_count: procedureCount.value,
string_count: stringCount.value,
}),
);
}
async #allAddressed(
+33
View File
@@ -527,6 +527,39 @@ export const parseSegmentImageBase = (
: undefined;
};
/** Project the same overview from complete lists or provider-owned aggregates. */
export const projectBinaryOverview = (input: {
readonly document: string;
readonly procedure_count: number;
readonly string_count: number;
readonly segments?: readonly SegmentSummary[];
}): JsonValue => {
const { document, procedure_count, string_count, segments } = input;
const imageBase =
segments === undefined ? undefined : parseSegmentImageBase(segments);
return {
document,
...(imageBase === undefined ? {} : { image_base: imageBase }),
...(segments === undefined
? {}
: {
segments: segments.map(
({ name, start, end, image_base, address_space }) => ({
name,
start,
end,
length: addressDistance(start, end),
...(image_base === undefined ? {} : { image_base }),
...(address_space === undefined ? {} : { address_space }),
}),
),
segment_count: segments.length,
}),
procedure_count,
string_count,
};
};
/** Parse direct or wrapped provider document names. */
export const parseDocuments = (
value: JsonValue,
+50 -1
View File
@@ -1,6 +1,55 @@
import { describe, expect, it } from "vitest";
import { parseGhidraInventoryResult } from "./GhidraInventoryValues.js";
import {
parseGhidraInventoryInput,
parseGhidraInventoryResult,
} from "./GhidraInventoryValues.js";
describe("provider-owned overview boundary", () => {
const overview = {
document: "fixture",
segments: [],
procedure_count: 0,
string_count: 0,
};
it("keeps exact empty counts and an observed empty segment set", () => {
expect(
parseGhidraInventoryInput("read_binary_inventory_summary", {}),
).toEqual({
ok: true,
value: {},
});
expect(
parseGhidraInventoryInput("read_binary_inventory_summary", {
document: "other",
}).ok,
).toBe(false);
expect(
parseGhidraInventoryResult("read_binary_inventory_summary", overview),
).toEqual({
ok: true,
value: { ...overview, segment_count: 0 },
});
});
it.each([
{ procedure_count: -1 },
{ string_count: 1.5 },
{ procedure_count: Number.MAX_SAFE_INTEGER + 1 },
{ string_count: null },
{ document: "" },
{ segments: [{ name: "incomplete" }] },
{ extra: "unexpected" },
])("rejects malformed aggregates before creating Evidence: %j", (change) => {
expect(
parseGhidraInventoryResult("read_binary_inventory_summary", {
...overview,
...change,
}).ok,
).toBe(false);
});
});
// Wire-boundary fixture only; real-provider acceptance belongs to the DOS verifier.
describe("containing-procedure complete body evidence", () => {
+11
View File
@@ -13,6 +13,7 @@ import {
import {
functionBodySchema,
functionBodyEntryAgrees,
projectBinaryOverview,
} from "../domain/hopperValues.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -23,6 +24,7 @@ import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"
* except the naming operations, which edit the ephemeral session database.
*/
export const GHIDRA_INVENTORY_OPERATIONS = [
"read_binary_inventory_summary",
"inspect_native_load_image",
"read_bytes",
"search_bytes",
@@ -78,6 +80,7 @@ const searchInput = {
};
const inputSchemas = {
read_binary_inventory_summary: z.strictObject({}),
search_bytes: z
.strictObject({
...bytePatternFields,
@@ -287,6 +290,14 @@ const containingProcedure = z.discriminatedUnion("found", [
]);
const resultSchemas = {
read_binary_inventory_summary: z
.strictObject({
document: identifier,
segments: z.array(segment),
procedure_count: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
string_count: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
})
.transform(projectBinaryOverview),
search_bytes: byteSearchResultSchema.superRefine((value, context) => {
const addresses = [
...value.matches.flatMap((match) => [
+19 -3
View File
@@ -21,12 +21,21 @@ export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
version: null,
});
/** Analyst operations implemented by the Ghidra inventory and function adapters. */
export const GHIDRA_OPERATIONS = Object.freeze([
/** Provider observations, including internal aggregates consumed by workflows. */
const providerOperations = Object.freeze([
...GHIDRA_INVENTORY_OPERATIONS,
...GHIDRA_FUNCTION_OPERATIONS,
]);
/** Public analyst tools backed by these observations; aggregates stay internal. */
export const GHIDRA_OPERATIONS = Object.freeze(
providerOperations.map((operation) =>
operation === "read_binary_inventory_summary"
? "binary_overview"
: operation,
),
);
/** Health limitations shared by every Ghidra-backed capability. */
export const healthLimitations = Object.freeze([
"The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.",
@@ -54,6 +63,13 @@ export const limitationsFor = (operation: string): readonly string[] => {
"Default-space addresses use lowercase 0x-prefixed hexadecimal; other address spaces use <percent-encoded-space>:0x<hex>.",
];
switch (operation) {
case "read_binary_inventory_summary":
return [
...common,
stringInventoryLimitation,
"Counts use the complete defined-string inventory and the same local/external function inventory as list_strings and list_procedures. They do not establish discovery of undefined strings or unidentified functions.",
"Memory-block end addresses are exclusive; segment lengths use those observed address coordinates.",
];
case "annotate_native_function":
return [
...common,
@@ -174,7 +190,7 @@ export const limitationsFor = (operation: string): readonly string[] => {
/** Provider-neutral capabilities advertised by every non-Windows Ghidra session. */
export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
GHIDRA_OPERATIONS.map((operation) => {
providerOperations.map((operation) => {
return Object.freeze({
provider: GHIDRA_PROVIDER_IDENTITY,
operation,
+26 -2
View File
@@ -94,6 +94,13 @@ const fixture = (version = "12.1.4", coverage?: RelationshipCoverage) => {
throw new Error("Expected an annotation name");
name = parameters.name;
}
if (operation === "read_binary_inventory_summary")
return ok({
document: "fixture",
segments: [],
procedure_count: 0,
string_count: 0,
});
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
dossier.procedure.name = name;
dossier.pseudocode = `void ${name}() {}`;
@@ -267,6 +274,7 @@ describe("Ghidra snapshot profile admission", () => {
describe("Ghidra snapshot cache", () => {
it.each([
{ operation: "read_binary_inventory_summary", parameters: {} },
{ operation: "analyze_function", parameters: { procedure: "0x1000" } },
{
operation: "procedure_pseudo_code",
@@ -327,6 +335,9 @@ describe("Ghidra snapshot cache", () => {
it("keeps annotation-dependent reads live after cancellation reimports the original artifact", async () => {
const { session, calls, cancellationReady } = fixture();
await open(session);
expect(
(await session.execute("read_binary_inventory_summary", {})).ok,
).toBe(true);
const parameters = { procedure: "0x1000", document: "fixture" };
expect(
await session.execute("procedure_pseudo_code", parameters),
@@ -342,13 +353,17 @@ describe("Ghidra snapshot cache", () => {
})
).ok,
).toBe(true);
for (let index = 0; index < 2; index += 1)
for (let index = 0; index < 2; index += 1) {
expect(
(await session.execute("read_binary_inventory_summary", {})).ok,
).toBe(true);
expect(
await session.execute("procedure_pseudo_code", parameters),
).toMatchObject({
ok: true,
value: { result: "void renamed() {}" },
});
}
expect(session.exportAnalysisSnapshot().ok).toBe(false);
const controller = new AbortController();
const cancelled = session.execute(
@@ -362,13 +377,22 @@ describe("Ghidra snapshot cache", () => {
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
for (let index = 0; index < 2; index += 1)
for (let index = 0; index < 2; index += 1) {
expect(
(await session.execute("read_binary_inventory_summary", {})).ok,
).toBe(true);
expect(
await session.execute("procedure_pseudo_code", parameters),
).toMatchObject({
ok: true,
value: { result: "void original() {}" },
});
}
expect(
calls.filter(
(operation) => operation === "read_binary_inventory_summary",
),
).toHaveLength(5);
expect(
calls.filter((operation) => operation === "procedure_pseudo_code"),
).toHaveLength(5);
@@ -180,6 +180,20 @@ const makeProvider = (
trace.calls.push(operation);
const results: Readonly<Record<string, unknown>> = {
health: null,
read_binary_inventory_summary: {
document: "fixture",
procedure_count: 1,
string_count: 1,
segment_count: 1,
segments: [
{
name: "__TEXT",
start: "0x1000",
end: "0x2000",
length: 4096,
},
],
},
list_segments: [
{
name: "__TEXT",
@@ -66,6 +66,16 @@ const fixturePort = (): AnalysisOperationPort => ({
// oxlint-disable-next-line complexity -- this fixture exhaustively serves the registered MCP surface.
execute: (name, arguments_) => {
switch (name) {
case "read_binary_inventory_summary":
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
"fixture",
name,
"This fixture exercises composed overview",
),
),
);
case "inspect_native_dispatch_metadata":
return Promise.resolve(
err(
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
closeEnhancedToolResources,
connect,
connect as connectTool,
jsonResult,
} from "./enhancedToolsHarness.js";
import type {
@@ -20,6 +20,23 @@ import { observed as ok } from "../../fixtures/analysisExecution.js";
afterEach(closeEnhancedToolResources);
// These fixtures exercise composition for providers without a native overview.
const connect = (analysis: AnalysisOperationPort) =>
connectTool({
execute: (operation, parameters, options) =>
operation === "read_binary_inventory_summary"
? Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"No native overview",
),
),
)
: analysis.execute(operation, parameters, options),
});
it("discovers declarations without counting duplicates or compiler bookkeeping", async () => {
const client = await connect({
execute: (name) => {
@@ -1,6 +1,7 @@
import { afterEach, expect, it } from "vitest";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { ok } from "../../../src/domain/result.js";
import { err, ok } from "../../../src/domain/result.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { connect, closeEnhancedToolResources } from "./enhancedToolsHarness.js";
@@ -11,6 +12,14 @@ it("retains provider coverage caveats in a composed MCP overview without repeate
const limitation = "Companion PDB symbol coverage is unverified.";
const client = await connect({
execute: async (operation) => {
if (operation === "read_binary_inventory_summary")
return err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"No native overview",
),
);
calls.push(operation);
const value = operation === "current_document" ? "fixture" : [];
return ok(
@@ -413,6 +413,7 @@ describe("Ghidra MCP evidence parity", () => {
],
});
const beforeOverview = harness.calls.length;
const mcpOverview = sessionEvidence(
session,
(
@@ -427,6 +428,9 @@ describe("Ghidra MCP evidence parity", () => {
{},
);
if (!directOverview.ok) throw directOverview.error;
expect(harness.calls.slice(beforeOverview)).toEqual([
"read_binary_inventory_summary",
]);
expect(mcpOverview.normalized_result).toEqual(directOverview.value);
expect(mcpOverview).toMatchObject({
provider: { id: "rea-workflow" },
+9
View File
@@ -144,6 +144,15 @@ type GhidraResultBuilder = (
) => JsonValue;
const resultBuilders = new Map<GhidraOperation, GhidraResultBuilder>([
[
"read_binary_inventory_summary",
() => ({
document: "fixture",
segments: resultFor("list_segments", {}),
procedure_count: 1,
string_count: 2,
}),
],
["list_documents", () => ["fixture"]],
[
"list_procedures",
@@ -7,6 +7,8 @@ import {
jsonResult,
} from "./enhancedToolsHarness.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
import { err } from "../../../src/domain/result.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import type { SegmentSummary } from "../../../src/domain/hopperValues.js";
afterEach(closeEnhancedToolResources);
@@ -25,6 +27,16 @@ const overviewClient = (segments: SegmentSummary[]) =>
connect({
execute: (name) => {
switch (name) {
case "read_binary_inventory_summary":
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
"fixture",
name,
"No native overview",
),
),
);
case "current_document":
return Promise.resolve(ok("fixture"));
case "list_documents":
@@ -716,6 +716,11 @@ describe("GhidraClient", () => {
const client = clientFor(launcher);
const started = await client.start();
const expectedCapabilities = GHIDRA_SESSION_CAPABILITIES.filter(
(value) =>
HOST_TRANSPORT === "unix-socket" ||
!GHIDRA_MUTATING_OPERATIONS.has(value),
);
expect(started).toMatchObject({
ok: true,
value: {
@@ -724,18 +729,22 @@ describe("GhidraClient", () => {
read_only: HOST_TRANSPORT !== "unix-socket",
analysis_complete: true,
analysis_timed_out: false,
capabilities: GHIDRA_SESSION_CAPABILITIES.filter(
(value) =>
HOST_TRANSPORT === "unix-socket" ||
!GHIDRA_MUTATING_OPERATIONS.has(value),
),
target: {
image_base: "0x1000",
default_address_space: "ram",
},
},
});
if (started.ok) expect(started.value).not.toHaveProperty("bridge_version");
if (started.ok) {
// Capability identity is an exact set; bridge declaration order is not a contract.
expect(new Set(started.value.capabilities)).toEqual(
new Set(expectedCapabilities),
);
expect(started.value.capabilities).toHaveLength(
expectedCapabilities.length,
);
expect(started.value).not.toHaveProperty("bridge_version");
}
if (HOST_TRANSPORT === "unix-socket")
expect(Buffer.byteLength(launcher.endpointPaths[0] ?? "")).toBeLessThan(
108,
+8
View File
@@ -255,6 +255,7 @@ const sessionInfo = ({
analysis_complete: !timedOut,
analysis_timed_out: timedOut,
capabilities: [
"read_binary_inventory_summary",
"ping",
"shutdown",
"inspect_native_load_image",
@@ -307,6 +308,13 @@ const sessionInfo = ({
const inventoryResultFor = (method, params) => {
switch (method) {
case "read_binary_inventory_summary":
return {
document: "fixture",
segments: inventoryResultFor("list_segments", {}),
procedure_count: inventoryResultFor("list_procedures", {}).length,
string_count: inventoryResultFor("list_strings", {}).length,
};
case "search_bytes": {
const continued = params.cursor === "fixture-second-page";
const paged = params.limit === 1 && !continued;