mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 09:55:46 +00:00
Preserve uncertainty in offline passive network comparisons when request identity, metadata, or per-kind inventory coverage is incomplete. Retain proven HTTP/WebSocket changes without using missing information to infer membership, duplicate counts, or equality. Compare redirect resource types only where both observations retain them. Keep metadata completeness with its owner, stream HTTP content digests, and require full coverage for multiplicity and WebSocket stream inventories. Preserve independently known metadata changes and full-inventory counts. Keep the original JavaScript configuration-escape, JSON, cancellation, SAZ and schema-documentation fixes compatible with current main. Split public CLI comparisons into isolated test cases without loosening timeouts. Validate actual Chromium/HTTP captures through CLI and real stdio SDK, with explicit limits and unchanged source bytes. Co-authored-by: To3akaRin <131595336+To3akaRin@users.noreply.github.com> Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
272 lines
8.0 KiB
TypeScript
272 lines
8.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
|
|
describe("JavaScript semantic analysis: native bindings 1", () => {
|
|
it("recovers child-process argv, env, stdio, listeners, and signals", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
import { spawn as launch } from "node:child_process";
|
|
function run() {
|
|
const child = launch(
|
|
"/bin/tool",
|
|
["--mode", "fast"],
|
|
{ env: { MODE: "test" }, stdio: ["ignore", "pipe", "pipe"] },
|
|
);
|
|
child.on("exit", onExit);
|
|
child.once("error", onError);
|
|
child.kill("SIGINT");
|
|
}
|
|
`);
|
|
|
|
expect(ir.childProcessSpawns).toEqual([
|
|
expect.objectContaining({
|
|
method: "spawn",
|
|
command: "/bin/tool",
|
|
argvCount: 2,
|
|
environmentSupplied: true,
|
|
stdioMode: "array",
|
|
resolution: "complete",
|
|
}),
|
|
]);
|
|
expect(
|
|
ir.childProcessInteractions.map(
|
|
({ kind, eventName, signalName, resolution }) => ({
|
|
kind,
|
|
eventName,
|
|
signalName,
|
|
resolution,
|
|
}),
|
|
),
|
|
).toEqual([
|
|
{
|
|
kind: "listener",
|
|
eventName: "exit",
|
|
signalName: null,
|
|
resolution: "complete",
|
|
},
|
|
{
|
|
kind: "listener",
|
|
eventName: "error",
|
|
signalName: null,
|
|
resolution: "complete",
|
|
},
|
|
{
|
|
kind: "signal",
|
|
eventName: null,
|
|
signalName: "SIGINT",
|
|
resolution: "complete",
|
|
},
|
|
]);
|
|
expect(ir.functionFingerprints[0]?.components.effects).toContain(
|
|
"child-process",
|
|
);
|
|
});
|
|
|
|
it("recovers configuration, requests, response consumers, and boundaries", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
import { readFileSync } from "node:fs";
|
|
import * as http from "node:http";
|
|
async function run(schema) {
|
|
const endpoint = process.env.API_URL ?? "https://fallback.test";
|
|
const mode = process.argv[2];
|
|
const raw = readFileSync("./config.json", "utf8");
|
|
const parsed = JSON.parse(raw);
|
|
const port = Number(process.env.PORT);
|
|
const validated = schema.parse(parsed);
|
|
const response = await fetch(endpoint, {
|
|
method: "POST",
|
|
body: validated,
|
|
});
|
|
const body = await response.json();
|
|
http.request("https://audit.test", { body });
|
|
return { mode, port, body };
|
|
}
|
|
`);
|
|
|
|
expect(
|
|
ir.configurationOperations.map(({ kind, key }) => ({ kind, key })),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
{ kind: "environment", key: "API_URL" },
|
|
{ kind: "default", key: "API_URL" },
|
|
{ kind: "argv", key: "2" },
|
|
{ kind: "file", key: "./config.json" },
|
|
{ kind: "environment", key: "PORT" },
|
|
]),
|
|
);
|
|
expect(
|
|
ir.requestOperations.map(({ kind, method, endpoint, resolution }) => ({
|
|
kind,
|
|
method,
|
|
endpoint,
|
|
resolution,
|
|
})),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
kind: "request",
|
|
method: "fetch",
|
|
endpoint: null,
|
|
resolution: "partial",
|
|
},
|
|
{
|
|
kind: "response-consumer",
|
|
method: "json",
|
|
endpoint: null,
|
|
resolution: "complete",
|
|
},
|
|
{
|
|
kind: "request",
|
|
method: "request",
|
|
endpoint: "https://audit.test",
|
|
resolution: "complete",
|
|
},
|
|
]),
|
|
);
|
|
expect(
|
|
ir.boundaryOperations.map(({ kind, method, resolution }) => ({
|
|
kind,
|
|
method,
|
|
resolution,
|
|
})),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
{ kind: "parse", method: "JSON.parse", resolution: "complete" },
|
|
{ kind: "coerce", method: "Number", resolution: "complete" },
|
|
{ kind: "parse", method: "parse", resolution: "partial" },
|
|
]),
|
|
);
|
|
expect(ir.functionFingerprints[0]?.components.effects).toContain("network");
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic analysis: native bindings 2", () => {
|
|
it("recovers built-in resource acquisition and exact local release", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
import { open } from "node:fs/promises";
|
|
import { connect as dial } from "node:net";
|
|
async function run(path) {
|
|
const file = await open(path);
|
|
const socket = dial({ port: 9000 });
|
|
await file.close();
|
|
socket.destroy();
|
|
}
|
|
`);
|
|
|
|
expect(
|
|
ir.resourceOperations.map(({ kind, method, resolution }) => ({
|
|
kind,
|
|
method,
|
|
resolution,
|
|
})),
|
|
).toEqual([
|
|
{ kind: "acquire", method: "open", resolution: "complete" },
|
|
{ kind: "acquire", method: "connect", resolution: "complete" },
|
|
{ kind: "release", method: "close", resolution: "complete" },
|
|
{ kind: "release", method: "destroy", resolution: "complete" },
|
|
]);
|
|
expect(ir.functionFingerprints[0]?.components.effects).toContain(
|
|
"resource",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic analysis: child-process signals", () => {
|
|
it("keeps unresolved kill signals distinct from default and literal signals", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
import { spawn } from "node:child_process";
|
|
function run(signal) {
|
|
const child = spawn("/bin/tool");
|
|
child.kill(signal);
|
|
child.kill();
|
|
child.kill("SIGINT");
|
|
child.kill(undefined);
|
|
child.kill(undefined as NodeJS.Signals | undefined);
|
|
child.kill(void 0);
|
|
child.kill(void signal);
|
|
}
|
|
function shadowed(undefined) {
|
|
const child = spawn("/bin/tool");
|
|
child.kill(undefined);
|
|
}
|
|
`);
|
|
|
|
expect(ir.childProcessSpawns).toHaveLength(2);
|
|
expect(
|
|
ir.childProcessInteractions.map(({ signalName }) => signalName),
|
|
).toEqual([
|
|
null,
|
|
"SIGTERM",
|
|
"SIGINT",
|
|
"SIGTERM",
|
|
"SIGTERM",
|
|
"SIGTERM",
|
|
"SIGTERM",
|
|
null,
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic analysis: configuration value escapes", () => {
|
|
const configurationReads = (body: string) =>
|
|
analyzeJavaScriptSemantics(`function run() { ${body} }`)
|
|
.configurationOperations.filter(({ kind }) => kind !== "default")
|
|
.map(({ kind, key }) => [kind, key]);
|
|
|
|
it.each([
|
|
[
|
|
"an environment value",
|
|
"return Number(process.env.PORT);",
|
|
"environment",
|
|
"PORT",
|
|
],
|
|
["an argv entry", "return Number(process.argv[2]);", "argv", "2"],
|
|
])("keeps a read after passing %s to a call", (_name, body, kind, key) => {
|
|
expect(configurationReads(body)).toEqual([[kind, key]]);
|
|
});
|
|
|
|
it.each([
|
|
"patch(process.argv[key]); return process.argv[3];",
|
|
"patch(process.argv.__proto__); return process.argv[3];",
|
|
"const args = process.argv; patch(args[key]); return args[3];",
|
|
])(
|
|
"retains uncertainty after an object-valued argv selection escapes: %s",
|
|
(body) => {
|
|
expect(configurationReads(body)).not.toContainEqual(["argv", "3"]);
|
|
},
|
|
);
|
|
|
|
it("preserves an earlier top-level read and unrelated configuration after an argv escape", () => {
|
|
const captured = analyzeJavaScriptSemantics(
|
|
"const saved = process.argv[3]; patch(process.argv[key]); globalThis.saved = saved;",
|
|
);
|
|
expect(
|
|
captured.configurationOperations.map(({ kind, key }) => [kind, key]),
|
|
).toContainEqual(["argv", "3"]);
|
|
expect(
|
|
configurationReads("patch(process.argv[key]); return process.env.PORT;"),
|
|
).toContainEqual(["environment", "PORT"]);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"a direct argv entry",
|
|
"process.argv[2] = {}; return Number(process.argv[2]);",
|
|
],
|
|
[
|
|
"an aliased argv entry",
|
|
"const argv = process.argv; argv[2] = {}; return Number(process.argv[2]);",
|
|
],
|
|
[
|
|
"the argv container",
|
|
"mutate(process.argv); return Number(process.argv[2]);",
|
|
],
|
|
[
|
|
"the env container",
|
|
"mutate(process.env); return Number(process.env.PORT);",
|
|
],
|
|
])("leaves a read unknown after replacing %s", (_name, body) => {
|
|
expect(configurationReads(body)).toEqual([]);
|
|
});
|
|
});
|