mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-22 19:01:14 +00:00
285 lines
9.0 KiB
TypeScript
285 lines
9.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const runInstallPolicyMock = vi.fn();
|
|
const findBlockedManifestDependenciesMock = vi.fn();
|
|
const findBlockedNodeModulesDirectoryMock = vi.fn();
|
|
const findBlockedNodeModulesFileAliasMock = vi.fn();
|
|
const findBlockedPackageDirectoryInPathMock = vi.fn();
|
|
const findBlockedPackageFileAliasInPathMock = vi.fn();
|
|
const getGlobalHookRunnerMock = vi.fn();
|
|
|
|
vi.mock("../security/install-policy.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../security/install-policy.js")>();
|
|
return {
|
|
...actual,
|
|
runInstallPolicy: (...args: unknown[]) => runInstallPolicyMock(...args),
|
|
};
|
|
});
|
|
|
|
vi.mock("./dependency-denylist.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("./dependency-denylist.js")>();
|
|
return {
|
|
...actual,
|
|
findBlockedManifestDependencies: (...args: unknown[]) =>
|
|
findBlockedManifestDependenciesMock(...args),
|
|
findBlockedNodeModulesDirectory: (...args: unknown[]) =>
|
|
findBlockedNodeModulesDirectoryMock(...args),
|
|
findBlockedNodeModulesFileAlias: (...args: unknown[]) =>
|
|
findBlockedNodeModulesFileAliasMock(...args),
|
|
findBlockedPackageDirectoryInPath: (...args: unknown[]) =>
|
|
findBlockedPackageDirectoryInPathMock(...args),
|
|
findBlockedPackageFileAliasInPath: (...args: unknown[]) =>
|
|
findBlockedPackageFileAliasInPathMock(...args),
|
|
};
|
|
});
|
|
|
|
vi.mock("./hook-runner-global.js", () => ({
|
|
getGlobalHookRunner: () => getGlobalHookRunnerMock(),
|
|
}));
|
|
|
|
const {
|
|
evaluateSkillInstallPolicyRuntime,
|
|
preflightPluginNpmInstallPolicyRuntime,
|
|
scanBundleInstallSourceRuntime,
|
|
scanFileInstallSourceRuntime,
|
|
} = await import("./install-security-scan.runtime.js");
|
|
|
|
function expectOnlyOperatorPolicyRan() {
|
|
expect(runInstallPolicyMock).toHaveBeenCalledTimes(1);
|
|
expect(findBlockedManifestDependenciesMock).not.toHaveBeenCalled();
|
|
expect(findBlockedNodeModulesDirectoryMock).not.toHaveBeenCalled();
|
|
expect(findBlockedNodeModulesFileAliasMock).not.toHaveBeenCalled();
|
|
expect(findBlockedPackageDirectoryInPathMock).not.toHaveBeenCalled();
|
|
expect(findBlockedPackageFileAliasInPathMock).not.toHaveBeenCalled();
|
|
expect(getGlobalHookRunnerMock).not.toHaveBeenCalled();
|
|
}
|
|
|
|
beforeEach(() => {
|
|
runInstallPolicyMock.mockReset();
|
|
findBlockedManifestDependenciesMock.mockReset();
|
|
findBlockedNodeModulesDirectoryMock.mockReset();
|
|
findBlockedNodeModulesFileAliasMock.mockReset();
|
|
findBlockedPackageDirectoryInPathMock.mockReset();
|
|
findBlockedPackageFileAliasInPathMock.mockReset();
|
|
getGlobalHookRunnerMock.mockReset();
|
|
});
|
|
|
|
describe("install security scan official bypass", () => {
|
|
it("bypasses plugin install friction for bundled OpenClaw sources", async () => {
|
|
const result = await scanBundleInstallSourceRuntime({
|
|
logger: {},
|
|
pluginId: "openclaw/kitchen-sink",
|
|
sourceDir: "/tmp/openclaw-bundled-plugin",
|
|
source: { kind: "bundled", authority: "openclaw", mutable: false, network: false },
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expectOnlyOperatorPolicyRan();
|
|
});
|
|
|
|
it("bypasses plugin install friction for official ClawHub sources", async () => {
|
|
const result = await scanBundleInstallSourceRuntime({
|
|
logger: {},
|
|
pluginId: "@openclaw/matrix",
|
|
sourceDir: "/tmp/openclaw-official-clawhub-plugin",
|
|
source: { kind: "clawhub", authority: "official", mutable: false, network: true },
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expectOnlyOperatorPolicyRan();
|
|
});
|
|
|
|
it("bypasses skill install friction for bundled OpenClaw sources", async () => {
|
|
const result = await evaluateSkillInstallPolicyRuntime({
|
|
installId: "node",
|
|
logger: {},
|
|
origin: {
|
|
type: "openclaw-bundled",
|
|
skillName: "peekaboo",
|
|
installId: "node",
|
|
},
|
|
source: { kind: "bundled", authority: "openclaw", mutable: false, network: false },
|
|
skillName: "peekaboo",
|
|
sourceDir: "/tmp/openclaw-bundled-skill/peekaboo",
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expectOnlyOperatorPolicyRan();
|
|
});
|
|
|
|
it("runs only operator policy for official immutable npm sources", async () => {
|
|
const result = await preflightPluginNpmInstallPolicyRuntime({
|
|
logger: {},
|
|
packageName: "@openclaw/matrix",
|
|
requestedSpecifier: "@openclaw/matrix@latest",
|
|
source: { kind: "npm", authority: "official", mutable: false, network: true },
|
|
sourcePath: "/tmp/openclaw-official-npm",
|
|
sourcePathKind: "directory",
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expectOnlyOperatorPolicyRan();
|
|
});
|
|
|
|
it("lets operator policy block official sources", async () => {
|
|
runInstallPolicyMock.mockResolvedValueOnce({
|
|
blocked: {
|
|
code: "security_scan_blocked",
|
|
reason: "blocked by operator policy",
|
|
},
|
|
});
|
|
|
|
const result = await scanBundleInstallSourceRuntime({
|
|
logger: {},
|
|
pluginId: "@openclaw/matrix",
|
|
sourceDir: "/tmp/openclaw-official-clawhub-plugin",
|
|
source: { kind: "clawhub", authority: "official", mutable: false, network: true },
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
blocked: {
|
|
code: "security_scan_blocked",
|
|
reason: "blocked by operator policy",
|
|
},
|
|
});
|
|
expectOnlyOperatorPolicyRan();
|
|
});
|
|
|
|
it("still runs install policy for mutable workspace skill sources", async () => {
|
|
runInstallPolicyMock.mockResolvedValueOnce({
|
|
blocked: {
|
|
code: "security_scan_blocked",
|
|
reason: "blocked by operator policy",
|
|
},
|
|
});
|
|
|
|
const result = await evaluateSkillInstallPolicyRuntime({
|
|
installId: "node",
|
|
logger: {},
|
|
origin: {
|
|
type: "workspace",
|
|
skillName: "local-skill",
|
|
installId: "node",
|
|
},
|
|
source: { kind: "workspace", authority: "user", mutable: true, network: false },
|
|
skillName: "local-skill",
|
|
sourceDir: "/tmp/local-skill",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
blocked: {
|
|
code: "security_scan_blocked",
|
|
reason: "blocked by operator policy",
|
|
},
|
|
});
|
|
expect(runInstallPolicyMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("legacy file install scan compatibility", () => {
|
|
it("preserves policy and hook metadata for published lazy install chunks", async () => {
|
|
const warnings: string[] = [];
|
|
const hasHooks = vi.fn().mockReturnValue(true);
|
|
const runBeforeInstall = vi.fn().mockResolvedValue(undefined);
|
|
getGlobalHookRunnerMock.mockReturnValue({ hasHooks, runBeforeInstall });
|
|
runInstallPolicyMock.mockResolvedValueOnce({
|
|
findings: [
|
|
{
|
|
ruleId: "registry-review",
|
|
severity: "warn",
|
|
message: "Registry requires review.",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await scanFileInstallSourceRuntime({
|
|
filePath: "/tmp/payload.js",
|
|
logger: { warn: (message) => warnings.push(message) },
|
|
mode: "update",
|
|
pluginId: "payload",
|
|
requestedSpecifier: "./payload.js",
|
|
});
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(warnings).toEqual(["Install policy: Registry requires review."]);
|
|
expect(runInstallPolicyMock).toHaveBeenCalledWith({
|
|
config: undefined,
|
|
logger: expect.any(Object),
|
|
request: {
|
|
targetName: "payload",
|
|
targetType: "plugin",
|
|
sourcePath: "/tmp/payload.js",
|
|
sourcePathKind: "file",
|
|
source: { kind: "file", authority: "user", mutable: true, network: false },
|
|
origin: { type: "plugin-file" },
|
|
request: {
|
|
kind: "plugin-file",
|
|
mode: "update",
|
|
requestedSpecifier: "./payload.js",
|
|
},
|
|
plugin: {
|
|
contentType: "file",
|
|
pluginId: "payload",
|
|
extensions: ["payload.js"],
|
|
},
|
|
},
|
|
});
|
|
expect(hasHooks).toHaveBeenCalledWith("before_install");
|
|
expect(runBeforeInstall).toHaveBeenCalledWith(
|
|
{
|
|
targetName: "payload",
|
|
targetType: "plugin",
|
|
origin: "plugin-file",
|
|
sourcePath: "/tmp/payload.js",
|
|
sourcePathKind: "file",
|
|
request: {
|
|
kind: "plugin-file",
|
|
mode: "update",
|
|
requestedSpecifier: "./payload.js",
|
|
},
|
|
builtinScan: {
|
|
status: "ok",
|
|
scannedFiles: 0,
|
|
critical: 0,
|
|
warn: 0,
|
|
info: 0,
|
|
findings: [],
|
|
},
|
|
plugin: {
|
|
contentType: "file",
|
|
pluginId: "payload",
|
|
extensions: ["payload.js"],
|
|
},
|
|
},
|
|
{
|
|
origin: "plugin-file",
|
|
targetType: "plugin",
|
|
requestKind: "plugin-file",
|
|
},
|
|
);
|
|
});
|
|
|
|
it("returns operator policy blocks before invoking hooks", async () => {
|
|
runInstallPolicyMock.mockResolvedValueOnce({
|
|
blocked: {
|
|
code: "security_scan_blocked",
|
|
reason: "blocked by operator policy",
|
|
},
|
|
});
|
|
|
|
const result = await scanFileInstallSourceRuntime({
|
|
filePath: "/tmp/payload.js",
|
|
logger: {},
|
|
pluginId: "payload",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
blocked: {
|
|
code: "security_scan_blocked",
|
|
reason: "blocked by operator policy",
|
|
},
|
|
});
|
|
expect(getGlobalHookRunnerMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|