mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
73dd8d6ed5
8 files changed
+118
-45
No files matched your search
@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisProtocolError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
windowsBatchCommand,
|
||||
WindowsBatchCommandError,
|
||||
} from "../process/WindowsBatchCommand.js";
|
||||
import { pcapToolUnavailable } from "./PcapConfiguration.js";
|
||||
|
||||
export interface PcapCommandLimits {
|
||||
readonly timeoutMs: number;
|
||||
@@ -168,8 +168,7 @@ export const runPcapCommand = async (options: {
|
||||
"code" in cause &&
|
||||
(cause.code === "ENOENT" || cause.code === "EACCES"))
|
||||
)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"pcap",
|
||||
throw pcapToolUnavailable(
|
||||
operation,
|
||||
`Cannot run selected tool ${command}: ${cause.message}`,
|
||||
details,
|
||||
@@ -181,8 +180,7 @@ export const runPcapCommand = async (options: {
|
||||
? cause.message
|
||||
: String(cause);
|
||||
if (options.inputPath === undefined)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"pcap",
|
||||
throw pcapToolUnavailable(
|
||||
operation,
|
||||
`The selected tool ${command} cannot report its version: ${diagnostic}`,
|
||||
details,
|
||||
|
||||
@@ -2,12 +2,31 @@ import { isAbsolute } from "node:path";
|
||||
|
||||
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
|
||||
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
|
||||
import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
/** Environment keys selecting the caller's tshark and capinfos binaries. */
|
||||
export const TSHARK_COMMAND_ENV = "REA_TSHARK_COMMAND";
|
||||
export const CAPINFOS_COMMAND_ENV = "REA_CAPINFOS_COMMAND";
|
||||
|
||||
const TOOL_REMEDIATION = `Supply Wireshark 4.x tshark and capinfos on PATH or through absolute ${TSHARK_COMMAND_ENV} and ${CAPINFOS_COMMAND_ENV}; REA does not install them.`;
|
||||
|
||||
/**
|
||||
* Host-tool refusals concern the caller's Wireshark installation, not the
|
||||
* target, so they carry their own guidance instead of the generic advice to
|
||||
* choose another target.
|
||||
*/
|
||||
export const pcapToolUnavailable = (
|
||||
operation: string,
|
||||
reason: string,
|
||||
options?: AnalysisErrorOptions,
|
||||
): AnalysisCapabilityUnavailableError =>
|
||||
new AnalysisCapabilityUnavailableError("pcap", operation, reason, {
|
||||
...options,
|
||||
userMessage: `${/[.!?]$/u.test(reason) ? reason : `${reason}.`} ${TOOL_REMEDIATION}`,
|
||||
});
|
||||
|
||||
/** How each tool was selected for this provider instance. */
|
||||
export interface PcapToolSelection {
|
||||
readonly tshark: { command: string; source: "environment" | "path" };
|
||||
|
||||
@@ -6,6 +6,8 @@ import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.
|
||||
import type { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
|
||||
import type { runPcapCommand } from "./PcapCommand.js";
|
||||
|
||||
const PRLIMIT_COMMAND_ENV = "REA_TSHARK_PRLIMIT_COMMAND";
|
||||
|
||||
/** Own native capture admission and the exact enforced resource profile. */
|
||||
export const preparePcapNativeBound = async (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
@@ -22,17 +24,20 @@ export const preparePcapNativeBound = async (
|
||||
],
|
||||
};
|
||||
}
|
||||
if (process.platform !== "linux")
|
||||
if (process.platform !== "linux") {
|
||||
const reason =
|
||||
"Packet summaries require an enforceable native memory bound: Linux with util-linux prlimit, or Windows with owned Job Objects. This host's capture boundary remains unsupported.";
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"pcap",
|
||||
"inspect_pcap_capture",
|
||||
"Packet summaries require an enforceable native memory bound: Linux with util-linux prlimit, or Windows with owned Job Objects. This host's capture boundary remains unsupported.",
|
||||
reason,
|
||||
{ userMessage: reason },
|
||||
);
|
||||
const configured =
|
||||
environment.REA_TSHARK_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
|
||||
}
|
||||
const configured = environment[PRLIMIT_COMMAND_ENV] ?? "/usr/bin/prlimit";
|
||||
try {
|
||||
if (!isAbsolute(configured))
|
||||
throw new Error("REA_TSHARK_PRLIMIT_COMMAND must be absolute.");
|
||||
throw new Error(`${PRLIMIT_COMMAND_ENV} must be absolute.`);
|
||||
const limiter = await realpath(configured);
|
||||
await access(limiter, constants.X_OK);
|
||||
const limits = await resources.readLimits({
|
||||
@@ -58,11 +63,16 @@ export const preparePcapNativeBound = async (
|
||||
return { nativeBound, limitations };
|
||||
} catch (cause) {
|
||||
if (cause instanceof AnalysisError) throw cause;
|
||||
const reason =
|
||||
"Cannot establish the selected Linux resource limiter before processing capture bytes.";
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"pcap",
|
||||
"inspect_pcap_capture",
|
||||
"Cannot establish the selected Linux resource limiter before processing capture bytes.",
|
||||
{ cause },
|
||||
reason,
|
||||
{
|
||||
cause,
|
||||
userMessage: `${reason} (${cause instanceof Error ? cause.message : String(cause)}) Check that ${PRLIMIT_COMMAND_ENV} (default /usr/bin/prlimit) names an absolute, executable util-linux prlimit.`,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
inspectPcapClient,
|
||||
resolvePcapTools,
|
||||
pcapConfigurationAvailability,
|
||||
pcapToolUnavailable,
|
||||
type PcapToolSelection,
|
||||
} from "./PcapConfiguration.js";
|
||||
import {
|
||||
@@ -346,12 +347,9 @@ export class PcapProvider {
|
||||
: signal?.aborted
|
||||
? new AnalysisCancelledError(request.operation, { cause })
|
||||
: cause instanceof PcapConfigurationFailure
|
||||
? new AnalysisCapabilityUnavailableError(
|
||||
"pcap",
|
||||
request.operation,
|
||||
cause.message,
|
||||
{ cause },
|
||||
)
|
||||
? pcapToolUnavailable(request.operation, cause.message, {
|
||||
cause,
|
||||
})
|
||||
: new AnalysisProtocolError(
|
||||
`Packet-capture analysis failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
{ cause },
|
||||
|
||||
@@ -43,7 +43,7 @@ export class PcapSnapshot {
|
||||
"pcap",
|
||||
OPERATION,
|
||||
cause.message,
|
||||
{ cause },
|
||||
{ cause, retryable: cause.retryable },
|
||||
);
|
||||
throw new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { serveCli } from "./cliServeHarness.js";
|
||||
import { expectConfigurationRefusal, serveCli } from "./cliServeHarness.js";
|
||||
|
||||
it.each(["inspect-pcap-client", "inspect-pcap-capture"])(
|
||||
"%s advertises a schema through the public CLI",
|
||||
@@ -11,13 +11,27 @@ it.each(["inspect-pcap-client", "inspect-pcap-capture"])(
|
||||
},
|
||||
);
|
||||
|
||||
it("reports missing tools with a capability refusal", async () => {
|
||||
const result = await serveCli(
|
||||
["inspect-pcap-capture", "/nonexistent/capture.pcap", "--json"],
|
||||
{
|
||||
REA_TSHARK_COMMAND: "/nonexistent/tshark",
|
||||
REA_CAPINFOS_COMMAND: "/nonexistent/capinfos",
|
||||
},
|
||||
it.each([
|
||||
["inspect-pcap-client"],
|
||||
["inspect-pcap-capture", "/nonexistent/capture.pcap"],
|
||||
])("%s names the missing Wireshark tool to configure", async (...argv) => {
|
||||
const result = await serveCli([...argv, "--json"], {
|
||||
REA_TSHARK_COMMAND: "/nonexistent/tshark",
|
||||
REA_CAPINFOS_COMMAND: "/nonexistent/capinfos",
|
||||
});
|
||||
expectConfigurationRefusal(
|
||||
result,
|
||||
"Cannot run selected tool /nonexistent/tshark",
|
||||
);
|
||||
expectConfigurationRefusal(result, "through absolute REA_TSHARK_COMMAND");
|
||||
});
|
||||
|
||||
it("names a relative tool selection as configuration to fix", async () => {
|
||||
const result = await serveCli(["inspect-pcap-client", "--json"], {
|
||||
REA_TSHARK_COMMAND: "tshark",
|
||||
});
|
||||
expectConfigurationRefusal(
|
||||
result,
|
||||
"REA_TSHARK_COMMAND must be an absolute path to the tshark binary",
|
||||
);
|
||||
expect(result.stdout).toContain("capability_unavailable");
|
||||
});
|
||||
@@ -246,12 +246,37 @@ it("reports relative configuration and missing capinfos as capability gaps", asy
|
||||
input: {},
|
||||
});
|
||||
if (result.ok) throw new Error("expected unavailable tools");
|
||||
expect(projectAnalysisError(result.error).code).toBe(
|
||||
"capability_unavailable",
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.code).toBe("capability_unavailable");
|
||||
expect(projected.message).toContain(
|
||||
"Supply Wireshark 4.x tshark and capinfos",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("names the prlimit selection when the Linux limiter is unusable", async () => {
|
||||
const root = await createTestTempDirectory("rea-pcap-prlimit-");
|
||||
const tools = await writeStubTools(root);
|
||||
const provider = new PcapProvider({
|
||||
environment: {
|
||||
REA_TSHARK_COMMAND: tools.tshark,
|
||||
REA_CAPINFOS_COMMAND: tools.capinfos,
|
||||
REA_TSHARK_PRLIMIT_COMMAND: "relative/prlimit",
|
||||
},
|
||||
});
|
||||
onTestFinished(() => provider.close());
|
||||
const result = await provider.execute({
|
||||
operation: "inspect_pcap_capture",
|
||||
input: { path: "/not-opened/capture.pcap" },
|
||||
});
|
||||
if (result.ok) throw new Error("expected an unusable limiter");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.code).toBe("capability_unavailable");
|
||||
expect(projected.message).toContain(
|
||||
"(REA_TSHARK_PRLIMIT_COMMAND must be absolute.) Check that REA_TSHARK_PRLIMIT_COMMAND",
|
||||
);
|
||||
});
|
||||
|
||||
const nodeTool = async (
|
||||
root: string,
|
||||
name: string,
|
||||
|
||||
@@ -87,17 +87,22 @@ it.runIf(hasNativeBound)(
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a path that is not a readable file", async () => {
|
||||
const result = await providerOver().execute({
|
||||
operation: "inspect_pcap_capture",
|
||||
input: { path: "/nonexistent/definitely-missing.pcap" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
});
|
||||
});
|
||||
// Hosts without a native bound refuse before touching the path; that order
|
||||
// is pinned by the unsupported-boundary test below.
|
||||
it.runIf(hasNativeBound)(
|
||||
"rejects a path that is not a readable file",
|
||||
async () => {
|
||||
const result = await providerOver().execute({
|
||||
operation: "inspect_pcap_capture",
|
||||
input: { path: "/nonexistent/definitely-missing.pcap" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("reports a missing binary as a capability gap", async () => {
|
||||
const provider = new PcapProvider({
|
||||
@@ -136,9 +141,13 @@ it.runIf(!hasNativeBound)(
|
||||
input: { path: "/not-opened/capture.pcap" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
});
|
||||
if (result.ok) return;
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.code).toBe("capability_unavailable");
|
||||
// The refusal concerns this host, so it must not suggest another target.
|
||||
expect(projected.message).toContain(
|
||||
"Linux with util-linux prlimit, or Windows with owned Job Objects",
|
||||
);
|
||||
expect(projected.remediation.action).toBe(projected.message);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user