Files
openclaw/src/security/audit-channel-synology-zalo.test.ts
2026-04-07 12:24:31 +08:00

203 lines
6.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
collectSynologyChatSecurityAuditFindings,
collectZalouserSecurityAuditFindings,
} from "../../test/helpers/channels/security-audit-contract.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { withChannelSecurityStateDir } from "./audit-channel-security.test-helpers.js";
import { collectChannelSecurityFindings } from "./audit-channel.js";
type SynologyAuditParams = Parameters<typeof collectSynologyChatSecurityAuditFindings>[0];
type ResolvedSynologyChatAccount = SynologyAuditParams["account"];
type ZalouserAuditParams = Parameters<typeof collectZalouserSecurityAuditFindings>[0];
type ResolvedZalouserAccount = ZalouserAuditParams["account"];
function stubZalouserPlugin(): ChannelPlugin {
return {
id: "zalouser",
meta: {
id: "zalouser",
label: "Zalo Personal",
selectionLabel: "Zalo Personal",
docsPath: "/docs/testing",
blurb: "test stub",
},
capabilities: {
chatTypes: ["direct", "group"],
},
security: {
collectAuditFindings: collectZalouserSecurityAuditFindings,
},
config: {
listAccountIds: () => ["default"],
inspectAccount: (cfg) => ({
accountId: "default",
enabled: true,
configured: true,
config: cfg.channels?.zalouser ?? {},
}),
resolveAccount: (cfg) =>
({
accountId: "default",
enabled: true,
config: cfg.channels?.zalouser ?? {},
}) as ResolvedZalouserAccount,
isEnabled: () => true,
isConfigured: () => true,
},
};
}
function createSynologyChatAccount(params: {
cfg: OpenClawConfig;
accountId: string;
}): ResolvedSynologyChatAccount {
const channel = params.cfg.channels?.["synology-chat"] ?? {};
const accountConfig =
params.accountId === "default" ? channel : (channel.accounts?.[params.accountId] ?? {});
return {
accountId: params.accountId,
dangerouslyAllowNameMatching:
Boolean(
(accountConfig as { dangerouslyAllowNameMatching?: boolean }).dangerouslyAllowNameMatching,
) ||
Boolean(
params.accountId === "default" &&
(channel as { dangerouslyAllowNameMatching?: boolean }).dangerouslyAllowNameMatching,
),
} as ResolvedSynologyChatAccount;
}
describe("security audit synology and zalo channel routing", () => {
it.each([
{
name: "audits Synology Chat base dangerous name matching",
cfg: {
channels: {
"synology-chat": {
token: "t",
incomingUrl: "https://nas.example.com/incoming",
dangerouslyAllowNameMatching: true,
},
},
} satisfies OpenClawConfig,
expectedMatch: {
checkId: "channels.synology-chat.reply.dangerous_name_matching_enabled",
severity: "info",
title: "Synology Chat dangerous name matching is enabled",
},
},
{
name: "audits non-default Synology Chat accounts for dangerous name matching",
cfg: {
channels: {
"synology-chat": {
token: "t",
incomingUrl: "https://nas.example.com/incoming",
accounts: {
alpha: {
token: "a",
incomingUrl: "https://nas.example.com/incoming-alpha",
},
beta: {
token: "b",
incomingUrl: "https://nas.example.com/incoming-beta",
dangerouslyAllowNameMatching: true,
},
},
},
},
} satisfies OpenClawConfig,
expectedMatch: {
checkId: "channels.synology-chat.reply.dangerous_name_matching_enabled",
severity: "info",
title: expect.stringContaining("(account: beta)"),
},
},
])("$name", async (testCase) => {
await withChannelSecurityStateDir(async () => {
const synologyChat = testCase.cfg.channels?.["synology-chat"];
if (!synologyChat) {
throw new Error("synology-chat config required");
}
const accountId = Object.keys(synologyChat.accounts ?? {}).includes("beta")
? "beta"
: "default";
const findings = collectSynologyChatSecurityAuditFindings({
account: createSynologyChatAccount({ cfg: testCase.cfg, accountId }),
accountId,
orderedAccountIds: Object.keys(synologyChat.accounts ?? {}),
hasExplicitAccountPath: accountId !== "default",
});
expect(findings).toEqual(
expect.arrayContaining([expect.objectContaining(testCase.expectedMatch)]),
);
});
});
it.each([
{
name: "warns when Zalouser group routing contains mutable group entries",
cfg: {
channels: {
zalouser: {
enabled: true,
groups: {
"Ops Room": { allow: true },
"group:g-123": { allow: true },
},
},
},
} satisfies OpenClawConfig,
expectedSeverity: "warn",
detailIncludes: ["channels.zalouser.groups:Ops Room"],
detailExcludes: ["group:g-123"],
},
{
name: "marks Zalouser mutable group routing as break-glass when dangerous matching is enabled",
cfg: {
channels: {
zalouser: {
enabled: true,
dangerouslyAllowNameMatching: true,
groups: {
"Ops Room": { allow: true },
},
},
},
} satisfies OpenClawConfig,
expectedSeverity: "info",
detailIncludes: ["out-of-scope"],
expectFindingMatch: {
checkId: "channels.zalouser.allowFrom.dangerous_name_matching_enabled",
severity: "info",
},
},
])("$name", async (testCase) => {
await withChannelSecurityStateDir(async () => {
const findings = await collectChannelSecurityFindings({
cfg: testCase.cfg,
plugins: [stubZalouserPlugin()],
});
const finding = findings.find(
(entry) => entry.checkId === "channels.zalouser.groups.mutable_entries",
);
expect(finding).toBeDefined();
expect(finding?.severity).toBe(testCase.expectedSeverity);
for (const snippet of testCase.detailIncludes) {
expect(finding?.detail).toContain(snippet);
}
for (const snippet of testCase.detailExcludes ?? []) {
expect(finding?.detail).not.toContain(snippet);
}
if (testCase.expectFindingMatch) {
expect(findings).toEqual(
expect.arrayContaining([expect.objectContaining(testCase.expectFindingMatch)]),
);
}
});
});
});