mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45a3d03dbf | ||
|
|
4e518b0f2b | ||
|
|
4983d30edf | ||
|
|
eb94309098 | ||
|
|
67ca695caf | ||
|
|
95dc5c0ad0 | ||
|
|
5df3e273ae | ||
|
|
196f0afbbc | ||
|
|
3368a036ae | ||
|
|
2e1e99283e | ||
|
|
2e8d463400 | ||
|
|
e62484931c | ||
|
|
89d5bb7056 | ||
|
|
75c7187290 |
No files matched your search
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Exercise the public one-shot CLI on a caller-owned saved project. The report
|
||||
// is intentionally synthetic; it is not a claim about Ghidra's analysis history.
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { createVerifierCommands } from "./lib/verify-commands.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
const installation = process.env.GHIDRA_INSTALL_DIR;
|
||||
if (!installation || !process.env.JAVA_HOME)
|
||||
throw new Error(
|
||||
"Set GHIDRA_INSTALL_DIR and JAVA_HOME to caller-installed Ghidra and JDK 21.",
|
||||
);
|
||||
const commands = await createVerifierCommands();
|
||||
let root;
|
||||
let commandsClosed = false;
|
||||
const targetHash = await withVerifierCleanup(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), "rea-function-bundle-"));
|
||||
const target = join(root, "fixture");
|
||||
const projectDir = join(root, "project");
|
||||
const scriptDir = join(root, "scripts");
|
||||
const source = join(root, "fixture.c");
|
||||
const project = join(projectDir, "SourceControl.gpr");
|
||||
const repo = resolve(process.cwd());
|
||||
const hash = (data) => createHash("sha256").update(data).digest("hex");
|
||||
const tree = async (directory, prefix = "") => {
|
||||
const result = [];
|
||||
for (const entry of (
|
||||
await readdir(directory, { withFileTypes: true })
|
||||
).sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const path = join(directory, entry.name);
|
||||
const relative = join(prefix, entry.name);
|
||||
if (entry.isDirectory()) result.push(...(await tree(path, relative)));
|
||||
else if (entry.isFile())
|
||||
result.push([relative, hash(await readFile(path))]);
|
||||
else throw new Error(`Unexpected project entry: ${relative}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const run = async (command, args, options = {}) =>
|
||||
commands.exec(command, args, {
|
||||
timeout: 120_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
...options,
|
||||
});
|
||||
await mkdir(projectDir);
|
||||
await mkdir(scriptDir);
|
||||
await writeFile(
|
||||
source,
|
||||
"__attribute__((noinline)) int leaf(int x) { return x + 1; }\nint main(void) { return leaf(2); }\n",
|
||||
);
|
||||
await writeFile(
|
||||
join(scriptDir, "SetHistory.java"),
|
||||
`import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.listing.Program;
|
||||
public class SetHistory extends GhidraScript {
|
||||
public void run() throws Exception {
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setString("REA disabled analyzers", "{\\"format\\":\\"rea-ghidra-disabled-analyzers-v1\\",\\"analyzers\\":[{\\"name\\":\\"Synthetic historical control\\",\\"status\\":\\"disabled\\"}]}");
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
await run(process.env.REA_CC ?? "cc", ["-O0", "-g", source, "-o", target]);
|
||||
await run(join(installation, "support", "analyzeHeadless"), [
|
||||
projectDir,
|
||||
"SourceControl",
|
||||
"-import",
|
||||
target,
|
||||
"-scriptPath",
|
||||
scriptDir,
|
||||
"-postScript",
|
||||
"SetHistory.java",
|
||||
]);
|
||||
const before = await tree(projectDir);
|
||||
const targetHash = hash(await readFile(target));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_GHIDRA_PROJECT_PATH: project,
|
||||
REA_GHIDRA_PROGRAM_NAME: "fixture",
|
||||
};
|
||||
const cli = join(repo, "scripts", "rea.mjs");
|
||||
for (const detail of ["complete", "summary"]) {
|
||||
const output = join(root, `${detail}.json`);
|
||||
const { stdout } = await run(
|
||||
process.execPath,
|
||||
[
|
||||
cli,
|
||||
"function",
|
||||
target,
|
||||
"main",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--detail",
|
||||
detail,
|
||||
"--evidence_output",
|
||||
output,
|
||||
"--format",
|
||||
"json",
|
||||
"--full-output",
|
||||
],
|
||||
{ env },
|
||||
);
|
||||
assert.equal(JSON.parse(stdout).ok, true);
|
||||
const bundle = JSON.parse(await readFile(output, "utf8"));
|
||||
const parent = bundle.records.find(
|
||||
(record) => record.operation === "analyze_function",
|
||||
);
|
||||
assert.ok(parent, "function observation missing from bundle");
|
||||
assert.ok(
|
||||
parent.evidence_links.length,
|
||||
"fixture did not produce a linked historical source",
|
||||
);
|
||||
const records = new Map(
|
||||
bundle.records.map((record) => [record.evidence_id, record]),
|
||||
);
|
||||
for (const record of bundle.records)
|
||||
for (const link of record.evidence_links)
|
||||
assert.ok(records.has(link), `bundle omits linked Evidence ${link}`);
|
||||
assert.ok(
|
||||
parent.evidence_links.some(
|
||||
(id) => records.get(id)?.operation === "health",
|
||||
),
|
||||
"historical health source missing",
|
||||
);
|
||||
}
|
||||
const existing = join(root, "existing.json");
|
||||
await writeFile(existing, "caller-owned\n");
|
||||
let failure;
|
||||
try {
|
||||
await run(
|
||||
process.execPath,
|
||||
[
|
||||
cli,
|
||||
"function",
|
||||
target,
|
||||
"main",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--detail",
|
||||
"summary",
|
||||
"--evidence_output",
|
||||
existing,
|
||||
"--format",
|
||||
"json",
|
||||
"--full-output",
|
||||
],
|
||||
{ env },
|
||||
);
|
||||
assert.fail("existing output unexpectedly overwritten");
|
||||
} catch (error) {
|
||||
if (error.code !== 1) throw error;
|
||||
failure = JSON.parse(error.snapshot.stdout.text);
|
||||
}
|
||||
assert.equal(await readFile(existing, "utf8"), "caller-owned\n");
|
||||
assert.equal(failure.ok, false);
|
||||
assert.equal(failure.error.details.source_closure.complete, true);
|
||||
assert.ok(
|
||||
failure.error.details.source_closure.records.some(
|
||||
(record) => record.operation === "health",
|
||||
),
|
||||
);
|
||||
assert.equal(hash(await readFile(target)), targetHash);
|
||||
assert.deepEqual(await tree(projectDir), before);
|
||||
return targetHash;
|
||||
}, [
|
||||
async () => {
|
||||
await commands.close();
|
||||
commandsClosed = true;
|
||||
},
|
||||
async () => {
|
||||
if (commandsClosed && root !== undefined)
|
||||
await rm(root, { recursive: true, force: true });
|
||||
},
|
||||
]);
|
||||
process.stdout.write(
|
||||
`PASS Ghidra function bundles: complete, summary, no-overwrite; target and project unchanged (${targetHash})\n`,
|
||||
);
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import type { Logger } from "pino";
|
||||
import { withEvidenceCallerInputPath } from "../domain/evidence.js";
|
||||
import {
|
||||
tryParseEvidence,
|
||||
withEvidenceCallerInputPath,
|
||||
} from "../domain/evidence.js";
|
||||
import {
|
||||
createImmutableEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
@@ -25,8 +28,10 @@ import {
|
||||
retainAnalysisResultSources,
|
||||
type AnalysisResultWithSources,
|
||||
} from "./investigation/recordEvidenceSources.js";
|
||||
import { resolveInvestigationEvidenceClosure } from "./investigation/InvestigationEvidence.js";
|
||||
import { createInvestigationEvidenceClosureChecker } from "./investigation/InvestigationEvidence.js";
|
||||
import {
|
||||
createInvestigationEvidenceClosureChecker,
|
||||
collectInvestigationEvidenceClosure,
|
||||
} from "./investigation/InvestigationEvidence.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
|
||||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
@@ -383,12 +388,12 @@ const runAnalysis = async (
|
||||
readonly evidenceParameters?: Readonly<Record<string, JsonValue>>;
|
||||
readonly onEvidenceBundle?: (bundle: EvidenceBundle) => void;
|
||||
readonly createSession?: () => BinarySession | Promise<BinarySession>;
|
||||
readonly includeEvidenceSources?: boolean;
|
||||
readonly providerId?: AnalysisProviderSelector;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
readonly espFlash?: EspFlashSelection;
|
||||
readonly includeEvidenceSources?: boolean;
|
||||
},
|
||||
): Promise<AnalysisResultWithSources<Evidence, JsonValue>> => {
|
||||
const { logger, signal, snapshotPath } = options;
|
||||
@@ -411,23 +416,19 @@ const runAnalysis = async (
|
||||
evidence: Evidence,
|
||||
supplied: readonly Evidence[] = [],
|
||||
): Result<null, AnalysisError> => {
|
||||
if (options.includeEvidenceSources !== true) return ok(null);
|
||||
const known = new Map(
|
||||
if (!options.includeEvidenceSources) return ok(null);
|
||||
const records = new Map(
|
||||
supplied.map((record) => [record.evidence_id, record]),
|
||||
);
|
||||
const closure = resolveInvestigationEvidenceClosure(
|
||||
const collected = collectInvestigationEvidenceClosure(
|
||||
{
|
||||
evidenceForAnalysis: (id) => {
|
||||
const record = known.get(id) ?? session.evidenceForAnalysis(id);
|
||||
if (record !== undefined) known.set(id, record);
|
||||
return record;
|
||||
},
|
||||
evidenceForAnalysis: (id) =>
|
||||
records.get(id) ?? session.evidenceForAnalysis(id),
|
||||
},
|
||||
evidence.evidence_links,
|
||||
);
|
||||
if (!closure.ok) return closure;
|
||||
sourceEvidence = closure.value.map((id) => known.get(id) as Evidence);
|
||||
return ok(null);
|
||||
sourceEvidence = collected.records;
|
||||
return collected.result;
|
||||
};
|
||||
const execute = async (): Promise<Result<Evidence, AnalysisError>> => {
|
||||
let isDirectory = false;
|
||||
@@ -512,12 +513,12 @@ const runAnalysis = async (
|
||||
});
|
||||
if (checkClosure(cached.evidence_links)) {
|
||||
const evidence = withEvidenceCallerInputPath(cached, path);
|
||||
const sources = captureSources(
|
||||
const captured = captureSources(
|
||||
evidence,
|
||||
snapshot.evidence_bundle.records,
|
||||
);
|
||||
if (!sources.ok)
|
||||
return err(sources.error.retainPartialObservation(evidence));
|
||||
if (!captured.ok)
|
||||
return err(captured.error.retainPartialObservation(evidence));
|
||||
options.onEvidenceBundle?.(snapshot.evidence_bundle);
|
||||
return ok(evidence);
|
||||
}
|
||||
@@ -548,20 +549,30 @@ const runAnalysis = async (
|
||||
evidenceProfile,
|
||||
saveSnapshot: snapshotPath !== undefined,
|
||||
});
|
||||
if (!executed.ok) return executed;
|
||||
if (!executed.ok) {
|
||||
if (options.includeEvidenceSources) {
|
||||
const partial = tryParseEvidence(executed.error.partialObservation);
|
||||
if (partial !== undefined) {
|
||||
const captured = captureSources(partial);
|
||||
if (!captured.ok)
|
||||
executed.error.retainSecondaryFailure(captured.error);
|
||||
}
|
||||
}
|
||||
return executed;
|
||||
}
|
||||
const { cachePolicy } = executed.value;
|
||||
const evidence = withEvidenceCallerInputPath(executed.value.evidence, path);
|
||||
// Provider execution may have already recorded the same semantic Evidence
|
||||
// without this invocation's request path. Import replaces that metadata
|
||||
// while retaining the authenticated result and snapshot binding.
|
||||
const captured = captureSources(evidence);
|
||||
if (!captured.ok)
|
||||
return err(captured.error.retainPartialObservation(evidence));
|
||||
const recorded = session.importEvidenceBundle(
|
||||
createImmutableEvidenceBundle([evidence]),
|
||||
);
|
||||
if (!recorded.ok)
|
||||
return err(recorded.error.retainPartialObservation(evidence));
|
||||
const sources = captureSources(evidence);
|
||||
if (!sources.ok)
|
||||
return err(sources.error.retainPartialObservation(evidence));
|
||||
if (isWorkflowEvidenceTool(tool)) {
|
||||
const unknowns = recordWorkflowUnknowns({
|
||||
name: tool,
|
||||
@@ -643,16 +654,15 @@ const runAnalysis = async (
|
||||
outcome.cleanupError.retainDiagnostics({
|
||||
analysis_snapshot: { ...publishedSnapshot },
|
||||
});
|
||||
return err(
|
||||
directAnalysisCleanupFailure(result, outcome.cleanupError, path),
|
||||
);
|
||||
return {
|
||||
...err(directAnalysisCleanupFailure(result, outcome.cleanupError, path)),
|
||||
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
|
||||
};
|
||||
}
|
||||
return result.ok
|
||||
? {
|
||||
...ok(result.value),
|
||||
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
|
||||
}
|
||||
: err(cliError(result.error, path));
|
||||
return {
|
||||
...(result.ok ? ok(result.value) : err(cliError(result.error, path))),
|
||||
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
|
||||
};
|
||||
};
|
||||
|
||||
const executeAnalysisTool = async (input: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { analysisErrorProjectionSchema } from "../../contracts/errorSchemas.js";
|
||||
import { InvestigationRecords } from "./InvestigationRecords.js";
|
||||
import {
|
||||
createInvestigationEvidenceClosureChecker,
|
||||
collectInvestigationEvidenceClosure,
|
||||
resolveInvestigationEvidenceClosure,
|
||||
} from "./InvestigationEvidence.js";
|
||||
|
||||
@@ -158,3 +159,25 @@ it("reuses a complete shared source graph without weakening missing-source check
|
||||
expect(lookups).toBe(3);
|
||||
expect(check([source.evidence_id, missing])).toBe(false);
|
||||
});
|
||||
|
||||
it("collects other reachable sources after a missing ancestor", () => {
|
||||
const missing = `ev_${"0".repeat(64)}`;
|
||||
const first = observation("/reachable");
|
||||
const incomplete = observation("/incomplete", [missing]);
|
||||
const records = new InvestigationRecords();
|
||||
for (const record of [first, incomplete]) {
|
||||
const added = records.recordEvidence(record);
|
||||
if (!added.ok) throw added.error;
|
||||
}
|
||||
const result = collectInvestigationEvidenceClosure(records, [
|
||||
first.evidence_id,
|
||||
incomplete.evidence_id,
|
||||
]);
|
||||
expect(result.result).toMatchObject({
|
||||
ok: false,
|
||||
error: { reason: "missing" },
|
||||
});
|
||||
expect(result.records.map((record) => record.evidence_id).sort()).toEqual(
|
||||
[first.evidence_id, incomplete.evidence_id].sort(),
|
||||
);
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import type { EvidenceReader } from "./InvestigationRecordPort.js";
|
||||
import { uniqueSorted } from "../../domain/canonicalOrdering.js";
|
||||
import type { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { ok, type Result } from "../../domain/result.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
|
||||
type Lookup = (id: string) => Evidence | undefined;
|
||||
|
||||
@@ -12,8 +12,10 @@ const visitClosure = (
|
||||
lookup: Lookup,
|
||||
seedIds: readonly string[],
|
||||
knownComplete?: ReadonlySet<string>,
|
||||
collect?: (record: Evidence) => void,
|
||||
): Result<Set<string>, EvidenceIntegrityError> => {
|
||||
const visited = new Set<string>();
|
||||
let failure: EvidenceIntegrityError | undefined;
|
||||
const pending = [...seedIds];
|
||||
while (pending.length > 0) {
|
||||
const evidenceId = pending.pop();
|
||||
@@ -28,12 +30,36 @@ const visitClosure = (
|
||||
lookup,
|
||||
"retained-closure",
|
||||
);
|
||||
if (!evidence.ok) return evidence;
|
||||
if (!evidence.ok) {
|
||||
if (collect === undefined) return evidence;
|
||||
failure ??= evidence.error;
|
||||
visited.add(evidenceId);
|
||||
continue;
|
||||
}
|
||||
visited.add(evidenceId);
|
||||
collect?.(evidence.value);
|
||||
for (const linkedId of evidence.value.evidence_links)
|
||||
pending.push(linkedId);
|
||||
}
|
||||
return ok(visited);
|
||||
return failure === undefined ? ok(visited) : err(failure);
|
||||
};
|
||||
|
||||
/** Gather every reachable authenticated record while retaining the first failure. */
|
||||
export const collectInvestigationEvidenceClosure = (
|
||||
reader: Pick<EvidenceReader, "evidenceForAnalysis">,
|
||||
seedIds: readonly string[],
|
||||
): {
|
||||
readonly records: Evidence[];
|
||||
readonly result: Result<null, EvidenceIntegrityError>;
|
||||
} => {
|
||||
const records: Evidence[] = [];
|
||||
const visited = visitClosure(
|
||||
(id) => reader.evidenceForAnalysis(id),
|
||||
seedIds,
|
||||
undefined,
|
||||
(record) => records.push(record),
|
||||
);
|
||||
return { records, result: visited.ok ? ok(null) : visited };
|
||||
};
|
||||
|
||||
/** Follow exact session references without exporting unrelated records or Unknowns. */
|
||||
|
||||
@@ -447,7 +447,11 @@ const registerFunctionCommand = (
|
||||
"analyze_function",
|
||||
),
|
||||
},
|
||||
{ ...directAnalysisOptionsFromCli(logger, options), signal },
|
||||
{
|
||||
...directAnalysisOptionsFromCli(logger, options),
|
||||
signal,
|
||||
includeEvidenceSources: options.evidence_output !== undefined,
|
||||
},
|
||||
);
|
||||
return publishCliAnalysisEvidence(complete, delivery);
|
||||
}),
|
||||
|
||||
@@ -89,6 +89,9 @@ it("summary delivery returns the compact view and writes a bundle inspect-analys
|
||||
evidence_bundle: { path: string; bytes: number };
|
||||
};
|
||||
expect(h.runDirectAnalysis).toHaveBeenCalledOnce();
|
||||
expect(h.runDirectAnalysis.mock.calls[0]?.[3]).toMatchObject({
|
||||
includeEvidenceSources: true,
|
||||
});
|
||||
expect(out.summary.normalized_result.kind).toBe("summary");
|
||||
expect(typeof out.summary.normalized_result.parent_evidence_id).toBe(
|
||||
"string",
|
||||
|
||||
@@ -55,10 +55,7 @@ const singleDefaultOptions: readonly {
|
||||
{ command: "inspect-signal-decoders", options: ["--offset"] },
|
||||
];
|
||||
|
||||
const optionLine = (
|
||||
help: string,
|
||||
option: string,
|
||||
): string | undefined => {
|
||||
const optionLine = (help: string, option: string): string | undefined => {
|
||||
const line = help
|
||||
.split(/\r?\n/u)
|
||||
.find((candidate) => candidate.trimStart().startsWith(`${option} `));
|
||||
@@ -80,7 +77,9 @@ describe("CLI help default annotations", () => {
|
||||
}
|
||||
const statements = line.match(/default/giu)?.length ?? 0;
|
||||
if (statements !== 1)
|
||||
repeated.push(`${command} ${option}: ${statements} default statements`);
|
||||
repeated.push(
|
||||
`${command} ${option}: ${statements} default statements`,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(repeated).toEqual([]);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { EvidenceIntegrityError } from "../../../src/domain/evidenceErrors.js";
|
||||
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
||||
import { projectCliAnalysisResult } from "../../../src/cli/projectCliAnalysisResult.js";
|
||||
import { publishCliAnalysisEvidence } from "../../../src/cli/analysisEvidenceDelivery.js";
|
||||
import { parseConfig } from "../../../src/config/parseConfig.js";
|
||||
import { readFile, symlink, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
@@ -159,6 +161,7 @@ interface SnapshotProviderOptions {
|
||||
readonly supportedOperations?: readonly CapabilityDescriptor["operation"][];
|
||||
readonly limitations?: readonly string[];
|
||||
readonly sourceEvidence?: boolean;
|
||||
readonly sourceMissingLink?: boolean;
|
||||
}
|
||||
|
||||
const makeProvider = (
|
||||
@@ -263,6 +266,9 @@ const makeProvider = (
|
||||
operation: "health",
|
||||
parameters: {},
|
||||
result: "fixture source",
|
||||
...(options.sourceMissingLink
|
||||
? { evidenceLinks: [`ev_${"0".repeat(64)}`] }
|
||||
: {}),
|
||||
}),
|
||||
],
|
||||
}
|
||||
@@ -1880,7 +1886,10 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
|
||||
});
|
||||
const dependencies = fixture.dependencies(provider);
|
||||
const parameters = { procedure: "0x1000", document: "fixture" };
|
||||
const options = { snapshotPath: fixture.snapshotPath };
|
||||
const options = {
|
||||
snapshotPath: fixture.snapshotPath,
|
||||
includeEvidenceSources: true,
|
||||
};
|
||||
const run = () =>
|
||||
runDirectAnalysis(
|
||||
dependencies,
|
||||
@@ -1888,8 +1897,12 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
|
||||
"procedure_pseudo_code",
|
||||
parameters,
|
||||
options,
|
||||
).then(projectCliAnalysisResult);
|
||||
const first = parseEvidence(await run());
|
||||
);
|
||||
const firstResult = await run();
|
||||
const first = parseEvidence(projectCliAnalysisResult(firstResult));
|
||||
expect(
|
||||
firstResult.sourceEvidence?.map((record) => record.evidence_id),
|
||||
).toEqual(first.evidence_links);
|
||||
expect(first.evidence_links).toHaveLength(1);
|
||||
const saved = await readAnalysisSnapshot(fixture.snapshotPath);
|
||||
if (!saved.ok) throw saved.error;
|
||||
@@ -1901,9 +1914,30 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
|
||||
),
|
||||
).toBe(true);
|
||||
const initialStarts = fixture.starts.length;
|
||||
expect(parseEvidence(await run()).evidence_links).toEqual(
|
||||
const replayed = await run();
|
||||
expect(
|
||||
parseEvidence(projectCliAnalysisResult(replayed)).evidence_links,
|
||||
).toEqual(first.evidence_links);
|
||||
expect(replayed.sourceEvidence?.map((record) => record.evidence_id)).toEqual(
|
||||
first.evidence_links,
|
||||
);
|
||||
const outputPath = join(
|
||||
dirname(fixture.snapshotPath),
|
||||
"replayed-evidence.json",
|
||||
);
|
||||
await publishCliAnalysisEvidence(replayed, {
|
||||
providerId: IDENTITY.id,
|
||||
operation: "procedure_pseudo_code",
|
||||
signal: new AbortController().signal,
|
||||
detail: "complete",
|
||||
evidenceOutput: outputPath,
|
||||
});
|
||||
const published = JSON.parse(await readFile(outputPath, "utf8")) as {
|
||||
records: Evidence[];
|
||||
};
|
||||
expect(published.records.map((record) => record.evidence_id).sort()).toEqual(
|
||||
[first.evidence_id, ...first.evidence_links].sort(),
|
||||
);
|
||||
expect(fixture.starts).toHaveLength(initialStarts);
|
||||
|
||||
const withoutSource = {
|
||||
@@ -1918,7 +1952,11 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
|
||||
writeAnalysisSnapshot(withoutSource, fixture.snapshotPath, true, resources),
|
||||
);
|
||||
if (!written.ok) throw written.error;
|
||||
const refreshed = parseEvidence(await run());
|
||||
const refreshedResult = await run();
|
||||
const refreshed = parseEvidence(projectCliAnalysisResult(refreshedResult));
|
||||
expect(
|
||||
refreshedResult.sourceEvidence?.map((record) => record.evidence_id),
|
||||
).toEqual(first.evidence_links);
|
||||
expect(fixture.starts).toHaveLength(initialStarts + 1);
|
||||
expect(refreshed.evidence_links).toEqual(first.evidence_links);
|
||||
const repaired = await readAnalysisSnapshot(fixture.snapshotPath);
|
||||
@@ -1930,6 +1968,146 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves captured sources when a transitive source is missing", async () => {
|
||||
const fixture = await createSnapshotReplayFixture(
|
||||
"rea-direct-partial-source-",
|
||||
);
|
||||
const provider = fixture.createProvider({
|
||||
supportedOperations: ["procedure_pseudo_code"],
|
||||
sourceEvidence: true,
|
||||
sourceMissingLink: true,
|
||||
});
|
||||
const result = await runDirectAnalysis(
|
||||
fixture.dependencies(provider),
|
||||
fixture.path,
|
||||
"procedure_pseudo_code",
|
||||
{ procedure: "0x1000", document: "fixture" },
|
||||
{ includeEvidenceSources: true },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.sourceEvidence).toHaveLength(1);
|
||||
const outputPath = join(dirname(fixture.snapshotPath), "missing-source.json");
|
||||
const output = await publishCliAnalysisEvidence(result, {
|
||||
providerId: IDENTITY.id,
|
||||
operation: "procedure_pseudo_code",
|
||||
signal: new AbortController().signal,
|
||||
detail: "complete",
|
||||
evidenceOutput: outputPath,
|
||||
});
|
||||
const failure = output as {
|
||||
details: {
|
||||
source_closure: {
|
||||
records: Evidence[];
|
||||
omitted: { evidence_id: string }[];
|
||||
complete: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(failure.details.source_closure.complete).toBe(false);
|
||||
expect(
|
||||
failure.details.source_closure.records.map((record) => record.evidence_id),
|
||||
).toEqual(result.sourceEvidence?.map((record) => record.evidence_id));
|
||||
expect(failure.details.source_closure.omitted).toEqual([
|
||||
{ evidence_id: `ev_${"0".repeat(64)}`, reason: "not_captured" },
|
||||
]);
|
||||
await expect(readFile(outputPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("retains completed function sources when cancellation arrives as execution returns", async () => {
|
||||
const fixture = await createSnapshotReplayFixture(
|
||||
"rea-direct-cancel-source-",
|
||||
);
|
||||
const provider = fixture.createProvider({
|
||||
supportedOperations: ["analyze_function"],
|
||||
sourceEvidence: true,
|
||||
});
|
||||
const session = createTestBinarySession(provider);
|
||||
const execute = session.execute.bind(session);
|
||||
const controller = new AbortController();
|
||||
session.execute = async (...args) => {
|
||||
const result = await execute(...args);
|
||||
if (args[0] === "analyze_function") controller.abort();
|
||||
return result;
|
||||
};
|
||||
const dependencies = {
|
||||
...fixture.dependencies(provider),
|
||||
createBinarySession: () => session,
|
||||
};
|
||||
const result = await runDirectAnalysis(
|
||||
dependencies,
|
||||
fixture.path,
|
||||
"analyze_function",
|
||||
{ procedure: "0x1000" },
|
||||
{ signal: controller.signal, includeEvidenceSources: true },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
const outputPath = join(dirname(fixture.snapshotPath), "cancelled.json");
|
||||
const output = await publishCliAnalysisEvidence(result, {
|
||||
providerId: IDENTITY.id,
|
||||
operation: "analyze_function",
|
||||
signal: controller.signal,
|
||||
detail: "complete",
|
||||
evidenceOutput: outputPath,
|
||||
});
|
||||
const failure = output as {
|
||||
code: string;
|
||||
details: {
|
||||
partial_observation: Evidence;
|
||||
source_closure: { records: Evidence[]; complete: boolean };
|
||||
};
|
||||
};
|
||||
expect(failure.code).toBe("cancelled");
|
||||
expect(failure.details.source_closure.complete).toBe(true);
|
||||
expect(
|
||||
failure.details.source_closure.records.map((record) => record.evidence_id),
|
||||
).toEqual(failure.details.partial_observation.evidence_links);
|
||||
await expect(readFile(outputPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("retains function sources when completed parent admission fails", async () => {
|
||||
const fixture = await createSnapshotReplayFixture(
|
||||
"rea-direct-parent-admission-",
|
||||
);
|
||||
const provider = fixture.createProvider({
|
||||
supportedOperations: ["analyze_function"],
|
||||
sourceEvidence: true,
|
||||
});
|
||||
const session = createTestBinarySession(provider);
|
||||
session.importEvidenceBundle = () =>
|
||||
err(new EvidenceIntegrityError("controlled parent admission failure"));
|
||||
const dependencies = {
|
||||
...fixture.dependencies(provider),
|
||||
createBinarySession: () => session,
|
||||
};
|
||||
const result = await runDirectAnalysis(
|
||||
dependencies,
|
||||
fixture.path,
|
||||
"analyze_function",
|
||||
{ procedure: "0x1000" },
|
||||
{ includeEvidenceSources: true },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
const outputPath = join(dirname(fixture.snapshotPath), "refused.json");
|
||||
const output = await publishCliAnalysisEvidence(result, {
|
||||
providerId: IDENTITY.id,
|
||||
operation: "analyze_function",
|
||||
signal: new AbortController().signal,
|
||||
detail: "complete",
|
||||
evidenceOutput: outputPath,
|
||||
});
|
||||
const failure = output as {
|
||||
details: {
|
||||
partial_observation: Evidence;
|
||||
source_closure: { records: Evidence[]; complete: boolean };
|
||||
};
|
||||
};
|
||||
expect(failure.details.source_closure.complete).toBe(true);
|
||||
expect(
|
||||
failure.details.source_closure.records.map((record) => record.evidence_id),
|
||||
).toEqual(failure.details.partial_observation.evidence_links);
|
||||
await expect(readFile(outputPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
it("runs decompile export against the provider after loading a pseudocode snapshot", async () => {
|
||||
const fixture = await createSnapshotReplayFixture("rea-directory-snapshot-");
|
||||
const original = fixture.createProvider({
|
||||
|
||||
@@ -592,6 +592,63 @@ it.each(["interrupt", "cleanup-failure", "interrupt-cleanup-failure"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["interrupt", "cleanup-failure"] as const)(
|
||||
"retains captured source closure when direct finalization fails: %s",
|
||||
async (scenario) => {
|
||||
const workflow = await composedWorkflowFixture();
|
||||
const controller = new AbortController();
|
||||
const close = workflow.session.close.bind(workflow.session);
|
||||
workflow.session.close = async () => {
|
||||
workflow.session.close = close;
|
||||
if (scenario === "interrupt") controller.abort();
|
||||
const closed = await close();
|
||||
if (scenario === "interrupt" || !closed.ok) return closed;
|
||||
return err(
|
||||
new ProviderCleanupError(
|
||||
"fixture",
|
||||
["owned-root"],
|
||||
{ reason: "controlled session close failure" },
|
||||
{ operation: "close_binary" },
|
||||
),
|
||||
);
|
||||
};
|
||||
const root = await createTestTempDirectory("rea-closure-direct-captured-");
|
||||
const complete = await runDirectAnalysis(
|
||||
workflow.dependencies,
|
||||
workflow.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ signal: controller.signal, includeEvidenceSources: true },
|
||||
);
|
||||
const output = await publishCliAnalysisEvidence(complete, {
|
||||
...delivery(root),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const failure = output as {
|
||||
code: string;
|
||||
details: { partial_observation: unknown };
|
||||
};
|
||||
expect(failure.code).toBe(
|
||||
scenario === "interrupt" ? "cancelled" : "cleanup_incomplete",
|
||||
);
|
||||
const links = parseEvidence(
|
||||
failure.details.partial_observation,
|
||||
).evidence_links;
|
||||
expect(links.length).toBeGreaterThan(0);
|
||||
expect(closureOf(output)).toMatchObject({
|
||||
publication: "not_published",
|
||||
complete: true,
|
||||
omitted_count: 0,
|
||||
});
|
||||
expect(
|
||||
closureOf(output)
|
||||
.records.map((record) => record.evidence_id)
|
||||
.sort(),
|
||||
).toEqual([...links].sort());
|
||||
expect(await readdir(root)).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("reports through the shared one-shot delivery when cancellation arrives during close", async () => {
|
||||
const root = await createTestTempDirectory("rea-closure-deliver-");
|
||||
const held = source("held");
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
These source-owned fixtures are compiled at verification time; generated
|
||||
binaries and Ghidra projects are never committed.
|
||||
|
||||
For the one-shot function bundle handoff, run `npm run build:cached` and then
|
||||
`node scripts/verify-real-ghidra-function-bundle.mjs` with `GHIDRA_INSTALL_DIR`
|
||||
and `JAVA_HOME` set. The verifier compiles its own target, creates a project
|
||||
with a synthetic history report, and exercises complete, summary, and
|
||||
no-overwrite CLI delivery. It checks the target and project files for changes.
|
||||
|
||||
- `../c/riscv.c` and `riscv-probe.S` produce source-owned RV32/RV64 ELF images
|
||||
with double-float ABI declarations and an independently known instruction.
|
||||
`../c/riscv-float-abi.c`, `riscv-probe.S` and `riscv-isa-probe.S` produce
|
||||
|
||||
Reference in new issue
Block a user