test: merge audit extension and workspace cases

This commit is contained in:
Peter Steinberger
2026-03-17 09:08:28 +00:00
parent 2c073e7bcb
commit 588c8be6ff

View File

@@ -250,6 +250,17 @@ describe("security audit", () => {
); );
}; };
const runSharedExtensionsAudit = async (config: OpenClawConfig) => {
return runSecurityAudit({
config,
includeFilesystem: true,
includeChannelSecurity: false,
stateDir: sharedExtensionsStateDir,
configPath: path.join(sharedExtensionsStateDir, "openclaw.json"),
execDockerRawFn: execDockerRawUnavailable,
});
};
const createSharedCodeSafetyFixture = async () => { const createSharedCodeSafetyFixture = async () => {
const stateDir = await makeTmpDir("audit-scanner-shared"); const stateDir = await makeTmpDir("audit-scanner-shared");
const workspaceDir = path.join(stateDir, "workspace"); const workspaceDir = path.join(stateDir, "workspace");
@@ -642,52 +653,64 @@ description: test skill
); );
}); });
it("warns for risky safeBinTrustedDirs entries", async () => { it("evaluates safeBinTrustedDirs risk findings", async () => {
const riskyGlobalTrustedDirs = const riskyGlobalTrustedDirs =
process.platform === "win32" process.platform === "win32"
? [String.raw`C:\Users\ci-user\bin`, String.raw`C:\Users\ci-user\.local\bin`] ? [String.raw`C:\Users\ci-user\bin`, String.raw`C:\Users\ci-user\.local\bin`]
: ["/usr/local/bin", "/tmp/openclaw-safe-bins"]; : ["/usr/local/bin", "/tmp/openclaw-safe-bins"];
const cfg: OpenClawConfig = { const cases = [
tools: { {
exec: { name: "warns for risky global and relative trusted dirs",
safeBinTrustedDirs: riskyGlobalTrustedDirs, cfg: {
}, tools: {
}, exec: {
agents: { safeBinTrustedDirs: riskyGlobalTrustedDirs,
list: [
{
id: "ops",
tools: {
exec: {
safeBinTrustedDirs: ["./relative-bin-dir"],
},
}, },
}, },
], agents: {
}, list: [
}; {
id: "ops",
const res = await audit(cfg); tools: {
const finding = res.findings.find( exec: {
(f) => f.checkId === "tools.exec.safe_bin_trusted_dirs_risky", safeBinTrustedDirs: ["./relative-bin-dir"],
); },
expect(finding?.severity).toBe("warn"); },
expect(finding?.detail).toContain(riskyGlobalTrustedDirs[0]); },
expect(finding?.detail).toContain(riskyGlobalTrustedDirs[1]); ],
expect(finding?.detail).toContain("agents.list.ops.tools.exec"); },
}); } satisfies OpenClawConfig,
assert: (res: SecurityAuditReport) => {
it("does not warn for non-risky absolute safeBinTrustedDirs entries", async () => { const finding = res.findings.find(
const cfg: OpenClawConfig = { (f) => f.checkId === "tools.exec.safe_bin_trusted_dirs_risky",
tools: { );
exec: { expect(finding?.severity).toBe("warn");
safeBinTrustedDirs: ["/usr/libexec"], expect(finding?.detail).toContain(riskyGlobalTrustedDirs[0]);
expect(finding?.detail).toContain(riskyGlobalTrustedDirs[1]);
expect(finding?.detail).toContain("agents.list.ops.tools.exec");
}, },
}, },
}; {
name: "ignores non-risky absolute dirs",
cfg: {
tools: {
exec: {
safeBinTrustedDirs: ["/usr/libexec"],
},
},
} satisfies OpenClawConfig,
assert: (res: SecurityAuditReport) => {
expectNoFinding(res, "tools.exec.safe_bin_trusted_dirs_risky");
},
},
] as const;
const res = await audit(cfg); await Promise.all(
expectNoFinding(res, "tools.exec.safe_bin_trusted_dirs_risky"); cases.map(async (testCase) => {
const res = await audit(testCase.cfg);
testCase.assert(res);
}),
);
}); });
it("evaluates loopback control UI and logging exposure findings", async () => { it("evaluates loopback control UI and logging exposure findings", async () => {
@@ -971,69 +994,80 @@ description: test skill
expect(res.findings.some((f) => f.checkId === "fs.config.perms_group_readable")).toBe(false); expect(res.findings.some((f) => f.checkId === "fs.config.perms_group_readable")).toBe(false);
}); });
it("warns when workspace skill files resolve outside workspace root", async () => { it("evaluates workspace skill path escape findings", async () => {
if (isWindows) { const cases = [
return; {
name: "warns when workspace skill files resolve outside workspace root",
supported: !isWindows,
setup: async () => {
const tmp = await makeTmpDir("workspace-skill-symlink-escape");
const stateDir = path.join(tmp, "state");
const workspaceDir = path.join(tmp, "workspace");
const outsideDir = path.join(tmp, "outside");
await fs.mkdir(stateDir, { recursive: true, mode: 0o700 });
await fs.mkdir(path.join(workspaceDir, "skills", "leak"), { recursive: true });
await fs.mkdir(outsideDir, { recursive: true });
const outsideSkillPath = path.join(outsideDir, "SKILL.md");
await fs.writeFile(outsideSkillPath, "# outside\n", "utf-8");
await fs.symlink(outsideSkillPath, path.join(workspaceDir, "skills", "leak", "SKILL.md"));
return { stateDir, workspaceDir, outsideSkillPath };
},
assert: (
res: SecurityAuditReport,
fixture: { stateDir: string; workspaceDir: string; outsideSkillPath: string },
) => {
const finding = res.findings.find((f) => f.checkId === "skills.workspace.symlink_escape");
expect(finding?.severity).toBe("warn");
expect(finding?.detail).toContain(fixture.outsideSkillPath);
},
},
{
name: "does not warn for workspace skills that stay inside workspace root",
supported: true,
setup: async () => {
const tmp = await makeTmpDir("workspace-skill-in-root");
const stateDir = path.join(tmp, "state");
const workspaceDir = path.join(tmp, "workspace");
await fs.mkdir(stateDir, { recursive: true, mode: 0o700 });
await fs.mkdir(path.join(workspaceDir, "skills", "safe"), { recursive: true });
await fs.writeFile(
path.join(workspaceDir, "skills", "safe", "SKILL.md"),
"# in workspace\n",
"utf-8",
);
return { stateDir, workspaceDir };
},
assert: (res: SecurityAuditReport) => {
expectNoFinding(res, "skills.workspace.symlink_escape");
},
},
] as const;
for (const testCase of cases) {
if (!testCase.supported) {
continue;
}
const fixture = await testCase.setup();
const configPath = path.join(fixture.stateDir, "openclaw.json");
await fs.writeFile(configPath, "{}\n", "utf-8");
if (!isWindows) {
await fs.chmod(configPath, 0o600);
}
const res = await runSecurityAudit({
config: { agents: { defaults: { workspace: fixture.workspaceDir } } },
includeFilesystem: true,
includeChannelSecurity: false,
stateDir: fixture.stateDir,
configPath,
execDockerRawFn: execDockerRawUnavailable,
});
testCase.assert(res, fixture);
} }
const tmp = await makeTmpDir("workspace-skill-symlink-escape");
const stateDir = path.join(tmp, "state");
const workspaceDir = path.join(tmp, "workspace");
const outsideDir = path.join(tmp, "outside");
await fs.mkdir(stateDir, { recursive: true, mode: 0o700 });
await fs.mkdir(path.join(workspaceDir, "skills", "leak"), { recursive: true });
await fs.mkdir(outsideDir, { recursive: true });
const outsideSkillPath = path.join(outsideDir, "SKILL.md");
await fs.writeFile(outsideSkillPath, "# outside\n", "utf-8");
await fs.symlink(outsideSkillPath, path.join(workspaceDir, "skills", "leak", "SKILL.md"));
const configPath = path.join(stateDir, "openclaw.json");
await fs.writeFile(configPath, "{}\n", "utf-8");
await fs.chmod(configPath, 0o600);
const res = await runSecurityAudit({
config: { agents: { defaults: { workspace: workspaceDir } } },
includeFilesystem: true,
includeChannelSecurity: false,
stateDir,
configPath,
execDockerRawFn: execDockerRawUnavailable,
});
const finding = res.findings.find((f) => f.checkId === "skills.workspace.symlink_escape");
expect(finding?.severity).toBe("warn");
expect(finding?.detail).toContain(outsideSkillPath);
});
it("does not warn for workspace skills that stay inside workspace root", async () => {
const tmp = await makeTmpDir("workspace-skill-in-root");
const stateDir = path.join(tmp, "state");
const workspaceDir = path.join(tmp, "workspace");
await fs.mkdir(stateDir, { recursive: true, mode: 0o700 });
await fs.mkdir(path.join(workspaceDir, "skills", "safe"), { recursive: true });
await fs.writeFile(
path.join(workspaceDir, "skills", "safe", "SKILL.md"),
"# in workspace\n",
"utf-8",
);
const configPath = path.join(stateDir, "openclaw.json");
await fs.writeFile(configPath, "{}\n", "utf-8");
if (!isWindows) {
await fs.chmod(configPath, 0o600);
}
const res = await runSecurityAudit({
config: { agents: { defaults: { workspace: workspaceDir } } },
includeFilesystem: true,
includeChannelSecurity: false,
stateDir,
configPath,
execDockerRawFn: execDockerRawUnavailable,
});
expect(res.findings.some((f) => f.checkId === "skills.workspace.symlink_escape")).toBe(false);
}); });
it("scores small-model risk by tool/sandbox exposure", async () => { it("scores small-model risk by tool/sandbox exposure", async () => {
@@ -3210,131 +3244,89 @@ description: test skill
expect(hasFinding(res, "hooks.installs_version_drift", "warn")).toBe(true); expect(hasFinding(res, "hooks.installs_version_drift", "warn")).toBe(true);
}); });
it("flags enabled extensions when tool policy can expose plugin tools", async () => { it("evaluates extension tool reachability findings", async () => {
const stateDir = sharedExtensionsStateDir; const cases = [
{
const cfg: OpenClawConfig = { name: "flags enabled extensions when tool policy can expose plugin tools",
plugins: { allow: ["some-plugin"] }, cfg: {
}; plugins: { allow: ["some-plugin"] },
const res = await runSecurityAudit({ } satisfies OpenClawConfig,
config: cfg, assert: (res: SecurityAuditReport) => {
includeFilesystem: true, expect(res.findings).toEqual(
includeChannelSecurity: false, expect.arrayContaining([
stateDir, expect.objectContaining({
configPath: path.join(stateDir, "openclaw.json"), checkId: "plugins.tools_reachable_permissive_policy",
execDockerRawFn: execDockerRawUnavailable, severity: "warn",
}); }),
]),
expect(res.findings).toEqual( );
expect.arrayContaining([
expect.objectContaining({
checkId: "plugins.tools_reachable_permissive_policy",
severity: "warn",
}),
]),
);
});
it("does not flag plugin tool reachability when profile is restrictive", async () => {
const stateDir = sharedExtensionsStateDir;
const cfg: OpenClawConfig = {
plugins: { allow: ["some-plugin"] },
tools: { profile: "coding" },
};
const res = await runSecurityAudit({
config: cfg,
includeFilesystem: true,
includeChannelSecurity: false,
stateDir,
configPath: path.join(stateDir, "openclaw.json"),
execDockerRawFn: execDockerRawUnavailable,
});
expect(
res.findings.some((f) => f.checkId === "plugins.tools_reachable_permissive_policy"),
).toBe(false);
});
it("flags unallowlisted extensions as critical when native skill commands are exposed", async () => {
const prevDiscordToken = process.env.DISCORD_BOT_TOKEN;
delete process.env.DISCORD_BOT_TOKEN;
const stateDir = sharedExtensionsStateDir;
try {
const cfg: OpenClawConfig = {
channels: {
discord: { enabled: true, token: "t" },
}, },
}; },
const res = await runSecurityAudit({ {
config: cfg, name: "does not flag plugin tool reachability when profile is restrictive",
includeFilesystem: true, cfg: {
includeChannelSecurity: false, plugins: { allow: ["some-plugin"] },
stateDir, tools: { profile: "coding" },
configPath: path.join(stateDir, "openclaw.json"), } satisfies OpenClawConfig,
execDockerRawFn: execDockerRawUnavailable, assert: (res: SecurityAuditReport) => {
}); expect(
res.findings.some((f) => f.checkId === "plugins.tools_reachable_permissive_policy"),
expect(res.findings).toEqual( ).toBe(false);
expect.arrayContaining([ },
expect.objectContaining({ },
checkId: "plugins.extensions_no_allowlist", {
severity: "critical", name: "flags unallowlisted extensions as critical when native skill commands are exposed",
}), cfg: {
]), channels: {
); discord: { enabled: true, token: "t" },
} finally {
if (prevDiscordToken == null) {
delete process.env.DISCORD_BOT_TOKEN;
} else {
process.env.DISCORD_BOT_TOKEN = prevDiscordToken;
}
}
});
it("treats SecretRef channel credentials as configured for extension allowlist severity", async () => {
const prevDiscordToken = process.env.DISCORD_BOT_TOKEN;
delete process.env.DISCORD_BOT_TOKEN;
const stateDir = sharedExtensionsStateDir;
try {
const cfg: OpenClawConfig = {
channels: {
discord: {
enabled: true,
token: {
source: "env",
provider: "default",
id: "DISCORD_BOT_TOKEN",
} as unknown as string,
}, },
} satisfies OpenClawConfig,
assert: (res: SecurityAuditReport) => {
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "plugins.extensions_no_allowlist",
severity: "critical",
}),
]),
);
}, },
}; },
const res = await runSecurityAudit({ {
config: cfg, name: "treats SecretRef channel credentials as configured for extension allowlist severity",
includeFilesystem: true, cfg: {
includeChannelSecurity: false, channels: {
stateDir, discord: {
configPath: path.join(stateDir, "openclaw.json"), enabled: true,
execDockerRawFn: execDockerRawUnavailable, token: {
}); source: "env",
provider: "default",
id: "DISCORD_BOT_TOKEN",
} as unknown as string,
},
},
} satisfies OpenClawConfig,
assert: (res: SecurityAuditReport) => {
expect(res.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "plugins.extensions_no_allowlist",
severity: "critical",
}),
]),
);
},
},
] as const;
expect(res.findings).toEqual( await withEnvAsync({ DISCORD_BOT_TOKEN: undefined }, async () => {
expect.arrayContaining([ await Promise.all(
expect.objectContaining({ cases.map(async (testCase) => {
checkId: "plugins.extensions_no_allowlist", const res = await runSharedExtensionsAudit(testCase.cfg);
severity: "critical", testCase.assert(res);
}), }),
]),
); );
} finally { });
if (prevDiscordToken == null) {
delete process.env.DISCORD_BOT_TOKEN;
} else {
process.env.DISCORD_BOT_TOKEN = prevDiscordToken;
}
}
}); });
it("does not scan plugin code safety findings when deep audit is disabled", async () => { it("does not scan plugin code safety findings when deep audit is disabled", async () => {