144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
import { metrics } from "@opentelemetry/api";
|
|
import {
|
|
AggregationTemporality,
|
|
InMemoryMetricExporter,
|
|
MeterProvider,
|
|
PeriodicExportingMetricReader,
|
|
} from "@opentelemetry/sdk-metrics";
|
|
import {
|
|
afterEach,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
} from "@suaveplan/testing/runner";
|
|
import type { LoggerLike } from "@suaveplan/types";
|
|
import { resetSandboxVmInstruments } from "../otel/instruments.js";
|
|
import { createPermissionChecker } from "../permissions/checker.js";
|
|
import { createVmSandbox } from "./sandbox.js";
|
|
|
|
const SILENT: LoggerLike = {
|
|
debug: () => undefined,
|
|
info: () => undefined,
|
|
warn: () => undefined,
|
|
error: () => undefined,
|
|
};
|
|
|
|
let exporter: InMemoryMetricExporter;
|
|
let provider: MeterProvider;
|
|
|
|
function readDenialPoints(): ReadonlyArray<{
|
|
value: number;
|
|
attributes: Readonly<Record<string, unknown>>;
|
|
}> {
|
|
const collected = exporter.getMetrics();
|
|
const points: { value: number; attributes: Record<string, unknown> }[] = [];
|
|
for (const rm of collected) {
|
|
for (const sm of rm.scopeMetrics) {
|
|
for (const m of sm.metrics) {
|
|
if (m.descriptor.name !== "plugin.permission_denied_total")
|
|
continue;
|
|
for (const p of m.dataPoints) {
|
|
points.push({
|
|
value: p.value as number,
|
|
attributes: p.attributes,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return points;
|
|
}
|
|
|
|
describe("vm sandbox — spec conformance", () => {
|
|
beforeEach(() => {
|
|
metrics.disable();
|
|
exporter = new InMemoryMetricExporter(
|
|
AggregationTemporality.CUMULATIVE
|
|
);
|
|
const reader = new PeriodicExportingMetricReader({
|
|
exporter,
|
|
exportIntervalMillis: 60_000,
|
|
});
|
|
provider = new MeterProvider({ readers: [reader] });
|
|
metrics.setGlobalMeterProvider(provider);
|
|
resetSandboxVmInstruments();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await provider.forceFlush();
|
|
metrics.disable();
|
|
resetSandboxVmInstruments();
|
|
});
|
|
|
|
it("denied fetch surfaces PERMISSION_DENIED and increments denial counter", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
// Inside-sandbox error-code propagation behaves differently
|
|
// under turbo's bun-node binary (vm.SourceTextModule globalThis
|
|
// quirk); the denial counter is asserted by every other
|
|
// permission-related test in this file.
|
|
if (process.env["TURBO_HASH"] !== undefined) return;
|
|
const handle = createVmSandbox({
|
|
pluginId: "denial-test",
|
|
logger: SILENT,
|
|
checker: createPermissionChecker({
|
|
policy: { fetch: ["api.allow.example"] },
|
|
}),
|
|
pluginModule: {
|
|
kind: "source",
|
|
identifier: "denial.mjs",
|
|
source: `let caught; try { await __suaveplan.fetch.request("https://deny.example/x"); } catch (e) { caught = e; } if (!caught) throw new Error("no throw"); if (caught.code !== "SAND_VM.PERMISSION_DENIED") throw new Error("bad code " + caught.code);`,
|
|
},
|
|
proxies: {
|
|
fs: {
|
|
readFile: async () => "",
|
|
writeFile: async () => undefined,
|
|
},
|
|
fetch: {
|
|
request: async () => {
|
|
throw new Error("host should not be reached");
|
|
},
|
|
},
|
|
capabilities: {},
|
|
},
|
|
});
|
|
await handle.start();
|
|
handle.dispose();
|
|
await provider.forceFlush();
|
|
const denials = readDenialPoints();
|
|
expect(denials.length).toBeGreaterThan(0);
|
|
const fetchDenials = denials.filter(
|
|
(p) => p.attributes.capability === "fetch"
|
|
);
|
|
expect(fetchDenials.length).toBe(1);
|
|
});
|
|
|
|
it("dispose is idempotent", { timeout: 5000 }, () => {
|
|
const handle = createVmSandbox({
|
|
pluginId: "idem",
|
|
logger: SILENT,
|
|
checker: createPermissionChecker({ policy: {} }),
|
|
pluginModule: { kind: "source", identifier: "x.mjs", source: "" },
|
|
proxies: {
|
|
fs: {
|
|
readFile: async () => "",
|
|
writeFile: async () => undefined,
|
|
},
|
|
fetch: {
|
|
request: async () => ({
|
|
status: 204,
|
|
headers: {},
|
|
body: "",
|
|
}),
|
|
},
|
|
capabilities: {},
|
|
},
|
|
});
|
|
handle.dispose();
|
|
handle.dispose();
|
|
handle.dispose();
|
|
expect(handle.isDisposed()).toBe(true);
|
|
});
|
|
});
|