perf(test): narrow security audit plugin scope test

This commit is contained in:
Peter Steinberger
2026-04-23 07:20:58 +01:00
parent a35ed6b170
commit 33d9e1aa83
2 changed files with 84 additions and 41 deletions

View File

@@ -18,11 +18,32 @@ vi.mock("../plugins/runtime/metadata-registry-loader.js", () => ({
loadPluginMetadataRegistrySnapshotMock(...args),
}));
let runSecurityAudit: typeof import("./audit.js").runSecurityAudit;
let collectPluginSecurityAuditFindings: typeof import("./audit.js").collectPluginSecurityAuditFindings;
function createAuditContext(params: {
sourceConfig: Parameters<typeof collectPluginSecurityAuditFindings>[0]["sourceConfig"];
plugins: Parameters<typeof collectPluginSecurityAuditFindings>[0]["plugins"];
}): Parameters<typeof collectPluginSecurityAuditFindings>[0] {
return {
cfg: params.sourceConfig,
sourceConfig: params.sourceConfig,
env: {},
platform: process.platform,
includeFilesystem: false,
includeChannelSecurity: true,
deep: false,
deepTimeoutMs: 5000,
stateDir: "/tmp/openclaw-test-state",
configPath: "/tmp/openclaw-test-config.json",
plugins: params.plugins,
configSnapshot: null,
codeSafetySummaryCache: new Map<string, Promise<unknown>>(),
};
}
describe("security audit read-only plugin scope", () => {
beforeAll(async () => {
({ runSecurityAudit } = await import("./audit.js"));
({ collectPluginSecurityAuditFindings } = await import("./audit.js"));
});
beforeEach(() => {
@@ -56,14 +77,12 @@ describe("security audit read-only plugin scope", () => {
});
resolveConfiguredChannelPluginIdsMock.mockReturnValue(["external-channel-plugin"]);
await runSecurityAudit({
config: sourceConfig,
sourceConfig,
env: {} as NodeJS.ProcessEnv,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [],
});
await collectPluginSecurityAuditFindings(
createAuditContext({
sourceConfig,
plugins: [],
}),
);
expect(resolveConfiguredChannelPluginIdsMock).toHaveBeenCalledWith(
expect.objectContaining({
@@ -95,14 +114,12 @@ describe("security audit read-only plugin scope", () => {
});
resolveConfiguredChannelPluginIdsMock.mockReturnValue(["external-channel-plugin"]);
await runSecurityAudit({
config: sourceConfig,
sourceConfig,
env: {} as NodeJS.ProcessEnv,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [{ id: "external-channel-plugin" }] as never,
});
await collectPluginSecurityAuditFindings(
createAuditContext({
sourceConfig,
plugins: [{ id: "external-channel-plugin" }] as never,
}),
);
expect(loadPluginMetadataRegistrySnapshotMock).toHaveBeenCalledWith(
expect.objectContaining({

View File

@@ -4,7 +4,6 @@ import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js";
import type { listChannelPlugins } from "../channels/plugins/index.js";
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/config.js";
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { type ExecApprovalsFile, loadExecApprovals } from "../infra/exec-approvals.js";
import { isInterpreterLikeAllowlistPattern } from "../infra/exec-inline-eval.js";
import {
@@ -13,11 +12,6 @@ import {
} from "../infra/exec-safe-bin-runtime-policy.js";
import { listRiskyConfiguredSafeBins } from "../infra/exec-safe-bin-semantics.js";
import { normalizeTrustedSafeBinDirs } from "../infra/exec-safe-bin-trust.js";
import {
hasConfiguredChannelsForReadOnlyScope,
resolveConfiguredChannelPluginIds,
} from "../plugins/channel-plugin-ids.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import { asNullableRecord } from "../shared/record-coerce.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
@@ -80,7 +74,7 @@ export type SecurityAuditOptions = {
probeGatewayFn?: ProbeGatewayFn;
};
type AuditExecutionContext = {
export type AuditExecutionContext = {
cfg: OpenClawConfig;
sourceConfig: OpenClawConfig;
env: NodeJS.ProcessEnv;
@@ -112,6 +106,13 @@ let pluginRegistryLoaderModulePromise:
let pluginMetadataRegistryLoaderModulePromise:
| Promise<typeof import("../plugins/runtime/metadata-registry-loader.js")>
| undefined;
let pluginAutoEnableModulePromise:
| Promise<typeof import("../config/plugin-auto-enable.js")>
| undefined;
let channelPluginIdsModulePromise:
| Promise<typeof import("../plugins/channel-plugin-ids.js")>
| undefined;
let pluginRuntimeModulePromise: Promise<typeof import("../plugins/runtime.js")> | undefined;
let gatewayProbeDepsPromise:
| Promise<{
buildGatewayConnectionDetails: typeof import("../gateway/call.js").buildGatewayConnectionDetails;
@@ -147,6 +148,21 @@ async function loadPluginMetadataRegistryLoaderModule() {
return await pluginMetadataRegistryLoaderModulePromise;
}
async function loadPluginAutoEnableModule() {
pluginAutoEnableModulePromise ??= import("../config/plugin-auto-enable.js");
return await pluginAutoEnableModulePromise;
}
async function loadChannelPluginIdsModule() {
channelPluginIdsModulePromise ??= import("../plugins/channel-plugin-ids.js");
return await channelPluginIdsModulePromise;
}
async function loadPluginRuntimeModule() {
pluginRuntimeModulePromise ??= import("../plugins/runtime.js");
return await pluginRuntimeModulePromise;
}
async function loadGatewayProbeDeps() {
gatewayProbeDepsPromise ??= Promise.all([
import("../gateway/call.js"),
@@ -325,11 +341,13 @@ export function collectGatewayConfigFindings(
});
}
async function collectPluginSecurityAuditFindings(
export async function collectPluginSecurityAuditFindings(
context: AuditExecutionContext,
): Promise<SecurityAuditFinding[]> {
const { getActivePluginRegistry } = await loadPluginRuntimeModule();
let collectors = getActivePluginRegistry()?.securityAuditCollectors ?? [];
if (collectors.length === 0) {
const { applyPluginAutoEnable } = await loadPluginAutoEnableModule();
const autoEnabled = applyPluginAutoEnable({
config: context.sourceConfig,
env: context.env,
@@ -360,6 +378,7 @@ async function collectPluginSecurityAuditFindings(
}
}
if (context.includeChannelSecurity && context.plugins !== undefined) {
const { resolveConfiguredChannelPluginIds } = await loadChannelPluginIdsModule();
const auditedChannelPluginIds = new Set(context.plugins.map((plugin) => plugin.id));
for (const pluginId of resolveConfiguredChannelPluginIds({
config: autoEnabled.config,
@@ -1008,21 +1027,28 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<Secu
);
}
const shouldAuditChannelSecurity =
context.includeChannelSecurity &&
(context.plugins !== undefined ||
hasConfiguredChannelsForReadOnlyScope({
config: cfg,
activationSourceConfig: context.sourceConfig,
workspaceDir: context.workspaceDir,
env,
}) ||
resolveConfiguredChannelPluginIds({
config: cfg,
activationSourceConfig: context.sourceConfig,
workspaceDir: context.workspaceDir,
env,
}).length > 0);
let shouldAuditChannelSecurity = false;
if (context.includeChannelSecurity) {
if (context.plugins !== undefined) {
shouldAuditChannelSecurity = true;
} else {
const { hasConfiguredChannelsForReadOnlyScope, resolveConfiguredChannelPluginIds } =
await loadChannelPluginIdsModule();
shouldAuditChannelSecurity =
hasConfiguredChannelsForReadOnlyScope({
config: cfg,
activationSourceConfig: context.sourceConfig,
workspaceDir: context.workspaceDir,
env,
}) ||
resolveConfiguredChannelPluginIds({
config: cfg,
activationSourceConfig: context.sourceConfig,
workspaceDir: context.workspaceDir,
env,
}).length > 0;
}
}
if (shouldAuditChannelSecurity) {
if (context.plugins === undefined) {
(await loadPluginRegistryLoaderModule()).ensurePluginRegistryLoaded({