feat(evidence): discover retained records without full payloads (#1682)
* feat(evidence): discover retained records without full payloads * fix(evidence): guide oversized results to retained discovery * style(tests): format retained discovery recovery assertion
This commit is contained in:
1 parent
c8820e03db
commit
17f704050f
15 files changed
+514
-7
No files matched your search
@@ -135,6 +135,16 @@ session to a selected path. The analysis remains complete in the ledger; avoid
|
||||
repeating it merely to request the same oversized response. Complete CLI JSON
|
||||
output also streams without a single MCP frame.
|
||||
|
||||
When the retained parent ID is unknown, use `get_evidence_bundle` with
|
||||
`detail: "summary"` and exact filters for operation, target SHA-256,
|
||||
analysis-profile digest or canonical procedure address. Select its returned
|
||||
Evidence ID for the next view instead of requesting the complete bundle.
|
||||
`retention: "complete-record"` means the whole record is stored; native
|
||||
value-flow truncation and unknown metadata remain separate. For an exported
|
||||
portable bundle, use `rea inspect-evidence-bundle` with JSON containing `path`,
|
||||
`detail: "summary"` and the same `filters`. Reading that file still requires
|
||||
validating its full contents within the local memory budget.
|
||||
|
||||
A client can also truncate a successful tool result without a REA transport
|
||||
error. Treat that preview as incomplete; use the retained parent ID for smaller
|
||||
views or export when the full record is needed. Do not parse the preview as
|
||||
|
||||
@@ -308,6 +308,23 @@ that reference for a summary, one section/module, or a stable page, or call
|
||||
bundle exports stream canonical JSON into an atomically published file. A broad
|
||||
follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
|
||||
preserves the complete session without sending it through a single MCP frame.
|
||||
To discover existing records without their large payloads, call
|
||||
`get_evidence_bundle` with `detail: "summary"`. Its `result.records` contain
|
||||
Evidence IDs, artifact/provider identities, analysis-profile digests and native
|
||||
procedure metadata. Optional `filters` match exact `evidence_id`, `operation`,
|
||||
`target_sha256`, `analysis_profile_digest` or `procedure_address`. Use a discovered
|
||||
ID with `inspect_analysis_view`; neither discovery nor retained selection starts
|
||||
a provider. `retention: "complete-record"` describes stored Evidence, while
|
||||
`native_dossier.value_flow` independently reports truncation, unavailable or
|
||||
unknown metadata. Missing omission counts remain null. Summary output carries
|
||||
every matching record; actual transport admission still applies. Omitted
|
||||
`detail` preserves complete canonical bundle delivery; filters require summary.
|
||||
|
||||
For a portable exported bundle, the CLI shares this projection:
|
||||
`rea inspect-evidence-bundle '{"path":"/tmp/evidence.json","detail":"summary","filters":{"operation":"analyze_function"}}' --format json`.
|
||||
The CLI validates and materializes that local bundle under its existing memory
|
||||
budget; summary delivery does not make arbitrary large files cheap to import.
|
||||
|
||||
Cancelling an export stops further serialization and removes its staging file.
|
||||
The destination changes only when a complete export is atomically published.
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import {
|
||||
summarizeEvidenceBundle,
|
||||
type EvidenceBundleSummary,
|
||||
type EvidenceRecordFilters,
|
||||
} from "../../domain/evidenceRecordSummary.js";
|
||||
|
||||
/** Select complete bundle delivery or discovery metadata without analysis payloads. */
|
||||
export const inspectEvidenceBundle = (
|
||||
bundle: EvidenceBundle,
|
||||
options: {
|
||||
readonly detail: "complete" | "summary";
|
||||
readonly filters?: EvidenceRecordFilters | undefined;
|
||||
},
|
||||
): Result<EvidenceBundle | EvidenceBundleSummary, AnalysisInputError> => {
|
||||
if (options.detail === "complete" && options.filters !== undefined)
|
||||
return err(
|
||||
new AnalysisInputError("get_evidence_bundle", undefined, [
|
||||
{
|
||||
path: ["filters"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Record filters require detail: summary; complete returns the canonical bundle.",
|
||||
expected: "detail: summary",
|
||||
},
|
||||
]),
|
||||
);
|
||||
return ok(
|
||||
options.detail === "summary"
|
||||
? summarizeEvidenceBundle(bundle, options.filters)
|
||||
: bundle,
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,12 @@
|
||||
import { z } from "incur";
|
||||
|
||||
import { inspectAnalysisViewValidated } from "../application/analysisView/AnalysisViewService.js";
|
||||
import { readEvidenceBundle } from "../application/EvidenceBundleFiles.js";
|
||||
import { inspectEvidenceBundle } from "../application/investigation/InspectEvidenceBundle.js";
|
||||
import {
|
||||
getEvidenceBundleInputSchema,
|
||||
importEvidenceBundleInputSchema,
|
||||
} from "../contracts/sessionToolSchemas.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
@@ -16,6 +22,53 @@ export const registerAnalysisViewCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectEvidenceBundle, {
|
||||
description:
|
||||
"Inspect complete or selected discovery metadata from a local Evidence bundle",
|
||||
args: z.object({
|
||||
inputJson: z
|
||||
.string()
|
||||
.describe(
|
||||
"JSON or JSON file with absolute path, detail and optional record filters",
|
||||
),
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectEvidenceBundle, async () => {
|
||||
const input = await parseCliJsonInput(
|
||||
args.inputJson,
|
||||
CLI_COMMANDS.inspectEvidenceBundle,
|
||||
);
|
||||
if (!input.ok) return input.error;
|
||||
const schema = getEvidenceBundleInputSchema.safeExtend({
|
||||
path: importEvidenceBundleInputSchema.shape.path,
|
||||
});
|
||||
const parsed = schema.safeParse(input.value);
|
||||
if (!parsed.success)
|
||||
return {
|
||||
error: "Evidence bundle inspection failed",
|
||||
...projectAnalysisError(
|
||||
analysisInputErrorFromIssues(
|
||||
CLI_COMMANDS.inspectEvidenceBundle,
|
||||
parsed.error.issues,
|
||||
input.value,
|
||||
),
|
||||
),
|
||||
};
|
||||
const loaded = await readEvidenceBundle(parsed.data.path);
|
||||
if (!loaded.ok)
|
||||
return {
|
||||
error: "Evidence bundle inspection failed",
|
||||
...projectAnalysisError(loaded.error),
|
||||
};
|
||||
const result = inspectEvidenceBundle(loaded.value, parsed.data);
|
||||
return result.ok
|
||||
? result.value
|
||||
: {
|
||||
error: "Evidence bundle inspection failed",
|
||||
...projectAnalysisError(result.error),
|
||||
};
|
||||
}),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.inspectAnalysisView, {
|
||||
description:
|
||||
"Project a selected view of completed layout, JavaScript application or native function Evidence JSON",
|
||||
|
||||
@@ -43,6 +43,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectBinaryLayout: "inspect-binary-layout",
|
||||
inspectPeResources: "inspect-pe-resources",
|
||||
inspectAnalysisView: "inspect-analysis-view",
|
||||
inspectEvidenceBundle: "inspect-evidence-bundle",
|
||||
inspectFirmwareRegions: "inspect-firmware-regions",
|
||||
extractFirmware: "extract-firmware",
|
||||
inspectAndroidPackage: "inspect-android-package",
|
||||
|
||||
@@ -159,7 +159,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
),
|
||||
session(
|
||||
"get_evidence_bundle",
|
||||
"Return every Evidence record and residual unknown currently retained by this session as one inline bundle for direct inspection or follow-up workflows.",
|
||||
"Return every retained Evidence record and residual unknown as a complete canonical bundle by default. detail=summary discovers retained records without copying or transferring their payloads; optionally filter by exact Evidence ID, operation, target SHA-256, analysis-profile digest or canonical native procedure address. Metadata distinguishes complete record retention from native value-flow truncation or unavailable/unknown coverage. Use a discovered evidence_id with inspect_analysis_view without restarting a provider. Filters apply only to summary delivery.",
|
||||
getEvidenceBundleInputSchema,
|
||||
),
|
||||
session(
|
||||
|
||||
@@ -6,9 +6,13 @@ import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { processTraceSpecificationSchema } from "../domain/process/processTraceSpecification.js";
|
||||
import { evidenceSchema } from "../domain/evidence.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
import { evidenceRecordFiltersSchema } from "../domain/evidenceRecordSummary.js";
|
||||
|
||||
/** Return the current canonical Evidence bundle inline. */
|
||||
export const getEvidenceBundleInputSchema = z.strictObject({});
|
||||
/** Complete retained Evidence, or selected discovery metadata without payloads. */
|
||||
export const getEvidenceBundleInputSchema = z.strictObject({
|
||||
detail: z.enum(["complete", "summary"]).default("complete"),
|
||||
filters: evidenceRecordFiltersSchema.optional(),
|
||||
});
|
||||
|
||||
/** Optional document selection for volatile navigation context. */
|
||||
export const navigationContextInputSchema = z.strictObject({
|
||||
|
||||
@@ -53,6 +53,10 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
|
||||
trace_native_ui_action: { action: "buildTapped:" },
|
||||
open_binary: { path: "/tmp/fixture" },
|
||||
export_evidence_bundle: { path: "/tmp/evidence.json" },
|
||||
get_evidence_bundle: {
|
||||
detail: "summary",
|
||||
filters: { operation: "analyze_function", procedure_address: "0x1000" },
|
||||
},
|
||||
inspect_address_context: { address: "0x1000" },
|
||||
import_evidence_bundle: { path: "/tmp/evidence.json" },
|
||||
capture_process_scenario: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { evidenceBundleSummarySchema } from "../domain/evidenceRecordSummary.js";
|
||||
import { BINARY_ARCHITECTURES } from "../domain/binaryTargetTypes.js";
|
||||
import { nativeFunctionAnnotationsSchema } from "../domain/native/nativeFunctionAnnotations.js";
|
||||
import { nativeLoadImageSchema } from "../domain/native/nativeLoadImage.js";
|
||||
@@ -470,7 +471,9 @@ export const sessionOutputSchemas = {
|
||||
unknowns: z.number().int().min(0),
|
||||
}),
|
||||
),
|
||||
get_evidence_bundle: lifecycleResultOf(evidenceBundleSchema),
|
||||
get_evidence_bundle: lifecycleResultOf(
|
||||
z.union([evidenceBundleSchema, evidenceBundleSummarySchema]),
|
||||
),
|
||||
get_navigation_context: lifecycleResultOf(
|
||||
z.object({
|
||||
document: z.string(),
|
||||
|
||||
@@ -47,7 +47,7 @@ export const analysisErrorRemediationAction = (
|
||||
return (
|
||||
error.remediationAction ??
|
||||
(error.resource === "transport"
|
||||
? "Export retained session evidence through export_evidence_bundle to a caller-selected path, or use complete CLI JSON output. Tools that accept retained-evidence references can inspect compatible records. The connection remains usable."
|
||||
? "Discover retained records with get_evidence_bundle and detail: summary, then inspect compatible records through inspect_analysis_view using an exact Evidence reference. Export complete retained session evidence through export_evidence_bundle to a caller-selected path, or use complete CLI JSON output. The connection remains usable."
|
||||
: error.resource === "cpu"
|
||||
? "Review the reported worker CPU limits and observed signal. Retry with sufficient CPU time or a smaller artifact; REA retains tighter inherited limits."
|
||||
: error.resource === "file-size"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createEvidence } from "./evidence.js";
|
||||
import { createEvidenceBundle } from "./evidenceBundle.js";
|
||||
import { summarizeEvidenceBundle } from "./evidenceRecordSummary.js";
|
||||
|
||||
it("preserves native identity independently of missing or incompatible value-flow metadata", () => {
|
||||
const records = [
|
||||
undefined,
|
||||
{ available: true, truncated: true },
|
||||
{ unexpected: "legacy" },
|
||||
{ available: false, reason: "Decompiler unavailable" },
|
||||
].map((flow) =>
|
||||
createEvidence(
|
||||
{ path: "/fixtures/native.exe", format: "pe", sha256: "a".repeat(64) },
|
||||
{ id: "provider", name: "Provider", version: "1" },
|
||||
{
|
||||
operation: "analyze_function",
|
||||
parameters: {},
|
||||
result: {
|
||||
procedure: { address: "space:0x10", name: "entry" },
|
||||
...(flow === undefined ? {} : { native_value_flow: flow }),
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
const summary = summarizeEvidenceBundle(createEvidenceBundle(records), {
|
||||
procedure_address: "space:0x10",
|
||||
});
|
||||
expect(summary.matching_records).toBe(4);
|
||||
const native = summary.records.map((r) => r.native_dossier);
|
||||
expect(
|
||||
native.every((r) => r.available && r.procedure_address === "space:0x10"),
|
||||
).toBe(true);
|
||||
expect(native).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ value_flow: { status: "not-recorded" } }),
|
||||
expect.objectContaining({
|
||||
value_flow: {
|
||||
status: "observed",
|
||||
truncated: true,
|
||||
omitted_operations_lower_bound: null,
|
||||
known_omitted_inputs: null,
|
||||
known_omitted_edges: null,
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value_flow: { status: "unknown", reason: expect.any(String) },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value_flow: { status: "unavailable", reason: "Decompiler unavailable" },
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { digestSchema, prefixedDigestSchema } from "./digests.js";
|
||||
import { evidenceSchema, type Evidence } from "./evidence.js";
|
||||
import type { EvidenceBundle } from "./evidenceBundle.js";
|
||||
|
||||
/** Exact retained-record selectors; selection never resolves a provider address. */
|
||||
export const evidenceRecordFiltersSchema = z.strictObject({
|
||||
evidence_id: prefixedDigestSchema("ev").optional(),
|
||||
operation: z.string().min(1).optional(),
|
||||
target_sha256: digestSchema.optional(),
|
||||
analysis_profile_digest: digestSchema.optional(),
|
||||
procedure_address: z.string().min(1).optional(),
|
||||
});
|
||||
export type EvidenceRecordFilters = z.infer<typeof evidenceRecordFiltersSchema>;
|
||||
|
||||
const countSchema = z.number().int().nonnegative();
|
||||
const nativeMetadataSchema = z.object({
|
||||
procedure: z.object({ address: z.string(), name: z.string().nullish() }),
|
||||
native_value_flow: z.unknown().optional(),
|
||||
});
|
||||
const valueFlowMetadataSchema = z.union([
|
||||
z.object({
|
||||
available: z.literal(true),
|
||||
truncated: z.boolean(),
|
||||
omitted_operations_lower_bound: countSchema.optional(),
|
||||
known_omitted_inputs: countSchema.optional(),
|
||||
known_omitted_edges: countSchema.optional(),
|
||||
}),
|
||||
z.object({ available: z.literal(false), reason: z.string() }),
|
||||
]);
|
||||
|
||||
const nativeSummarySchema = z.discriminatedUnion("available", [
|
||||
z.strictObject({
|
||||
available: z.literal(true),
|
||||
procedure_address: z.string(),
|
||||
procedure_name: z.string().nullable(),
|
||||
value_flow: z.discriminatedUnion("status", [
|
||||
z.strictObject({
|
||||
status: z.literal("observed"),
|
||||
truncated: z.boolean(),
|
||||
omitted_operations_lower_bound: countSchema.nullable(),
|
||||
known_omitted_inputs: countSchema.nullable(),
|
||||
known_omitted_edges: countSchema.nullable(),
|
||||
}),
|
||||
z.strictObject({ status: z.literal("unavailable"), reason: z.string() }),
|
||||
z.strictObject({ status: z.literal("not-recorded") }),
|
||||
z.strictObject({ status: z.literal("unknown"), reason: z.string() }),
|
||||
]),
|
||||
}),
|
||||
z.strictObject({
|
||||
available: z.literal(false),
|
||||
reason: z.enum(["different-operation", "not-recorded-or-incompatible"]),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Metadata for complete retained records, independent of analysis completeness. */
|
||||
export const evidenceBundleSummarySchema = z.strictObject({
|
||||
kind: z.literal("evidence-bundle-summary"),
|
||||
filters: z.record(z.string(), z.string()),
|
||||
total_retained_records: countSchema,
|
||||
matching_records: countSchema,
|
||||
unknown_revisions: countSchema,
|
||||
records: z.array(
|
||||
z.strictObject({
|
||||
evidence_id: evidenceSchema.shape.evidence_id,
|
||||
subject: evidenceSchema.shape.subject,
|
||||
provider: evidenceSchema.shape.provider,
|
||||
operation: evidenceSchema.shape.operation,
|
||||
confidence: evidenceSchema.shape.confidence,
|
||||
authority: evidenceSchema.shape.authority,
|
||||
analysis_profile_digest: digestSchema.nullable(),
|
||||
retention: z.literal("complete-record"),
|
||||
limitation_count: countSchema,
|
||||
native_dossier: nativeSummarySchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
export type EvidenceBundleSummary = z.infer<typeof evidenceBundleSummarySchema>;
|
||||
|
||||
const nativeSummary = (
|
||||
evidence: Evidence,
|
||||
): z.infer<typeof nativeSummarySchema> => {
|
||||
if (evidence.operation !== "analyze_function")
|
||||
return { available: false, reason: "different-operation" };
|
||||
// Read only producer metadata. Parsing the full dossier would allocate every
|
||||
// large collection merely to discover its already authenticated parent ID.
|
||||
const parsed = nativeMetadataSchema.safeParse(evidence.normalized_result);
|
||||
if (!parsed.success)
|
||||
return { available: false, reason: "not-recorded-or-incompatible" };
|
||||
const flow = valueFlowMetadataSchema.safeParse(parsed.data.native_value_flow);
|
||||
return {
|
||||
available: true,
|
||||
procedure_address: parsed.data.procedure.address,
|
||||
procedure_name: parsed.data.procedure.name ?? null,
|
||||
value_flow:
|
||||
parsed.data.native_value_flow == null
|
||||
? { status: "not-recorded" }
|
||||
: !flow.success
|
||||
? {
|
||||
status: "unknown",
|
||||
reason: "Value-flow metadata has an incompatible representation.",
|
||||
}
|
||||
: flow.data.available
|
||||
? {
|
||||
status: "observed",
|
||||
truncated: flow.data.truncated,
|
||||
omitted_operations_lower_bound:
|
||||
flow.data.omitted_operations_lower_bound ?? null,
|
||||
known_omitted_inputs: flow.data.known_omitted_inputs ?? null,
|
||||
known_omitted_edges: flow.data.known_omitted_edges ?? null,
|
||||
}
|
||||
: { status: "unavailable", reason: flow.data.reason },
|
||||
};
|
||||
};
|
||||
|
||||
/** Discover retained Evidence without reading or copying its complete payloads. */
|
||||
export const summarizeEvidenceBundle = (
|
||||
bundle: EvidenceBundle,
|
||||
filters: EvidenceRecordFilters = {},
|
||||
): EvidenceBundleSummary => {
|
||||
const selectedFilters: Record<string, string> = {};
|
||||
for (const [field, value] of Object.entries(filters))
|
||||
if (value !== undefined) selectedFilters[field] = value;
|
||||
const records: EvidenceBundleSummary["records"] = [];
|
||||
for (const evidence of bundle.records) {
|
||||
if (
|
||||
(filters.evidence_id !== undefined &&
|
||||
filters.evidence_id !== evidence.evidence_id) ||
|
||||
(filters.operation !== undefined &&
|
||||
filters.operation !== evidence.operation) ||
|
||||
(filters.target_sha256 !== undefined &&
|
||||
filters.target_sha256 !== evidence.subject?.digest.sha256) ||
|
||||
(filters.analysis_profile_digest !== undefined &&
|
||||
filters.analysis_profile_digest !== evidence.analysis_profile?.digest)
|
||||
)
|
||||
continue;
|
||||
const native = nativeSummary(evidence);
|
||||
if (
|
||||
filters.procedure_address !== undefined &&
|
||||
(!native.available ||
|
||||
filters.procedure_address !== native.procedure_address)
|
||||
)
|
||||
continue;
|
||||
records.push({
|
||||
evidence_id: evidence.evidence_id,
|
||||
subject: evidence.subject,
|
||||
provider: evidence.provider,
|
||||
operation: evidence.operation,
|
||||
confidence: evidence.confidence,
|
||||
authority: evidence.authority,
|
||||
analysis_profile_digest: evidence.analysis_profile?.digest ?? null,
|
||||
retention: "complete-record",
|
||||
limitation_count: evidence.limitations.length,
|
||||
native_dossier: native,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: "evidence-bundle-summary",
|
||||
filters: selectedFilters,
|
||||
total_retained_records: bundle.records.length,
|
||||
matching_records: records.length,
|
||||
unknown_revisions: bundle.unknowns.length,
|
||||
records,
|
||||
};
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { ok } from "../domain/result.js";
|
||||
import { inspectEvidenceBundle } from "../application/investigation/InspectEvidenceBundle.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
|
||||
interface EvidenceToolRegistration {
|
||||
@@ -72,9 +73,9 @@ const registerSnapshotEvidenceTool = ({
|
||||
server.registerTool(
|
||||
snapshotContract.name,
|
||||
toolRegistrationOptions(snapshotContract),
|
||||
() =>
|
||||
(input) =>
|
||||
server.delivery.toCallToolResult(
|
||||
ok(bundleForSerialization(session)),
|
||||
inspectEvidenceBundle(bundleForSerialization(session), input),
|
||||
snapshotContract,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -9,6 +9,53 @@ import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analysisViewLayoutEvidence } from "../../fixtures/analysisView.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { inspectEvidenceBundle } from "../../../src/application/investigation/InspectEvidenceBundle.js";
|
||||
|
||||
cliTest(
|
||||
"discovers native records from a complete portable bundle using the shared workflow",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-evidence-discovery-cli-");
|
||||
const parent = createEvidence(
|
||||
{ path: "/fixtures/native.exe", format: "pe", sha256: "a".repeat(64) },
|
||||
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
|
||||
{
|
||||
operation: "analyze_function",
|
||||
parameters: {},
|
||||
result: ghidraFunctionDossier(),
|
||||
},
|
||||
);
|
||||
const bundle = createEvidenceBundle([parent]);
|
||||
const path = join(root, "evidence.json");
|
||||
await writeFile(path, JSON.stringify(bundle));
|
||||
const options = {
|
||||
detail: "summary" as const,
|
||||
filters: { evidence_id: parent.evidence_id },
|
||||
};
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"inspect-evidence-bundle",
|
||||
JSON.stringify({ path, ...options }),
|
||||
"--json",
|
||||
],
|
||||
environment: { HOME: root, XDG_CONFIG_HOME: root, XDG_CACHE_HOME: root },
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
const expected = inspectEvidenceBundle(bundle, options);
|
||||
if (!expected.ok) throw expected.error;
|
||||
expect(result.json).toEqual(expected.value);
|
||||
const complete = await cli.run({
|
||||
arguments: [
|
||||
"inspect-evidence-bundle",
|
||||
JSON.stringify({ path }),
|
||||
"--json",
|
||||
],
|
||||
environment: { HOME: root, XDG_CONFIG_HOME: root, XDG_CACHE_HOME: root },
|
||||
});
|
||||
expect(complete.exitCode).toBe(0);
|
||||
expect(complete.json).toEqual(bundle);
|
||||
},
|
||||
);
|
||||
cliTest(
|
||||
"projects one section from a JSON file of inline layout Evidence",
|
||||
async ({ cli }) => {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { z } from "zod";
|
||||
import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import { jsonObjectSchema } from "../../../src/domain/jsonValue.js";
|
||||
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
|
||||
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
@@ -46,6 +48,115 @@ const connect = async (binaryLayout?: BinaryLayoutService) => {
|
||||
return { client, session };
|
||||
};
|
||||
|
||||
it("discovers an oversized native dossier by exact metadata and reads its retained view", async () => {
|
||||
const { client, session } = await connect();
|
||||
const provider = { id: "ghidra", name: "Ghidra", version: "12.1.4" };
|
||||
const profile = createAnalysisProfile(provider, { language: "fixture" });
|
||||
const parent = createEvidence(
|
||||
{ path: "/fixtures/large.exe", format: "pe", sha256: "a".repeat(64) },
|
||||
provider,
|
||||
{
|
||||
operation: "analyze_function",
|
||||
parameters: { procedure: "0x401000" },
|
||||
analysisProfile: profile,
|
||||
result: {
|
||||
...jsonObjectSchema.parse(ghidraFunctionDossier()),
|
||||
pseudocode: "x".repeat(6 * 1024 * 1024),
|
||||
},
|
||||
rawResult: null,
|
||||
},
|
||||
);
|
||||
const other = createEvidence(
|
||||
{ path: "/fixtures/other.exe", format: "pe", sha256: "b".repeat(64) },
|
||||
provider,
|
||||
{
|
||||
operation: "analyze_function",
|
||||
parameters: {},
|
||||
result: ghidraFunctionDossier(),
|
||||
},
|
||||
);
|
||||
for (const record of [parent, other]) {
|
||||
const retained = session.recordEvidence(record);
|
||||
if (!retained.ok) throw retained.error;
|
||||
}
|
||||
const broad = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
const broadError = parseMcpToolError(broad).error;
|
||||
expect(broadError.code).toBe("resource_constraint");
|
||||
expect(broadError.remediation?.action).toContain(
|
||||
"get_evidence_bundle and detail: summary",
|
||||
);
|
||||
const response = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {
|
||||
detail: "summary",
|
||||
filters: {
|
||||
operation: "analyze_function",
|
||||
target_sha256: "a".repeat(64),
|
||||
analysis_profile_digest: profile.digest,
|
||||
procedure_address: "0x401000",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const result = toolContract("get_evidence_bundle").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
).result;
|
||||
expect(result).toMatchObject({
|
||||
kind: "evidence-bundle-summary",
|
||||
total_retained_records: 2,
|
||||
matching_records: 1,
|
||||
records: [
|
||||
{
|
||||
evidence_id: parent.evidence_id,
|
||||
analysis_profile_digest: profile.digest,
|
||||
retention: "complete-record",
|
||||
native_dossier: { available: true, procedure_address: "0x401000" },
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(JSON.stringify(response)).not.toContain("pseudocode");
|
||||
expect(
|
||||
JSON.parse(response.content.find((c) => c.type === "text")?.text ?? "null"),
|
||||
).toEqual(response.structuredContent);
|
||||
const page = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: { kind: "native", facet: "pseudocode", offset: 0, limit: 32 },
|
||||
},
|
||||
});
|
||||
expect(page.isError).not.toBe(true);
|
||||
expect(
|
||||
toolContract("inspect_analysis_view").outputSchema.parse(
|
||||
page.structuredContent,
|
||||
).normalized_result,
|
||||
).toMatchObject({
|
||||
item: { text: "x".repeat(32) },
|
||||
coverage: { total: 6 * 1024 * 1024, exhausted: false },
|
||||
});
|
||||
const empty = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {
|
||||
detail: "summary",
|
||||
filters: { evidence_id: `ev_${"e".repeat(64)}` },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
toolContract("get_evidence_bundle").outputSchema.parse(
|
||||
empty.structuredContent,
|
||||
).result,
|
||||
).toMatchObject({ matching_records: 0, records: [] });
|
||||
await client.ping();
|
||||
const invalid = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: { filters: { operation: "analyze_function" } },
|
||||
});
|
||||
expect(parseMcpToolError(invalid).error.code).toBe("invalid_request");
|
||||
});
|
||||
|
||||
it("advertises exact schemas and projects one layout section from retained Evidence", async () => {
|
||||
const layout = analysisViewLayoutFixture();
|
||||
const service = new BinaryLayoutService({
|
||||
|
||||
Reference in new issue
Block a user