mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
1201c25542
* fix(ci): retire unused internal analysis declarations * test: use an admitted app bundle for provider routing (cherry picked from commit00aa17f12a) * test: keep bundle routing coverage outside oversized group (cherry picked from commit8658353b0d) * fix(ci): align regression fixtures with current admission contracts * test: bind extraction close fixtures to their ZIP digest (cherry picked from commit626d0250db) * test(cli): match typed analysis outcomes in function detail fixture * test(ghidra): preserve empty Java property separator framing * test(electron): retain unknown IPC ownership for native module gaps * fix(electron): share source-scoped call path identity * test(captures): assert safe source identity on SAZ refusal * test(apple): match root occurrence kind to inventory manifest * fix(examples): align directory root occurrence identity * test(electron): align equivalent fixture assertions with upstream * test(process): assert retained capture root identity * test(windows): declare the PE fixture optional header * test(ci): refresh owned registration and verifier fixtures * test(setup): use owned registrations for refresh fixtures * test(process): control unavailable ownership in CLI cleanup fixture * test(process): guard controlled compiler output arguments * test(setup): align equivalent entrypoint guards with upstream * test(process): release the cleanup target through an owned gate * test(ci): align semantic identities and owned fixture facts * test(pcap): retain the upstream DNS wire fixture branch * test(pcap): use the canonical wire ownership fixture * test(pcap): assert exact DNS wire query labels * test(ci): preserve admission and ownership fixture prerequisites * test(ci): align authenticated fixture and cleanup boundaries * test(windows): align native artifact fixtures with upstream * test(android): request complete evidence for inline MCP contracts * test: avoid pinning diagnostic wording * test: verify cleanup diagnostics at their source * test: strengthen evidence and cleanup checks * test: avoid pinning registration guidance --------- Co-authored-by: Plastic-time <Plastic-time@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
491 lines
16 KiB
TypeScript
491 lines
16 KiB
TypeScript
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
import {
|
|
captureProcessScenarioFile,
|
|
compareProcessEvidenceFiles,
|
|
} from "../../../src/application/process/ProcessCli.js";
|
|
import { createEvidence } from "../../../src/domain/evidence.js";
|
|
import { PROCESS_PROVIDER } from "../../../src/domain/process/processEvidenceProvider.js";
|
|
import { INVESTIGATION_EXAMPLES } from "../../../src/contracts/investigationExamples.js";
|
|
import { observeProcessStartIdentity } from "../../../src/process/ProcessOwnershipObservation.js";
|
|
import {
|
|
analysisCliErrorEnvelopeSchema,
|
|
analysisErrorProjectionSchema,
|
|
} from "../../../src/contracts/errorSchemas.js";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const CLI_INTEGRATION_TIMEOUT_MS = 60_000;
|
|
|
|
const cleanupTargetSource = `
|
|
const fs = process.getBuiltinModule("node:fs");
|
|
const [marker, release] = process.argv.slice(2);
|
|
if (!marker || !release) throw new Error("Missing cleanup target paths");
|
|
fs.writeFileSync(marker, String(process.pid));
|
|
setTimeout(() => process.exit(2), ${CLI_INTEGRATION_TIMEOUT_MS}).unref();
|
|
const waiting = setInterval(() => {
|
|
if (!fs.existsSync(release)) return;
|
|
clearInterval(waiting);
|
|
}, 10);
|
|
`;
|
|
|
|
const writeUnavailableOwnershipCompiler = async (root: string) => {
|
|
// Controlled protocol facts force the refusal independently of native
|
|
// platform-binary visibility. No token ever authorizes a signal.
|
|
const helper = `#!${process.execPath}
|
|
const arguments_ = process.argv.slice(2);
|
|
const identities = arguments_[0] === "--identities";
|
|
const pids = (identities ? arguments_.slice(1) : arguments_).map(Number);
|
|
console.log(JSON.stringify({ results: pids.map(pid => identities ? {
|
|
pid, state: "readable", identity: "controlled-start-" + pid,
|
|
lifecycle: "non-zombie",
|
|
relation: { state: "observed", parent_pid: 1, process_group_id: pid },
|
|
} : {
|
|
pid, state: "unavailable", reason: "platform_binary_environment_withheld",
|
|
}) }));
|
|
`;
|
|
const compiler = join(root, "controlled-swiftc");
|
|
await writeFile(
|
|
compiler,
|
|
`#!${process.execPath}
|
|
const fs = process.getBuiltinModule("node:fs");
|
|
const outputIndex = process.argv.indexOf("-o");
|
|
const output = process.argv[outputIndex + 1];
|
|
if (outputIndex < 0 || !output || output.startsWith("-"))
|
|
throw new Error("Missing controlled helper output");
|
|
fs.writeFileSync(output, ${JSON.stringify(helper)});
|
|
fs.chmodSync(output, 0o755);
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
return compiler;
|
|
};
|
|
|
|
describe("documented process CLI workflow", () => {
|
|
cliTest(
|
|
"feeds the documented JSON capture into the comparison CLI",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-process-cli-");
|
|
const guide = await readFile(
|
|
new URL("../../../docs/process-capture.md", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const recipe = /```sh\nrea capture-process ([^\n]+)\n```/u.exec(
|
|
guide,
|
|
)?.[1];
|
|
if (recipe === undefined)
|
|
throw new Error("Missing process capture recipe");
|
|
const [argumentsText, output] = recipe.split(" > ");
|
|
if (argumentsText === undefined || output === undefined)
|
|
throw new Error("Missing process capture redirection");
|
|
const args = argumentsText.split(/\s+/u);
|
|
const input = args[0];
|
|
if (input === undefined) throw new Error("Missing scenario path");
|
|
await writeFile(
|
|
join(root, input),
|
|
JSON.stringify({
|
|
executable: process.execPath,
|
|
arguments: ["-e", "process.stdout.write('documented-capture')"],
|
|
}),
|
|
);
|
|
const capture = await cli.run({
|
|
arguments: ["capture-process", ...args],
|
|
cwd: root,
|
|
});
|
|
expect(capture.exitCode, capture.stdout || capture.stderr).toBe(0);
|
|
expect(capture.json).toMatchObject({
|
|
operation: "capture_process_scenario",
|
|
});
|
|
await writeFile(join(root, output), capture.stdout);
|
|
const comparison = await cli.run({
|
|
arguments: ["compare-process-captures", output, output, "--json"],
|
|
cwd: root,
|
|
});
|
|
expect(comparison.exitCode, comparison.stdout || comparison.stderr).toBe(
|
|
0,
|
|
);
|
|
expect(comparison.json).toMatchObject({
|
|
operation: "compare_process_captures",
|
|
});
|
|
},
|
|
CLI_INTEGRATION_TIMEOUT_MS,
|
|
);
|
|
});
|
|
|
|
describe("process CLI cleanup errors", () => {
|
|
cliTest.skipIf(process.platform !== "darwin")(
|
|
"reports the retained capture root with a controlled unavailable ownership reader",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-process-cli-cleanup-");
|
|
const compiler = await writeUnavailableOwnershipCompiler(root);
|
|
const scenario = join(root, "scenario.json");
|
|
const marker = join(root, "target.pid");
|
|
const release = join(root, "release-target");
|
|
const target = join(root, "cleanup-target.cjs");
|
|
await writeFile(target, cleanupTargetSource);
|
|
await writeFile(
|
|
scenario,
|
|
JSON.stringify({
|
|
executable: process.execPath,
|
|
arguments: [target, marker, release],
|
|
timeout_ms: 500,
|
|
idle_timeout_ms: 500,
|
|
finalization_ms: 100,
|
|
}),
|
|
);
|
|
try {
|
|
// A retained PTY can keep its CLI process alive. Read the completed
|
|
// compiled command result before releasing the child and joining exit.
|
|
if (cli.sharedCliRun === undefined)
|
|
throw new Error("Expected the compiled CLI command session");
|
|
const result = await cli.sharedCliRun({
|
|
arguments: ["capture-process", scenario, "--json"],
|
|
environment: {
|
|
TMPDIR: root,
|
|
REA_DARWIN_SWIFTC_PATH: compiler,
|
|
SDKROOT: root,
|
|
REA_DARWIN_MODULE_CACHE_PATH: join(root, "modules"),
|
|
},
|
|
});
|
|
const targetPid = Number(await readFile(marker, "utf8"));
|
|
expect(Number.isSafeInteger(targetPid) && targetPid > 0).toBe(true);
|
|
expect(process.kill(targetPid, 0), "the target remains alive").toBe(
|
|
true,
|
|
);
|
|
expect(result.exitCode, result.stdout || result.stderr).toBe(1);
|
|
const retained = (await readdir(root)).filter((name) =>
|
|
/^rea-process-(?!token-)/u.test(name),
|
|
);
|
|
expect(retained).toHaveLength(1);
|
|
const retainedRoot = retained[0];
|
|
if (retainedRoot === undefined)
|
|
throw new Error("Missing retained root");
|
|
expect(result.json).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
message: expect.stringContaining(
|
|
"platform_binary_environment_withheld",
|
|
),
|
|
details: {
|
|
resources: ["owned_process_group", join(root, retainedRoot)],
|
|
cleanup_report: {
|
|
temporary_root: { state: "unverified" },
|
|
},
|
|
},
|
|
});
|
|
} finally {
|
|
// Release even when the CLI or an assertion fails. A target starting
|
|
// after this point also sees the release and exits without a signal.
|
|
await writeFile(release, "release");
|
|
const targetPid = await readFile(marker, "utf8").then(
|
|
(value) => Number(value),
|
|
(cause: unknown) => {
|
|
if (
|
|
cause instanceof Error &&
|
|
"code" in cause &&
|
|
cause.code === "ENOENT"
|
|
)
|
|
return undefined;
|
|
throw cause;
|
|
},
|
|
);
|
|
if (targetPid !== undefined) {
|
|
expect(Number.isSafeInteger(targetPid) && targetPid > 0).toBe(true);
|
|
await expect
|
|
.poll(() => observeProcessStartIdentity(targetPid), {
|
|
timeout: 7_000,
|
|
interval: 25,
|
|
})
|
|
.toBeUndefined();
|
|
}
|
|
}
|
|
},
|
|
CLI_INTEGRATION_TIMEOUT_MS,
|
|
);
|
|
});
|
|
|
|
describe("process CLI errors", () => {
|
|
cliTest(
|
|
"returns canonical JSON errors for missing and malformed process files",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-process-cli-errors-");
|
|
const malformed = join(root, "malformed.json");
|
|
await writeFile(malformed, "not-json");
|
|
const cases = [
|
|
["capture-process", join(root, "missing.json"), "--json"],
|
|
["capture-process", malformed, "--json"],
|
|
[
|
|
"compare-process-captures",
|
|
join(root, "missing-left.json"),
|
|
join(root, "missing-right.json"),
|
|
"--json",
|
|
],
|
|
] as const;
|
|
|
|
for (const arguments_ of cases) {
|
|
const result = await cli.run({
|
|
arguments: arguments_,
|
|
cwd: root,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(
|
|
analysisCliErrorEnvelopeSchema.safeParse(result.json).success,
|
|
).toBe(true);
|
|
expect(result.json).toMatchObject({
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
retryable: false,
|
|
details: {
|
|
operation: expect.stringMatching(
|
|
/^(capture_process_scenario|compare_process_captures)$/u,
|
|
),
|
|
issues: [
|
|
expect.objectContaining({
|
|
path: ["input_file"],
|
|
reason: "invalid_value",
|
|
}),
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
const full = await cli.run({
|
|
arguments: [
|
|
"capture-process",
|
|
join(root, "missing.json"),
|
|
"--json",
|
|
"--full-output",
|
|
],
|
|
cwd: root,
|
|
});
|
|
expect(full.exitCode).toBe(1);
|
|
expect(full.json).toMatchObject({ ok: false });
|
|
if (typeof full.json !== "object" || full.json === null)
|
|
throw new Error("Expected a full-output CLI error document");
|
|
const fullError = Reflect.get(full.json, "error");
|
|
expect(analysisErrorProjectionSchema.safeParse(fullError).success).toBe(
|
|
true,
|
|
);
|
|
},
|
|
CLI_INTEGRATION_TIMEOUT_MS,
|
|
);
|
|
|
|
it(
|
|
"exits unsuccessfully without writing failure-shaped evidence",
|
|
async () => {
|
|
await expect(
|
|
execFileAsync(
|
|
process.execPath,
|
|
["scripts/rea.mjs", "capture-process", "/missing/scenario.json"],
|
|
{ cwd: process.cwd() },
|
|
),
|
|
).rejects.toMatchObject({
|
|
code: 1,
|
|
stdout: expect.stringContaining("category: invalid_input"),
|
|
});
|
|
},
|
|
CLI_INTEGRATION_TIMEOUT_MS,
|
|
);
|
|
|
|
it("rejects NUL arguments before process launch", async () => {
|
|
const root = await createTestTempDirectory("rea-process-cli-");
|
|
const scenario = join(root, "nul-argument.json");
|
|
await writeFile(
|
|
scenario,
|
|
JSON.stringify({ executable: process.execPath, arguments: ["\0"] }),
|
|
);
|
|
|
|
expect(await captureProcessScenarioFile(scenario)).toMatchObject({
|
|
error: "Process command failed",
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
details: {
|
|
operation: "capture_process_scenario",
|
|
issues: [
|
|
{
|
|
path: ["arguments", 0],
|
|
reason: "invalid_format",
|
|
expected: "regex",
|
|
message:
|
|
"Values passed to operating-system APIs cannot contain NUL",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("process CLI environment key diagnostics", () => {
|
|
it("reports the reserved process environment key constraint", async () => {
|
|
const root = await createTestTempDirectory("rea-process-cli-");
|
|
const scenario = join(root, "reserved-environment.json");
|
|
await writeFile(
|
|
scenario,
|
|
JSON.stringify({
|
|
executable: process.execPath,
|
|
environment: { REA_PROCESS_RUN_ID: "caller-value" },
|
|
}),
|
|
);
|
|
|
|
expect(await captureProcessScenarioFile(scenario)).toMatchObject({
|
|
error: "Process command failed",
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
details: {
|
|
operation: "capture_process_scenario",
|
|
issues: [
|
|
{
|
|
path: ["environment", "REA_PROCESS_RUN_ID"],
|
|
reason: "invalid_format",
|
|
expected: "regex",
|
|
message: "REA_PROCESS_RUN_ID is reserved by the process adapter",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("process CLI evidence validation", () => {
|
|
it("captures the minimal executable-and-arguments scenario", async () => {
|
|
const root = await createTestTempDirectory("rea-process-cli-");
|
|
const scenario = join(root, "scenario.json");
|
|
await writeFile(
|
|
scenario,
|
|
JSON.stringify({
|
|
executable: process.execPath,
|
|
arguments: ["-e", "process.stdout.write('minimal-capture')"],
|
|
}),
|
|
);
|
|
const evidence = await captureProcessScenarioFile(scenario);
|
|
expect(evidence).toMatchObject({
|
|
predicate_type: "rea.process-capture",
|
|
operation: "capture_process_scenario",
|
|
});
|
|
expect(JSON.stringify(evidence)).toContain("minimal-capture");
|
|
});
|
|
|
|
it("rejects unrelated capture evidence", async () => {
|
|
const root = await createTestTempDirectory("rea-process-cli-");
|
|
const invalidCapture = join(root, "invalid-capture.json");
|
|
const unrelated = join(root, "unrelated.json");
|
|
await writeFile(
|
|
invalidCapture,
|
|
JSON.stringify(
|
|
createEvidence(undefined, PROCESS_PROVIDER, {
|
|
predicateType: "rea.process-capture/other",
|
|
operation: "capture_process_scenario",
|
|
parameters: {},
|
|
result: {},
|
|
}),
|
|
),
|
|
);
|
|
await writeFile(
|
|
unrelated,
|
|
JSON.stringify(
|
|
createEvidence(undefined, PROCESS_PROVIDER, {
|
|
predicateType: "unrelated/v1",
|
|
operation: "other",
|
|
parameters: {},
|
|
result: {},
|
|
}),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
await compareProcessEvidenceFiles(invalidCapture, invalidCapture),
|
|
).toMatchObject({
|
|
error: "Process command failed",
|
|
category: "invalid_input",
|
|
});
|
|
const failure = await compareProcessEvidenceFiles(unrelated, unrelated);
|
|
expect(analysisCliErrorEnvelopeSchema.safeParse(failure).success).toBe(
|
|
true,
|
|
);
|
|
expect(failure).toMatchObject({
|
|
error: "Process command failed",
|
|
code: "invalid_request",
|
|
retryable: false,
|
|
category: "invalid_input",
|
|
message:
|
|
"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",
|
|
details: {
|
|
operation: "compare_process_captures",
|
|
issues: [{ path: ["left"], reason: "invalid_value" }],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("classifies malformed capture evidence as invalid input", async () => {
|
|
const root = await createTestTempDirectory("rea-process-cli-");
|
|
const malformed = join(root, "malformed-evidence.json");
|
|
await writeFile(malformed, "{}");
|
|
|
|
expect(
|
|
await compareProcessEvidenceFiles(malformed, malformed),
|
|
).toMatchObject({
|
|
error: "Process command failed",
|
|
category: "invalid_input",
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "compare_process_captures",
|
|
issues: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: ["parameters"],
|
|
reason: "missing_argument",
|
|
}),
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
it(
|
|
"reports the JSON depth constraint through the compiled comparison CLI",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-process-cli-");
|
|
const left = join(root, "left.json");
|
|
const right = join(root, "right.json");
|
|
const input = INVESTIGATION_EXAMPLES.compare_process_captures.input;
|
|
await writeFile(left, JSON.stringify(input.left));
|
|
await writeFile(right, JSON.stringify(input.right));
|
|
const cli = fileURLToPath(
|
|
new URL("../../../scripts/rea.mjs", import.meta.url),
|
|
);
|
|
const cliArguments = [
|
|
cli,
|
|
"compare-process-captures",
|
|
left,
|
|
right,
|
|
"--json",
|
|
];
|
|
const baseline = await execFileAsync(process.execPath, cliArguments);
|
|
expect(JSON.parse(baseline.stdout)).toMatchObject({
|
|
operation: "compare_process_captures",
|
|
});
|
|
const nested = '{"nested":'.repeat(10_000) + "1" + "}".repeat(10_000);
|
|
await writeFile(
|
|
left,
|
|
JSON.stringify({
|
|
...input.left,
|
|
parameters: { attack: "depth-placeholder" },
|
|
}).replace('"depth-placeholder"', nested),
|
|
);
|
|
await expect(
|
|
execFileAsync(process.execPath, cliArguments),
|
|
).rejects.toMatchObject({
|
|
code: 1,
|
|
stdout: expect.stringContaining("maximum nesting depth"),
|
|
});
|
|
},
|
|
CLI_INTEGRATION_TIMEOUT_MS,
|
|
);
|