chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -40,11 +40,19 @@ export type SecurityAuditFinding = {
const SMALL_MODEL_PARAM_B_MAX = 300;
function expandTilde(p: string, env: NodeJS.ProcessEnv): string | null {
if (!p.startsWith("~")) return p;
if (!p.startsWith("~")) {
return p;
}
const home = typeof env.HOME === "string" && env.HOME.trim() ? env.HOME.trim() : null;
if (!home) return null;
if (p === "~") return home;
if (p.startsWith("~/") || p.startsWith("~\\")) return path.join(home, p.slice(2));
if (!home) {
return null;
}
if (p === "~") {
return home;
}
if (p.startsWith("~/") || p.startsWith("~\\")) {
return path.join(home, p.slice(2));
}
return null;
}
@@ -54,17 +62,25 @@ function summarizeGroupPolicy(cfg: OpenClawConfig): {
other: number;
} {
const channels = cfg.channels as Record<string, unknown> | undefined;
if (!channels || typeof channels !== "object") return { open: 0, allowlist: 0, other: 0 };
if (!channels || typeof channels !== "object") {
return { open: 0, allowlist: 0, other: 0 };
}
let open = 0;
let allowlist = 0;
let other = 0;
for (const value of Object.values(channels)) {
if (!value || typeof value !== "object") continue;
if (!value || typeof value !== "object") {
continue;
}
const section = value as Record<string, unknown>;
const policy = section.groupPolicy;
if (policy === "open") open += 1;
else if (policy === "allowlist") allowlist += 1;
else other += 1;
if (policy === "open") {
open += 1;
} else if (policy === "allowlist") {
allowlist += 1;
} else {
other += 1;
}
}
return { open, allowlist, other };
}
@@ -159,7 +175,9 @@ export function collectSecretsInConfigFindings(cfg: OpenClawConfig): SecurityAud
export function collectHooksHardeningFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
const findings: SecurityAuditFinding[] = [];
if (cfg.hooks?.enabled !== true) return findings;
if (cfg.hooks?.enabled !== true) {
return findings;
}
const token = typeof cfg.hooks?.token === "string" ? cfg.hooks.token.trim() : "";
if (token && token.length < 24) {
@@ -209,24 +227,32 @@ export function collectHooksHardeningFindings(cfg: OpenClawConfig): SecurityAudi
type ModelRef = { id: string; source: string };
function addModel(models: ModelRef[], raw: unknown, source: string) {
if (typeof raw !== "string") return;
if (typeof raw !== "string") {
return;
}
const id = raw.trim();
if (!id) return;
if (!id) {
return;
}
models.push({ id, source });
}
function collectModels(cfg: OpenClawConfig): ModelRef[] {
const out: ModelRef[] = [];
addModel(out, cfg.agents?.defaults?.model?.primary, "agents.defaults.model.primary");
for (const f of cfg.agents?.defaults?.model?.fallbacks ?? [])
for (const f of cfg.agents?.defaults?.model?.fallbacks ?? []) {
addModel(out, f, "agents.defaults.model.fallbacks");
}
addModel(out, cfg.agents?.defaults?.imageModel?.primary, "agents.defaults.imageModel.primary");
for (const f of cfg.agents?.defaults?.imageModel?.fallbacks ?? [])
for (const f of cfg.agents?.defaults?.imageModel?.fallbacks ?? []) {
addModel(out, f, "agents.defaults.imageModel.fallbacks");
}
const list = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
for (const agent of list ?? []) {
if (!agent || typeof agent !== "object") continue;
if (!agent || typeof agent !== "object") {
continue;
}
const id =
typeof (agent as { id?: unknown }).id === "string" ? (agent as { id: string }).id : "";
const model = (agent as { model?: unknown }).model;
@@ -236,7 +262,9 @@ function collectModels(cfg: OpenClawConfig): ModelRef[] {
addModel(out, (model as { primary?: unknown }).primary, `agents.list.${id}.model.primary`);
const fallbacks = (model as { fallbacks?: unknown }).fallbacks;
if (Array.isArray(fallbacks)) {
for (const f of fallbacks) addModel(out, f, `agents.list.${id}.model.fallbacks`);
for (const f of fallbacks) {
addModel(out, f, `agents.list.${id}.model.fallbacks`);
}
}
}
}
@@ -259,10 +287,16 @@ function inferParamBFromIdOrName(text: string): number | null {
let best: number | null = null;
for (const match of matches) {
const numRaw = match[1];
if (!numRaw) continue;
if (!numRaw) {
continue;
}
const value = Number(numRaw);
if (!Number.isFinite(value) || value <= 0) continue;
if (best === null || value > best) best = value;
if (!Number.isFinite(value) || value <= 0) {
continue;
}
if (best === null || value > best) {
best = value;
}
}
return best;
}
@@ -289,7 +323,9 @@ function isClaude45OrHigher(id: string): boolean {
export function collectModelHygieneFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
const findings: SecurityAuditFinding[] = [];
const models = collectModels(cfg);
if (models.length === 0) return findings;
if (models.length === 0) {
return findings;
}
const weakMatches = new Map<string, { model: string; source: string; reasons: string[] }>();
const addWeakMatch = (model: string, source: string, reason: string) => {
@@ -299,7 +335,9 @@ export function collectModelHygieneFindings(cfg: OpenClawConfig): SecurityAuditF
weakMatches.set(key, { model, source, reasons: [reason] });
return;
}
if (!existing.reasons.includes(reason)) existing.reasons.push(reason);
if (!existing.reasons.includes(reason)) {
existing.reasons.push(reason);
}
};
for (const entry of models) {
@@ -373,10 +411,14 @@ function extractAgentIdFromSource(source: string): string | null {
}
function pickToolPolicy(config?: { allow?: string[]; deny?: string[] }): SandboxToolPolicy | null {
if (!config) return null;
if (!config) {
return null;
}
const allow = Array.isArray(config.allow) ? config.allow : undefined;
const deny = Array.isArray(config.deny) ? config.deny : undefined;
if (!allow && !deny) return null;
if (!allow && !deny) {
return null;
}
return { allow, deny };
}
@@ -389,13 +431,19 @@ function resolveToolPolicies(params: {
const policies: SandboxToolPolicy[] = [];
const profile = params.agentTools?.profile ?? params.cfg.tools?.profile;
const profilePolicy = resolveToolProfilePolicy(profile);
if (profilePolicy) policies.push(profilePolicy);
if (profilePolicy) {
policies.push(profilePolicy);
}
const globalPolicy = pickToolPolicy(params.cfg.tools ?? undefined);
if (globalPolicy) policies.push(globalPolicy);
if (globalPolicy) {
policies.push(globalPolicy);
}
const agentPolicy = pickToolPolicy(params.agentTools);
if (agentPolicy) policies.push(agentPolicy);
if (agentPolicy) {
policies.push(agentPolicy);
}
if (params.sandboxMode === "all") {
const sandboxPolicy = resolveSandboxToolPolicyForAgent(params.cfg, params.agentId ?? undefined);
@@ -418,14 +466,20 @@ function hasWebSearchKey(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
function isWebSearchEnabled(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
const enabled = cfg.tools?.web?.search?.enabled;
if (enabled === false) return false;
if (enabled === true) return true;
if (enabled === false) {
return false;
}
if (enabled === true) {
return true;
}
return hasWebSearchKey(cfg, env);
}
function isWebFetchEnabled(cfg: OpenClawConfig): boolean {
const enabled = cfg.tools?.web?.fetch?.enabled;
if (enabled === false) return false;
if (enabled === false) {
return false;
}
return true;
}
@@ -443,17 +497,23 @@ export function collectSmallModelRiskFindings(params: {
}): SecurityAuditFinding[] {
const findings: SecurityAuditFinding[] = [];
const models = collectModels(params.cfg).filter((entry) => !entry.source.includes("imageModel"));
if (models.length === 0) return findings;
if (models.length === 0) {
return findings;
}
const smallModels = models
.map((entry) => {
const paramB = inferParamBFromIdOrName(entry.id);
if (!paramB || paramB > SMALL_MODEL_PARAM_B_MAX) return null;
if (!paramB || paramB > SMALL_MODEL_PARAM_B_MAX) {
return null;
}
return { ...entry, paramB };
})
.filter((entry): entry is { id: string; source: string; paramB: number } => Boolean(entry));
if (smallModels.length === 0) return findings;
if (smallModels.length === 0) {
return findings;
}
let hasUnsafe = false;
const modelLines: string[] = [];
@@ -473,19 +533,29 @@ export function collectSmallModelRiskFindings(params: {
});
const exposed: string[] = [];
if (isWebSearchEnabled(params.cfg, params.env)) {
if (isToolAllowedByPolicies("web_search", policies)) exposed.push("web_search");
if (isToolAllowedByPolicies("web_search", policies)) {
exposed.push("web_search");
}
}
if (isWebFetchEnabled(params.cfg)) {
if (isToolAllowedByPolicies("web_fetch", policies)) exposed.push("web_fetch");
if (isToolAllowedByPolicies("web_fetch", policies)) {
exposed.push("web_fetch");
}
}
if (isBrowserEnabled(params.cfg)) {
if (isToolAllowedByPolicies("browser", policies)) exposed.push("browser");
if (isToolAllowedByPolicies("browser", policies)) {
exposed.push("browser");
}
}
for (const tool of exposed) {
exposureSet.add(tool);
}
for (const tool of exposed) exposureSet.add(tool);
const sandboxLabel = sandboxMode === "all" ? "sandbox=all" : `sandbox=${sandboxMode}`;
const exposureLabel = exposed.length > 0 ? ` web=[${exposed.join(", ")}]` : " web=[off]";
const safe = sandboxMode === "all" && exposed.length === 0;
if (!safe) hasUnsafe = true;
if (!safe) {
hasUnsafe = true;
}
const statusLabel = safe ? "ok" : "unsafe";
modelLines.push(
`- ${entry.id} (${entry.paramB}B) @ ${entry.source} (${statusLabel}; ${sandboxLabel};${exposureLabel})`,
@@ -523,14 +593,18 @@ export async function collectPluginsTrustFindings(params: {
const findings: SecurityAuditFinding[] = [];
const extensionsDir = path.join(params.stateDir, "extensions");
const st = await safeStat(extensionsDir);
if (!st.ok || !st.isDir) return findings;
if (!st.ok || !st.isDir) {
return findings;
}
const entries = await fs.readdir(extensionsDir, { withFileTypes: true }).catch(() => []);
const pluginDirs = entries
.filter((e) => e.isDirectory())
.map((e) => e.name)
.filter(Boolean);
if (pluginDirs.length === 0) return findings;
if (pluginDirs.length === 0) {
return findings;
}
const allow = params.cfg.plugins?.allow;
const allowConfigured = Array.isArray(allow) && allow.length > 0;
@@ -623,21 +697,32 @@ function resolveIncludePath(baseConfigPath: string, includePath: string): string
function listDirectIncludes(parsed: unknown): string[] {
const out: string[] = [];
const visit = (value: unknown) => {
if (!value) return;
if (Array.isArray(value)) {
for (const item of value) visit(item);
if (!value) {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
visit(item);
}
return;
}
if (typeof value !== "object") {
return;
}
if (typeof value !== "object") return;
const rec = value as Record<string, unknown>;
const includeVal = rec[INCLUDE_KEY];
if (typeof includeVal === "string") out.push(includeVal);
else if (Array.isArray(includeVal)) {
if (typeof includeVal === "string") {
out.push(includeVal);
} else if (Array.isArray(includeVal)) {
for (const item of includeVal) {
if (typeof item === "string") out.push(item);
if (typeof item === "string") {
out.push(item);
}
}
}
for (const v of Object.values(rec)) visit(v);
for (const v of Object.values(rec)) {
visit(v);
}
};
visit(parsed);
return out;
@@ -651,14 +736,20 @@ async function collectIncludePathsRecursive(params: {
const result: string[] = [];
const walk = async (basePath: string, parsed: unknown, depth: number): Promise<void> => {
if (depth > MAX_INCLUDE_DEPTH) return;
if (depth > MAX_INCLUDE_DEPTH) {
return;
}
for (const raw of listDirectIncludes(parsed)) {
const resolved = resolveIncludePath(basePath, raw);
if (visited.has(resolved)) continue;
if (visited.has(resolved)) {
continue;
}
visited.add(resolved);
result.push(resolved);
const rawText = await fs.readFile(resolved, "utf-8").catch(() => null);
if (!rawText) continue;
if (!rawText) {
continue;
}
const nestedParsed = (() => {
try {
return JSON5.parse(rawText);
@@ -684,14 +775,18 @@ export async function collectIncludeFilePermFindings(params: {
execIcacls?: ExecFn;
}): Promise<SecurityAuditFinding[]> {
const findings: SecurityAuditFinding[] = [];
if (!params.configSnapshot.exists) return findings;
if (!params.configSnapshot.exists) {
return findings;
}
const configPath = params.configSnapshot.path;
const includePaths = await collectIncludePathsRecursive({
configPath,
parsed: params.configSnapshot.parsed,
});
if (includePaths.length === 0) return findings;
if (includePaths.length === 0) {
return findings;
}
for (const p of includePaths) {
// eslint-disable-next-line no-await-in-loop
@@ -700,7 +795,9 @@ export async function collectIncludeFilePermFindings(params: {
platform: params.platform,
exec: params.execIcacls,
});
if (!perms.ok) continue;
if (!perms.ok) {
continue;
}
if (perms.worldWritable || perms.groupWritable) {
findings.push({
checkId: "fs.config_include.perms_writable",
@@ -908,18 +1005,27 @@ export async function collectStateDeepFilesystemFindings(params: {
function listGroupPolicyOpen(cfg: OpenClawConfig): string[] {
const out: string[] = [];
const channels = cfg.channels as Record<string, unknown> | undefined;
if (!channels || typeof channels !== "object") return out;
if (!channels || typeof channels !== "object") {
return out;
}
for (const [channelId, value] of Object.entries(channels)) {
if (!value || typeof value !== "object") continue;
if (!value || typeof value !== "object") {
continue;
}
const section = value as Record<string, unknown>;
if (section.groupPolicy === "open") out.push(`channels.${channelId}.groupPolicy`);
if (section.groupPolicy === "open") {
out.push(`channels.${channelId}.groupPolicy`);
}
const accounts = section.accounts;
if (accounts && typeof accounts === "object") {
for (const [accountId, accountVal] of Object.entries(accounts)) {
if (!accountVal || typeof accountVal !== "object") continue;
if (!accountVal || typeof accountVal !== "object") {
continue;
}
const acc = accountVal as Record<string, unknown>;
if (acc.groupPolicy === "open")
if (acc.groupPolicy === "open") {
out.push(`channels.${channelId}.accounts.${accountId}.groupPolicy`);
}
}
}
}
@@ -929,7 +1035,9 @@ function listGroupPolicyOpen(cfg: OpenClawConfig): string[] {
export function collectExposureMatrixFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
const findings: SecurityAuditFinding[] = [];
const openGroups = listGroupPolicyOpen(cfg);
if (openGroups.length === 0) return findings;
if (openGroups.length === 0) {
return findings;
}
const elevatedEnabled = cfg.tools?.elevated?.enabled !== false;
if (elevatedEnabled) {

View File

@@ -153,31 +153,43 @@ export function formatPermissionRemediation(params: {
}
export function modeBits(mode: number | null): number | null {
if (mode == null) return null;
if (mode == null) {
return null;
}
return mode & 0o777;
}
export function formatOctal(bits: number | null): string {
if (bits == null) return "unknown";
if (bits == null) {
return "unknown";
}
return bits.toString(8).padStart(3, "0");
}
export function isWorldWritable(bits: number | null): boolean {
if (bits == null) return false;
if (bits == null) {
return false;
}
return (bits & 0o002) !== 0;
}
export function isGroupWritable(bits: number | null): boolean {
if (bits == null) return false;
if (bits == null) {
return false;
}
return (bits & 0o020) !== 0;
}
export function isWorldReadable(bits: number | null): boolean {
if (bits == null) return false;
if (bits == null) {
return false;
}
return (bits & 0o004) !== 0;
}
export function isGroupReadable(bits: number | null): boolean {
if (bits == null) return false;
if (bits == null) {
return false;
}
return (bits & 0o040) !== 0;
}

View File

@@ -429,8 +429,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevStateDir == null) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = prevStateDir;
if (prevStateDir == null) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = prevStateDir;
}
}
});
@@ -475,8 +478,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevStateDir == null) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = prevStateDir;
if (prevStateDir == null) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = prevStateDir;
}
}
});
@@ -520,8 +526,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevStateDir == null) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = prevStateDir;
if (prevStateDir == null) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = prevStateDir;
}
}
});
@@ -559,8 +568,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevStateDir == null) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = prevStateDir;
if (prevStateDir == null) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = prevStateDir;
}
}
});
@@ -599,8 +611,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevStateDir == null) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = prevStateDir;
if (prevStateDir == null) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = prevStateDir;
}
}
});
@@ -637,8 +652,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevStateDir == null) delete process.env.OPENCLAW_STATE_DIR;
else process.env.OPENCLAW_STATE_DIR = prevStateDir;
if (prevStateDir == null) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = prevStateDir;
}
}
});
@@ -785,8 +803,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevToken === undefined) delete process.env.OPENCLAW_GATEWAY_TOKEN;
else process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
if (prevToken === undefined) {
delete process.env.OPENCLAW_GATEWAY_TOKEN;
} else {
process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
}
}
});
@@ -905,14 +926,26 @@ describe("security audit", () => {
]),
);
} finally {
if (prevDiscordToken == null) delete process.env.DISCORD_BOT_TOKEN;
else process.env.DISCORD_BOT_TOKEN = prevDiscordToken;
if (prevTelegramToken == null) delete process.env.TELEGRAM_BOT_TOKEN;
else process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken;
if (prevSlackBotToken == null) delete process.env.SLACK_BOT_TOKEN;
else process.env.SLACK_BOT_TOKEN = prevSlackBotToken;
if (prevSlackAppToken == null) delete process.env.SLACK_APP_TOKEN;
else process.env.SLACK_APP_TOKEN = prevSlackAppToken;
if (prevDiscordToken == null) {
delete process.env.DISCORD_BOT_TOKEN;
} else {
process.env.DISCORD_BOT_TOKEN = prevDiscordToken;
}
if (prevTelegramToken == null) {
delete process.env.TELEGRAM_BOT_TOKEN;
} else {
process.env.TELEGRAM_BOT_TOKEN = prevTelegramToken;
}
if (prevSlackBotToken == null) {
delete process.env.SLACK_BOT_TOKEN;
} else {
process.env.SLACK_BOT_TOKEN = prevSlackBotToken;
}
if (prevSlackAppToken == null) {
delete process.env.SLACK_APP_TOKEN;
} else {
process.env.SLACK_APP_TOKEN = prevSlackAppToken;
}
}
});
@@ -949,8 +982,11 @@ describe("security audit", () => {
]),
);
} finally {
if (prevDiscordToken == null) delete process.env.DISCORD_BOT_TOKEN;
else process.env.DISCORD_BOT_TOKEN = prevDiscordToken;
if (prevDiscordToken == null) {
delete process.env.DISCORD_BOT_TOKEN;
} else {
process.env.DISCORD_BOT_TOKEN = prevDiscordToken;
}
}
});

