mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
e9fc8fb30c
* fix(javascript): preserve literal percent runtime paths * fix(javascript): ignore resolve prefixes before the final anchor * fix(electron): retain application roots for loadFile declarations * fix(electron): resolve loadFile from enclosing application package * fix(electron): retain outer application root across package scopes * fix(electron): derive application ownership from inventoried packages * fix(electron): retain resolved package entrypoint ownership * fix(electron): preserve competing application ownership * fix(electron): retain invalid and omitted runtime ownership * fix(electron): preserve omitted module ownership and runtime paths * fix(electron): preserve proven roots before confining page paths * fix(electron): retain possible owners behind missing imports * fix(electron): distinguish absent imports and owned page candidates * fix(electron): keep unavailable JSON out of owner gaps * fix(electron): retain exact loader formats and default entry status * fix(electron): retain unavailable entrypoint ownership candidates * test(ghidra): preserve Windows Node probe prerequisites * test(ghidra): assert cancellation during profile shutdown * fix(electron): separate examined imports from frontend ownership * fix(electron): retain unavailable directory entrypoint owners * test(jeb): retain Linux ownership prerequisite in isolation * fix(javascript): retain unchecked loader ownership and literal main paths * fix(javascript): preserve runtime paths and call-site module aliases * fix(javascript): bound module alias traversal and preserve projections * fix(javascript): retain host module and nested artifact path boundaries * fix(javascript): preserve ESM and archive module loader provenance * test(javascript): distinguish unmapped containers from missing exports * fix: retain default module aliases and Electron builtin imports * fix: preserve host file URL and inward ASAR ownership * fix: preserve CommonJS file URL classification in CI fixtures * fix: retain bound module loaders and Windows entrypoint uncertainty * fix: retain ASAR uncertainty and invalidate changed Electron app roots * fix(javascript): retain ownership uncertainty across loader boundaries * fix(javascript): preserve loader sources and bound alias queries * fix(javascript): retain destructured loader sources * fix(javascript): preserve loader uncertainty at resolution boundaries * fix(javascript): preserve callable and package launch ownership * fix(javascript): preserve captured values across member reads * test(process): assert tagged filesystem cancellation failures * fix(javascript): preserve inventory bases and invocation read timing * fix(javascript): preserve ownership across indirect and unread loads * test(javascript): order fixture directory creation on Windows * test(javascript): share ordered filesystem fixture writer * fix(javascript): unify loader provenance and package validation * fix(javascript): preserve captured Node require loader values * fix(javascript): preserve loader invocation authority * fix(javascript): verify generic loader invocation authority * fix(javascript): preserve conditional loader method alternatives * fix(javascript): require proven factories for source association * fix(javascript): honor replaced Electron app path setters * fix(javascript): prove path and assignment source ownership * fix(javascript): retain native delegate eligibility through shared writes * fix(process): settle stale baseline errors after confirmed absence * fix(javascript): preserve assignment and path boundary evidence * fix(javascript): prove source-directory resolve anchors * fix: preserve application path provenance and portable schemas * fix(javascript): retain CommonJS exported setter effects * fix: isolate Linux identity observations and retain native module exports * fix(javascript): preserve published app and helper provenance * fix(javascript): normalize typed stores and native path fixtures
307 lines
8.7 KiB
TypeScript
307 lines
8.7 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import {
|
|
ProcessCaptureSnapshotFailure,
|
|
snapshotRoots,
|
|
} from "./FilesystemSnapshot.js";
|
|
import { buildCaptureResult } from "./ProcessCaptureLifecycle.js";
|
|
import { isInitializedPtyRoot } from "./ProcessSampling.js";
|
|
import { readLinuxChildren } from "./LinuxProcessSampling.js";
|
|
import { TerminalRenderer } from "./TerminalRenderer.js";
|
|
import { parseProcessScenario } from "../../domain/process/processScenario.js";
|
|
import type { ProcessCapture } from "../../domain/process/processCaptureParsing.js";
|
|
import type { ProcessCaptureTruncationDetails } from "../../domain/process/processCaptureCoverage.js";
|
|
import { emptyProcessCapture as emptyCapture } from "../../domain/process/processCapture.fixture.js";
|
|
|
|
const base = {
|
|
executable: "/bin/sh",
|
|
working_directory: "/tmp",
|
|
};
|
|
|
|
const emptyFilesystemCoverage = () =>
|
|
emptyCapture().truncation_details.filesystem_before;
|
|
|
|
const truncationDetails = (
|
|
frames: readonly ProcessCapture["frames"][number][],
|
|
renderedFrames: readonly ProcessCapture["rendered_frames"][number][],
|
|
samples: readonly ProcessCapture["process_samples"][number][],
|
|
): ProcessCaptureTruncationDetails => {
|
|
const emptyDetails = emptyCapture().truncation_details;
|
|
const rawBytes = frames.reduce(
|
|
(total, frame) => total + Buffer.byteLength(frame.raw_data ?? frame.data),
|
|
0,
|
|
);
|
|
const renderedBytes = renderedFrames.reduce(
|
|
(total, frame) =>
|
|
frame.lines.reduce(
|
|
(sum, line) => sum + Buffer.byteLength(line),
|
|
total + Buffer.byteLength(frame.serialized_state),
|
|
),
|
|
0,
|
|
);
|
|
return {
|
|
raw_terminal: {
|
|
...emptyDetails.raw_terminal,
|
|
observed_bytes: rawBytes,
|
|
retained_bytes: rawBytes,
|
|
observed_frames: frames.length,
|
|
retained_frames: frames.length,
|
|
},
|
|
rendered_terminal: {
|
|
...emptyDetails.rendered_terminal,
|
|
observed_bytes: renderedBytes,
|
|
retained_bytes: renderedBytes,
|
|
observed_frames: renderedFrames.length,
|
|
retained_frames: renderedFrames.length,
|
|
},
|
|
filesystem_before: emptyDetails.filesystem_before,
|
|
filesystem_after: emptyDetails.filesystem_after,
|
|
process: {
|
|
...emptyDetails.process,
|
|
retained_samples: samples.length,
|
|
},
|
|
};
|
|
};
|
|
|
|
it("returns detached terminal observations", async () => {
|
|
const observed: string[] = [];
|
|
const renderer = new TerminalRenderer({
|
|
columns: 40,
|
|
rows: 12,
|
|
scrollback: 100,
|
|
maxBytes: 100_000,
|
|
normalize: (value, maxBytes) => {
|
|
const utf8Bytes = Buffer.byteLength(value);
|
|
return utf8Bytes <= maxBytes
|
|
? { state: "normalized", value, utf8Bytes }
|
|
: { state: "over_budget" };
|
|
},
|
|
recordEvent: (collection, index) =>
|
|
observed.push(`${collection}:${String(index)}`),
|
|
});
|
|
renderer.write("A", 20);
|
|
renderer.resize(40, 12, 10);
|
|
for (let index = 0; index < 20; index += 1)
|
|
renderer.resize(40, 12, 11 + index);
|
|
const frames = await renderer.frames();
|
|
expect(frames).toHaveLength(22);
|
|
expect(frames.slice(0, 2).map(({ at_ms }) => at_ms)).toEqual([20, 10]);
|
|
expect(observed).toHaveLength(22);
|
|
const frame = frames[0];
|
|
if (frame === undefined) throw new Error("expected observed frame");
|
|
const originalCursor = frame.cursor_x;
|
|
Reflect.set(frame, "cursor_x", 999);
|
|
const reread = await renderer.frames();
|
|
expect(reread).toHaveLength(frames.length);
|
|
expect(reread[0]?.cursor_x).toBe(originalCursor);
|
|
await renderer.dispose();
|
|
});
|
|
|
|
it("preserves rendered observation order instead of timestamp sorting", () => {
|
|
const capture = emptyCapture();
|
|
const renderedFrames: ProcessCapture["rendered_frames"] = [
|
|
{
|
|
sequence: 0,
|
|
at_ms: 20,
|
|
columns: 1,
|
|
rows: 1,
|
|
cursor_x: 0,
|
|
cursor_y: 0,
|
|
active_buffer: "normal",
|
|
lines: ["first"],
|
|
serialized_state: "first",
|
|
},
|
|
{
|
|
sequence: 1,
|
|
at_ms: 10,
|
|
columns: 1,
|
|
rows: 1,
|
|
cursor_x: 0,
|
|
cursor_y: 0,
|
|
active_buffer: "normal",
|
|
lines: ["second"],
|
|
serialized_state: "second",
|
|
},
|
|
];
|
|
const result = buildCaptureResult({
|
|
frames: [],
|
|
exit: { exitCode: 0, reason: "exited" },
|
|
samples: [],
|
|
before: {
|
|
files: [],
|
|
truncated: false,
|
|
completeRoots: [],
|
|
coverage: emptyFilesystemCoverage(),
|
|
},
|
|
after: {
|
|
files: [],
|
|
truncated: false,
|
|
completeRoots: [],
|
|
coverage: emptyFilesystemCoverage(),
|
|
},
|
|
truncationDetails: truncationDetails([], renderedFrames, []),
|
|
scenario: parseProcessScenario(base),
|
|
rootPid: 1,
|
|
samplingPartial: false,
|
|
renderedFrames,
|
|
interactions: [],
|
|
checkpoints: capture.filesystem_checkpoints,
|
|
settlement: {
|
|
state: capture.settlement.state,
|
|
elapsed_ms: capture.settlement.elapsed_ms,
|
|
},
|
|
manifest: capture.manifest,
|
|
eventJournal: [],
|
|
});
|
|
|
|
expect(result.rendered_frames).toEqual(renderedFrames);
|
|
});
|
|
|
|
it("marks redacted scripted input as an interaction unknown", () => {
|
|
const capture = emptyCapture();
|
|
const result = buildCaptureResult({
|
|
frames: [],
|
|
exit: { exitCode: 0, reason: "exited" },
|
|
samples: [],
|
|
before: {
|
|
files: [],
|
|
truncated: false,
|
|
completeRoots: [],
|
|
coverage: emptyFilesystemCoverage(),
|
|
},
|
|
after: {
|
|
files: [],
|
|
truncated: false,
|
|
completeRoots: [],
|
|
coverage: emptyFilesystemCoverage(),
|
|
},
|
|
truncationDetails: truncationDetails([], [], []),
|
|
scenario: parseProcessScenario({
|
|
...base,
|
|
events: [{ type: "input", at_ms: 0, data: "secret", sensitive: true }],
|
|
}),
|
|
rootPid: 1,
|
|
samplingPartial: false,
|
|
renderedFrames: [],
|
|
interactions: [
|
|
{
|
|
sequence: 0,
|
|
scheduled_at_ms: 0,
|
|
dispatched_at_ms: 0,
|
|
type: "input",
|
|
data: "<redacted-input:6-bytes>",
|
|
outcome: "dispatched",
|
|
},
|
|
],
|
|
checkpoints: capture.filesystem_checkpoints,
|
|
settlement: {
|
|
state: capture.settlement.state,
|
|
elapsed_ms: capture.settlement.elapsed_ms,
|
|
},
|
|
manifest: capture.manifest,
|
|
eventJournal: [],
|
|
});
|
|
|
|
expect(result.residual_unknowns).toContainEqual({
|
|
scope: "interaction",
|
|
reason:
|
|
"Sensitive scripted input values are redacted from process capture Evidence.",
|
|
});
|
|
});
|
|
|
|
it("collects and deduplicates children from every Linux thread", async () => {
|
|
const signal = new AbortController().signal;
|
|
expect(
|
|
await readLinuxChildren(100, signal, {
|
|
taskIds: () => Promise.resolve([100, 101, 102]),
|
|
children: (_pid, taskId) =>
|
|
Promise.resolve(
|
|
taskId === 100 ? "201 202" : taskId === 101 ? "202 203" : "",
|
|
),
|
|
}),
|
|
).toEqual({
|
|
value: [201, 202, 203],
|
|
diagnostics: { count: 0, first: null, interrupted: false },
|
|
});
|
|
});
|
|
|
|
it("admits PTY samples only after stable session and token setup", () => {
|
|
const initialized = {
|
|
pid: 100,
|
|
parent_pid: 10,
|
|
process_group_id: 100,
|
|
session_id: 100,
|
|
startTime: "200",
|
|
};
|
|
expect(
|
|
isInitializedPtyRoot({
|
|
rootPid: 100,
|
|
expectedRunId: "run-token",
|
|
before: { ...initialized, process_group_id: 10, session_id: 10 },
|
|
observedRunId: undefined,
|
|
after: initialized,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isInitializedPtyRoot({
|
|
rootPid: 100,
|
|
expectedRunId: "run-token",
|
|
before: initialized,
|
|
observedRunId: "run-token",
|
|
after: { ...initialized, startTime: "201" },
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isInitializedPtyRoot({
|
|
rootPid: 100,
|
|
expectedRunId: "run-token",
|
|
before: initialized,
|
|
observedRunId: "run-token",
|
|
after: initialized,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isInitializedPtyRoot({
|
|
rootPid: 100,
|
|
expectedRunId: "run-token",
|
|
before: { ...initialized, session_id: null },
|
|
observedRunId: "run-token",
|
|
after: { ...initialized, session_id: null },
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("cancels filesystem snapshots before traversing declared roots", async () => {
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
const failure: unknown = await snapshotRoots(
|
|
parseProcessScenario({
|
|
...base,
|
|
filesystem_observation_paths: ["/tmp"],
|
|
}),
|
|
controller.signal,
|
|
).catch((cause: unknown) => cause);
|
|
expect(failure).toBeInstanceOf(ProcessCaptureSnapshotFailure);
|
|
expect(failure).toMatchObject({
|
|
_tag: "ProcessCaptureError",
|
|
reason: "cancelled",
|
|
userCategory: "cancelled",
|
|
cause: expect.objectContaining({ name: "AbortError" }),
|
|
snapshot: {
|
|
files: [],
|
|
truncated: true,
|
|
completeRoots: [],
|
|
coverage: {
|
|
hashed_bytes: 0,
|
|
enumeration_omissions: [],
|
|
hash_omissions: [],
|
|
},
|
|
},
|
|
cleanupLeases: [],
|
|
readFailures: [],
|
|
});
|
|
expect(failure instanceof Error ? failure.cause : undefined).toBe(
|
|
controller.signal.reason,
|
|
);
|
|
});
|