Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2cffbdfdc | ||
|
|
5e046bb374 | ||
|
|
4924706e25 | ||
|
|
211d4b2219 | ||
|
|
cb6dc4f3ef | ||
|
|
658c29b7fc | ||
|
|
a22adc7809 | ||
|
|
e74e4d8f0b | ||
|
|
f4eeba3654 | ||
|
|
bbecf6fb05 |
No files matched your search
@@ -10,10 +10,17 @@ Supply an existing TShark 4.x installation **on Linux**, with util-linux
|
|||||||
`prlimit` available at `/usr/bin/prlimit`. Override its absolute path with
|
`prlimit` available at `/usr/bin/prlimit`. Override its absolute path with
|
||||||
`REA_TSHARK_PRLIMIT_COMMAND` if required. Unsupported hosts, including macOS
|
`REA_TSHARK_PRLIMIT_COMMAND` if required. Unsupported hosts, including macOS
|
||||||
and Windows, are refused before inspection because this integration requires
|
and Windows, are refused before inspection because this integration requires
|
||||||
an enforced address-space limit:
|
an enforced address-space limit.
|
||||||
|
|
||||||
|
TShark selection follows the same rule as [`inspect_pcap_capture`](pcap-analysis.md):
|
||||||
|
an absolute `REA_TSHARK_COMMAND`, otherwise `tshark` from the first absolute
|
||||||
|
`PATH` entry that holds an executable file. A relative `REA_TSHARK_COMMAND` is
|
||||||
|
refused rather than looked up. Either way the selected executable is resolved,
|
||||||
|
hashed and rechecked after each run, and Evidence records its
|
||||||
|
`selection_source` (`environment` or `path`):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export REA_TSHARK_COMMAND=/absolute/path/to/tshark
|
export REA_TSHARK_COMMAND=/absolute/path/to/tshark # optional when tshark is on PATH
|
||||||
rea inspect-packet-capture /artifacts/device.pcapng \
|
rea inspect-packet-capture /artifacts/device.pcapng \
|
||||||
--fields ip.addr --fields udp.port --fields data.data \
|
--fields ip.addr --fields udp.port --fields data.data \
|
||||||
--display_filter 'udp' --format json
|
--display_filter 'udp' --format json
|
||||||
|
|||||||
+10
-1
@@ -17,7 +17,16 @@ rea inspect-pcap-capture /captures/session.pcapng --json
|
|||||||
```
|
```
|
||||||
|
|
||||||
Without explicit tool commands, this family resolves them through PATH and
|
Without explicit tool commands, this family resolves them through PATH and
|
||||||
records that selection source. Client inspection reads no capture file. Capture
|
records that selection source. On Linux and macOS it runs the tool found in the
|
||||||
|
first absolute PATH entry holding an executable file, the same selection the
|
||||||
|
[packet capture tools](packet-captures.md) make; relative PATH entries are
|
||||||
|
skipped. Windows keeps PATH/PATHEXT resolution in the process launcher. Client
|
||||||
|
inspection reads no capture file; on hosts where PATH/PATHEXT does not expose
|
||||||
|
an absolute candidate, its executable digest and resolved path are reported as
|
||||||
|
unresolved. Otherwise the canonical executable path and SHA-256 are reported.
|
||||||
|
The pcap provider and packet tools share the selected-tool identity check, and
|
||||||
|
changes to a resolved executable during a run fail with artifact-changed
|
||||||
|
Evidence. Capture
|
||||||
inspection requires Linux with util-linux prlimit or Windows with owned Job
|
inspection requires Linux with util-linux prlimit or Windows with owned Job
|
||||||
Objects. macOS capture inspection returns a capability gap because the new
|
Objects. macOS capture inspection returns a capability gap because the new
|
||||||
native memory boundary is unverified; its version probes remain available.
|
native memory boundary is unverified; its version probes remain available.
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ const workflowAvailabilityFor = ({
|
|||||||
: {
|
: {
|
||||||
reason: "provider_missing",
|
reason: "provider_missing",
|
||||||
remediation:
|
remediation:
|
||||||
"On Linux, supply TShark 4.x via absolute REA_TSHARK_COMMAND and util-linux prlimit (REA_TSHARK_PRLIMIT_COMMAND, default /usr/bin/prlimit). No active binary target or hardware is required; the tool is checked at execution.",
|
"On Linux, supply TShark 4.x on PATH or through absolute REA_TSHARK_COMMAND, and util-linux prlimit (REA_TSHARK_PRLIMIT_COMMAND, default /usr/bin/prlimit). No active binary target or hardware is required; the tool is checked at execution.",
|
||||||
};
|
};
|
||||||
if (name === "decode_signal_recording" || name === "inspect_signal_decoders")
|
if (name === "decode_signal_recording" || name === "inspect_signal_decoders")
|
||||||
return policy.signalRecordingEnabled === true
|
return policy.signalRecordingEnabled === true
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ import {
|
|||||||
OwnedCommandFailure,
|
OwnedCommandFailure,
|
||||||
} from "../process/OwnedCommand.js";
|
} from "../process/OwnedCommand.js";
|
||||||
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
||||||
|
import {
|
||||||
|
TSHARK_COMMAND_ENV,
|
||||||
|
assertWiresharkExecutableIdentity,
|
||||||
|
wiresharkExecutableIdentity,
|
||||||
|
locateWiresharkTool,
|
||||||
|
parseWiresharkVersion,
|
||||||
|
selectWiresharkTool,
|
||||||
|
} from "../pcap/PcapConfiguration.js";
|
||||||
|
|
||||||
const OPERATION = "inspect_packet_capture";
|
const OPERATION = "inspect_packet_capture";
|
||||||
export const TSHARK_PROVIDER_IDENTITY = {
|
export const TSHARK_PROVIDER_IDENTITY = {
|
||||||
@@ -139,18 +147,22 @@ export class TsharkCaptureProvider implements PacketCapturePort {
|
|||||||
await this.#resources.release();
|
await this.#resources.release();
|
||||||
if (this.platform !== "linux")
|
if (this.platform !== "linux")
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Packet inspection requires Linux with util-linux prlimit and REA_TSHARK_COMMAND so expanded captures are processed under an enforced address-space limit.",
|
"Packet inspection requires Linux with util-linux prlimit and TShark 4.x (on PATH or through absolute REA_TSHARK_COMMAND) so expanded captures are processed under an enforced address-space limit.",
|
||||||
|
);
|
||||||
|
const selection = selectWiresharkTool(this.environment, "tshark");
|
||||||
|
const configured = locateWiresharkTool(selection, this.environment);
|
||||||
|
if (configured === undefined)
|
||||||
|
throw new Error(
|
||||||
|
`TShark 4.x was not found on PATH; add it to PATH or set absolute ${TSHARK_COMMAND_ENV}.`,
|
||||||
);
|
);
|
||||||
const configured = this.environment.REA_TSHARK_COMMAND ?? "";
|
|
||||||
if (!isAbsolute(configured))
|
|
||||||
throw new Error("Supply absolute REA_TSHARK_COMMAND for TShark 4.x.");
|
|
||||||
const command = await realpath(configured);
|
const command = await realpath(configured);
|
||||||
await access(command, constants.X_OK);
|
await access(command, constants.X_OK);
|
||||||
const tool = await this.#resources.read(
|
const toolSnapshot = await this.#resources.read(
|
||||||
command,
|
command,
|
||||||
64 * 1024 * 1024,
|
64 * 1024 * 1024,
|
||||||
options?.signal,
|
options?.signal,
|
||||||
);
|
);
|
||||||
|
const tool = wiresharkExecutableIdentity(toolSnapshot);
|
||||||
const configuredLimiter =
|
const configuredLimiter =
|
||||||
this.environment.REA_TSHARK_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
|
this.environment.REA_TSHARK_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
|
||||||
if (!isAbsolute(configuredLimiter))
|
if (!isAbsolute(configuredLimiter))
|
||||||
@@ -220,13 +232,13 @@ export class TsharkCaptureProvider implements PacketCapturePort {
|
|||||||
"Configured resource limiter changed during analysis.",
|
"Configured resource limiter changed during analysis.",
|
||||||
{ capturedOutput },
|
{ capturedOutput },
|
||||||
);
|
);
|
||||||
if (current.sha256 !== tool.sha256)
|
assertWiresharkExecutableIdentity(
|
||||||
throw new AnalysisArtifactChangedError(
|
operation,
|
||||||
operation,
|
command,
|
||||||
command,
|
tool,
|
||||||
"Configured TShark executable changed during analysis.",
|
wiresharkExecutableIdentity(current),
|
||||||
{ capturedOutput },
|
{ capturedOutput },
|
||||||
);
|
);
|
||||||
phase = previousPhase;
|
phase = previousPhase;
|
||||||
resourcePath = "path" in input ? input.path : command;
|
resourcePath = "path" in input ? input.path : command;
|
||||||
if (output.exitCode !== 0)
|
if (output.exitCode !== 0)
|
||||||
@@ -245,11 +257,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
|
|||||||
};
|
};
|
||||||
phase = "configuration";
|
phase = "configuration";
|
||||||
const version = await execute(["--version"]);
|
const version = await execute(["--version"]);
|
||||||
if (
|
if (parseWiresharkVersion(version.stdout.text, "tshark") === null)
|
||||||
!/^TShark \(Wireshark\) 4\.[0-9]+\.[0-9]+(?:[ .(\r\n]|$)/u.test(
|
|
||||||
version.stdout.text,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Expected TShark 4.x; inspect the selected installation.",
|
"Expected TShark 4.x; inspect the selected installation.",
|
||||||
);
|
);
|
||||||
@@ -257,6 +265,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
|
|||||||
diagnostics,
|
diagnostics,
|
||||||
tool: {
|
tool: {
|
||||||
configured_path: configured,
|
configured_path: configured,
|
||||||
|
selection_source: selection.source,
|
||||||
path: command,
|
path: command,
|
||||||
sha256: tool.sha256,
|
sha256: tool.sha256,
|
||||||
version_banner: version.stdout.text,
|
version_banner: version.stdout.text,
|
||||||
@@ -553,7 +562,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
|
|||||||
...(observedOutput === undefined
|
...(observedOutput === undefined
|
||||||
? {}
|
? {}
|
||||||
: { capturedOutput: observedOutput }),
|
: { capturedOutput: observedOutput }),
|
||||||
userMessage: `${reason} On Linux, configure TShark 4.x through REA_TSHARK_COMMAND and util-linux prlimit through REA_TSHARK_PRLIMIT_COMMAND (default /usr/bin/prlimit).`,
|
userMessage: `${reason} On Linux, supply TShark 4.x on PATH or through absolute REA_TSHARK_COMMAND, and util-linux prlimit through REA_TSHARK_PRLIMIT_COMMAND (default /usr/bin/prlimit).`,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const CAPTURE_TOOL_CONTRACTS = [
|
|||||||
...toolContractMetadata("inspect_packet_fields"),
|
...toolContractMetadata("inspect_packet_fields"),
|
||||||
kind: "artifact-provider",
|
kind: "artifact-provider",
|
||||||
description:
|
description:
|
||||||
"Discover protocols or field names/types registered in the installed TShark 4.x. kind defaults to protocols; use fields and optional exact protocol abbreviation to select field definitions. query searches name/abbreviation/description by case-insensitive literal substring. Return 50 matches by default with offset/limit paging; null requests all remaining within resource limits. Preserve type/base/bitmask/description, duplicate registrations, source line numbers, coverage, tool identity and diagnostics. Linux, REA_TSHARK_COMMAND and prlimit required. No capture file, interface or binary target needed. Registrations describe tool capabilities, not observed traffic. Each call rereads the glossary; use selected fields with inspect_packet_capture.",
|
"Discover protocols or field names/types registered in the installed TShark 4.x. kind defaults to protocols; use fields and optional exact protocol abbreviation to select field definitions. query searches name/abbreviation/description by case-insensitive literal substring. Return 50 matches by default with offset/limit paging; null requests all remaining within resource limits. Preserve type/base/bitmask/description, duplicate registrations, source line numbers, coverage, tool identity and diagnostics. Linux, TShark 4.x (PATH or absolute REA_TSHARK_COMMAND) and prlimit required. No capture file, interface or binary target needed. Registrations describe tool capabilities, not observed traffic. Each call rereads the glossary; use selected fields with inspect_packet_capture.",
|
||||||
inputSchema: inspectPacketFieldsInputSchema,
|
inputSchema: inspectPacketFieldsInputSchema,
|
||||||
outputSchema: evidenceResultOf(packetFieldsCatalogSchema),
|
outputSchema: evidenceResultOf(packetFieldsCatalogSchema),
|
||||||
examples: [
|
examples: [
|
||||||
@@ -31,7 +31,7 @@ export const CAPTURE_TOOL_CONTRACTS = [
|
|||||||
...toolContractMetadata("inspect_packet_capture"),
|
...toolContractMetadata("inspect_packet_capture"),
|
||||||
kind: "artifact-provider",
|
kind: "artifact-provider",
|
||||||
description:
|
description:
|
||||||
"Inspect caller-selected fields in an existing local packet capture with TShark 4.x. Return 50 matching frames by default, with match_offset/match_limit paging and coverage counts; match_limit:null requests all remaining matches. Preserve repeated values, frame numbers, display filter, decode-as rules and pass mode in inline Evidence with source/tool identity and diagnostics. Optional packet_limit counts input packets before filtering; output paging does not shorten dissection or two-pass reassembly. Requires Linux, util-linux prlimit, and absolute REA_TSHARK_COMMAND. File-only analysis; no live capture or device communication. Missing fields or no matches do not establish absence of device traffic.",
|
"Inspect caller-selected fields in an existing local packet capture with TShark 4.x. Return 50 matching frames by default, with match_offset/match_limit paging and coverage counts; match_limit:null requests all remaining matches. Preserve repeated values, frame numbers, display filter, decode-as rules and pass mode in inline Evidence with source/tool identity and diagnostics. Optional packet_limit counts input packets before filtering; output paging does not shorten dissection or two-pass reassembly. Requires Linux, util-linux prlimit, and TShark 4.x on PATH or through absolute REA_TSHARK_COMMAND. File-only analysis; no live capture or device communication. Missing fields or no matches do not establish absence of device traffic.",
|
||||||
inputSchema: inspectPacketCaptureInputSchema,
|
inputSchema: inspectPacketCaptureInputSchema,
|
||||||
outputSchema: evidenceResultOf(packetCaptureSchema),
|
outputSchema: evidenceResultOf(packetCaptureSchema),
|
||||||
examples: [
|
examples: [
|
||||||
|
|||||||
@@ -74,6 +74,23 @@ const diagnostics = (offset: number, limit: number) =>
|
|||||||
limit,
|
limit,
|
||||||
}) as const;
|
}) as const;
|
||||||
|
|
||||||
|
it("reads historical capture Evidence without inventing executable identity", () => {
|
||||||
|
const result = projectAnalysisView(capture(""), { kind: "summary" });
|
||||||
|
if (!result.ok) throw result.error;
|
||||||
|
if (result.value.kind !== "summary")
|
||||||
|
throw new Error("Expected a packet summary.");
|
||||||
|
if (!("client" in result.value.summary))
|
||||||
|
throw new Error("Expected packet client facts.");
|
||||||
|
expect(result.value.summary.client).toMatchObject({
|
||||||
|
tshark_resolved_path: null,
|
||||||
|
tshark_sha256: null,
|
||||||
|
tshark_identity_status: "unresolved",
|
||||||
|
capinfos_resolved_path: null,
|
||||||
|
capinfos_sha256: null,
|
||||||
|
capinfos_identity_status: "unresolved",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("pages diagnostics without splitting a surrogate pair", () => {
|
it("pages diagnostics without splitting a surrogate pair", () => {
|
||||||
const parent = capture("ab😀cd");
|
const parent = capture("ab😀cd");
|
||||||
expect(projectAnalysisView(parent, diagnostics(3, 2))).toMatchObject({
|
expect(projectAnalysisView(parent, diagnostics(3, 2))).toMatchObject({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { pcapResultSchemas } from "../pcap/pcapAnalysis.js";
|
import { pcapEvidenceSchemas } from "../pcap/pcapAnalysis.js";
|
||||||
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
|
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
|
||||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||||
import { err, ok, type Result } from "../result.js";
|
import { err, ok, type Result } from "../result.js";
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "./analysisViewCoverage.js";
|
} from "./analysisViewCoverage.js";
|
||||||
import { analysisTextPage } from "./analysisViewText.js";
|
import { analysisTextPage } from "./analysisViewText.js";
|
||||||
|
|
||||||
const captureSchema = pcapResultSchemas.inspect_pcap_capture;
|
const captureSchema = pcapEvidenceSchemas.inspect_pcap_capture;
|
||||||
const fields = captureSchema.shape;
|
const fields = captureSchema.shape;
|
||||||
const pageShape = {
|
const pageShape = {
|
||||||
kind: z.literal("pcap"),
|
kind: z.literal("pcap"),
|
||||||
|
|||||||
@@ -123,6 +123,11 @@ export const packetCaptureSchema = z
|
|||||||
diagnostics: z.string(),
|
diagnostics: z.string(),
|
||||||
tool: z.strictObject({
|
tool: z.strictObject({
|
||||||
configured_path: z.string(),
|
configured_path: z.string(),
|
||||||
|
selection_source: z
|
||||||
|
.enum(["environment", "path"])
|
||||||
|
.describe(
|
||||||
|
"Whether REA_TSHARK_COMMAND selected the tool or PATH located it",
|
||||||
|
),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
||||||
version_banner: z.string(),
|
version_banner: z.string(),
|
||||||
|
|||||||
@@ -41,9 +41,29 @@ const client = z.strictObject({
|
|||||||
tshark_command: z.string().min(1),
|
tshark_command: z.string().min(1),
|
||||||
tshark_source: z.enum(["environment", "path"]),
|
tshark_source: z.enum(["environment", "path"]),
|
||||||
tshark_version: z.string().min(1).nullable(),
|
tshark_version: z.string().min(1).nullable(),
|
||||||
|
tshark_resolved_path: localPathStringSchema.nullable(),
|
||||||
|
tshark_sha256: digestSchema.nullable(),
|
||||||
|
tshark_identity_status: z.enum(["verified", "unresolved"]),
|
||||||
capinfos_command: z.string().min(1),
|
capinfos_command: z.string().min(1),
|
||||||
capinfos_source: z.enum(["environment", "path"]),
|
capinfos_source: z.enum(["environment", "path"]),
|
||||||
capinfos_version: z.string().min(1).nullable(),
|
capinfos_version: z.string().min(1).nullable(),
|
||||||
|
capinfos_resolved_path: localPathStringSchema.nullable(),
|
||||||
|
capinfos_sha256: digestSchema.nullable(),
|
||||||
|
capinfos_identity_status: z.enum(["verified", "unresolved"]),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Defaults for historical report Evidence written before executable identity was recorded. */
|
||||||
|
const legacyClientIdentity = client.extend({
|
||||||
|
tshark_resolved_path: localPathStringSchema.nullable().default(null),
|
||||||
|
tshark_sha256: digestSchema.nullable().default(null),
|
||||||
|
tshark_identity_status: z
|
||||||
|
.enum(["verified", "unresolved"])
|
||||||
|
.default("unresolved"),
|
||||||
|
capinfos_resolved_path: localPathStringSchema.nullable().default(null),
|
||||||
|
capinfos_sha256: digestSchema.nullable().default(null),
|
||||||
|
capinfos_identity_status: z
|
||||||
|
.enum(["verified", "unresolved"])
|
||||||
|
.default("unresolved"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const endpoint = z.strictObject({
|
const endpoint = z.strictObject({
|
||||||
@@ -108,3 +128,10 @@ export const pcapResultSchemas = {
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/** Read compatibility for retained capture Evidence from earlier REA versions. */
|
||||||
|
export const pcapEvidenceSchemas = {
|
||||||
|
inspect_pcap_capture: pcapResultSchemas.inspect_pcap_capture.extend({
|
||||||
|
client: legacyClientIdentity,
|
||||||
|
}),
|
||||||
|
} as const;
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
||||||
|
import { join, relative } from "node:path";
|
||||||
|
|
||||||
|
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||||
import {
|
import {
|
||||||
CAPINFOS_COMMAND_ENV,
|
CAPINFOS_COMMAND_ENV,
|
||||||
TSHARK_COMMAND_ENV,
|
TSHARK_COMMAND_ENV,
|
||||||
|
locateWiresharkTool,
|
||||||
|
parseWiresharkVersion,
|
||||||
resolvePcapTools,
|
resolvePcapTools,
|
||||||
|
selectWiresharkTool,
|
||||||
|
wiresharkExecutableIdentity,
|
||||||
} from "./PcapConfiguration.js";
|
} from "./PcapConfiguration.js";
|
||||||
import { parseCapinfosOutput, parseTsharkJsonOutput } from "./PcapOutput.js";
|
import { parseCapinfosOutput, parseTsharkJsonOutput } from "./PcapOutput.js";
|
||||||
|
|
||||||
@@ -20,13 +28,51 @@ describe("resolvePcapTools", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to PATH resolution without the environment keys", () => {
|
it("keeps bare PATH selections for the Windows PATH/PATHEXT launcher", () => {
|
||||||
expect(resolvePcapTools({})).toEqual({
|
expect(resolvePcapTools({}, "win32")).toEqual({
|
||||||
tshark: { command: "tshark", source: "path" },
|
tshark: { command: "tshark", source: "path" },
|
||||||
capinfos: { command: "capinfos", source: "path" },
|
capinfos: { command: "capinfos", source: "path" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails closed when no absolute PATH entry holds the tool", () => {
|
||||||
|
expect(() => resolvePcapTools({ PATH: "relative-only" }, "linux")).toThrow(
|
||||||
|
/tshark was not found in an absolute PATH entry/u,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Windows X_OK reports existence only, so executable-bit selection is POSIX-only.
|
||||||
|
it.skipIf(process.platform === "win32")(
|
||||||
|
"selects the same executable as the packet tools ahead of a relative PATH entry",
|
||||||
|
async () => {
|
||||||
|
const root = await createTestTempDirectory("rea-wireshark-shared-");
|
||||||
|
const decoy = join(root, "relative-decoy");
|
||||||
|
const installed = join(root, "installed");
|
||||||
|
for (const directory of [decoy, installed]) {
|
||||||
|
await mkdir(directory);
|
||||||
|
for (const tool of ["tshark", "capinfos"]) {
|
||||||
|
await writeFile(join(directory, tool), "");
|
||||||
|
await chmod(join(directory, tool), 0o700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const environment = {
|
||||||
|
PATH: [relative(process.cwd(), decoy), installed].join(":"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const pcap = resolvePcapTools(environment, "linux");
|
||||||
|
expect(pcap).toEqual({
|
||||||
|
tshark: { command: join(installed, "tshark"), source: "path" },
|
||||||
|
capinfos: { command: join(installed, "capinfos"), source: "path" },
|
||||||
|
});
|
||||||
|
expect(pcap.tshark.command).toBe(
|
||||||
|
locateWiresharkTool(
|
||||||
|
selectWiresharkTool(environment, "tshark"),
|
||||||
|
environment,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("rejects a relative tshark path explicitly", () => {
|
it("rejects a relative tshark path explicitly", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
resolvePcapTools({ [TSHARK_COMMAND_ENV]: "bin/tshark" }),
|
resolvePcapTools({ [TSHARK_COMMAND_ENV]: "bin/tshark" }),
|
||||||
@@ -40,6 +86,88 @@ describe("resolvePcapTools", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("locateWiresharkTool", () => {
|
||||||
|
it("keeps an environment selection as the configured absolute path", () => {
|
||||||
|
expect(
|
||||||
|
locateWiresharkTool(
|
||||||
|
{ command: "/opt/wireshark/tshark", source: "environment" },
|
||||||
|
{ PATH: "/usr/bin" },
|
||||||
|
),
|
||||||
|
).toBe("/opt/wireshark/tshark");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Windows X_OK reports existence only, so executable-bit selection is POSIX-only.
|
||||||
|
it.skipIf(process.platform === "win32")(
|
||||||
|
"uses the first absolute PATH entry holding an executable regular file",
|
||||||
|
async () => {
|
||||||
|
const root = await createTestTempDirectory("rea-wireshark-path-");
|
||||||
|
const relative = join(root, "relative");
|
||||||
|
const notExecutable = join(root, "not-executable");
|
||||||
|
const directoryEntry = join(root, "directory-entry");
|
||||||
|
const selected = join(root, "selected");
|
||||||
|
const later = join(root, "later");
|
||||||
|
for (const directory of [notExecutable, directoryEntry, selected, later])
|
||||||
|
await mkdir(directory);
|
||||||
|
await writeFile(join(notExecutable, "tshark"), "");
|
||||||
|
await chmod(join(notExecutable, "tshark"), 0o600);
|
||||||
|
await mkdir(join(directoryEntry, "tshark"));
|
||||||
|
for (const directory of [selected, later]) {
|
||||||
|
await writeFile(join(directory, "tshark"), "");
|
||||||
|
await chmod(join(directory, "tshark"), 0o700);
|
||||||
|
}
|
||||||
|
const PATH = [
|
||||||
|
"relative-entry",
|
||||||
|
relative,
|
||||||
|
notExecutable,
|
||||||
|
directoryEntry,
|
||||||
|
selected,
|
||||||
|
later,
|
||||||
|
].join(":");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
locateWiresharkTool({ command: "tshark", source: "path" }, { PATH }),
|
||||||
|
).toBe(join(selected, "tshark"));
|
||||||
|
expect(
|
||||||
|
locateWiresharkTool(
|
||||||
|
{ command: "tshark", source: "path" },
|
||||||
|
{ PATH: [relative, notExecutable, directoryEntry].join(":") },
|
||||||
|
),
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
locateWiresharkTool({ command: "tshark", source: "path" }, {}),
|
||||||
|
).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseWiresharkVersion", () => {
|
||||||
|
it("admits Wireshark 4.x banners for either tool and rejects other majors", () => {
|
||||||
|
expect(
|
||||||
|
parseWiresharkVersion(
|
||||||
|
"\nTShark (Wireshark) 4.6.9 (Git v4.6.9 packaged as 4.6.9-1)\n",
|
||||||
|
"tshark",
|
||||||
|
),
|
||||||
|
).toBe("TShark (Wireshark) 4.6.9 (Git v4.6.9 packaged as 4.6.9-1)");
|
||||||
|
expect(
|
||||||
|
parseWiresharkVersion("Capinfos (Wireshark) 4.0.17\n", "capinfos"),
|
||||||
|
).toBe("Capinfos (Wireshark) 4.0.17");
|
||||||
|
expect(parseWiresharkVersion("tcpdump 4.99.4\n", "tshark")).toBeNull();
|
||||||
|
expect(() =>
|
||||||
|
parseWiresharkVersion("TShark (Wireshark) 3.6.2\n", "tshark"),
|
||||||
|
).toThrow(/requires Wireshark 4\.x/u);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects executable snapshots to compact identity facts", () => {
|
||||||
|
const bytes = new Uint8Array(64);
|
||||||
|
const identity = wiresharkExecutableIdentity({
|
||||||
|
bytes,
|
||||||
|
sha256: "a".repeat(64),
|
||||||
|
});
|
||||||
|
expect(identity).toEqual({ bytes: 64, sha256: "a".repeat(64) });
|
||||||
|
expect(Object.values(identity)).not.toContain(bytes);
|
||||||
|
});
|
||||||
|
|
||||||
const REAL_CAPINFOS = [
|
const REAL_CAPINFOS = [
|
||||||
"File name: /work/rich-capture.pcap",
|
"File name: /work/rich-capture.pcap",
|
||||||
"File type: Wireshark/tcpdump/... - pcap",
|
"File type: Wireshark/tcpdump/... - pcap",
|
||||||
|
|||||||
+130
-56
@@ -1,10 +1,43 @@
|
|||||||
import { isAbsolute } from "node:path";
|
import { accessSync, constants, statSync } from "node:fs";
|
||||||
|
import { delimiter, isAbsolute, join } from "node:path";
|
||||||
|
|
||||||
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
|
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
|
||||||
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
|
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
|
||||||
import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js";
|
import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js";
|
||||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||||
import type { JsonValue } from "../domain/jsonValue.js";
|
import type { JsonValue } from "../domain/jsonValue.js";
|
||||||
|
import { AnalysisArtifactChangedError } from "../domain/analysisErrorCore.js";
|
||||||
|
|
||||||
|
export interface WiresharkExecutableIdentity {
|
||||||
|
readonly bytes: number;
|
||||||
|
readonly sha256: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Retain only identity facts after a bounded executable read. */
|
||||||
|
export const wiresharkExecutableIdentity = (snapshot: {
|
||||||
|
readonly bytes: Uint8Array;
|
||||||
|
readonly sha256: string;
|
||||||
|
}): WiresharkExecutableIdentity => ({
|
||||||
|
bytes: snapshot.bytes.byteLength,
|
||||||
|
sha256: snapshot.sha256,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Shared identity owner used to verify each caller-selected tool run. */
|
||||||
|
export const assertWiresharkExecutableIdentity = (
|
||||||
|
operation: string,
|
||||||
|
path: string,
|
||||||
|
expected: WiresharkExecutableIdentity,
|
||||||
|
actual: WiresharkExecutableIdentity,
|
||||||
|
options?: AnalysisErrorOptions,
|
||||||
|
): void => {
|
||||||
|
if (actual.bytes !== expected.bytes || actual.sha256 !== expected.sha256)
|
||||||
|
throw new AnalysisArtifactChangedError(
|
||||||
|
operation,
|
||||||
|
path,
|
||||||
|
"The selected Wireshark executable changed during analysis.",
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/** Environment keys selecting the caller's tshark and capinfos binaries. */
|
/** Environment keys selecting the caller's tshark and capinfos binaries. */
|
||||||
export const TSHARK_COMMAND_ENV = "REA_TSHARK_COMMAND";
|
export const TSHARK_COMMAND_ENV = "REA_TSHARK_COMMAND";
|
||||||
@@ -27,64 +60,99 @@ export const pcapToolUnavailable = (
|
|||||||
userMessage: `${/[.!?]$/u.test(reason) ? reason : `${reason}.`} ${TOOL_REMEDIATION}`,
|
userMessage: `${/[.!?]$/u.test(reason) ? reason : `${reason}.`} ${TOOL_REMEDIATION}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** How each tool was selected for this provider instance. */
|
/** How one caller-supplied Wireshark tool was selected. */
|
||||||
export interface PcapToolSelection {
|
export interface WiresharkToolSelection {
|
||||||
readonly tshark: { command: string; source: "environment" | "path" };
|
readonly command: string;
|
||||||
readonly capinfos: { command: string; source: "environment" | "path" };
|
readonly source: "environment" | "path";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How each tool was selected for this provider instance. */
|
||||||
|
export interface PcapToolSelection {
|
||||||
|
readonly tshark: WiresharkToolSelection;
|
||||||
|
readonly capinfos: WiresharkToolSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOL_COMMAND_ENV = {
|
||||||
|
tshark: TSHARK_COMMAND_ENV,
|
||||||
|
capinfos: CAPINFOS_COMMAND_ENV,
|
||||||
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the caller-supplied tshark and capinfos binaries. Each accepts an
|
* The one selection rule for a caller-supplied Wireshark tool: an absolute
|
||||||
* absolute environment path or falls back to PATH resolution, mirroring the
|
* environment path, otherwise the bare tool name resolved through PATH. A
|
||||||
* mitmdump pattern. REA never installs the Wireshark suite.
|
* relative environment value is configuration to fix, never a PATH lookup.
|
||||||
|
* REA never installs the Wireshark suite.
|
||||||
|
*/
|
||||||
|
export const selectWiresharkTool = (
|
||||||
|
environment: Readonly<Record<string, string | undefined>>,
|
||||||
|
tool: "tshark" | "capinfos",
|
||||||
|
): WiresharkToolSelection => {
|
||||||
|
const key = TOOL_COMMAND_ENV[tool];
|
||||||
|
const configured = environment[key];
|
||||||
|
if (configured === undefined || configured.trim() === "")
|
||||||
|
return { command: tool, source: "path" };
|
||||||
|
if (!isAbsolute(configured))
|
||||||
|
throw new PcapConfigurationFailure(
|
||||||
|
`${tool}_not_absolute`,
|
||||||
|
`${key} must be an absolute path to the ${tool} binary`,
|
||||||
|
{ configured },
|
||||||
|
);
|
||||||
|
return { command: configured, source: "environment" };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the caller-supplied tshark and capinfos binaries to the executables
|
||||||
|
* the packet tools would also select. Windows keeps PATH/PATHEXT resolution in
|
||||||
|
* the launcher, which the POSIX locator does not model.
|
||||||
*/
|
*/
|
||||||
export const resolvePcapTools = (
|
export const resolvePcapTools = (
|
||||||
environment: Readonly<Record<string, string | undefined>>,
|
environment: Readonly<Record<string, string | undefined>>,
|
||||||
|
platform: NodeJS.Platform = process.platform,
|
||||||
): PcapToolSelection => {
|
): PcapToolSelection => {
|
||||||
const tsharkEnv = environment[TSHARK_COMMAND_ENV];
|
// Report every configuration error before consulting PATH for either tool.
|
||||||
const capinfosEnv = environment[CAPINFOS_COMMAND_ENV];
|
const selected = {
|
||||||
if (
|
tshark: selectWiresharkTool(environment, "tshark"),
|
||||||
tsharkEnv !== undefined &&
|
capinfos: selectWiresharkTool(environment, "capinfos"),
|
||||||
tsharkEnv.trim() !== "" &&
|
|
||||||
!isAbsolute(tsharkEnv)
|
|
||||||
)
|
|
||||||
throw new PcapConfigurationFailure(
|
|
||||||
"tshark_not_absolute",
|
|
||||||
`${TSHARK_COMMAND_ENV} must be an absolute path to the tshark binary`,
|
|
||||||
{ configured: tsharkEnv },
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
capinfosEnv !== undefined &&
|
|
||||||
capinfosEnv.trim() !== "" &&
|
|
||||||
!isAbsolute(capinfosEnv)
|
|
||||||
)
|
|
||||||
throw new PcapConfigurationFailure(
|
|
||||||
"capinfos_not_absolute",
|
|
||||||
`${CAPINFOS_COMMAND_ENV} must be an absolute path to the capinfos binary`,
|
|
||||||
{ configured: capinfosEnv },
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
tshark: {
|
|
||||||
command:
|
|
||||||
tsharkEnv === undefined || tsharkEnv.trim() === ""
|
|
||||||
? "tshark"
|
|
||||||
: tsharkEnv,
|
|
||||||
source:
|
|
||||||
tsharkEnv === undefined || tsharkEnv.trim() === ""
|
|
||||||
? "path"
|
|
||||||
: "environment",
|
|
||||||
},
|
|
||||||
capinfos: {
|
|
||||||
command:
|
|
||||||
capinfosEnv === undefined || capinfosEnv.trim() === ""
|
|
||||||
? "capinfos"
|
|
||||||
: capinfosEnv,
|
|
||||||
source:
|
|
||||||
capinfosEnv === undefined || capinfosEnv.trim() === ""
|
|
||||||
? "path"
|
|
||||||
: "environment",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
const locate = (tool: "tshark" | "capinfos"): WiresharkToolSelection => {
|
||||||
|
const selection = selected[tool];
|
||||||
|
if (selection.source === "environment" || platform === "win32")
|
||||||
|
return selection;
|
||||||
|
const command = locateWiresharkTool(selection, environment);
|
||||||
|
if (command === undefined)
|
||||||
|
throw new PcapConfigurationFailure(
|
||||||
|
`${tool}_missing`,
|
||||||
|
`${tool} was not found in an absolute PATH entry; add it to PATH or set ${TOOL_COMMAND_ENV[tool]} to its absolute path`,
|
||||||
|
);
|
||||||
|
return { command, source: "path" };
|
||||||
|
};
|
||||||
|
return { tshark: locate("tshark"), capinfos: locate("capinfos") };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate a selected tool as an absolute executable so its identity can be
|
||||||
|
* hashed. Environment selections are already absolute. PATH selections use the
|
||||||
|
* first absolute PATH entry holding an executable regular file; relative PATH
|
||||||
|
* entries are skipped because they would depend on the working directory.
|
||||||
|
* Returns undefined when PATH holds no such executable.
|
||||||
|
*/
|
||||||
|
export const locateWiresharkTool = (
|
||||||
|
selection: WiresharkToolSelection,
|
||||||
|
environment: Readonly<Record<string, string | undefined>>,
|
||||||
|
): string | undefined => {
|
||||||
|
if (selection.source === "environment" || isAbsolute(selection.command))
|
||||||
|
return selection.command;
|
||||||
|
for (const directory of (environment.PATH ?? "").split(delimiter)) {
|
||||||
|
if (!isAbsolute(directory)) continue;
|
||||||
|
const candidate = join(directory, selection.command);
|
||||||
|
try {
|
||||||
|
accessSync(candidate, constants.X_OK);
|
||||||
|
if (statSync(candidate).isFile()) return candidate;
|
||||||
|
} catch {
|
||||||
|
// Absent or non-executable candidates leave later PATH entries eligible.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PcapConfigurationFailureCode =
|
export type PcapConfigurationFailureCode =
|
||||||
@@ -108,8 +176,11 @@ export class PcapConfigurationFailure extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Recognize the selected Wireshark tool's version banner. */
|
/**
|
||||||
const parseVersion = (
|
* Recognize the selected Wireshark tool's version banner: null when the first
|
||||||
|
* line is not a Wireshark banner, a failure when its major version is not 4.
|
||||||
|
*/
|
||||||
|
export const parseWiresharkVersion = (
|
||||||
stdout: string,
|
stdout: string,
|
||||||
tool: "tshark" | "capinfos",
|
tool: "tshark" | "capinfos",
|
||||||
): string | null => {
|
): string | null => {
|
||||||
@@ -127,7 +198,7 @@ const parseVersion = (
|
|||||||
if (parsed[1] !== "4")
|
if (parsed[1] !== "4")
|
||||||
throw new PcapConfigurationFailure(
|
throw new PcapConfigurationFailure(
|
||||||
`${tool}_version_unsupported`,
|
`${tool}_version_unsupported`,
|
||||||
"Packet summary inspection currently requires Wireshark 4.x tools; select an installation with the admitted output profile.",
|
"Packet capture inspection currently requires Wireshark 4.x tools; select an installation with the admitted output profile.",
|
||||||
{ version: line },
|
{ version: line },
|
||||||
);
|
);
|
||||||
return line;
|
return line;
|
||||||
@@ -143,8 +214,11 @@ export const inspectPcapClient = async (
|
|||||||
selection: PcapToolSelection,
|
selection: PcapToolSelection,
|
||||||
probe: (command: string) => Promise<string>,
|
probe: (command: string) => Promise<string>,
|
||||||
): Promise<PcapClientIdentity> => ({
|
): Promise<PcapClientIdentity> => ({
|
||||||
tsharkVersion: parseVersion(await probe(selection.tshark.command), "tshark"),
|
tsharkVersion: parseWiresharkVersion(
|
||||||
capinfosVersion: parseVersion(
|
await probe(selection.tshark.command),
|
||||||
|
"tshark",
|
||||||
|
),
|
||||||
|
capinfosVersion: parseWiresharkVersion(
|
||||||
await probe(selection.capinfos.command),
|
await probe(selection.capinfos.command),
|
||||||
"capinfos",
|
"capinfos",
|
||||||
),
|
),
|
||||||
|
|||||||
+144
-20
@@ -23,11 +23,15 @@ import { err, ok, type Result } from "../domain/result.js";
|
|||||||
import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
|
import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
|
||||||
import {
|
import {
|
||||||
PcapConfigurationFailure,
|
PcapConfigurationFailure,
|
||||||
|
assertWiresharkExecutableIdentity,
|
||||||
inspectPcapClient,
|
inspectPcapClient,
|
||||||
|
locateWiresharkTool,
|
||||||
resolvePcapTools,
|
resolvePcapTools,
|
||||||
pcapConfigurationAvailability,
|
pcapConfigurationAvailability,
|
||||||
pcapToolUnavailable,
|
pcapToolUnavailable,
|
||||||
|
wiresharkExecutableIdentity,
|
||||||
type PcapToolSelection,
|
type PcapToolSelection,
|
||||||
|
type WiresharkExecutableIdentity,
|
||||||
} from "./PcapConfiguration.js";
|
} from "./PcapConfiguration.js";
|
||||||
import {
|
import {
|
||||||
parseCapinfosOutput,
|
parseCapinfosOutput,
|
||||||
@@ -46,8 +50,11 @@ import { createPcapEvidence } from "../domain/pcap/pcapEvidence.js";
|
|||||||
import { preparePcapNativeBound } from "./PcapNativeBound.js";
|
import { preparePcapNativeBound } from "./PcapNativeBound.js";
|
||||||
import { PcapSnapshot } from "./PcapSnapshot.js";
|
import { PcapSnapshot } from "./PcapSnapshot.js";
|
||||||
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
||||||
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||||
|
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
|
||||||
import { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
|
import { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
|
||||||
import { readLinuxResourceLimits } from "../process/LinuxResourceLimits.js";
|
import { readLinuxResourceLimits } from "../process/LinuxResourceLimits.js";
|
||||||
|
import { realpath } from "node:fs/promises";
|
||||||
|
|
||||||
export const PCAP_PROVIDER_IDENTITY = {
|
export const PCAP_PROVIDER_IDENTITY = {
|
||||||
id: "pcap",
|
id: "pcap",
|
||||||
@@ -128,6 +135,7 @@ const pcapCommandCleanupError = (
|
|||||||
export interface PcapProviderOptions {
|
export interface PcapProviderOptions {
|
||||||
readonly environment?: Readonly<Record<string, string | undefined>>;
|
readonly environment?: Readonly<Record<string, string | undefined>>;
|
||||||
readonly launcher?: PcapCommandLauncher;
|
readonly launcher?: PcapCommandLauncher;
|
||||||
|
readonly artifactReader?: typeof readStableArtifact;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RetainedPcapCommand {
|
interface RetainedPcapCommand {
|
||||||
@@ -148,18 +156,13 @@ interface RetainedSnapshotFailure {
|
|||||||
|
|
||||||
/** Inspect saved captures through bounded, owned tool processes and one stable input copy. */
|
/** Inspect saved captures through bounded, owned tool processes and one stable input copy. */
|
||||||
export class PcapProvider {
|
export class PcapProvider {
|
||||||
readonly #resources = new ArtifactAnalysisResources(
|
readonly #resources: ArtifactAnalysisResources;
|
||||||
"pcap",
|
|
||||||
"inspect_pcap_capture",
|
|
||||||
readStableArtifact,
|
|
||||||
readLinuxResourceLimits,
|
|
||||||
{
|
|
||||||
message: "A packet summary is already active; retry after it completes.",
|
|
||||||
details: { max_concurrent_inspections: 1 },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
private readonly environment: Readonly<Record<string, string | undefined>>;
|
private readonly environment: Readonly<Record<string, string | undefined>>;
|
||||||
private readonly launcher: PcapCommandLauncher | undefined;
|
private readonly launcher: PcapCommandLauncher | undefined;
|
||||||
|
private readonly wiresharkIdentities = new Map<
|
||||||
|
string,
|
||||||
|
{ readonly path: string; readonly identity: WiresharkExecutableIdentity }
|
||||||
|
>();
|
||||||
private readonly snapshots = new Set<PcapSnapshot>();
|
private readonly snapshots = new Set<PcapSnapshot>();
|
||||||
private readonly snapshotFailures = new Map<
|
private readonly snapshotFailures = new Map<
|
||||||
PcapSnapshot,
|
PcapSnapshot,
|
||||||
@@ -173,6 +176,17 @@ export class PcapProvider {
|
|||||||
constructor(options: PcapProviderOptions = {}) {
|
constructor(options: PcapProviderOptions = {}) {
|
||||||
this.environment = options.environment ?? process.env;
|
this.environment = options.environment ?? process.env;
|
||||||
this.launcher = options.launcher;
|
this.launcher = options.launcher;
|
||||||
|
this.#resources = new ArtifactAnalysisResources(
|
||||||
|
"pcap",
|
||||||
|
"inspect_pcap_capture",
|
||||||
|
options.artifactReader ?? readStableArtifact,
|
||||||
|
readLinuxResourceLimits,
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"A packet summary is already active; retry after it completes.",
|
||||||
|
details: { max_concurrent_inspections: 1 },
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async run(
|
private async run(
|
||||||
@@ -187,10 +201,24 @@ export class PcapProvider {
|
|||||||
let cleanup: PcapCommandCleanup | undefined;
|
let cleanup: PcapCommandCleanup | undefined;
|
||||||
let retainedCommand: RetainedPcapCommand | undefined;
|
let retainedCommand: RetainedPcapCommand | undefined;
|
||||||
let output: Awaited<ReturnType<typeof runPcapCommand>>;
|
let output: Awaited<ReturnType<typeof runPcapCommand>>;
|
||||||
|
const selected = this.wiresharkIdentities.get(command);
|
||||||
try {
|
try {
|
||||||
|
if (selected !== undefined) {
|
||||||
|
const before = await this.#resources.read(
|
||||||
|
selected.path,
|
||||||
|
64 * 1024 * 1024,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
assertWiresharkExecutableIdentity(
|
||||||
|
operation,
|
||||||
|
selected.path,
|
||||||
|
selected.identity,
|
||||||
|
wiresharkExecutableIdentity(before),
|
||||||
|
);
|
||||||
|
}
|
||||||
output = await runPcapCommand({
|
output = await runPcapCommand({
|
||||||
operation,
|
operation,
|
||||||
command,
|
command: selected?.path ?? command,
|
||||||
arguments: args,
|
arguments: args,
|
||||||
limits,
|
limits,
|
||||||
signal,
|
signal,
|
||||||
@@ -219,6 +247,25 @@ export class PcapProvider {
|
|||||||
cleanup = { reason, resources, capturedOutput };
|
cleanup = { reason, resources, capturedOutput };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (selected !== undefined) {
|
||||||
|
const after = await this.#resources.read(
|
||||||
|
selected.path,
|
||||||
|
64 * 1024 * 1024,
|
||||||
|
);
|
||||||
|
assertWiresharkExecutableIdentity(
|
||||||
|
operation,
|
||||||
|
selected.path,
|
||||||
|
selected.identity,
|
||||||
|
wiresharkExecutableIdentity(after),
|
||||||
|
{
|
||||||
|
capturedOutput: {
|
||||||
|
stdout: output.stdout,
|
||||||
|
stderr: output.stderr,
|
||||||
|
truncated: output.cleanup?.capturedOutput.truncated ?? false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
if (
|
if (
|
||||||
retainedCommand !== undefined &&
|
retainedCommand !== undefined &&
|
||||||
@@ -263,6 +310,40 @@ export class PcapProvider {
|
|||||||
|
|
||||||
private async client(operation: PcapOperation, signal?: AbortSignal) {
|
private async client(operation: PcapOperation, signal?: AbortSignal) {
|
||||||
const selection = resolvePcapTools(this.environment);
|
const selection = resolvePcapTools(this.environment);
|
||||||
|
this.wiresharkIdentities.clear();
|
||||||
|
for (const [name, selected] of [
|
||||||
|
["tshark", selection.tshark],
|
||||||
|
["capinfos", selection.capinfos],
|
||||||
|
] as const) {
|
||||||
|
const path = locateWiresharkTool(selected, this.environment);
|
||||||
|
if (path === undefined) continue;
|
||||||
|
let resolvedPath: string;
|
||||||
|
try {
|
||||||
|
resolvedPath = await realpath(path);
|
||||||
|
} catch (cause) {
|
||||||
|
if (
|
||||||
|
cause instanceof Error &&
|
||||||
|
"code" in cause &&
|
||||||
|
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
|
||||||
|
)
|
||||||
|
throw new PcapConfigurationFailure(
|
||||||
|
`${name}_missing`,
|
||||||
|
`The selected ${name} executable could not be found at its configured path.`,
|
||||||
|
{ command: selected.command },
|
||||||
|
{ cause },
|
||||||
|
);
|
||||||
|
throw cause;
|
||||||
|
}
|
||||||
|
const snapshot = await this.#resources.read(
|
||||||
|
resolvedPath,
|
||||||
|
64 * 1024 * 1024,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
this.wiresharkIdentities.set(selected.command, {
|
||||||
|
path: resolvedPath,
|
||||||
|
identity: wiresharkExecutableIdentity(snapshot),
|
||||||
|
});
|
||||||
|
}
|
||||||
const identity = await inspectPcapClient(
|
const identity = await inspectPcapClient(
|
||||||
selection,
|
selection,
|
||||||
async (command) =>
|
async (command) =>
|
||||||
@@ -292,9 +373,30 @@ export class PcapProvider {
|
|||||||
tshark_command: selection.tshark.command,
|
tshark_command: selection.tshark.command,
|
||||||
tshark_source: selection.tshark.source,
|
tshark_source: selection.tshark.source,
|
||||||
tshark_version: identity.tsharkVersion,
|
tshark_version: identity.tsharkVersion,
|
||||||
|
tshark_resolved_path:
|
||||||
|
this.wiresharkIdentities.get(selection.tshark.command)?.path ?? null,
|
||||||
|
tshark_sha256:
|
||||||
|
this.wiresharkIdentities.get(selection.tshark.command)?.identity
|
||||||
|
.sha256 ?? null,
|
||||||
|
tshark_identity_status: this.wiresharkIdentities.has(
|
||||||
|
selection.tshark.command,
|
||||||
|
)
|
||||||
|
? "verified"
|
||||||
|
: "unresolved",
|
||||||
capinfos_command: selection.capinfos.command,
|
capinfos_command: selection.capinfos.command,
|
||||||
capinfos_source: selection.capinfos.source,
|
capinfos_source: selection.capinfos.source,
|
||||||
capinfos_version: identity.capinfosVersion,
|
capinfos_version: identity.capinfosVersion,
|
||||||
|
capinfos_resolved_path:
|
||||||
|
this.wiresharkIdentities.get(selection.capinfos.command)?.path ??
|
||||||
|
null,
|
||||||
|
capinfos_sha256:
|
||||||
|
this.wiresharkIdentities.get(selection.capinfos.command)?.identity
|
||||||
|
.sha256 ?? null,
|
||||||
|
capinfos_identity_status: this.wiresharkIdentities.has(
|
||||||
|
selection.capinfos.command,
|
||||||
|
)
|
||||||
|
? "verified"
|
||||||
|
: "unresolved",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -567,17 +669,39 @@ export class PcapProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
result = err(
|
const classifyArtifactFailure = (failure: unknown): AnalysisError =>
|
||||||
cause instanceof AnalysisError
|
failure instanceof AnalysisError
|
||||||
? cause
|
? failure
|
||||||
: cause instanceof PcapConfigurationFailure
|
: failure instanceof ArtifactReaderFailure &&
|
||||||
? pcapToolUnavailable(request.operation, cause.message, {
|
failure.reason === "cancelled"
|
||||||
cause,
|
? new AnalysisCancelledError(request.operation, {
|
||||||
|
cause: artifactFileCancellationCause(failure),
|
||||||
|
...(failure.partialObservation === undefined
|
||||||
|
? {}
|
||||||
|
: { partialObservation: failure.partialObservation }),
|
||||||
})
|
})
|
||||||
: new AnalysisProtocolError(
|
: new AnalysisProtocolError(
|
||||||
`Packet-capture analysis failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
`Packet-capture analysis failed: ${failure instanceof Error ? failure.message : String(failure)}`,
|
||||||
{ cause },
|
{ cause: failure },
|
||||||
),
|
);
|
||||||
|
result = err(
|
||||||
|
cause instanceof ArtifactReaderFailure
|
||||||
|
? this.#resources.cleanupFailure(
|
||||||
|
cause,
|
||||||
|
classifyArtifactFailure,
|
||||||
|
undefined,
|
||||||
|
request.operation,
|
||||||
|
)
|
||||||
|
: cause instanceof AnalysisError
|
||||||
|
? cause
|
||||||
|
: cause instanceof PcapConfigurationFailure
|
||||||
|
? pcapToolUnavailable(request.operation, cause.message, {
|
||||||
|
cause,
|
||||||
|
})
|
||||||
|
: new AnalysisProtocolError(
|
||||||
|
`Packet-capture analysis failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||||
|
{ cause },
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (signal?.aborted && result.ok)
|
if (signal?.aborted && result.ok)
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysis
|
|||||||
import type { ApktoolResourceAnalysisPort } from "../application/apktool/ApktoolResourceAnalysisPort.js";
|
import type { ApktoolResourceAnalysisPort } from "../application/apktool/ApktoolResourceAnalysisPort.js";
|
||||||
import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js";
|
import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js";
|
||||||
import { PcapAnalysisService } from "../application/pcap/PcapAnalysisService.js";
|
import { PcapAnalysisService } from "../application/pcap/PcapAnalysisService.js";
|
||||||
|
import {
|
||||||
|
locateWiresharkTool,
|
||||||
|
selectWiresharkTool,
|
||||||
|
} from "../pcap/PcapConfiguration.js";
|
||||||
import { ApktoolResourceAnalysisService } from "../application/apktool/ApktoolResourceAnalysisService.js";
|
import { ApktoolResourceAnalysisService } from "../application/apktool/ApktoolResourceAnalysisService.js";
|
||||||
import type { FlutterBuildAnalysisPort } from "../application/flutter/FlutterBuildAnalysisPort.js";
|
import type { FlutterBuildAnalysisPort } from "../application/flutter/FlutterBuildAnalysisPort.js";
|
||||||
import { FlutterBuildAnalysisService } from "../application/flutter/FlutterBuildAnalysisService.js";
|
import { FlutterBuildAnalysisService } from "../application/flutter/FlutterBuildAnalysisService.js";
|
||||||
@@ -198,6 +202,21 @@ export interface CreateServerOptions {
|
|||||||
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
|
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Apply the shared TShark selection rule without starting the tool. */
|
||||||
|
const tsharkLocatable = (environment: Readonly<NodeJS.ProcessEnv>): boolean => {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
locateWiresharkTool(
|
||||||
|
selectWiresharkTool(environment, "tshark"),
|
||||||
|
environment,
|
||||||
|
) !== undefined
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// A relative REA_TSHARK_COMMAND is configuration the tool reports at execution.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const installSessionToolAvailability = (
|
const installSessionToolAvailability = (
|
||||||
session: BinarySessionPort | undefined,
|
session: BinarySessionPort | undefined,
|
||||||
options: CreateServerOptions,
|
options: CreateServerOptions,
|
||||||
@@ -214,7 +233,7 @@ const installSessionToolAvailability = (
|
|||||||
optionalFeatures: {
|
optionalFeatures: {
|
||||||
packetCaptureEnabled:
|
packetCaptureEnabled:
|
||||||
options.packetCapture !== undefined ||
|
options.packetCapture !== undefined ||
|
||||||
(linux && isAbsolute(environment.REA_TSHARK_COMMAND ?? "")),
|
(linux && tsharkLocatable(environment)),
|
||||||
signalRecordingEnabled:
|
signalRecordingEnabled:
|
||||||
options.signalRecording !== undefined ||
|
options.signalRecording !== undefined ||
|
||||||
(linux && isAbsolute(environment.REA_SIGROK_COMMAND ?? "")),
|
(linux && isAbsolute(environment.REA_SIGROK_COMMAND ?? "")),
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ else { if(readFileSync('capture', 'utf8') !== 'recording bytes') throw Error('wr
|
|||||||
const configuredCommand =
|
const configuredCommand =
|
||||||
mode === "symlink" ? join(directory, "configured-tool") : script;
|
mode === "symlink" ? join(directory, "configured-tool") : script;
|
||||||
if (mode === "symlink") await symlink(script, configuredCommand);
|
if (mode === "symlink") await symlink(script, configuredCommand);
|
||||||
|
if (mode === "path") await symlink(script, join(directory, "tshark"));
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
let calls = 0;
|
let calls = 0;
|
||||||
let mayClean = !failCleanup;
|
let mayClean = !failCleanup;
|
||||||
@@ -49,7 +50,9 @@ else { if(readFileSync('capture', 'utf8') !== 'recording bytes') throw Error('wr
|
|||||||
const provider = new TsharkCaptureProvider(
|
const provider = new TsharkCaptureProvider(
|
||||||
{
|
{
|
||||||
...process.env,
|
...process.env,
|
||||||
REA_TSHARK_COMMAND: configuredCommand,
|
...(mode === "path"
|
||||||
|
? { PATH: directory, REA_TSHARK_COMMAND: undefined }
|
||||||
|
: { REA_TSHARK_COMMAND: configuredCommand }),
|
||||||
REA_TSHARK_PRLIMIT_COMMAND: configuredCommand,
|
REA_TSHARK_PRLIMIT_COMMAND: configuredCommand,
|
||||||
},
|
},
|
||||||
async (input) => {
|
async (input) => {
|
||||||
@@ -408,6 +411,41 @@ it("refuses unsupported hosts before launching or allocating a workspace", async
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("selects TShark from PATH with the shared rule and records the selection", async () => {
|
||||||
|
const f = await fixture("path");
|
||||||
|
const result = await f.service.inspect({ path: f.path, fields: ["ip.addr"] });
|
||||||
|
if (!result.ok) throw result.error;
|
||||||
|
expect(result.value.normalized_result).toMatchObject({
|
||||||
|
tool: {
|
||||||
|
configured_path: join(f.directory, "tshark"),
|
||||||
|
selection_source: "path",
|
||||||
|
path: join(f.directory, "producer.mjs"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses a relative REA_TSHARK_COMMAND instead of searching PATH", async () => {
|
||||||
|
const provider = new TsharkCaptureProvider(
|
||||||
|
{ REA_TSHARK_COMMAND: "tshark", PATH: "/usr/bin" },
|
||||||
|
async () => {
|
||||||
|
throw new Error("Must not launch");
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
throw new Error("Must not allocate");
|
||||||
|
},
|
||||||
|
"linux",
|
||||||
|
);
|
||||||
|
const result = await new PacketCaptureService(provider).inspect({
|
||||||
|
path: "/recording.pcap",
|
||||||
|
fields: ["ip.addr"],
|
||||||
|
});
|
||||||
|
if (result.ok) throw new Error("Expected configuration refusal");
|
||||||
|
expect(result.error).toMatchObject({
|
||||||
|
_tag: "AnalysisCapabilityUnavailableError",
|
||||||
|
reason: "REA_TSHARK_COMMAND must be an absolute path to the tshark binary",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves complete Evidence when aborted during post-run integrity checks", async () => {
|
it("preserves complete Evidence when aborted during post-run integrity checks", async () => {
|
||||||
const f = await fixture("post-run-abort");
|
const f = await fixture("post-run-abort");
|
||||||
const result = await f.service.inspect(
|
const result = await f.service.inspect(
|
||||||
@@ -435,6 +473,7 @@ it("preserves configured symlink paths separately from resolved tool identities"
|
|||||||
expect(result.value.normalized_result).toMatchObject({
|
expect(result.value.normalized_result).toMatchObject({
|
||||||
tool: {
|
tool: {
|
||||||
configured_path: join(f.directory, "configured-tool"),
|
configured_path: join(f.directory, "configured-tool"),
|
||||||
|
selection_source: "environment",
|
||||||
path: join(f.directory, "producer.mjs"),
|
path: join(f.directory, "producer.mjs"),
|
||||||
},
|
},
|
||||||
resource_limiter: {
|
resource_limiter: {
|
||||||
|
|||||||
@@ -15,9 +15,15 @@ const clientIdentity = {
|
|||||||
tshark_command: "/usr/bin/tshark",
|
tshark_command: "/usr/bin/tshark",
|
||||||
tshark_source: "path",
|
tshark_source: "path",
|
||||||
tshark_version: "TShark (Wireshark) 4.6.6",
|
tshark_version: "TShark (Wireshark) 4.6.6",
|
||||||
|
tshark_resolved_path: null,
|
||||||
|
tshark_sha256: null,
|
||||||
|
tshark_identity_status: "unresolved",
|
||||||
capinfos_command: "/usr/bin/capinfos",
|
capinfos_command: "/usr/bin/capinfos",
|
||||||
capinfos_source: "path",
|
capinfos_source: "path",
|
||||||
capinfos_version: "Capinfos (Wireshark) 4.6.6",
|
capinfos_version: "Capinfos (Wireshark) 4.6.6",
|
||||||
|
capinfos_resolved_path: null,
|
||||||
|
capinfos_sha256: null,
|
||||||
|
capinfos_identity_status: "unresolved",
|
||||||
};
|
};
|
||||||
|
|
||||||
it("advertises exact valid pcap schemas and records inline evidence through MCP", async () => {
|
it("advertises exact valid pcap schemas and records inline evidence through MCP", async () => {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const captureFixture = async (root: string): Promise<string> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it("inspects both tools without reading a capture", async () => {
|
it("inspects both tools without reading a capture", async () => {
|
||||||
const { provider } = await providerWith();
|
const { provider, root } = await providerWith();
|
||||||
const result = await evidenceResult(provider, {
|
const result = await evidenceResult(provider, {
|
||||||
operation: "inspect_pcap_client",
|
operation: "inspect_pcap_client",
|
||||||
input: {},
|
input: {},
|
||||||
@@ -149,11 +149,50 @@ it("inspects both tools without reading a capture", async () => {
|
|||||||
expect(result.client).toMatchObject({
|
expect(result.client).toMatchObject({
|
||||||
tshark_source: "environment",
|
tshark_source: "environment",
|
||||||
tshark_version: "TShark (Wireshark) 4.6.6-stub",
|
tshark_version: "TShark (Wireshark) 4.6.6-stub",
|
||||||
|
tshark_sha256: createHash("sha256")
|
||||||
|
.update(await readFile(join(root, "tshark")))
|
||||||
|
.digest("hex"),
|
||||||
|
tshark_identity_status: "verified",
|
||||||
capinfos_source: "environment",
|
capinfos_source: "environment",
|
||||||
capinfos_version: "Capinfos (Wireshark) 4.6.6-stub",
|
capinfos_version: "Capinfos (Wireshark) 4.6.6-stub",
|
||||||
|
capinfos_sha256: createHash("sha256")
|
||||||
|
.update(await readFile(join(root, "capinfos")))
|
||||||
|
.digest("hex"),
|
||||||
|
capinfos_identity_status: "verified",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects a selected TShark whose bytes change during its version probe", async () => {
|
||||||
|
const root = await createTestTempDirectory("rea-pcap-tool-identity-");
|
||||||
|
const { tshark, capinfos } = await writeStubTools(root);
|
||||||
|
await writeFile(
|
||||||
|
tshark,
|
||||||
|
`#!/bin/sh
|
||||||
|
if [ "$1" = "--version" ]; then
|
||||||
|
printf 'TShark (Wireshark) 4.6.6-stub\\n'
|
||||||
|
printf '\\n# changed during version probe\\n' >> "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 2
|
||||||
|
`,
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
const provider = new PcapProvider({
|
||||||
|
environment: { REA_TSHARK_COMMAND: tshark, REA_CAPINFOS_COMMAND: capinfos },
|
||||||
|
});
|
||||||
|
onTestFinished(() => provider.close());
|
||||||
|
const result = await provider.execute({
|
||||||
|
operation: "inspect_pcap_client",
|
||||||
|
input: {},
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok)
|
||||||
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||||
|
code: "artifact_changed",
|
||||||
|
details: { path: tshark },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("inspects a capture with endpoints, DNS, TLS SNI, and HTTP", async () => {
|
it("inspects a capture with endpoints, DNS, TLS SNI, and HTTP", async () => {
|
||||||
const { provider, root } = await providerWith();
|
const { provider, root } = await providerWith();
|
||||||
const path = await captureFixture(root);
|
const path = await captureFixture(root);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { dirname, join } from "node:path";
|
|||||||
import { expect, onTestFinished, test, vi } from "vitest";
|
import { expect, onTestFinished, test, vi } from "vitest";
|
||||||
|
|
||||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||||
|
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
|
||||||
|
import { readStableArtifact } from "../../../src/artifacts/readStableArtifact.js";
|
||||||
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
||||||
import { resolvedAnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
import { resolvedAnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
||||||
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
|
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
|
||||||
@@ -62,6 +64,7 @@ const fixture = async (
|
|||||||
reason: unknown,
|
reason: unknown,
|
||||||
failedCleanupAttempts: number,
|
failedCleanupAttempts: number,
|
||||||
activeVersion = false,
|
activeVersion = false,
|
||||||
|
artifactReader: typeof readStableArtifact = readStableArtifact,
|
||||||
) => {
|
) => {
|
||||||
const root = await createTestTempDirectory("rea-pcap-provider-cancel-");
|
const root = await createTestTempDirectory("rea-pcap-provider-cancel-");
|
||||||
const tools = await writeTools(root, mode, activeVersion);
|
const tools = await writeTools(root, mode, activeVersion);
|
||||||
@@ -100,6 +103,7 @@ const fixture = async (
|
|||||||
REA_CAPINFOS_COMMAND: tools.capinfos,
|
REA_CAPINFOS_COMMAND: tools.capinfos,
|
||||||
},
|
},
|
||||||
launcher,
|
launcher,
|
||||||
|
artifactReader,
|
||||||
});
|
});
|
||||||
onTestFinished(async () => provider.close());
|
onTestFinished(async () => provider.close());
|
||||||
return { provider, root, capture, cleanupAttempts: () => cleanupAttempts };
|
return { provider, root, capture, cleanupAttempts: () => cleanupAttempts };
|
||||||
@@ -123,6 +127,31 @@ it("availability preserves the producer's exact cancellation reason", async () =
|
|||||||
expect(failure).toMatchObject({ cause: reason });
|
expect(failure).toMatchObject({ cause: reason });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("executable identity hashing preserves artifact-read cancellation", async () => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const reason = { caller: "cancel executable identity read" };
|
||||||
|
const { provider } = await fixture(
|
||||||
|
"active",
|
||||||
|
controller,
|
||||||
|
reason,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
async () => {
|
||||||
|
throw new ArtifactReaderFailure("cancelled", "identity read cancelled", {
|
||||||
|
cancellation: { reason },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const result = await provider.execute(
|
||||||
|
{ operation: "inspect_pcap_client", input: {} },
|
||||||
|
{ signal: controller.signal },
|
||||||
|
);
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (result.ok) return;
|
||||||
|
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||||
|
expect(result.error.cause).toBe(reason);
|
||||||
|
});
|
||||||
|
|
||||||
it("active cancellation stays primary through retained command and snapshot cleanup retry", async () => {
|
it("active cancellation stays primary through retained command and snapshot cleanup retry", async () => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const reason = "caller cancellation token";
|
const reason = "caller cancellation token";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* refusal paths, and cancellation before spawn.
|
* refusal paths, and cancellation before spawn.
|
||||||
*/
|
*/
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { open, writeFile } from "node:fs/promises";
|
import { open, readFile, realpath, symlink, writeFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { expect, it, onTestFinished, vi } from "vitest";
|
import { expect, it, onTestFinished, vi } from "vitest";
|
||||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||||
@@ -52,6 +52,41 @@ const evidenceResult = async (
|
|||||||
return result.value.result as Record<string, unknown>;
|
return result.value.result as Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Windows keeps PATH/PATHEXT resolution in the launcher; POSIX hosts locate the file.
|
||||||
|
it.skipIf(isWindows)(
|
||||||
|
"runs and reports the tools located on PATH rather than bare names",
|
||||||
|
async () => {
|
||||||
|
const directory = await createTestTempDirectory("rea-pcap-path-");
|
||||||
|
await symlink(tsharkStub, join(directory, "tshark"));
|
||||||
|
await symlink(capinfosStub, join(directory, "capinfos"));
|
||||||
|
const provider = new PcapProvider({
|
||||||
|
environment: { PATH: `relative-entry:${directory}` },
|
||||||
|
});
|
||||||
|
onTestFinished(() => provider.close());
|
||||||
|
const result = await evidenceResult(provider, {
|
||||||
|
operation: "inspect_pcap_client",
|
||||||
|
input: {},
|
||||||
|
});
|
||||||
|
expect(result.client).toMatchObject({
|
||||||
|
tshark_command: join(directory, "tshark"),
|
||||||
|
tshark_source: "path",
|
||||||
|
tshark_version: expect.stringContaining("4.6.6") as unknown,
|
||||||
|
tshark_resolved_path: await realpath(tsharkStub),
|
||||||
|
tshark_sha256: createHash("sha256")
|
||||||
|
.update(await readFile(join(directory, "tshark")))
|
||||||
|
.digest("hex"),
|
||||||
|
tshark_identity_status: "verified",
|
||||||
|
capinfos_command: join(directory, "capinfos"),
|
||||||
|
capinfos_source: "path",
|
||||||
|
capinfos_resolved_path: await realpath(capinfosStub),
|
||||||
|
capinfos_sha256: createHash("sha256")
|
||||||
|
.update(await readFile(join(directory, "capinfos")))
|
||||||
|
.digest("hex"),
|
||||||
|
capinfos_identity_status: "verified",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("inspects both tools through cross-platform stubs", async () => {
|
it("inspects both tools through cross-platform stubs", async () => {
|
||||||
const result = await evidenceResult(providerOver(), {
|
const result = await evidenceResult(providerOver(), {
|
||||||
operation: "inspect_pcap_client",
|
operation: "inspect_pcap_client",
|
||||||
@@ -60,11 +95,81 @@ it("inspects both tools through cross-platform stubs", async () => {
|
|||||||
expect(result.client).toMatchObject({
|
expect(result.client).toMatchObject({
|
||||||
tshark_source: "environment",
|
tshark_source: "environment",
|
||||||
tshark_version: expect.stringContaining("4.6.6") as unknown,
|
tshark_version: expect.stringContaining("4.6.6") as unknown,
|
||||||
|
tshark_resolved_path: await realpath(tsharkStub),
|
||||||
capinfos_source: "environment",
|
capinfos_source: "environment",
|
||||||
capinfos_version: expect.stringContaining("4.6.6") as unknown,
|
capinfos_version: expect.stringContaining("4.6.6") as unknown,
|
||||||
|
capinfos_resolved_path: await realpath(capinfosStub),
|
||||||
|
tshark_sha256: createHash("sha256")
|
||||||
|
.update(await readFile(tsharkStub))
|
||||||
|
.digest("hex"),
|
||||||
|
tshark_identity_status: "verified",
|
||||||
|
capinfos_sha256: createHash("sha256")
|
||||||
|
.update(await readFile(capinfosStub))
|
||||||
|
.digest("hex"),
|
||||||
|
capinfos_identity_status: "verified",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("projects a missing configured tool as a capability gap", async () => {
|
||||||
|
const provider = new PcapProvider({
|
||||||
|
environment: {
|
||||||
|
REA_TSHARK_COMMAND: tsharkStub,
|
||||||
|
REA_CAPINFOS_COMMAND: join(import.meta.dirname, "missing-capinfos"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onTestFinished(() => provider.close());
|
||||||
|
const result = await provider.execute({
|
||||||
|
operation: "inspect_pcap_client",
|
||||||
|
input: {},
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (result.ok) return;
|
||||||
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||||
|
code: "capability_unavailable",
|
||||||
|
message: expect.stringContaining(
|
||||||
|
"Supply Wireshark 4.x tshark and capinfos",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(isWindows)(
|
||||||
|
"rejects TShark that changes during its version probe",
|
||||||
|
async () => {
|
||||||
|
const directory = await createTestTempDirectory("rea-pcap-mutated-tool-");
|
||||||
|
const tshark = join(directory, "tshark");
|
||||||
|
const capinfos = join(directory, "capinfos");
|
||||||
|
await writeFile(
|
||||||
|
tshark,
|
||||||
|
`#!/bin/sh
|
||||||
|
if [ "$1" = "--version" ]; then
|
||||||
|
printf 'TShark (Wireshark) 4.6.6-stub\\n'
|
||||||
|
printf '\\n# changed during version probe\\n' >> "$0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 2
|
||||||
|
`,
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
await symlink(capinfosStub, capinfos);
|
||||||
|
const provider = new PcapProvider({
|
||||||
|
environment: {
|
||||||
|
REA_TSHARK_COMMAND: tshark,
|
||||||
|
REA_CAPINFOS_COMMAND: capinfos,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onTestFinished(() => provider.close());
|
||||||
|
const result = await provider.execute({
|
||||||
|
operation: "inspect_pcap_client",
|
||||||
|
input: {},
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok)
|
||||||
|
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||||
|
code: "artifact_changed",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("resolves a retained tool-probe error after its process owner closes", async () => {
|
it("resolves a retained tool-probe error after its process owner closes", async () => {
|
||||||
let mayClean = false;
|
let mayClean = false;
|
||||||
let launches = 0;
|
let launches = 0;
|
||||||
|
|||||||
Reference in new issue
Block a user