mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 03:31:35 -10:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcb67417ef | ||
|
|
ba297dfc88 | ||
|
|
8fca1f525a | ||
|
|
da2e18db79 | ||
|
|
317ebccc5e |
No files matched your search
@@ -1,33 +1,13 @@
|
||||
import { projectCliAnalysisResult } from "../../../src/cli/projectCliAnalysisResult.js";
|
||||
import { chmod, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { withArtifactResourceScope } from "../../fixtures/artifactInventory.js";
|
||||
import { createInspectionResources } from "../../fixtures/inspectionResources.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { writeAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js";
|
||||
import { createDirectAnalysis } from "../../../src/composition/directAnalysis.js";
|
||||
import type { AnalysisSnapshot } from "../../../src/domain/analysisSnapshot.js";
|
||||
import {
|
||||
snapshotBinding,
|
||||
snapshotTarget,
|
||||
} from "../../../src/domain/analysisSnapshot.js";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { REA_WORKFLOW_PROVIDER } from "../../../src/domain/investigationProviders.js";
|
||||
import { workflowAnalysisProfile } from "../../../src/application/WorkflowAnalysisProfile.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../../src/hopper/HopperProviderMetadata.js";
|
||||
|
||||
import { resolveHopperAnalysisProfile } from "../../../src/hopper/HopperAnalysisProfile.js";
|
||||
|
||||
let inspectionResources = createInspectionResources();
|
||||
afterEach(async () => {
|
||||
await inspectionResources.close();
|
||||
inspectionResources = createInspectionResources();
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
@@ -60,55 +40,4 @@ describe("direct analysis snapshot files", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not replay unbound historical Evidence from a supplied snapshot", async () => {
|
||||
const directory = await createTestTempDirectory("rea-direct-snapshot-");
|
||||
const snapshotPath = join(directory, "analysis.json");
|
||||
const launcherPath = join(directory, "hopper-launcher");
|
||||
await writeFile(launcherPath, "fixture Hopper launcher");
|
||||
await chmod(launcherPath, 0o755);
|
||||
const target = await parseBinaryTarget(process.execPath);
|
||||
if (!target.ok) throw target.error;
|
||||
const resolved = await resolveHopperAnalysisProfile(target.value, {
|
||||
launcherPath,
|
||||
loaderArgsOverride: [],
|
||||
provider: HOPPER_PROVIDER_IDENTITY,
|
||||
resourceScope: inspectionResources.owner,
|
||||
});
|
||||
if (!resolved.ok || resolved.value.profile === null)
|
||||
throw new Error("fixture Hopper profile did not resolve");
|
||||
const profile = resolved.value.profile;
|
||||
const workflowProfile = workflowAnalysisProfile(profile);
|
||||
const evidence = createEvidence(target.value, REA_WORKFLOW_PROVIDER, {
|
||||
operation: "binary_overview",
|
||||
parameters: {},
|
||||
result: { cached: true },
|
||||
analysisProfile: workflowProfile,
|
||||
});
|
||||
const snapshot: AnalysisSnapshot = {
|
||||
target: snapshotTarget(target.value),
|
||||
binding: snapshotBinding(profile),
|
||||
entries: [],
|
||||
workflow_entries: [],
|
||||
evidence_bundle: createEvidenceBundle([evidence]),
|
||||
};
|
||||
const written = await withArtifactResourceScope((resources) =>
|
||||
writeAnalysisSnapshot(snapshot, snapshotPath, false, resources),
|
||||
);
|
||||
if (!written.ok) throw written.error;
|
||||
vi.stubEnv("HOPPER_LAUNCHER_PATH", launcherPath);
|
||||
|
||||
await expect(
|
||||
createDirectAnalysis(process.env)
|
||||
.runDirectAnalysis(
|
||||
process.execPath,
|
||||
"binary_overview",
|
||||
{},
|
||||
{
|
||||
snapshotPath,
|
||||
},
|
||||
)
|
||||
.then(projectCliAnalysisResult),
|
||||
).resolves.not.toEqual(evidence);
|
||||
});
|
||||
});
|
||||
@@ -150,6 +150,7 @@ const expectSourceSnapshotRetained = (snapshot: AnalysisSnapshot) => {
|
||||
|
||||
interface SnapshotProviderTrace {
|
||||
readonly starts: string[];
|
||||
readonly sources: Map<string, Evidence>;
|
||||
readonly queries: Array<{
|
||||
readonly operation: string;
|
||||
readonly parameters: Readonly<Record<string, unknown>>;
|
||||
@@ -256,25 +257,24 @@ const makeProvider = (
|
||||
`Unmodeled snapshot provider operation: ${operation}`,
|
||||
);
|
||||
const result = results[operation];
|
||||
const source = options.sourceEvidence
|
||||
? createEvidence(target, profile.provider, {
|
||||
operation: "health",
|
||||
parameters: {},
|
||||
result: "fixture source",
|
||||
...(options.sourceMissingLink
|
||||
? { evidenceLinks: [`ev_${"0".repeat(64)}`] }
|
||||
: {}),
|
||||
})
|
||||
: undefined;
|
||||
if (source !== undefined)
|
||||
trace.sources.set(source.evidence_id, source);
|
||||
return ok(
|
||||
createAnalysisExecution(result, profile.provider, {
|
||||
analysisProfile: profile,
|
||||
rawResult: result,
|
||||
limitations: options.limitations ?? [],
|
||||
...(options.sourceEvidence
|
||||
? {
|
||||
sourceEvidence: [
|
||||
createEvidence(target, profile.provider, {
|
||||
operation: "health",
|
||||
parameters: {},
|
||||
result: "fixture source",
|
||||
...(options.sourceMissingLink
|
||||
? { evidenceLinks: [`ev_${"0".repeat(64)}`] }
|
||||
: {}),
|
||||
}),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
...(source === undefined ? {} : { sourceEvidence: [source] }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
@@ -293,6 +293,7 @@ const createSnapshotReplayFixture = async (
|
||||
const snapshotPath = join(directory, snapshotRelativePath);
|
||||
await writeFile(path, "fixture");
|
||||
const starts: string[] = [];
|
||||
const sources = new Map<string, Evidence>();
|
||||
const queries: SnapshotProviderTrace["queries"] = [];
|
||||
const resolutions: string[] = [];
|
||||
|
||||
@@ -300,13 +301,14 @@ const createSnapshotReplayFixture = async (
|
||||
path,
|
||||
snapshotPath,
|
||||
starts,
|
||||
sources,
|
||||
get calls() {
|
||||
return queries.map(({ operation }) => operation);
|
||||
},
|
||||
queries,
|
||||
resolutions,
|
||||
createProvider(options: SnapshotProviderOptions = {}): AnalysisProvider {
|
||||
return makeProvider({ starts, queries, resolutions }, options);
|
||||
return makeProvider({ starts, sources, queries, resolutions }, options);
|
||||
},
|
||||
dependencies(provider: AnalysisProvider): DirectAnalysisDependencies {
|
||||
return {
|
||||
@@ -1075,7 +1077,7 @@ describe("Ghidra composed workflow runtime-budget admission", () => {
|
||||
});
|
||||
|
||||
describe("binary overview snapshot replay", () => {
|
||||
it("replays the identical binary overview without provider startup or calls", async () => {
|
||||
it("replays bound overviews without provider work and recomposes unbound history", async () => {
|
||||
const fixture = await createSnapshotReplayFixture("rea-workflow-snapshot-");
|
||||
const provider = fixture.createProvider();
|
||||
const dependencies = fixture.dependencies(provider);
|
||||
@@ -1159,6 +1161,47 @@ describe("binary overview snapshot replay", () => {
|
||||
(record) => record.evidence_id === firstEvidence.evidence_id,
|
||||
)?.caller_input_path,
|
||||
).toBe(fixture.path);
|
||||
|
||||
// Historical observations remain importable data without a replay binding.
|
||||
// Retain the primitive entries and one current-profile historical overview,
|
||||
// but omit its workflow entry and the bound current overview record.
|
||||
const historicalOnly = withEarlierHistoricalEvidence(
|
||||
loaded.value,
|
||||
fixture.path,
|
||||
current,
|
||||
);
|
||||
const unboundPath = join(dirname(fixture.snapshotPath), "unbound.json");
|
||||
const unboundWritten = await withArtifactResourceScope((resources) =>
|
||||
writeAnalysisSnapshot(
|
||||
{
|
||||
...historicalOnly,
|
||||
workflow_entries: [],
|
||||
evidence_bundle: createEvidenceBundle(
|
||||
historicalOnly.evidence_bundle.records.filter(
|
||||
(record) => record.evidence_id !== current.evidence_id,
|
||||
),
|
||||
),
|
||||
},
|
||||
unboundPath,
|
||||
false,
|
||||
resources,
|
||||
),
|
||||
);
|
||||
if (!unboundWritten.ok) throw unboundWritten.error;
|
||||
const recomposed = parseEvidence(
|
||||
await runDirectAnalysis(
|
||||
dependencies,
|
||||
fixture.path,
|
||||
"binary_overview",
|
||||
{},
|
||||
{ snapshotPath: unboundPath },
|
||||
).then(projectCliAnalysisResult),
|
||||
);
|
||||
expect(recomposed.operation).toBe("binary_overview");
|
||||
expect(recomposed.normalized_result).toEqual(
|
||||
firstEvidence.normalized_result,
|
||||
);
|
||||
expect(recomposed.caller_input_path).toBe(fixture.path);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2279,6 +2322,8 @@ it("publishes captured function sources when completed parent admission fails",
|
||||
{ includeEvidenceSources: true },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
const sourceIds = [...fixture.sources.keys()];
|
||||
expect(sourceIds).toHaveLength(1);
|
||||
const outputPath = join(dirname(fixture.snapshotPath), "refused.json");
|
||||
const output = await publishCliAnalysisEvidence(result, {
|
||||
providerId: IDENTITY.id,
|
||||
@@ -2299,8 +2344,9 @@ it("publishes captured function sources when completed parent admission fails",
|
||||
(record) => record.operation === "analyze_function",
|
||||
);
|
||||
expect(parent).toBeDefined();
|
||||
expect(parent?.evidence_links).toEqual(sourceIds);
|
||||
expect(bundle.records.map((record) => record.evidence_id).sort()).toEqual(
|
||||
[parent!.evidence_id, ...parent!.evidence_links].sort(),
|
||||
[parent!.evidence_id, ...sourceIds].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -107,7 +107,21 @@ mcpTest(
|
||||
name: "inspect_native_initializers",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(missing.isError).toBe(true);
|
||||
expect(parseMcpToolError(missing).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
operation: "inspect_native_initializers",
|
||||
issues: [
|
||||
{
|
||||
path: ["architecture"],
|
||||
reason: "invalid_value",
|
||||
message: expect.stringMatching(
|
||||
/arm64 \(subtype 0x0\).*select one architecture/u,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const selected = await client.callTool({
|
||||
name: "inspect_native_initializers",
|
||||
arguments: { path, architecture: "arm64" },
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import spawn from "cross-spawn";
|
||||
import { existsSync } from "node:fs";
|
||||
import { registerHooks } from "node:module";
|
||||
|
||||
const supervisor = new URL(
|
||||
"../../scripts/run-exclusive-command.mjs",
|
||||
import.meta.url,
|
||||
).href;
|
||||
|
||||
// Replace only the supervisor's dependency, leaving its actual source and the
|
||||
// public wrapper intact. Other imports still use the real implementation.
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (specifier === "cross-spawn" && context.parentURL === supervisor)
|
||||
return { url: import.meta.url, shortCircuit: true };
|
||||
return nextResolve(specifier, context);
|
||||
},
|
||||
});
|
||||
|
||||
export default function spawnWithPostSpawnError(command, args, options) {
|
||||
const child = spawn(command, args, options);
|
||||
if (args.at(-1) !== "rea-runner-post-spawn-error") return child;
|
||||
let timer;
|
||||
child.once("close", () => clearInterval(timer));
|
||||
child.once("spawn", () => {
|
||||
timer = setInterval(() => {
|
||||
if (!existsSync(process.env.REA_TEST_RUNNER_READY_PATH)) return;
|
||||
clearInterval(timer);
|
||||
process.stdout.write("controlled-post-spawn-error\n");
|
||||
child.emit("error", new Error("controlled post-spawn child error"));
|
||||
child.emit("error", new Error("controlled secondary child error"));
|
||||
}, 5);
|
||||
});
|
||||
return child;
|
||||
}
|
||||
@@ -32,10 +32,7 @@ import {
|
||||
import { prepareProcessOwnershipInspection } from "../../src/process/ProcessOwnershipObservation.js";
|
||||
import { spawnOwnedProviderProcess } from "../../src/process/ProviderProcess.js";
|
||||
import { PrivateRuntimeRoot } from "../../src/process/PrivateRuntimeRoot.js";
|
||||
import {
|
||||
withArtifactFileHandle,
|
||||
ArtifactFileCleanupFailure,
|
||||
} from "../../src/artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactFileCleanupFailure } from "../../src/artifacts/ArtifactFileOperation.js";
|
||||
import * as artifacts from "../../src/artifacts/readStableArtifact.js";
|
||||
import {
|
||||
recordedCrashFixture,
|
||||
@@ -406,13 +403,6 @@ it.runIf(supportedHost).each(["decoder", "debugger"])(
|
||||
const resources = args[4];
|
||||
if (resources === undefined)
|
||||
throw new Error("Reply must have an artifact resource owner");
|
||||
const file = await open(reply, "r");
|
||||
const close = file.close.bind(file);
|
||||
emergencyCloses.push(close);
|
||||
file.close = async () => {
|
||||
if (!releaseAllowed) throw closeFailure;
|
||||
await close();
|
||||
};
|
||||
const retain = resources.retain.bind(resources);
|
||||
const spy = vi
|
||||
.spyOn(resources, "retain")
|
||||
@@ -421,12 +411,25 @@ it.runIf(supportedHost).each(["decoder", "debugger"])(
|
||||
controller.abort(reason);
|
||||
});
|
||||
try {
|
||||
return await withArtifactFileHandle(
|
||||
reply,
|
||||
file,
|
||||
return await read(
|
||||
args[0],
|
||||
args[1],
|
||||
args[2],
|
||||
{
|
||||
lstat: (filePath) => lstat(filePath, { bigint: true }),
|
||||
open: async (filePath, flags) => {
|
||||
const file = await open(filePath, flags);
|
||||
const close = file.close.bind(file);
|
||||
emergencyCloses.push(close);
|
||||
file.close = async () => {
|
||||
if (!releaseAllowed) throw closeFailure;
|
||||
await close();
|
||||
};
|
||||
return file;
|
||||
},
|
||||
},
|
||||
resources,
|
||||
controller.signal,
|
||||
() => read(...args),
|
||||
args[5],
|
||||
);
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
|
||||
@@ -7,19 +7,14 @@ import {
|
||||
utimes,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
const supervisorPath = fileURLToPath(
|
||||
new URL("../../scripts/run-exclusive-command.mjs", import.meta.url),
|
||||
);
|
||||
|
||||
const runnerPath = fileURLToPath(
|
||||
new URL("../../scripts/run-exclusive.mjs", import.meta.url),
|
||||
);
|
||||
@@ -654,39 +649,10 @@ it.runIf(process.platform !== "win32")(
|
||||
const readyPath = join(directory, "command-ready");
|
||||
const gatePath = join(directory, "allow-command-exit");
|
||||
const closedPath = join(directory, "command-closed");
|
||||
const source = await readFile(supervisorPath, "utf8");
|
||||
const crossSpawn = pathToFileURL(
|
||||
createRequire(import.meta.url).resolve("cross-spawn"),
|
||||
const preload = new URL(
|
||||
"../fixtures/runnerPostSpawnError.mjs",
|
||||
import.meta.url,
|
||||
).href;
|
||||
const spawnCall =
|
||||
/child = spawn\(command\[0\], command\.slice\(1\), \{\n\s+cwd: process\.cwd\(\),\n\s+env: process\.env,\n\s+stdio: "inherit",\n\s+\}\);/u;
|
||||
const postSpawnInjection = `child.once("spawn", () => {
|
||||
const waitForReady = setInterval(() => {
|
||||
if (!existsSync(${JSON.stringify(readyPath)})) return;
|
||||
clearInterval(waitForReady);
|
||||
process.stdout.write("controlled-post-spawn-error\\n");
|
||||
child.emit("error", new Error("controlled post-spawn child error"));
|
||||
child.emit("error", new Error("controlled secondary child error"));
|
||||
}, 5);
|
||||
});`;
|
||||
expect(source).toMatch(spawnCall);
|
||||
const injectedSource = source
|
||||
.replace(
|
||||
'import spawn from "cross-spawn";',
|
||||
`import spawn from ${JSON.stringify(crossSpawn)};\nimport { existsSync } from "node:fs";`,
|
||||
)
|
||||
.replace(spawnCall, (call) => `${call}\n ${postSpawnInjection}`);
|
||||
expect(injectedSource).toContain(postSpawnInjection);
|
||||
expect(injectedSource).toContain("controlled post-spawn child error");
|
||||
const copiedSupervisor = join(directory, "run-exclusive-command.mjs");
|
||||
const copiedHelperDirectory = join(directory, "lib");
|
||||
await mkdir(copiedHelperDirectory, { recursive: true });
|
||||
await copyFile(
|
||||
join(process.cwd(), "scripts/lib/windows-owned-command.mjs"),
|
||||
join(copiedHelperDirectory, "windows-owned-command.mjs"),
|
||||
);
|
||||
await writeFile(copiedSupervisor, injectedSource);
|
||||
|
||||
const gatedCommand = `
|
||||
const fs = require("node:fs");
|
||||
fs.writeFileSync(${JSON.stringify(readyPath)}, "ready");
|
||||
@@ -698,17 +664,34 @@ it.runIf(process.platform !== "win32")(
|
||||
process.stdout.write("command-closed\\n");
|
||||
}, 5);
|
||||
`;
|
||||
const supervisor = spawn(
|
||||
const runner = spawn(
|
||||
process.execPath,
|
||||
[copiedSupervisor, "test", process.execPath, "-e", gatedCommand],
|
||||
{ cwd: directory, stdio: ["ignore", "pipe", "pipe", "ipc"] },
|
||||
[
|
||||
runnerPath,
|
||||
"test",
|
||||
process.execPath,
|
||||
"-e",
|
||||
gatedCommand,
|
||||
"rea-runner-post-spawn-error",
|
||||
],
|
||||
{
|
||||
cwd: directory,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, `--import=${preload}`]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
REA_TEST_RUNNER_READY_PATH: readyPath,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (supervisor.stdout === null || supervisor.stderr === null) {
|
||||
supervisor.kill("SIGKILL");
|
||||
throw new Error("supervisor output pipes were not created");
|
||||
if (runner.stdout === null || runner.stderr === null) {
|
||||
runner.kill("SIGKILL");
|
||||
throw new Error("runner output pipes were not created");
|
||||
}
|
||||
const stdoutStream = supervisor.stdout;
|
||||
const stderrStream = supervisor.stderr;
|
||||
const stdoutStream = runner.stdout;
|
||||
const stderrStream = runner.stderr;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const outputWaiters = new Map<string, (() => void)[]>();
|
||||
@@ -730,19 +713,17 @@ it.runIf(process.platform !== "win32")(
|
||||
resolveOutput,
|
||||
]);
|
||||
}),
|
||||
`supervisor did not write ${marker}`,
|
||||
`runner did not write ${marker}`,
|
||||
);
|
||||
};
|
||||
const exited = withTimeout(
|
||||
const closed = withTimeout(
|
||||
new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
|
||||
(resolveExit, reject) => {
|
||||
supervisor.once("error", reject);
|
||||
supervisor.once("exit", (code, signal) =>
|
||||
resolveExit({ code, signal }),
|
||||
);
|
||||
runner.once("error", reject);
|
||||
runner.once("close", (code, signal) => resolveExit({ code, signal }));
|
||||
},
|
||||
),
|
||||
"supervisor did not exit",
|
||||
"runner did not exit",
|
||||
);
|
||||
try {
|
||||
await waitForOutput("controlled-post-spawn-error");
|
||||
@@ -759,7 +740,7 @@ it.runIf(process.platform !== "win32")(
|
||||
|
||||
await writeFile(gatePath, "open");
|
||||
await waitForOutput("command-closed");
|
||||
expect(await exited).toEqual({ code: 1, signal: null });
|
||||
expect(await closed).toEqual({ code: 1, signal: null });
|
||||
expect(stderr).toContain("controlled post-spawn child error");
|
||||
expect(stderr).not.toContain("controlled secondary child error");
|
||||
expect(
|
||||
@@ -771,7 +752,7 @@ it.runIf(process.platform !== "win32")(
|
||||
} finally {
|
||||
await writeFile(gatePath, "open");
|
||||
await waitForOutput("command-closed").catch(() => undefined);
|
||||
await exited.catch(() => undefined);
|
||||
await closed.catch(() => undefined);
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user