Add opt-in Frida session status summaries linked to complete retained Evidence. Share canonical session facts across contracts and preserve observed state, source identity and capture truncation. Validated with native SDK/client workflows, default delivery compatibility, lifecycle and provenance tests. The completed large-history task uses 50.9% fewer estimated request tokens; no general CPU or latency improvement is claimed. Fixes #5640
268 lines
9.5 KiB
JavaScript
268 lines
9.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { createWriteStream } from "node:fs";
|
|
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { pathToFileURL } from "node:url";
|
|
import { finished } from "node:stream/promises";
|
|
import { parseArgs } from "node:util";
|
|
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
|
|
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
|
|
const { values } = parseArgs({
|
|
options: {
|
|
runtime: { type: "string" },
|
|
output: { type: "string" },
|
|
detail: { type: "string", default: "complete" },
|
|
capture: { type: "string", default: "libc" },
|
|
},
|
|
});
|
|
const { runtime, output, detail, capture } = values;
|
|
assert.ok(runtime && output);
|
|
assert.ok(["complete", "summary"].includes(detail));
|
|
assert.ok(["libc", "small"].includes(capture));
|
|
const require = createRequire(join(runtime, "package.json"));
|
|
const { Client } = await import(
|
|
pathToFileURL(require.resolve("@modelcontextprotocol/client"))
|
|
);
|
|
const { StdioClientTransport } = await import(
|
|
pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio"))
|
|
);
|
|
const { parseLinuxProcessStat } = await import(
|
|
pathToFileURL(join(runtime, "dist/process/LinuxProcessStat.js"))
|
|
);
|
|
await mkdir(output, { mode: 0o700 });
|
|
const account = await mkdtemp(join(tmpdir(), "rea-frida-real-"));
|
|
const serverLog = createWriteStream(join(output, "server.log"));
|
|
const serverLogFinished = finished(serverLog);
|
|
serverLogFinished.catch(() => {});
|
|
const client = new Client({ name: "rea-frida-audit", version: "1" });
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [join(runtime, "scripts/rea.mjs"), "mcp"],
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HOME: account,
|
|
NODE_OPTIONS: "--max-old-space-size=2048",
|
|
UV_THREADPOOL_SIZE: "1",
|
|
REA_LOG_LEVEL: "silent",
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
transport.stderr?.pipe(serverLog, { end: false });
|
|
const report = {
|
|
question:
|
|
"Retain a complete caller-requested libc export inventory, then check only session identity/state and message capture coverage twice, preserving complete Evidence.",
|
|
detail,
|
|
capture,
|
|
calls: [],
|
|
status: "pending",
|
|
};
|
|
const transcript = [];
|
|
const caller = createRetainedQueryCaller(client, (receipt, event) => {
|
|
report.calls.push(receipt);
|
|
transcript.push(event);
|
|
});
|
|
const call = async (name, input) => {
|
|
const r = await caller(name, input, name);
|
|
assert.ok(!r.isError, JSON.stringify(r));
|
|
return r.structuredContent;
|
|
};
|
|
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
let session;
|
|
let birth;
|
|
const stat = async () => {
|
|
try {
|
|
return parseLinuxProcessStat(
|
|
await readFile(`/proc/${session.pid}/stat`, "utf8"),
|
|
);
|
|
} catch (e) {
|
|
if (e.code === "ENOENT") return null;
|
|
throw e;
|
|
}
|
|
};
|
|
try {
|
|
await client.connect(transport);
|
|
const catalog = await client.listTools();
|
|
await writeFile(join(output, "catalog.json"), JSON.stringify(catalog));
|
|
report.catalog_tools = catalog.tools.length;
|
|
const defs = new Map(catalog.tools.map((t) => [t.name, t]));
|
|
const orig = client.callTool.bind(client);
|
|
client.callTool = (input, options) =>
|
|
orig(input, {
|
|
...options,
|
|
timeout: 30000,
|
|
toolDefinition: defs.get(input.name),
|
|
});
|
|
report.devices = await call("list_frida_devices", {});
|
|
session = await call("start_frida_session", {
|
|
mode: "spawn",
|
|
device_id: "local",
|
|
program: "/usr/bin/sleep",
|
|
argv: ["8"],
|
|
});
|
|
report.session = session;
|
|
birth = await stat();
|
|
assert.ok(birth);
|
|
report.target_birth = birth;
|
|
report.same_owned_cgroup =
|
|
(await readFile(`/proc/${session.pid}/cgroup`, "utf8")) ===
|
|
(await readFile("/proc/self/cgroup", "utf8"));
|
|
assert.ok(report.same_owned_cgroup);
|
|
const source = `send({kind:'ready',pid:Process.id});
|
|
setTimeout(() => {
|
|
if (${JSON.stringify(capture)} === 'small') { send({kind:'complete',total:0}); return; }
|
|
const module = Process.enumerateModules().find(m => /(^|\\/)libc\\.so\\.6$/.test(m.path));
|
|
if (!module) { send({kind:'missing-libc'}); return; }
|
|
const exports = module.enumerateExports().map(e => ({type:e.type,name:e.name,address:e.address.toString()}));
|
|
send({kind:'module',pid:Process.id,path:module.path,base:module.base.toString(),size:module.size,total:exports.length});
|
|
for (let offset = 0; offset < exports.length; offset += 64) send({kind:'exports',offset,items:exports.slice(offset,offset+64)});
|
|
send({kind:'complete',total:exports.length});
|
|
}, 300);`;
|
|
await writeFile(join(output, "selected-script.js"), source);
|
|
report.source_sha256 = createHash("sha256").update(source).digest("hex");
|
|
const loaded = await call("load_frida_script", {
|
|
session_id: session.session_id,
|
|
source_kind: "inline",
|
|
source,
|
|
});
|
|
assert.equal(loaded.normalized_result.source_sha256, report.source_sha256);
|
|
report.load = loaded.normalized_result;
|
|
await call("resume_frida_session", { session_id: session.session_id });
|
|
await delay(750);
|
|
const snapshots = [];
|
|
for (let n = 0; n < 2; n++)
|
|
snapshots.push(
|
|
await call("frida_session_status", {
|
|
session_id: session.session_id,
|
|
...(detail === "summary" ? { detail } : {}),
|
|
}),
|
|
);
|
|
await call("export_evidence_bundle", { path: join(output, "evidence.json") });
|
|
const bundle = JSON.parse(
|
|
await readFile(join(output, "evidence.json"), "utf8"),
|
|
);
|
|
for (const snapshot of snapshots) {
|
|
assert.equal(snapshot.raw_result, null);
|
|
const parent =
|
|
detail === "summary"
|
|
? bundle.records.find(
|
|
(e) =>
|
|
e.evidence_id === snapshot.normalized_result.parent_evidence_id,
|
|
)
|
|
: snapshot;
|
|
assert.ok(parent);
|
|
assert.equal(parent.authority, "external-service");
|
|
assert.equal(parent.environment, null);
|
|
assert.equal(parent.raw_result, null);
|
|
assert.equal(parent.operation, "frida_session_status");
|
|
assert.deepEqual(parent.parameters, { session_id: session.session_id });
|
|
const r = parent.normalized_result;
|
|
assert.equal(r.pid, session.pid);
|
|
assert.equal(r.state, "running");
|
|
assert.equal(r.messages_truncated, false);
|
|
const payloads = r.messages
|
|
.filter((m) => m.type === "send")
|
|
.map((m) => m.payload);
|
|
const ready = payloads.find((p) => p?.kind === "ready");
|
|
const complete = payloads.find((p) => p?.kind === "complete");
|
|
assert.equal(ready?.pid, session.pid);
|
|
assert.ok(complete);
|
|
let count = 0;
|
|
let header;
|
|
if (capture === "libc") {
|
|
header = payloads.find((p) => p?.kind === "module");
|
|
assert.ok(header);
|
|
assert.equal(header.pid, session.pid);
|
|
for (const chunk of payloads.filter((p) => p?.kind === "exports")) {
|
|
assert.equal(chunk.offset, count);
|
|
count += chunk.items.length;
|
|
}
|
|
assert.equal(count, header.total);
|
|
assert.ok(count > 1000);
|
|
const maps = await readFile(`/proc/${session.pid}/maps`, "utf8");
|
|
assert.ok(maps.includes(header.path));
|
|
}
|
|
assert.equal(count, complete.total);
|
|
if (detail === "summary") {
|
|
const view = snapshot.normalized_result;
|
|
assert.equal(view.parent_operation, parent.operation);
|
|
assert.equal(view.parent_digest, parent.evidence_id.slice(3));
|
|
assert.equal(view.artifact, null);
|
|
assert.deepEqual(view.source_parameters, parent.parameters);
|
|
assert.deepEqual(view.source_provider, parent.provider);
|
|
const { messages, ...metadata } = r;
|
|
assert.deepEqual(view.summary, {
|
|
...metadata,
|
|
messages_retained: messages.length,
|
|
});
|
|
assert.ok(view.unknowns.length > 0);
|
|
assert.ok(view.limitations.some((m) => m.includes("operating system")));
|
|
assert.deepEqual(
|
|
bundle.records.find((e) => e.evidence_id === snapshot.evidence_id),
|
|
snapshot,
|
|
);
|
|
}
|
|
report.answer = {
|
|
pid: r.pid,
|
|
state: r.state,
|
|
scripts: r.scripts.length,
|
|
messages: r.messages.length,
|
|
messages_truncated: r.messages_truncated,
|
|
exports: count,
|
|
module_path: header?.path ?? null,
|
|
};
|
|
}
|
|
report.native_oracle =
|
|
"Caller-authored script digest and contiguous chunk coverage; PID/birthtime/cgroup and libc path independently checked in procfs. Export addresses and native export completeness not independently qualified.";
|
|
report.exported_bundle_bytes = Buffer.byteLength(
|
|
await readFile(join(output, "evidence.json")),
|
|
);
|
|
await call("close_frida_session", { session_id: session.session_id });
|
|
session.closed = true;
|
|
const deadline = performance.now() + 12000;
|
|
while (true) {
|
|
const current = await stat();
|
|
if (
|
|
current === null ||
|
|
current.startTime !== birth.startTime ||
|
|
["zombie", "dead"].includes(current.lifecycle)
|
|
) {
|
|
report.original_target_exited = true;
|
|
break;
|
|
}
|
|
assert.ok(
|
|
performance.now() < deadline,
|
|
"Original target exit not observed",
|
|
);
|
|
await delay(50);
|
|
}
|
|
report.status = "passed";
|
|
} catch (error) {
|
|
report.status = "failed";
|
|
report.failure = String(error?.stack ?? error);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (session && !session.closed)
|
|
try {
|
|
await call("close_frida_session", { session_id: session.session_id });
|
|
} catch (error) {
|
|
report.session_cleanup_failure = String(error);
|
|
process.exitCode = 1;
|
|
}
|
|
if (
|
|
!(await finalizeRetainedQueryReceipt({
|
|
report,
|
|
client,
|
|
transport,
|
|
account,
|
|
serverLog,
|
|
serverLogFinished,
|
|
output,
|
|
transcript,
|
|
}))
|
|
)
|
|
process.exitCode = 1;
|
|
}
|