View File

@@ -87,15 +87,21 @@ function countBySeverity(findings: SecurityAuditFinding[]): SecurityAuditSummary
let warn = 0;
let info = 0;
for (const f of findings) {
if (f.severity === "critical") critical += 1;
else if (f.severity === "warn") warn += 1;
else info += 1;
if (f.severity === "critical") {
critical += 1;
} else if (f.severity === "warn") {
warn += 1;
} else {
info += 1;
}
}
return { critical, warn, info };
}
function normalizeAllowFromList(list: Array<string | number> | undefined | null): string[] {
if (!Array.isArray(list)) return [];
if (!Array.isArray(list)) {
return [];
}
return list.map((v) => String(v).trim()).filter(Boolean);
}
@@ -373,11 +379,15 @@ function collectBrowserControlFindings(cfg: OpenClawConfig): SecurityAuditFindin
return findings;
}
if (!resolved.enabled) return findings;
if (!resolved.enabled) {
return findings;
}
for (const name of Object.keys(resolved.profiles)) {
const profile = resolveProfile(resolved, name);
if (!profile || profile.cdpIsLoopback) continue;
if (!profile || profile.cdpIsLoopback) {
continue;
}
let url: URL;
try {
url = new URL(profile.cdpUrl);
@@ -400,7 +410,9 @@ function collectBrowserControlFindings(cfg: OpenClawConfig): SecurityAuditFindin
function collectLoggingFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
const redact = cfg.logging?.redactSensitive;
if (redact !== "off") return [];
if (redact !== "off") {
return [];
}
return [
{
checkId: "logging.redact_off",
@@ -418,8 +430,12 @@ function collectElevatedFindings(cfg: OpenClawConfig): SecurityAuditFinding[] {
const allowFrom = cfg.tools?.elevated?.allowFrom ?? {};
const anyAllowFromKeys = Object.keys(allowFrom).length > 0;
if (enabled === false) return findings;
if (!anyAllowFromKeys) return findings;
if (enabled === false) {
return findings;
}
if (!anyAllowFromKeys) {
return findings;
}
for (const [provider, list] of Object.entries(allowFrom)) {
const normalized = normalizeAllowFromList(list);
@@ -450,9 +466,15 @@ async function collectChannelSecurityFindings(params: {
const findings: SecurityAuditFinding[] = [];
const coerceNativeSetting = (value: unknown): boolean | "auto" | undefined => {
if (value === true) return true;
if (value === false) return false;
if (value === "auto") return "auto";
if (value === true) {
return true;
}
if (value === false) {
return false;
}
if (value === "auto") {
return "auto";
}
return undefined;
};
@@ -526,7 +548,9 @@ async function collectChannelSecurityFindings(params: {
};
for (const plugin of params.plugins) {
if (!plugin.security) continue;
if (!plugin.security) {
continue;
}
const accountIds = plugin.config.listAccountIds(params.cfg);
const defaultAccountId = resolveChannelDefaultAccountId({
plugin,
@@ -535,11 +559,15 @@ async function collectChannelSecurityFindings(params: {
});
const account = plugin.config.resolveAccount(params.cfg, defaultAccountId);
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, params.cfg) : true;
if (!enabled) continue;
if (!enabled) {
continue;
}
const configured = plugin.config.isConfigured
? await plugin.config.isConfigured(account, params.cfg)
: true;
if (!configured) continue;
if (!configured) {
continue;
}
if (plugin.id === "discord") {
const discordCfg =
@@ -567,13 +595,21 @@ async function collectChannelSecurityFindings(params: {
const guildEntries = (discordCfg.guilds as Record<string, unknown> | undefined) ?? {};
const guildsConfigured = Object.keys(guildEntries).length > 0;
const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => {
if (!guild || typeof guild !== "object") return false;
if (!guild || typeof guild !== "object") {
return false;
}
const g = guild as Record<string, unknown>;
if (Array.isArray(g.users) && g.users.length > 0) return true;
if (Array.isArray(g.users) && g.users.length > 0) {
return true;
}
const channels = g.channels;
if (!channels || typeof channels !== "object") return false;
if (!channels || typeof channels !== "object") {
return false;
}
return Object.values(channels as Record<string, unknown>).some((channel) => {
if (!channel || typeof channel !== "object") return false;
if (!channel || typeof channel !== "object") {
return false;
}
const c = channel as Record<string, unknown>;
return Array.isArray(c.users) && c.users.length > 0;
});
@@ -662,7 +698,9 @@ async function collectChannelSecurityFindings(params: {
normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
const channels = (slackCfg.channels as Record<string, unknown> | undefined) ?? {};
const hasAnyChannelUsersAllowlist = Object.values(channels).some((value) => {
if (!value || typeof value !== "object") return false;
if (!value || typeof value !== "object") {
return false;
}
const channel = value as Record<string, unknown>;
return Array.isArray(channel.users) && channel.users.length > 0;
});
@@ -706,7 +744,9 @@ async function collectChannelSecurityFindings(params: {
});
for (const message of warnings ?? []) {
const trimmed = String(message).trim();
if (!trimmed) continue;
if (!trimmed) {
continue;
}
findings.push({
checkId: `channels.${plugin.id}.warning.${findings.length + 1}`,
severity: classifyChannelWarningSeverity(trimmed),
@@ -718,7 +758,9 @@ async function collectChannelSecurityFindings(params: {
if (plugin.id === "telegram") {
const allowTextCommands = params.cfg.commands?.text !== false;
if (!allowTextCommands) continue;
if (!allowTextCommands) {
continue;
}
const telegramCfg =
(account as { config?: Record<string, unknown> } | null)?.config ??
@@ -730,7 +772,9 @@ async function collectChannelSecurityFindings(params: {
const groupsConfigured = Boolean(groups) && Object.keys(groups ?? {}).length > 0;
const groupAccessPossible =
groupPolicy === "open" || (groupPolicy === "allowlist" && groupsConfigured);
if (!groupAccessPossible) continue;
if (!groupAccessPossible) {
continue;
}
const storeAllowFrom = await readChannelAllowFromStore("telegram").catch(() => []);
const storeHasWildcard = storeAllowFrom.some((v) => String(v).trim() === "*");
@@ -741,14 +785,22 @@ async function collectChannelSecurityFindings(params: {
const anyGroupOverride = Boolean(
groups &&
Object.values(groups).some((value) => {
if (!value || typeof value !== "object") return false;
if (!value || typeof value !== "object") {
return false;
}
const group = value as Record<string, unknown>;
const allowFrom = Array.isArray(group.allowFrom) ? group.allowFrom : [];
if (allowFrom.length > 0) return true;
if (allowFrom.length > 0) {
return true;
}
const topics = group.topics;
if (!topics || typeof topics !== "object") return false;
if (!topics || typeof topics !== "object") {
return false;
}
return Object.values(topics as Record<string, unknown>).some((topicValue) => {
if (!topicValue || typeof topicValue !== "object") return false;
if (!topicValue || typeof topicValue !== "object") {
return false;
}
const topic = topicValue as Record<string, unknown>;
const topicAllow = Array.isArray(topic.allowFrom) ? topic.allowFrom : [];
return topicAllow.length > 0;

View File

@@ -171,8 +171,14 @@ export function isExternalHookSession(sessionKey: string): boolean {
* Extracts the hook type from a session key.
*/
export function getHookType(sessionKey: string): ExternalContentSource {
if (sessionKey.startsWith("hook:gmail:")) return "email";
if (sessionKey.startsWith("hook:webhook:")) return "webhook";
if (sessionKey.startsWith("hook:")) return "webhook";
if (sessionKey.startsWith("hook:gmail:")) {
return "email";
}
if (sessionKey.startsWith("hook:webhook:")) {
return "webhook";
}
if (sessionKey.startsWith("hook:")) {
return "webhook";
}
return "unknown";
}

View File

@@ -192,11 +192,15 @@ function setGroupPolicyAllowlist(params: {
changes: string[];
policyFlips: Set<string>;
}): void {
if (!params.cfg.channels) return;
if (!params.cfg.channels) {
return;
}
const section = params.cfg.channels[params.channel as keyof OpenClawConfig["channels"]] as
| Record<string, unknown>
| undefined;
if (!section || typeof section !== "object") return;
if (!section || typeof section !== "object") {
return;
}
const topPolicy = section.groupPolicy;
if (topPolicy === "open") {
@@ -206,10 +210,16 @@ function setGroupPolicyAllowlist(params: {
}
const accounts = section.accounts;
if (!accounts || typeof accounts !== "object") return;
if (!accounts || typeof accounts !== "object") {
return;
}
for (const [accountId, accountValue] of Object.entries(accounts)) {
if (!accountId) continue;
if (!accountValue || typeof accountValue !== "object") continue;
if (!accountId) {
continue;
}
if (!accountValue || typeof accountValue !== "object") {
continue;
}
const account = accountValue as Record<string, unknown>;
if (account.groupPolicy === "open") {
account.groupPolicy = "allowlist";
@@ -228,15 +238,25 @@ function setWhatsAppGroupAllowFromFromStore(params: {
policyFlips: Set<string>;
}): void {
const section = params.cfg.channels?.whatsapp as Record<string, unknown> | undefined;
if (!section || typeof section !== "object") return;
if (params.storeAllowFrom.length === 0) return;
if (!section || typeof section !== "object") {
return;
}
if (params.storeAllowFrom.length === 0) {
return;
}
const maybeApply = (prefix: string, obj: Record<string, unknown>) => {
if (!params.policyFlips.has(prefix)) return;
if (!params.policyFlips.has(prefix)) {
return;
}
const allowFrom = Array.isArray(obj.allowFrom) ? obj.allowFrom : [];
const groupAllowFrom = Array.isArray(obj.groupAllowFrom) ? obj.groupAllowFrom : [];
if (allowFrom.length > 0) return;
if (groupAllowFrom.length > 0) return;
if (allowFrom.length > 0) {
return;
}
if (groupAllowFrom.length > 0) {
return;
}
obj.groupAllowFrom = params.storeAllowFrom;
params.changes.push(`${prefix}groupAllowFrom=pairing-store`);
};
@@ -244,9 +264,13 @@ function setWhatsAppGroupAllowFromFromStore(params: {
maybeApply("channels.whatsapp.", section);
const accounts = section.accounts;
if (!accounts || typeof accounts !== "object") return;
if (!accounts || typeof accounts !== "object") {
return;
}
for (const [accountId, accountValue] of Object.entries(accounts)) {
if (!accountValue || typeof accountValue !== "object") continue;
if (!accountValue || typeof accountValue !== "object") {
continue;
}
const account = accountValue as Record<string, unknown>;
maybeApply(`channels.whatsapp.accounts.${accountId}.`, account);
}
@@ -284,21 +308,32 @@ function applyConfigFixes(params: { cfg: OpenClawConfig; env: NodeJS.ProcessEnv
function listDirectIncludes(parsed: unknown): string[] {
const out: string[] = [];
const visit = (value: unknown) => {
if (!value) return;
if (Array.isArray(value)) {
for (const item of value) visit(item);
if (!value) {
return;
}
if (Array.isArray(value)) {
for (const item of value) {
visit(item);
}
return;
}
if (typeof value !== "object") {
return;
}
if (typeof value !== "object") return;
const rec = value as Record<string, unknown>;
const includeVal = rec[INCLUDE_KEY];
if (typeof includeVal === "string") out.push(includeVal);
else if (Array.isArray(includeVal)) {
if (typeof includeVal === "string") {
out.push(includeVal);
} else if (Array.isArray(includeVal)) {
for (const item of includeVal) {
if (typeof item === "string") out.push(item);
if (typeof item === "string") {
out.push(item);
}
}
}
for (const v of Object.values(rec)) visit(v);
for (const v of Object.values(rec)) {
visit(v);
}
};
visit(parsed);
return out;
@@ -320,14 +355,20 @@ async function collectIncludePathsRecursive(params: {
const result: string[] = [];
const walk = async (basePath: string, parsed: unknown, depth: number): Promise<void> => {
if (depth > MAX_INCLUDE_DEPTH) return;
if (depth > MAX_INCLUDE_DEPTH) {
return;
}
for (const raw of listDirectIncludes(parsed)) {
const resolved = resolveIncludePath(basePath, raw);
if (visited.has(resolved)) continue;
if (visited.has(resolved)) {
continue;
}
visited.add(resolved);
result.push(resolved);
const rawText = await fs.readFile(resolved, "utf-8").catch(() => null);
if (!rawText) continue;
if (!rawText) {
continue;
}
const nestedParsed = (() => {
try {
return JSON5.parse(rawText);
@@ -362,8 +403,12 @@ async function chmodCredentialsAndAgentState(params: {
const credsEntries = await fs.readdir(credsDir, { withFileTypes: true }).catch(() => []);
for (const entry of credsEntries) {
if (!entry.isFile()) continue;
if (!entry.name.endsWith(".json")) continue;
if (!entry.isFile()) {
continue;
}
if (!entry.name.endsWith(".json")) {
continue;
}
const p = path.join(credsDir, entry.name);
// eslint-disable-next-line no-await-in-loop
params.actions.push(await safeChmod({ path: p, mode: 0o600, require: "file" }));
@@ -373,10 +418,14 @@ async function chmodCredentialsAndAgentState(params: {
ids.add(resolveDefaultAgentId(params.cfg));
const list = Array.isArray(params.cfg.agents?.list) ? params.cfg.agents?.list : [];
for (const agent of list ?? []) {
if (!agent || typeof agent !== "object") continue;
if (!agent || typeof agent !== "object") {
continue;
}
const id =
typeof (agent as { id?: unknown }).id === "string" ? (agent as { id: string }).id.trim() : "";
if (id) ids.add(id);
if (id) {
ids.add(id);
}
}
for (const agentId of ids) {

View File

@@ -42,7 +42,9 @@ const normalize = (value: string) => value.trim().toLowerCase();
export function resolveWindowsUserPrincipal(env?: NodeJS.ProcessEnv): string | null {
const username = env?.USERNAME?.trim() || os.userInfo().username?.trim();
if (!username) return null;
if (!username) {
return null;
}
const domain = env?.USERDOMAIN?.trim();
return domain ? `${domain}\\${username}` : username;
}
@@ -54,7 +56,9 @@ function buildTrustedPrincipals(env?: NodeJS.ProcessEnv): Set<string> {
trusted.add(normalize(principal));
const parts = principal.split("\\");
const userOnly = parts.at(-1);
if (userOnly) trusted.add(normalize(userOnly));
if (userOnly) {
trusted.add(normalize(userOnly));
}
}
return trusted;
}
@@ -65,10 +69,12 @@ function classifyPrincipal(
): "trusted" | "world" | "group" {
const normalized = normalize(principal);
const trusted = buildTrustedPrincipals(env);
if (trusted.has(normalized) || TRUSTED_SUFFIXES.some((s) => normalized.endsWith(s)))
if (trusted.has(normalized) || TRUSTED_SUFFIXES.some((s) => normalized.endsWith(s))) {
return "trusted";
if (WORLD_PRINCIPALS.has(normalized) || WORLD_SUFFIXES.some((s) => normalized.endsWith(s)))
}
if (WORLD_PRINCIPALS.has(normalized) || WORLD_SUFFIXES.some((s) => normalized.endsWith(s))) {
return "world";
}
return "group";
}
@@ -89,7 +95,9 @@ export function parseIcaclsOutput(output: string, targetPath: string): WindowsAc
for (const rawLine of output.split(/\r?\n/)) {
const line = rawLine.trimEnd();
if (!line.trim()) continue;
if (!line.trim()) {
continue;
}
const trimmed = line.trim();
const lower = trimmed.toLowerCase();
if (
@@ -107,10 +115,14 @@ export function parseIcaclsOutput(output: string, targetPath: string): WindowsAc
} else if (lower.startsWith(quotedLower)) {
entry = trimmed.slice(quotedTarget.length).trim();
}
if (!entry) continue;
if (!entry) {
continue;
}
const idx = entry.indexOf(":");
if (idx === -1) continue;
if (idx === -1) {
continue;
}
const principal = entry.slice(0, idx).trim();
const rawRights = entry.slice(idx + 1).trim();
@@ -119,9 +131,13 @@ export function parseIcaclsOutput(output: string, targetPath: string): WindowsAc
.match(/\(([^)]+)\)/g)
?.map((token) => token.slice(1, -1).trim())
.filter(Boolean) ?? [];
if (tokens.some((token) => token.toUpperCase() === "DENY")) continue;
if (tokens.some((token) => token.toUpperCase() === "DENY")) {
continue;
}
const rights = tokens.filter((token) => !INHERIT_FLAGS.has(token.toUpperCase()));
if (rights.length === 0) continue;
if (rights.length === 0) {
continue;
}
const { canRead, canWrite } = rightsFromTokens(rights);
entries.push({ principal, rights, rawRights, canRead, canWrite });
}
@@ -138,9 +154,13 @@ export function summarizeWindowsAcl(
const untrustedGroup: WindowsAclEntry[] = [];
for (const entry of entries) {
const classification = classifyPrincipal(entry.principal, env);
if (classification === "trusted") trusted.push(entry);
else if (classification === "world") untrustedWorld.push(entry);
else untrustedGroup.push(entry);
if (classification === "trusted") {
trusted.push(entry);
} else if (classification === "world") {
untrustedWorld.push(entry);
} else {
untrustedGroup.push(entry);
}
}
return { trusted, untrustedWorld, untrustedGroup };
}
@@ -169,9 +189,13 @@ export async function inspectWindowsAcl(
}
export function formatWindowsAclSummary(summary: WindowsAclSummary): string {
if (!summary.ok) return "unknown";
if (!summary.ok) {
return "unknown";
}
const untrusted = [...summary.untrustedWorld, ...summary.untrustedGroup];
if (untrusted.length === 0) return "trusted-only";
if (untrusted.length === 0) {
return "trusted-only";
}
return untrusted.map((entry) => `${entry.principal}:${entry.rawRights}`).join(", ");
}
@@ -189,7 +213,9 @@ export function createIcaclsResetCommand(
opts: { isDir: boolean; env?: NodeJS.ProcessEnv },
): { command: string; args: string[]; display: string } | null {
const user = resolveWindowsUserPrincipal(opts.env);
if (!user) return null;
if (!user) {
return null;
}
const grant = opts.isDir ? "(OI)(CI)F" : "F";
const args = [
targetPath,