feat(security): expand audit and safe --fix
This commit is contained in:
@@ -2,8 +2,32 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { runSecurityAudit } from "./audit.js";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
describe("security audit", () => {
|
||||
it("includes an attack surface summary (info)", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: { whatsapp: { groupPolicy: "open" }, telegram: { groupPolicy: "allowlist" } },
|
||||
tools: { elevated: { enabled: true, allowFrom: { whatsapp: ["+1"] } } },
|
||||
hooks: { enabled: true },
|
||||
browser: { enabled: true },
|
||||
};
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ checkId: "summary.attack_surface", severity: "info" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags non-loopback bind without auth as critical", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
gateway: {
|
||||
@@ -175,4 +199,124 @@ describe("security audit", () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns on legacy model configuration", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
agents: { defaults: { model: { primary: "openai/gpt-3.5-turbo" } } },
|
||||
};
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ checkId: "models.legacy", severity: "warn" })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns when hooks token looks short", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
hooks: { enabled: true, token: "short" },
|
||||
};
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ checkId: "hooks.token_too_short", severity: "warn" })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns when state/config look like a synced folder", async () => {
|
||||
const cfg: ClawdbotConfig = {};
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
stateDir: "/Users/test/Dropbox/.clawdbot",
|
||||
configPath: "/Users/test/Dropbox/.clawdbot/clawdbot.json",
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ checkId: "fs.synced_dir", severity: "warn" })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags group/world-readable config include files", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-audit-"));
|
||||
const stateDir = path.join(tmp, "state");
|
||||
await fs.mkdir(stateDir, { recursive: true, mode: 0o700 });
|
||||
|
||||
const includePath = path.join(stateDir, "extra.json5");
|
||||
await fs.writeFile(includePath, "{ logging: { redactSensitive: 'off' } }\n", "utf-8");
|
||||
await fs.chmod(includePath, 0o644);
|
||||
|
||||
const configPath = path.join(stateDir, "clawdbot.json");
|
||||
await fs.writeFile(configPath, `{ "$include": "./extra.json5" }\n`, "utf-8");
|
||||
await fs.chmod(configPath, 0o600);
|
||||
|
||||
const cfg: ClawdbotConfig = { logging: { redactSensitive: "off" } };
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: false,
|
||||
stateDir,
|
||||
configPath,
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ checkId: "fs.config_include.perms_world_readable", severity: "critical" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags extensions without plugins.allow", async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-audit-"));
|
||||
const stateDir = path.join(tmp, "state");
|
||||
await fs.mkdir(path.join(stateDir, "extensions", "some-plugin"), { recursive: true, mode: 0o700 });
|
||||
|
||||
const cfg: ClawdbotConfig = {};
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: false,
|
||||
stateDir,
|
||||
configPath: path.join(stateDir, "clawdbot.json"),
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ checkId: "plugins.extensions_no_allowlist", severity: "warn" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags open groupPolicy when tools.elevated is enabled", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
tools: { elevated: { enabled: true, allowFrom: { whatsapp: ["+1"] } } },
|
||||
channels: { whatsapp: { groupPolicy: "open" } },
|
||||
};
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: cfg,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: false,
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "security.exposure.open_groups_with_elevated",
|
||||
severity: "critical",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user