Files
clawdbot/src/commands/doctor-security.ts
2026-01-13 08:40:39 +00:00

119 lines
3.8 KiB
TypeScript

import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
import { listChannelPlugins } from "../channels/plugins/index.js";
import type { ChannelId } from "../channels/plugins/types.js";
import type { ClawdbotConfig } from "../config/config.js";
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
import { note } from "../terminal/note.js";
export async function noteSecurityWarnings(cfg: ClawdbotConfig) {
const warnings: string[] = [];
const warnDmPolicy = async (params: {
label: string;
provider: ChannelId;
dmPolicy: string;
allowFrom?: Array<string | number> | null;
policyPath?: string;
allowFromPath: string;
approveHint: string;
normalizeEntry?: (raw: string) => string;
}) => {
const dmPolicy = params.dmPolicy;
const policyPath = params.policyPath ?? `${params.allowFromPath}policy`;
const configAllowFrom = (params.allowFrom ?? []).map((v) =>
String(v).trim(),
);
const hasWildcard = configAllowFrom.includes("*");
const storeAllowFrom = await readChannelAllowFromStore(
params.provider,
).catch(() => []);
const normalizedCfg = configAllowFrom
.filter((v) => v !== "*")
.map((v) => (params.normalizeEntry ? params.normalizeEntry(v) : v))
.map((v) => v.trim())
.filter(Boolean);
const normalizedStore = storeAllowFrom
.map((v) => (params.normalizeEntry ? params.normalizeEntry(v) : v))
.map((v) => v.trim())
.filter(Boolean);
const allowCount = Array.from(
new Set([...normalizedCfg, ...normalizedStore]),
).length;
if (dmPolicy === "open") {
const allowFromPath = `${params.allowFromPath}allowFrom`;
warnings.push(
`- ${params.label} DMs: OPEN (${policyPath}="open"). Anyone can DM it.`,
);
if (!hasWildcard) {
warnings.push(
`- ${params.label} DMs: config invalid — "open" requires ${allowFromPath} to include "*".`,
);
}
return;
}
if (dmPolicy === "disabled") {
warnings.push(
`- ${params.label} DMs: disabled (${policyPath}="disabled").`,
);
return;
}
if (allowCount === 0) {
warnings.push(
`- ${params.label} DMs: locked (${policyPath}="${dmPolicy}") with no allowlist; unknown senders will be blocked / get a pairing code.`,
);
warnings.push(` ${params.approveHint}`);
}
};
for (const plugin of listChannelPlugins()) {
if (!plugin.security) continue;
const accountIds = plugin.config.listAccountIds(cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
cfg,
accountIds,
});
const account = plugin.config.resolveAccount(cfg, defaultAccountId);
const enabled = plugin.config.isEnabled
? plugin.config.isEnabled(account, cfg)
: true;
if (!enabled) continue;
const configured = plugin.config.isConfigured
? await plugin.config.isConfigured(account, cfg)
: true;
if (!configured) continue;
const dmPolicy = plugin.security.resolveDmPolicy?.({
cfg,
accountId: defaultAccountId,
account,
});
if (dmPolicy) {
await warnDmPolicy({
label: plugin.meta.label ?? plugin.id,
provider: plugin.id,
dmPolicy: dmPolicy.policy,
allowFrom: dmPolicy.allowFrom,
policyPath: dmPolicy.policyPath,
allowFromPath: dmPolicy.allowFromPath,
approveHint: dmPolicy.approveHint,
normalizeEntry: dmPolicy.normalizeEntry,
});
}
if (plugin.security.collectWarnings) {
const extra = await plugin.security.collectWarnings({
cfg,
accountId: defaultAccountId,
account,
});
if (extra?.length) warnings.push(...extra);
}
}
if (warnings.length > 0) {
note(warnings.join("\n"), "Security");
}
}