Merge remote-tracking branch 'origin/main'

This commit is contained in:
morluto committed 2026-10-13 13:40:50 +08:00
commit 73dd8d6ed5
8 files changed
+118 -45

No files matched your search

+3 -5
View File
@@ -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,
+19
View File
@@ -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" };
+17 -7
View File
@@ -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.`,
},
);
}
};
+4 -6
View File
@@ -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 },
+1 -1
View File
@@ -43,7 +43,7 @@ export class PcapSnapshot {
"pcap",
OPERATION,
cause.message,
{ cause },
{ cause, retryable: cause.retryable },
);
throw new AnalysisResourceConstraintError(
OPERATION,
+23 -9
View File
@@ -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");
});
+27 -2
View File
@@ -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);
},
);