openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
235 lines (234 loc) • 10.5 kB
JavaScript
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 };