mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(cli): export source Evidence for native value traces (#8297)
* fix(cli): export source Evidence for native value traces * fix(cli): export source Evidence for native value traces * fix(cli): preserve trace completeness in summaries
This commit is contained in:
1 parent
064bb3f713
commit
bd7002b441
9 files changed
+570
-18
No files matched your search
@@ -434,6 +434,11 @@ nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
|
||||
32 MiB. Pagination returns edges whose source is on the node page and preserves
|
||||
stable IDs for endpoints outside that page.
|
||||
|
||||
The one-shot CLI accepts `--evidence_output <new-path>` to save the trace and
|
||||
the transitive source Evidence closure named by its links. The destination is
|
||||
never overwritten. `--detail summary` requires `--evidence_output`; the bundle
|
||||
still retains the complete trace and source Evidence.
|
||||
|
||||
Within one value trace, each exact procedure selector is attempted at most once,
|
||||
including failed analyses. A failed seed returns the provider's original typed
|
||||
failure and corrective guidance; it cannot establish a trace or target identity.
|
||||
|
||||
@@ -17,7 +17,10 @@ import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import type { Logger } from "pino";
|
||||
import { withEvidenceCallerInputPath } from "../domain/evidence.js";
|
||||
import { createImmutableEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import {
|
||||
createImmutableEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import { retainAnalysisResultSources } from "./investigation/recordEvidenceSources.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
|
||||
@@ -139,6 +142,8 @@ export const runDirectAnalysis = async (
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
readonly espFlash?: EspFlashSelection;
|
||||
/** Capture the exact session Evidence available to a one-shot CLI adapter. */
|
||||
readonly onEvidenceBundle?: (bundle: EvidenceBundle) => void;
|
||||
} = {},
|
||||
): Promise<Result<Evidence, JsonValue>> =>
|
||||
withProcessCancellation(options.signal, (signal) =>
|
||||
@@ -146,6 +151,9 @@ export const runDirectAnalysis = async (
|
||||
logger: options.logger ?? silentLogger,
|
||||
snapshotPath: options.snapshotPath,
|
||||
signal,
|
||||
...(options.onEvidenceBundle === undefined
|
||||
? {}
|
||||
: { onEvidenceBundle: options.onEvidenceBundle }),
|
||||
...targetSelection(options),
|
||||
...(options.providerId === undefined
|
||||
? {}
|
||||
@@ -364,6 +372,7 @@ const runAnalysis = async (
|
||||
readonly snapshotPath: string | undefined;
|
||||
readonly signal: AbortSignal;
|
||||
readonly evidenceParameters?: Readonly<Record<string, JsonValue>>;
|
||||
readonly onEvidenceBundle?: (bundle: EvidenceBundle) => void;
|
||||
readonly createSession?: () => BinarySession | Promise<BinarySession>;
|
||||
readonly providerId?: AnalysisProviderSelector;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
@@ -458,8 +467,10 @@ const runAnalysis = async (
|
||||
: routeProviderIdentity(route, tool),
|
||||
evidenceProfile,
|
||||
});
|
||||
if (cached !== undefined)
|
||||
if (cached !== undefined) {
|
||||
options.onEvidenceBundle?.(snapshot.evidence_bundle);
|
||||
return ok(withEvidenceCallerInputPath(cached, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
const openOptions = {
|
||||
@@ -552,6 +563,7 @@ const runAnalysis = async (
|
||||
return err(written.error.retainPartialObservation(evidence));
|
||||
}
|
||||
}
|
||||
options.onEvidenceBundle?.(session.exportEvidenceBundle());
|
||||
return ok(evidence);
|
||||
};
|
||||
const outcome = await withSessionCleanup<{
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { EvidenceReferenceError } from "../domain/evidenceErrors.js";
|
||||
import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import type { ZodSafeParseResult } from "zod";
|
||||
import { bindCliEvidenceCallerPath } from "./cliEvidenceCallerPath.js";
|
||||
import { resolveInvestigationEvidenceClosure } from "../application/investigation/InvestigationEvidence.js";
|
||||
|
||||
export const analysisEvidenceDeliveryOptions = {
|
||||
detail: z
|
||||
@@ -78,6 +80,42 @@ export const cliEvidenceDeliveryInputError = (
|
||||
])
|
||||
: undefined;
|
||||
|
||||
/** Resolve only the authenticated Evidence records linked by one CLI result. */
|
||||
export const cliEvidenceSourceClosure = (
|
||||
root: Evidence,
|
||||
available: readonly Evidence[],
|
||||
): Result<Evidence[], AnalysisError> => {
|
||||
const recordsById = new Map(
|
||||
available.map((record) => [record.evidence_id, record]),
|
||||
);
|
||||
const closure = resolveInvestigationEvidenceClosure(
|
||||
{ evidenceForAnalysis: (evidenceId) => recordsById.get(evidenceId) },
|
||||
root.evidence_links,
|
||||
);
|
||||
if (!closure.ok) {
|
||||
if (closure.error instanceof EvidenceReferenceError)
|
||||
return err(
|
||||
new EvidenceReferenceError(
|
||||
closure.error.evidenceId,
|
||||
closure.error.reason,
|
||||
{
|
||||
cause: closure.error,
|
||||
expected: closure.error.expected,
|
||||
actual: closure.error.actual,
|
||||
referenceScope: closure.error.referenceScope,
|
||||
diagnostics: { operation: root.operation },
|
||||
},
|
||||
),
|
||||
);
|
||||
return err(closure.error);
|
||||
}
|
||||
return ok(
|
||||
closure.value
|
||||
.filter((evidenceId) => evidenceId !== root.evidence_id)
|
||||
.map((evidenceId) => recordsById.get(evidenceId)!),
|
||||
);
|
||||
};
|
||||
|
||||
/** Parse delivery JSON once and enforce its owning schema before opening a target. */
|
||||
export const parseCliAnalysisEvidenceDelivery = <
|
||||
Delivery extends {
|
||||
|
||||
@@ -30,9 +30,12 @@ import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
cliEvidenceDeliveryInputError,
|
||||
cliEvidenceSourceClosure,
|
||||
publishCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
|
||||
const bytePatternInputSchema = z
|
||||
.object(bytePatternFields)
|
||||
@@ -236,25 +239,69 @@ export const registerCoreNativeCommands = (
|
||||
.max(5000)
|
||||
.default(1000)
|
||||
.describe("Maximum nodes on this page"),
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
...nativeOpenOptionFields,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.traceNativeValues, () =>
|
||||
runDirectAnalysis(
|
||||
args.path,
|
||||
"trace_native_values",
|
||||
{
|
||||
procedure: args.procedure,
|
||||
max_depth: options.maxDepth,
|
||||
max_functions: options.maxFunctions,
|
||||
max_call_sites: options.maxCallSites,
|
||||
max_nodes: options.maxNodes,
|
||||
max_edges: options.maxEdges,
|
||||
offset: options.offset,
|
||||
limit: options.limit,
|
||||
},
|
||||
directAnalysisOptionsFromCli(logger, options),
|
||||
).then(projectCliAnalysisResult),
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.traceNativeValues, async () => {
|
||||
const delivery = {
|
||||
providerId: "auto",
|
||||
operation: "trace_native_values",
|
||||
signal,
|
||||
detail: options.detail,
|
||||
evidenceOutput: options.evidence_output,
|
||||
};
|
||||
const invalid = cliEvidenceDeliveryInputError(delivery);
|
||||
if (invalid !== undefined) return projectAnalysisError(invalid);
|
||||
let sessionBundle: EvidenceBundle | undefined;
|
||||
const complete = await runDirectAnalysis(
|
||||
args.path,
|
||||
"trace_native_values",
|
||||
{
|
||||
procedure: args.procedure,
|
||||
max_depth: options.maxDepth,
|
||||
max_functions: options.maxFunctions,
|
||||
max_call_sites: options.maxCallSites,
|
||||
max_nodes: options.maxNodes,
|
||||
max_edges: options.maxEdges,
|
||||
offset: options.offset,
|
||||
limit: options.limit,
|
||||
},
|
||||
{
|
||||
...directAnalysisOptionsFromCli(logger, options),
|
||||
signal,
|
||||
...(options.evidence_output === undefined
|
||||
? {}
|
||||
: {
|
||||
onEvidenceBundle: (bundle: EvidenceBundle) => {
|
||||
sessionBundle = bundle;
|
||||
},
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!complete.ok || options.evidence_output === undefined)
|
||||
return publishCliAnalysisEvidence(complete, delivery);
|
||||
if (sessionBundle === undefined)
|
||||
return projectAnalysisError(
|
||||
new AnalysisOutputError(
|
||||
"trace_native_values",
|
||||
"The CLI session did not return its retained Evidence bundle.",
|
||||
).retainPartialObservation(complete.value),
|
||||
);
|
||||
const sources = cliEvidenceSourceClosure(
|
||||
complete.value,
|
||||
sessionBundle.records,
|
||||
);
|
||||
if (!sources.ok)
|
||||
return projectAnalysisError(
|
||||
sources.error.retainPartialObservation(complete.value),
|
||||
);
|
||||
return publishCliAnalysisEvidence(complete, {
|
||||
...delivery,
|
||||
sourceEvidence: sources.value,
|
||||
});
|
||||
}),
|
||||
),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.inspectNativeDataType, {
|
||||
|
||||
@@ -472,6 +472,9 @@ const requestErrorDetails = (
|
||||
return {
|
||||
evidence_id: error.evidenceId,
|
||||
reason: error.reason,
|
||||
...(typeof error.diagnostics?.operation === "string"
|
||||
? { operation: error.diagnostics.operation }
|
||||
: {}),
|
||||
...(error.referenceScope === undefined
|
||||
? {}
|
||||
: { reference_scope: error.referenceScope }),
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
NATIVE_OBSERVATION_OPERATIONS,
|
||||
} from "../native/nativeObservation.js";
|
||||
import { projectNativeObservationView } from "./nativeObservationView.js";
|
||||
import { projectNativeValueTraceView } from "./nativeValueTraceView.js";
|
||||
import { projectDevicetreeView } from "./devicetreeView.js";
|
||||
import { projectApkSignatureView } from "./apkSignatureView.js";
|
||||
import { byteEntropySchema } from "../native/byteEntropy.js";
|
||||
@@ -303,6 +304,7 @@ const retainedViewProjectors = new Map<string, RetainedViewProjector>([
|
||||
...NATIVE_OBSERVATION_OPERATIONS.map((operation) =>
|
||||
retain(operation, projectNativeObservationView),
|
||||
),
|
||||
retain("trace_native_values", projectNativeValueTraceView),
|
||||
retain("inspect_erlang_beam", projectErlangBeamView),
|
||||
retain("inspect_devicetree", projectDevicetreeView),
|
||||
retain("inspect_native_exports", projectNativeExportsView),
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
NATIVE_OBSERVATION_OPERATIONS,
|
||||
type NativeObservationOperation,
|
||||
} from "../native/nativeObservation.js";
|
||||
import { nativeValueTraceSummarySchema } from "./nativeValueTraceView.js";
|
||||
import {
|
||||
devicetreeViewRequestSchema,
|
||||
devicetreeViewItemsSchema,
|
||||
@@ -348,6 +349,7 @@ const viewResultBase = {
|
||||
"inspect_gpu_code_objects",
|
||||
...NATIVE_INVENTORY_OPERATIONS,
|
||||
...NATIVE_OBSERVATION_OPERATIONS,
|
||||
"trace_native_values",
|
||||
"match_native_functions",
|
||||
]),
|
||||
parent_digest: digestSchema,
|
||||
@@ -388,6 +390,14 @@ export const nativeObservationSummaryViewResultSchemas = {
|
||||
),
|
||||
get_call_graph: nativeObservationSummaryFor("get_call_graph"),
|
||||
};
|
||||
|
||||
export const nativeValueTraceSummaryViewResultSchema = z.strictObject({
|
||||
...summaryViewResultBase,
|
||||
artifact: artifactSchema.nullable(),
|
||||
parent_operation: z.literal("trace_native_values"),
|
||||
summary: nativeValueTraceSummarySchema,
|
||||
});
|
||||
|
||||
const nativeObservationSummaryViewResultSchema = z.discriminatedUnion(
|
||||
"parent_operation",
|
||||
[
|
||||
@@ -1120,6 +1130,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
}),
|
||||
z.discriminatedUnion("parent_operation", [
|
||||
nativeObservationSummaryViewResultSchema,
|
||||
nativeValueTraceSummaryViewResultSchema,
|
||||
erlangBeamSummaryViewResultSchema,
|
||||
devicetreeSummaryViewResultSchema,
|
||||
pythonPycSummaryViewResultSchema,
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { z } from "zod";
|
||||
import { nativeValueTraceSchema } from "../native/nativeValueTrace.js";
|
||||
import { err, ok } from "../result.js";
|
||||
import type { AnalysisViewParent } from "./analysisViewParent.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
assertOwnedView,
|
||||
type AdmittedView,
|
||||
type SummaryAnalysisView,
|
||||
} from "./analysisViewShape.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import {
|
||||
boundedViewMessages,
|
||||
completeWithinViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
|
||||
/** Counts and pagination state for a bounded native value trace page. */
|
||||
export const nativeValueTraceSummarySchema = z.strictObject({
|
||||
seed: z.string(),
|
||||
target_sha256: z.string(),
|
||||
node_count: z.int().nonnegative(),
|
||||
total_nodes: z.int().nonnegative(),
|
||||
edge_count: z.int().nonnegative(),
|
||||
total_edges: z.int().nonnegative(),
|
||||
procedure_count: z.int().nonnegative(),
|
||||
unknown_count: z.int().nonnegative(),
|
||||
decompilations: z.int().nonnegative(),
|
||||
next_offset: z.int().nonnegative().nullable(),
|
||||
page_truncated: nativeValueTraceSchema.shape.page_truncated,
|
||||
traversal: nativeValueTraceSchema.shape.traversal,
|
||||
truncated: z.boolean(),
|
||||
limitation_count: z.int().nonnegative(),
|
||||
});
|
||||
|
||||
const readTrace = createAnalysisViewResultReader(nativeValueTraceSchema);
|
||||
|
||||
/** Keep the bounded graph page and its completeness state in source Evidence. */
|
||||
export const projectNativeValueTraceView = (
|
||||
parent: AnalysisViewParent,
|
||||
view: AdmittedView,
|
||||
) => {
|
||||
if (view.kind !== "summary")
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
{
|
||||
path: ["view"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Native value trace Evidence supports the summary view; retrieve parent Evidence for graph nodes and edges.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
assertOwnedView<SummaryAnalysisView>(view, "summary");
|
||||
const parsed = readTrace(parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const trace = parsed.value;
|
||||
return ok({
|
||||
kind: "summary" as const,
|
||||
view,
|
||||
parent_evidence_id: parent.evidenceId,
|
||||
parent_operation: "trace_native_values" as const,
|
||||
parent_digest: parent.evidenceId.slice(3),
|
||||
artifact: parent.artifact ?? null,
|
||||
summary: {
|
||||
seed: trace.seed,
|
||||
target_sha256: trace.target_sha256,
|
||||
node_count: trace.nodes.length,
|
||||
total_nodes: trace.total_nodes,
|
||||
edge_count: trace.edges.length,
|
||||
total_edges: trace.total_edges,
|
||||
procedure_count: trace.procedures.length,
|
||||
unknown_count: trace.unknowns.length,
|
||||
decompilations: trace.decompilations,
|
||||
next_offset: trace.next_offset,
|
||||
page_truncated: trace.page_truncated,
|
||||
traversal: trace.traversal,
|
||||
truncated: trace.truncated,
|
||||
limitation_count: trace.limitations.length,
|
||||
},
|
||||
coverage: completeWithinViewCoverage(1, 1),
|
||||
limitations: boundedViewMessages(
|
||||
[...parent.limitations, ...trace.limitations],
|
||||
parent.evidenceId,
|
||||
"limitations",
|
||||
),
|
||||
unknowns: boundedViewMessages(
|
||||
trace.unknowns.map(
|
||||
({ procedure, address, reason }) =>
|
||||
`${procedure}${address === null ? "" : ` at ${address}`}: ${reason}`,
|
||||
),
|
||||
parent.evidenceId,
|
||||
"unknowns",
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,339 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { Cli } from "incur";
|
||||
import pino from "pino";
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
|
||||
import { registerCoreNativeCommands } from "../../../src/cli/coreNativeCommands.js";
|
||||
import { projectCliAnalysisResult } from "../../../src/cli/projectCliAnalysisResult.js";
|
||||
import type { CliInstance } from "../../../src/cli/types.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import {
|
||||
createEvidenceBundle,
|
||||
parseEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../../../src/domain/evidenceBundle.js";
|
||||
import { REA_WORKFLOW_PROVIDER } from "../../../src/domain/investigationProviders.js";
|
||||
import { nativeValueTraceSchema } from "../../../src/domain/native/nativeValueTrace.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import { composedWorkflowFixture } from "../../fixtures/composedWorkflowEvidence.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const sourceProvider = { id: "fixture", name: "Fixture", version: "1" };
|
||||
const sourceTarget = {
|
||||
path: "/fixtures/native.exe",
|
||||
format: "pe" as const,
|
||||
sha256: "a".repeat(64),
|
||||
};
|
||||
|
||||
const createTraceFixture = () => {
|
||||
const leaf = createEvidence(sourceTarget, sourceProvider, {
|
||||
operation: "read_native_source",
|
||||
parameters: { address: "0x1000" },
|
||||
result: { text: "return 0;" },
|
||||
});
|
||||
const procedure = createEvidence(sourceTarget, sourceProvider, {
|
||||
operation: "analyze_function",
|
||||
parameters: { procedure: "main" },
|
||||
result: { procedure: { address: "0x1000" } },
|
||||
evidenceLinks: [leaf.evidence_id],
|
||||
});
|
||||
const traceResult = nativeValueTraceSchema.parse({
|
||||
seed: "main",
|
||||
target_sha256: sourceTarget.sha256,
|
||||
nodes: [
|
||||
{
|
||||
id: "node-1",
|
||||
procedure: "0x1000",
|
||||
evidence_id: procedure.evidence_id,
|
||||
kind: "operation",
|
||||
operation: null,
|
||||
parameter: null,
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
procedures: [
|
||||
{
|
||||
address: "0x1000",
|
||||
depth: 0,
|
||||
evidence_id: procedure.evidence_id,
|
||||
provider: "fixture",
|
||||
analysis_profile_digest: null,
|
||||
},
|
||||
],
|
||||
unknowns: [
|
||||
{ procedure: "0x1000", address: null, reason: "fixture uncertainty" },
|
||||
],
|
||||
total_nodes: 2,
|
||||
total_edges: 0,
|
||||
decompilations: 1,
|
||||
next_offset: 1,
|
||||
page_truncated: true,
|
||||
traversal: { status: "complete" },
|
||||
truncated: true,
|
||||
limitations: ["fixture trace limitation"],
|
||||
});
|
||||
const trace = createEvidence(sourceTarget, REA_WORKFLOW_PROVIDER, {
|
||||
operation: "trace_native_values",
|
||||
parameters: { procedure: "main" },
|
||||
result: traceResult,
|
||||
confidence: "derived",
|
||||
evidenceLinks: [procedure.evidence_id],
|
||||
});
|
||||
const unrelated = createEvidence(sourceTarget, sourceProvider, {
|
||||
operation: "address_name",
|
||||
parameters: { address: "0x2000" },
|
||||
result: { name: "unrelated" },
|
||||
});
|
||||
const sessionBundle = createEvidenceBundle([
|
||||
trace,
|
||||
procedure,
|
||||
leaf,
|
||||
unrelated,
|
||||
]);
|
||||
return { leaf, procedure, trace, unrelated, sessionBundle };
|
||||
};
|
||||
|
||||
type DirectAnalysisRunner = Parameters<typeof registerCoreNativeCommands>[2];
|
||||
|
||||
const createTraceCli = (run: DirectAnalysisRunner) => {
|
||||
const cli = Cli.create("rea-trace-evidence-test", {
|
||||
version: "1",
|
||||
sync: false,
|
||||
});
|
||||
registerCoreNativeCommands(
|
||||
cli as unknown as CliInstance,
|
||||
pino({ enabled: false }),
|
||||
run,
|
||||
);
|
||||
return cli;
|
||||
};
|
||||
|
||||
const runJsonCli = async (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
args: string[],
|
||||
) => {
|
||||
let stdout = "";
|
||||
await cli.serve(args, {
|
||||
env: {},
|
||||
stdout: (text: string) => {
|
||||
stdout += text;
|
||||
},
|
||||
exit: () => undefined,
|
||||
});
|
||||
return JSON.parse(stdout) as Record<string, unknown>;
|
||||
};
|
||||
|
||||
const verifyDirectAnalysisEvidenceBundleCallback = async (
|
||||
snapshotPath: string,
|
||||
) => {
|
||||
const fixture = await composedWorkflowFixture();
|
||||
let captured: EvidenceBundle | undefined;
|
||||
const direct = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
fixture.dependencies,
|
||||
fixture.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ snapshotPath, onEvidenceBundle: (bundle) => (captured = bundle) },
|
||||
).then(projectCliAnalysisResult),
|
||||
);
|
||||
expect(captured?.records.map(({ evidence_id }) => evidence_id)).toContain(
|
||||
direct.evidence_id,
|
||||
);
|
||||
|
||||
let replayedBundle: EvidenceBundle | undefined;
|
||||
const replayed = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
fixture.dependencies,
|
||||
fixture.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{
|
||||
snapshotPath,
|
||||
onEvidenceBundle: (bundle) => (replayedBundle = bundle),
|
||||
},
|
||||
).then(projectCliAnalysisResult),
|
||||
);
|
||||
expect(replayed).toEqual(direct);
|
||||
expect(
|
||||
replayedBundle?.records.map(({ evidence_id }) => evidence_id),
|
||||
).toContain(replayed.evidence_id);
|
||||
};
|
||||
|
||||
it("exports only the transitive source Evidence closure for a native value trace", async () => {
|
||||
const { leaf, procedure, trace, unrelated, sessionBundle } =
|
||||
createTraceFixture();
|
||||
const run = vi.fn<DirectAnalysisRunner>(
|
||||
async (_path, _tool, _arguments, options) => {
|
||||
options?.onEvidenceBundle?.(sessionBundle);
|
||||
return ok(trace);
|
||||
},
|
||||
);
|
||||
const cli = createTraceCli(run);
|
||||
const directory = await createTestTempDirectory("rea-trace-evidence-");
|
||||
const destination = join(directory, "trace-evidence.json");
|
||||
const delivered = await runJsonCli(cli, [
|
||||
"trace-native-values",
|
||||
sourceTarget.path,
|
||||
"main",
|
||||
"--evidence_output",
|
||||
destination,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expect(delivered.evidence_id).toBe(trace.evidence_id);
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
const exported = parseEvidenceBundle(
|
||||
JSON.parse(await readFile(destination, "utf8")),
|
||||
);
|
||||
expect(exported.records.map(({ evidence_id }) => evidence_id).sort()).toEqual(
|
||||
[trace.evidence_id, procedure.evidence_id, leaf.evidence_id].sort(),
|
||||
);
|
||||
expect(
|
||||
exported.records.some(
|
||||
({ evidence_id }) => evidence_id === unrelated.evidence_id,
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
const summaryOutput = await runJsonCli(cli, [
|
||||
"trace-native-values",
|
||||
"/missing-target",
|
||||
"main",
|
||||
"--detail",
|
||||
"summary",
|
||||
"--json",
|
||||
]);
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
expect(JSON.stringify(summaryOutput)).toContain("evidence_output");
|
||||
});
|
||||
|
||||
it("publishes a bounded trace summary with a complete evidence bundle", async () => {
|
||||
const { leaf, procedure, trace, sessionBundle } = createTraceFixture();
|
||||
const run = vi.fn<DirectAnalysisRunner>(
|
||||
async (_path, _tool, _arguments, options) => {
|
||||
options?.onEvidenceBundle?.(sessionBundle);
|
||||
return ok(trace);
|
||||
},
|
||||
);
|
||||
const cli = createTraceCli(run);
|
||||
const directory = await createTestTempDirectory("rea-trace-summary-");
|
||||
const destination = join(directory, "trace-summary-evidence.json");
|
||||
const receipt = await runJsonCli(cli, [
|
||||
"trace-native-values",
|
||||
sourceTarget.path,
|
||||
"main",
|
||||
"--detail",
|
||||
"summary",
|
||||
"--evidence_output",
|
||||
destination,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
const summaryResult = (
|
||||
receipt.summary as { normalized_result?: unknown } | undefined
|
||||
)?.normalized_result;
|
||||
const expectedSummary = {
|
||||
kind: "summary",
|
||||
parent_operation: "trace_native_values",
|
||||
parent_evidence_id: trace.evidence_id,
|
||||
summary: {
|
||||
seed: "main",
|
||||
node_count: 1,
|
||||
total_nodes: 2,
|
||||
edge_count: 0,
|
||||
procedure_count: 1,
|
||||
unknown_count: 1,
|
||||
decompilations: 1,
|
||||
next_offset: 1,
|
||||
page_truncated: true,
|
||||
traversal: { status: "complete" },
|
||||
truncated: true,
|
||||
limitation_count: 1,
|
||||
},
|
||||
};
|
||||
expect(summaryResult).toMatchObject(expectedSummary);
|
||||
const summaryEvidence = parseEvidence(receipt.summary);
|
||||
expect(summaryEvidence.operation).toBe("inspect_analysis_view");
|
||||
const bundle = parseEvidenceBundle(
|
||||
JSON.parse(await readFile(destination, "utf8")),
|
||||
);
|
||||
expect(bundle.records.map(({ evidence_id }) => evidence_id).sort()).toEqual(
|
||||
[trace.evidence_id, procedure.evidence_id, leaf.evidence_id].sort(),
|
||||
);
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("retains completed trace Evidence when the session returns no bundle", async () => {
|
||||
const { trace } = createTraceFixture();
|
||||
const run = vi.fn<DirectAnalysisRunner>(async () => ok(trace));
|
||||
const cli = createTraceCli(run);
|
||||
const directory = await createTestTempDirectory("rea-trace-no-bundle-");
|
||||
const destination = join(directory, "missing-bundle.json");
|
||||
const error = await runJsonCli(cli, [
|
||||
"trace-native-values",
|
||||
sourceTarget.path,
|
||||
"main",
|
||||
"--evidence_output",
|
||||
destination,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expect(error).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
details: {
|
||||
operation: "trace_native_values",
|
||||
partial_observation: {
|
||||
operation: "trace_native_values",
|
||||
normalized_result: { seed: "main" },
|
||||
},
|
||||
},
|
||||
});
|
||||
await expect(readFile(destination, "utf8")).rejects.toThrow();
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("reports a missing linked source with its identity and completed trace", async () => {
|
||||
const { leaf, procedure, trace } = createTraceFixture();
|
||||
const run = vi.fn<DirectAnalysisRunner>(
|
||||
async (_path, _tool, _arguments, options) => {
|
||||
options?.onEvidenceBundle?.(createEvidenceBundle([trace, procedure]));
|
||||
return ok(trace);
|
||||
},
|
||||
);
|
||||
const cli = createTraceCli(run);
|
||||
const directory = await createTestTempDirectory("rea-trace-missing-source-");
|
||||
const destination = join(directory, "missing-source.json");
|
||||
const error = await runJsonCli(cli, [
|
||||
"trace-native-values",
|
||||
sourceTarget.path,
|
||||
"main",
|
||||
"--evidence_output",
|
||||
destination,
|
||||
"--json",
|
||||
]);
|
||||
|
||||
expect(error).toMatchObject({
|
||||
code: "evidence_integrity_mismatch",
|
||||
details: {
|
||||
evidence_id: leaf.evidence_id,
|
||||
reason: "missing",
|
||||
operation: "trace_native_values",
|
||||
reference_scope: "retained-closure",
|
||||
partial_observation: {
|
||||
operation: "trace_native_values",
|
||||
normalized_result: { seed: "main" },
|
||||
},
|
||||
},
|
||||
});
|
||||
await expect(readFile(destination, "utf8")).rejects.toThrow();
|
||||
expect(run).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("replays direct-analysis Evidence bundle callbacks without rerunning", async () => {
|
||||
const directory = await createTestTempDirectory("rea-trace-callback-");
|
||||
await verifyDirectAnalysisEvidenceBundleCallback(
|
||||
join(directory, "analysis-snapshot.json"),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user