197 lines
6.8 KiB
TypeScript
197 lines
6.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { expect, it } from "vitest";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
|
|
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
|
|
const createPcapToolFixture = async (root: string) => {
|
|
const path = join(root, "caller capture.pcap");
|
|
const bytes = Buffer.from("recording fixture bytes");
|
|
const sha = createHash("sha256").update(bytes).digest("hex");
|
|
await writeFile(path, bytes);
|
|
const tshark = join(root, "tshark");
|
|
const capinfos = join(root, "capinfos");
|
|
const marker = join(root, "reads");
|
|
const frames = Array.from({ length: 132 }, (_, index) => ({
|
|
_source: {
|
|
layers: {
|
|
"frame.number": [String(index + 1)],
|
|
"frame.protocols": ["ip:udp:dns"],
|
|
"ip.src": ["192.0.2.1"],
|
|
"ip.dst": ["192.0.2.2"],
|
|
"dns.qry.name": ["many.example"],
|
|
"dns.flags.response": ["True"],
|
|
"dns.a": [`198.51.100.${index}`],
|
|
"tls.handshake.extensions_server_name": [`host${index}.example`],
|
|
"http.request.full_uri": [
|
|
"http://user:password@example.invalid/p?q=keep",
|
|
],
|
|
},
|
|
},
|
|
}));
|
|
await writeFile(
|
|
tshark,
|
|
`#!/bin/sh\nif [ "$1" = "--version" ]; then printf 'TShark (Wireshark) 4.6.6-emulated\\n'; exit 0; fi\nprintf 'read\\n' >> ${quote(marker)}\nprintf '%s' ${quote(JSON.stringify(frames))}\nprintf 'retained warning' >&2\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
await writeFile(
|
|
capinfos,
|
|
`#!/bin/sh\nif [ "$1" = "--version" ]; then printf 'Capinfos (Wireshark) 4.6.6-emulated\\n'; exit 0; fi\nprintf '%s\\n' ${quote(`File type: pcap\nFile encapsulation: Ethernet\nNumber of packets: 132\nSHA256: ${sha}`)}\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const environment = {
|
|
REA_TSHARK_COMMAND: tshark,
|
|
REA_CAPINFOS_COMMAND: capinfos,
|
|
};
|
|
return { path, marker, environment };
|
|
};
|
|
|
|
// Native tools are emulated here; this proves the production CLI/MCP boundary,
|
|
// retention and paging, not Wireshark's real dissector semantics.
|
|
it.runIf(process.platform === "linux")(
|
|
"retains every fact and pages late DNS/SNI rows after source deletion without another tool launch",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-pcap-view-");
|
|
const { path, marker, environment } = await createPcapToolFixture(root);
|
|
const complete = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
join(import.meta.dirname, "../../../scripts/rea.mjs"),
|
|
"inspect-pcap-capture",
|
|
path,
|
|
"--json",
|
|
],
|
|
{ env: { ...process.env, ...environment } },
|
|
);
|
|
const parent = parseEvidence(JSON.parse(complete.stdout));
|
|
expect(parent.normalized_result).toMatchObject({
|
|
tls_sni_count: 132,
|
|
tls_sni_values: expect.arrayContaining(["host131.example"]),
|
|
dns_queries: [{ query: "many.example", response_address_count: 132 }],
|
|
});
|
|
expect(JSON.stringify(parent)).not.toContain("user:password");
|
|
const { call } = await connectLocalToolsMcp(environment);
|
|
const response = await call("inspect_pcap_capture", {
|
|
path,
|
|
detail: "summary",
|
|
});
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
const summary = parseEvidence(response.structuredContent).normalized_result;
|
|
expect(summary).toMatchObject({
|
|
parent_evidence_id: parent.evidence_id,
|
|
summary: {
|
|
tls_sni_count: 132,
|
|
dns_query_count: 1,
|
|
retained_counts: { tls_sni_values: 132 },
|
|
diagnostics_code_units: { tshark: 16, capinfos: 0 },
|
|
},
|
|
});
|
|
expect(JSON.stringify(summary)).not.toContain("host131.example");
|
|
await rm(path);
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: parent.evidence_id,
|
|
};
|
|
const before = await readFile(marker, "utf8");
|
|
const cases = [
|
|
[
|
|
{ kind: "pcap", facet: "endpoints", limit: 1 },
|
|
{ items: [{ address: "192.0.2.1", packet_count: 132 }] },
|
|
],
|
|
[
|
|
{ kind: "pcap", facet: "dns-queries", limit: 1 },
|
|
{
|
|
items: [
|
|
{ query: "many.example", retained_response_address_count: 132 },
|
|
],
|
|
},
|
|
],
|
|
[
|
|
{
|
|
kind: "pcap",
|
|
facet: "dns-addresses",
|
|
query: "many.example",
|
|
offset: 130,
|
|
limit: 2,
|
|
},
|
|
{ items: ["198.51.100.130", "198.51.100.131"] },
|
|
],
|
|
[
|
|
{ kind: "pcap", facet: "tls-sni", offset: 130, limit: 2 },
|
|
{ items: ["host130.example", "host131.example"] },
|
|
],
|
|
[
|
|
{ kind: "pcap", facet: "protocols" },
|
|
{ items: [{ protocol: "ip:udp:dns", packet_count: 132 }] },
|
|
],
|
|
[
|
|
{ kind: "pcap", facet: "http-uris" },
|
|
{ items: ["http://example.invalid/p?q=keep"] },
|
|
],
|
|
[{ kind: "pcap", facet: "unattributed-addresses" }, { items: [] }],
|
|
[
|
|
{
|
|
kind: "pcap",
|
|
facet: "diagnostics",
|
|
stream: "tshark",
|
|
offset: 9,
|
|
limit: 7,
|
|
},
|
|
{ text: "warning", status: "retained" },
|
|
],
|
|
] as const;
|
|
for (const [view, expected] of cases) {
|
|
const response = await call("inspect_analysis_view", { source, view });
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
expect(
|
|
parseEvidence(response.structuredContent).normalized_result,
|
|
).toMatchObject({ page: expected, source_coverage: "complete" });
|
|
}
|
|
const workflow = join(root, "portable.json");
|
|
const view = {
|
|
kind: "pcap",
|
|
facet: "dns-addresses",
|
|
query: "many.example",
|
|
offset: 130,
|
|
limit: 2,
|
|
};
|
|
await writeFile(
|
|
workflow,
|
|
JSON.stringify({ source: { kind: "inline", evidence: parent }, view }),
|
|
);
|
|
const portable = await promisify(execFile)(process.execPath, [
|
|
join(import.meta.dirname, "../../../scripts/rea.mjs"),
|
|
"inspect-analysis-view",
|
|
workflow,
|
|
"--json",
|
|
]);
|
|
const retained = parseEvidence(
|
|
(await call("inspect_analysis_view", { source, view })).structuredContent,
|
|
);
|
|
expect(
|
|
parseEvidence(JSON.parse(portable.stdout)).normalized_result,
|
|
).toEqual(retained.normalized_result);
|
|
expect(await readFile(marker, "utf8")).toBe(before);
|
|
expect(
|
|
parseMcpToolError(
|
|
await call("inspect_analysis_view", {
|
|
source,
|
|
view: {
|
|
kind: "pcap",
|
|
facet: "dns-addresses",
|
|
query: "missing.example",
|
|
},
|
|
}),
|
|
),
|
|
).toMatchObject({ error: { code: "invalid_request" } });
|
|
},
|
|
60_000,
|
|
);
|