UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

235 lines (234 loc) 10.5 kB
import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js"; import { t as resolveDmAllowAuditState } from "./dm-allow-state-DgWvIrZa.js"; import { i as resolveChannelDefaultAccountId } from "./helpers-BLGguEiN.js"; import { n as isDangerousNameMatchingEnabled } from "./dangerous-name-matching-D4QRC91G.js"; import { t as inspectReadOnlyChannelAccount } from "./read-only-account-inspect-BVDfcwnb.js"; import { n as hasResolvedCredentialValue, t as hasConfiguredUnavailableCredentialStatus } from "./account-snapshot-fields-VbzFdu8Y.js"; //#region src/security/audit-channel.ts /** Classify free-form channel warnings into audit severities. */ function classifyChannelWarningSeverity(message) { const s = message.toLowerCase(); if (s.includes("dms: open") || s.includes("grouppolicy=\"open\"") || s.includes("dmpolicy=\"open\"")) return "critical"; if (s.includes("allows any") || s.includes("anyone can dm") || s.includes("public")) return "critical"; if (s.includes("locked") || s.includes("disabled")) return "info"; return "warn"; } function dedupeFindings(findings) { const seen = /* @__PURE__ */ new Set(); const out = []; for (const finding of findings) { const key = [ finding.checkId, finding.severity, finding.title, finding.detail ?? "", finding.remediation ?? "" ].join("\n"); if (seen.has(key)) continue; seen.add(key); out.push(finding); } return out; } function hasExplicitProviderAccountConfig(cfg, provider, accountId) { const channel = cfg.channels?.[provider]; if (!channel || typeof channel !== "object") return false; const accounts = channel.accounts; if (!accounts || typeof accounts !== "object") return false; return Object.hasOwn(accounts, accountId); } function formatChannelAccountNote(params) { return params.orderedAccountIds.length > 1 || params.hasExplicitAccountPath ? ` (account: ${params.accountId})` : ""; } /** Collect channel-specific security findings across active channel plugins/accounts. */ async function collectChannelSecurityFindings$1(params) { const findings = []; const sourceConfig = params.sourceConfig ?? params.cfg; const inspectChannelAccount = async (plugin, cfg, accountId) => { if (plugin.config.inspectAccount) return await plugin.config.inspectAccount(cfg, accountId); return await inspectReadOnlyChannelAccount({ channelId: plugin.id, cfg, accountId }); }; const asAccountRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null; const resolveChannelAuditAccount = async (plugin, accountId) => { const diagnostics = []; const sourceInspectedAccount = await inspectChannelAccount(plugin, sourceConfig, accountId); const resolvedInspectedAccount = await inspectChannelAccount(plugin, params.cfg, accountId); const sourceInspection = sourceInspectedAccount; const resolvedInspection = resolvedInspectedAccount; let resolvedAccount = resolvedInspectedAccount; if (!resolvedAccount) try { resolvedAccount = plugin.config.resolveAccount(params.cfg, accountId); } catch (error) { diagnostics.push(`${plugin.id}:${accountId}: failed to resolve account (${formatErrorMessage(error)}).`); } if (!resolvedAccount && sourceInspectedAccount) resolvedAccount = sourceInspectedAccount; if (!resolvedAccount) return { account: {}, enabled: false, configured: false, diagnostics }; const useSourceUnavailableAccount = Boolean(sourceInspectedAccount && hasConfiguredUnavailableCredentialStatus(sourceInspectedAccount) && (!hasResolvedCredentialValue(resolvedAccount) || sourceInspection?.configured === true && resolvedInspection?.configured === false)); const account = useSourceUnavailableAccount ? sourceInspectedAccount : resolvedAccount; const selectedInspection = useSourceUnavailableAccount ? sourceInspection : resolvedInspection; const accountRecord = asAccountRecord(account); let enabled = typeof selectedInspection?.enabled === "boolean" ? selectedInspection.enabled : typeof accountRecord?.enabled === "boolean" ? accountRecord.enabled : true; if (typeof selectedInspection?.enabled !== "boolean" && typeof accountRecord?.enabled !== "boolean" && plugin.config.isEnabled) try { enabled = plugin.config.isEnabled(account, params.cfg); } catch (error) { enabled = false; diagnostics.push(`${plugin.id}:${accountId}: failed to evaluate enabled state (${formatErrorMessage(error)}).`); } let configured = typeof selectedInspection?.configured === "boolean" ? selectedInspection.configured : typeof accountRecord?.configured === "boolean" ? accountRecord.configured : true; if (typeof selectedInspection?.configured !== "boolean" && typeof accountRecord?.configured !== "boolean" && plugin.config.isConfigured) try { configured = await plugin.config.isConfigured(account, params.cfg); } catch (error) { configured = false; diagnostics.push(`${plugin.id}:${accountId}: failed to evaluate configured state (${formatErrorMessage(error)}).`); } return { account, enabled, configured, diagnostics }; }; const warnDmPolicy = async (input) => { const policyPath = input.policyPath ?? `${input.allowFromPath}policy`; const { hasWildcard, isMultiUserDm } = await resolveDmAllowAuditState({ provider: input.provider, accountId: input.accountId, allowFrom: input.allowFrom, dmPolicy: input.dmPolicy, normalizeEntry: input.normalizeEntry }); const dmScope = params.cfg.session?.dmScope ?? "main"; if (input.dmPolicy === "open") { const allowFromKey = `${input.allowFromPath}allowFrom`; findings.push({ checkId: `channels.${input.provider}.dm.open`, severity: "critical", title: `${input.label} DMs are open`, detail: `${policyPath}="open" allows anyone to DM the bot.`, remediation: `Use pairing/allowlist; if you really need open DMs, ensure ${allowFromKey} includes "*".` }); if (!hasWildcard) findings.push({ checkId: `channels.${input.provider}.dm.open_invalid`, severity: "warn", title: `${input.label} DM config looks inconsistent`, detail: `"open" requires ${allowFromKey} to include "*".` }); } if (input.dmPolicy === "disabled") { findings.push({ checkId: `channels.${input.provider}.dm.disabled`, severity: "info", title: `${input.label} DMs are disabled`, detail: `${policyPath}="disabled" ignores inbound DMs.` }); return; } if (dmScope === "main" && isMultiUserDm) findings.push({ checkId: `channels.${input.provider}.dm.scope_main_multiuser`, severity: "warn", title: `${input.label} DMs share the main session`, detail: "Multiple DM senders currently share the main session, which can leak context across users.", remediation: "Run: " + formatCliCommand("openclaw config set session.dmScope \"per-channel-peer\"") + " (or \"per-account-channel-peer\" for multi-account channels) to isolate DM sessions per sender." }); }; for (const plugin of params.plugins) { if (!plugin.security) continue; const accountIds = plugin.config.listAccountIds(sourceConfig); const orderedAccountIds = uniqueStrings([resolveChannelDefaultAccountId({ plugin, cfg: sourceConfig, accountIds }), ...accountIds]); for (const accountId of orderedAccountIds) { const hasExplicitAccountPath = hasExplicitProviderAccountConfig(sourceConfig, plugin.id, accountId); const { account, enabled, configured, diagnostics } = await resolveChannelAuditAccount(plugin, accountId); for (const diagnostic of diagnostics) findings.push({ checkId: `channels.${plugin.id}.account.read_only_resolution`, severity: "warn", title: `${plugin.meta.label ?? plugin.id} account could not be fully resolved`, detail: diagnostic, remediation: "Ensure referenced secrets are available in this shell or run with a running gateway snapshot so security audit can inspect the full channel configuration." }); if (!enabled) continue; if (!configured) continue; const accountConfig = account?.config; if (isDangerousNameMatchingEnabled(accountConfig)) { const accountNote = formatChannelAccountNote({ orderedAccountIds, hasExplicitAccountPath, accountId }); findings.push({ checkId: `channels.${plugin.id}.allowFrom.dangerous_name_matching_enabled`, severity: "info", title: `${plugin.meta.label ?? plugin.id} dangerous name matching is enabled${accountNote}`, detail: "dangerouslyAllowNameMatching=true re-enables mutable name/email/tag matching for sender authorization. This is a break-glass compatibility mode, not a hardened default.", remediation: "Prefer stable sender IDs in allowlists, then disable dangerouslyAllowNameMatching." }); } const dmPolicy = plugin.security.resolveDmPolicy?.({ cfg: params.cfg, accountId, account }); if (dmPolicy) await warnDmPolicy({ label: plugin.meta.label ?? plugin.id, provider: plugin.id, accountId, dmPolicy: dmPolicy.policy, allowFrom: dmPolicy.allowFrom, policyPath: dmPolicy.policyPath, allowFromPath: dmPolicy.allowFromPath, normalizeEntry: dmPolicy.normalizeEntry }); if (plugin.security.collectWarnings) { const warnings = await plugin.security.collectWarnings({ cfg: params.cfg, accountId, account }); for (const message of warnings ?? []) { const trimmed = message.trim(); if (!trimmed) continue; findings.push({ checkId: `channels.${plugin.id}.warning.${findings.length + 1}`, severity: classifyChannelWarningSeverity(trimmed), title: `${plugin.meta.label ?? plugin.id} security warning`, detail: trimmed.replace(/^-\s*/, "") }); } } if (plugin.security.collectAuditFindings) { const auditFindings = await plugin.security.collectAuditFindings({ cfg: params.cfg, sourceConfig, accountId, account, orderedAccountIds, hasExplicitAccountPath }); for (const finding of auditFindings ?? []) findings.push(finding); } } } return dedupeFindings(findings); } //#endregion //#region src/security/audit-channel.collect.runtime.ts /** Runtime facade for channel security collection, kept mockable for audit tests. */ function collectChannelSecurityFindings(...args) { return collectChannelSecurityFindings$1(...args); } //#endregion export { collectChannelSecurityFindings };