openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
693 lines (692 loc) • 29.4 kB
JavaScript
import { c as normalizeOptionalLowercaseString, l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js";
import { d as normalizeStringEntries, p as normalizeTrimmedStringList, y as uniqueStrings } from "./string-normalization-DsCfAx8q.js";
import { i as getOrCreatePromise } from "./lazy-promise-DGqyc4Y4.js";
import { i as createLazyRuntimeNamedExport, r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js";
import { n as isPathInside } from "./path-safety-Bi0ppMWC.js";
import { M as extensionUsesSkippedScannerPath } from "./redact-BtvPPfTi.js";
import { T as statRegularFile, g as readRegularFile } from "./fs-safe-B6pvPGnf.js";
import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { C as resolveOAuthDir } from "./paths-D2sRr1a_.js";
import { r as LEGACY_IMPLICIT_AGENT_ID } from "./session-key-BnWWjqNc.js";
import { n as MANIFEST_KEY } from "./legacy-names-NIXaj2oi.js";
import { p as resolveAuthProfileDatabaseFilePaths } from "./sqlite-MN_7y26V.js";
import { a as loadWorkspaceSkills } from "./workspace-skill-loader-_a5Sci8B.js";
import { t as collectIncludePathsRecursive } from "./includes-scan-DS8yzT0B.js";
import path from "node:path";
import fs from "node:fs/promises";
import { clearTimeout, setTimeout } from "node:timers";
//#region src/security/installed-plugin-dirs.ts
const IGNORED_INSTALLED_PLUGIN_DIR_NAMES = /* @__PURE__ */ new Set(["node_modules", ".openclaw-install-backups"]);
/**
* Decide whether an installed-plugin directory should be skipped by security audits.
* This filters generated install debris while keeping real plugin roots visible to scans.
*/
function shouldIgnoreInstalledPluginDirName(name) {
const normalized = normalizeOptionalLowercaseString(name);
if (!normalized) return true;
if (IGNORED_INSTALLED_PLUGIN_DIR_NAMES.has(normalized)) return true;
if (normalized.startsWith(".")) return true;
if (normalized.endsWith(".bak")) return true;
if (normalized.includes(".backup-")) return true;
if (normalized.includes(".disabled")) return true;
return false;
}
/**
* Lists installed plugin directories under the state extensions dir. Read
* failures surface through `onReadError` so audits can report scan problems,
* except a missing extensions dir, which is the normal no-plugins state.
*/
async function listInstalledPluginDirs(params) {
const extensionsDir = path.join(params.stateDir, "extensions");
if (!(await fs.stat(extensionsDir).catch((err) => {
const code = err?.code;
if (code !== "ENOENT" && code !== "ENOTDIR") params.onReadError?.(err);
return null;
}))?.isDirectory()) return {
extensionsDir,
pluginDirs: []
};
return {
extensionsDir,
pluginDirs: (await fs.readdir(extensionsDir, { withFileTypes: true }).catch((err) => {
params.onReadError?.(err);
return [];
})).filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => !shouldIgnoreInstalledPluginDirName(name)).filter(Boolean)
};
}
//#endregion
//#region src/security/audit-extra.async.ts
/**
* Asynchronous security audit collector functions.
*
* These functions perform I/O (filesystem, config reads) to detect security issues.
*/
const DEFAULT_SANDBOX_BROWSER_DOCKER_PROBE_TIMEOUT_MS = 5e3;
const loadConfigModule = createLazyRuntimeModule(() => import("./config/config.js"));
const loadAuditFsModule = createLazyRuntimeModule(() => import("./audit-fs-Db1yHgCp.js"));
const loadAgentScopeModule = createLazyRuntimeModule(() => import("./agent-scope-BHLMs19p.js"));
const loadAgentWorkspaceDirsModule = createLazyRuntimeModule(() => import("./workspace-dirs-W0GO5ss3.js"));
const loadSkillSourceModule = createLazyRuntimeModule(() => import("./source-C03Gx07v.js"));
const loadSkillScannerModule = createLazyRuntimeModule(() => import("./scanner-BW4bRiC3.js"));
const loadExecDockerRaw = createLazyRuntimeNamedExport(() => import("./docker-hZQaWun_.js"), "execDockerRaw");
const loadSandboxBrowserSecurityHashEpoch = createLazyRuntimeNamedExport(() => import("./constants-Bmo03HGb.js"), "SANDBOX_BROWSER_SECURITY_HASH_EPOCH");
function expandTilde(p, env) {
if (!p.startsWith("~")) return p;
const home = normalizeOptionalString(env.HOME) ?? null;
if (!home) return null;
if (p === "~") return home;
if (p.startsWith("~/") || p.startsWith("~\\")) return path.join(home, p.slice(2));
return null;
}
const MAX_PLUGIN_MANIFEST_BYTES = 1048576;
const MAX_SKILL_AUDIT_FILE_BYTES = 256e3;
async function readPluginManifestExtensions(pluginPath) {
const manifestPath = path.join(pluginPath, "package.json");
const statResult = await statRegularFile(manifestPath);
if (statResult.missing) return [];
if (statResult.stat.size > MAX_PLUGIN_MANIFEST_BYTES) throw new Error(`Plugin manifest at ${manifestPath} is too large (${statResult.stat.size} bytes, max ${MAX_PLUGIN_MANIFEST_BYTES})`);
const { buffer } = await readRegularFile({
filePath: manifestPath,
maxBytes: MAX_PLUGIN_MANIFEST_BYTES
});
const raw = buffer.toString("utf-8");
if (!raw.trim()) return [];
let parsed;
try {
parsed = JSON.parse(raw);
} catch (err) {
throw new Error(`Failed to parse plugin manifest at ${manifestPath}: ${String(err)}`, { cause: err });
}
const extensions = parsed?.[MANIFEST_KEY]?.extensions;
if (!Array.isArray(extensions)) return [];
return normalizeTrimmedStringList(extensions);
}
function formatCodeSafetyDetails(findings, rootDir) {
return findings.map((finding) => {
const relPath = path.relative(rootDir, finding.file);
const normalizedPath = (relPath && relPath !== "." && !relPath.startsWith("..") ? relPath : path.basename(finding.file)).replaceAll("\\", "/");
return ` - [${finding.ruleId}] ${finding.message} (${normalizedPath}:${finding.line})`;
}).join("\n");
}
function buildCodeSafetySummaryCacheKey(params) {
const includeFiles = normalizeStringEntries(params.includeFiles);
const includeKey = includeFiles.length > 0 ? includeFiles.toSorted().join("\0") : "";
return `${params.dirPath}\u0000${includeKey}`;
}
async function getCodeSafetySummary(params) {
const cacheKey = buildCodeSafetySummaryCacheKey({
dirPath: params.dirPath,
includeFiles: params.includeFiles
});
const scan = async () => {
return await (await loadSkillScannerModule()).scanDirectoryWithSummary(params.dirPath, { includeFiles: params.includeFiles });
};
return params.summaryCache ? await getOrCreatePromise(params.summaryCache, cacheKey, scan) : await scan();
}
async function getSkillCodeSafetySummary(params) {
const [summary, skillContent, skillScanner] = await Promise.all([
getCodeSafetySummary({
dirPath: params.dirPath,
summaryCache: params.summaryCache
}),
readRegularFile({
filePath: params.skillFilePath,
maxBytes: MAX_SKILL_AUDIT_FILE_BYTES
}).then(({ buffer }) => buffer.toString("utf-8")),
loadSkillScannerModule()
]);
const skillFindings = [...skillScanner.scanSkillContent(skillContent, params.skillFilePath), ...skillScanner.scanSource(skillContent, params.skillFilePath)];
return {
...summary,
scannedFiles: summary.scannedFiles + 1,
critical: summary.critical + skillFindings.filter((finding) => finding.severity === "critical").length,
warn: summary.warn + skillFindings.filter((finding) => finding.severity === "warn").length,
info: summary.info + skillFindings.filter((finding) => finding.severity === "info").length,
findings: [...summary.findings, ...skillFindings]
};
}
function normalizeDockerLabelValue(raw) {
const trimmed = normalizeOptionalString(raw) ?? "";
if (!trimmed || trimmed === "<no value>") return null;
return trimmed;
}
var DockerProbeTimeoutError = class extends Error {
constructor(timeoutMs) {
super(`Docker probe timed out after ${timeoutMs}ms`);
this.name = "DockerProbeTimeoutError";
}
};
function normalizeDockerProbeTimeoutMs(timeoutMs) {
if (Number.isFinite(timeoutMs) && timeoutMs !== void 0) return Math.max(250, Math.floor(timeoutMs));
return DEFAULT_SANDBOX_BROWSER_DOCKER_PROBE_TIMEOUT_MS;
}
async function withDockerProbeTimeout(timeoutMs, run) {
const controller = new AbortController();
let timeout;
let timedOut = false;
const timeoutPromise = new Promise((_, reject) => {
timeout = setTimeout(() => {
timedOut = true;
controller.abort();
reject(new DockerProbeTimeoutError(timeoutMs));
}, timeoutMs);
});
try {
return await Promise.race([run(controller.signal), timeoutPromise]);
} catch (err) {
if (timedOut || controller.signal.aborted) throw new DockerProbeTimeoutError(timeoutMs);
throw err;
} finally {
if (timeout) clearTimeout(timeout);
}
}
function isDockerProbeTimeoutError(error) {
return error instanceof DockerProbeTimeoutError;
}
async function listSandboxBrowserContainers(params) {
try {
const result = await withDockerProbeTimeout(params.timeoutMs, (signal) => params.execDockerRawFn([
"ps",
"-a",
"--filter",
"label=openclaw.sandboxBrowser=1",
"--format",
"{{.Names}}"
], {
allowFailure: true,
signal
}));
if (result.code !== 0) return null;
return normalizeStringEntries(result.stdout.toString("utf8").split(/\r?\n/));
} catch (err) {
if (isDockerProbeTimeoutError(err)) params.onTimeout?.();
return null;
}
}
async function readSandboxBrowserHashLabels(params) {
try {
const result = await withDockerProbeTimeout(params.timeoutMs, (signal) => params.execDockerRawFn([
"inspect",
"-f",
"{{ index .Config.Labels \"openclaw.configHash\" }} {{ index .Config.Labels \"openclaw.browserConfigEpoch\" }}",
params.containerName
], {
allowFailure: true,
signal
}));
if (result.code !== 0) return null;
const [hashRaw, epochRaw] = result.stdout.toString("utf8").split(" ");
return {
configHash: normalizeDockerLabelValue(hashRaw),
epoch: normalizeDockerLabelValue(epochRaw)
};
} catch (err) {
if (isDockerProbeTimeoutError(err)) params.onTimeout?.();
return null;
}
}
function parsePublishedHostFromDockerPortLine(line) {
const trimmed = normalizeOptionalString(line) ?? "";
const rhs = trimmed.includes("->") ? normalizeOptionalString(trimmed.split("->").at(-1)) ?? "" : trimmed;
if (!rhs) return null;
const bracketHost = rhs.match(/^\[([^\]]+)\]:\d+$/);
if (bracketHost?.[1]) return bracketHost[1];
const hostPort = rhs.match(/^([^:]+):\d+$/);
if (hostPort?.[1]) return hostPort[1];
return null;
}
function isLoopbackPublishHost(host) {
const normalized = normalizeOptionalLowercaseString(host);
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
}
async function readSandboxBrowserPortMappings(params) {
try {
const result = await withDockerProbeTimeout(params.timeoutMs, (signal) => params.execDockerRawFn(["port", params.containerName], {
allowFailure: true,
signal
}));
if (result.code !== 0) return null;
return normalizeStringEntries(result.stdout.toString("utf8").split(/\r?\n/));
} catch (err) {
if (isDockerProbeTimeoutError(err)) params.onTimeout?.();
return null;
}
}
async function collectSandboxBrowserHashLabelFindings(params) {
const findings = [];
const timeoutMs = normalizeDockerProbeTimeoutMs(params?.timeoutMs);
let timedOut = false;
const markTimedOut = () => {
timedOut = true;
};
const [execFn, browserHashEpoch] = await Promise.all([params?.execDockerRawFn ? Promise.resolve(params.execDockerRawFn) : loadExecDockerRaw(), loadSandboxBrowserSecurityHashEpoch()]);
const containers = await listSandboxBrowserContainers({
execDockerRawFn: execFn,
timeoutMs,
onTimeout: markTimedOut
});
if (!containers || containers.length === 0) {
if (timedOut) findings.push(buildSandboxBrowserDockerProbeTimeoutFinding(timeoutMs));
return findings;
}
const missingHash = [];
const staleEpoch = [];
const nonLoopbackPublished = [];
for (const containerName of containers) {
const labels = await readSandboxBrowserHashLabels({
containerName,
execDockerRawFn: execFn,
timeoutMs,
onTimeout: markTimedOut
});
if (timedOut) break;
if (!labels) continue;
if (!labels.configHash) missingHash.push(containerName);
if (labels.epoch !== browserHashEpoch) staleEpoch.push(containerName);
const portMappings = await readSandboxBrowserPortMappings({
containerName,
execDockerRawFn: execFn,
timeoutMs,
onTimeout: markTimedOut
});
if (timedOut) break;
if (!portMappings?.length) continue;
const exposedMappings = portMappings.filter((line) => {
const host = parsePublishedHostFromDockerPortLine(line);
return Boolean(host && !isLoopbackPublishHost(host));
});
if (exposedMappings.length > 0) nonLoopbackPublished.push(`${containerName} (${exposedMappings.join("; ")})`);
}
if (missingHash.length > 0) findings.push({
checkId: "sandbox.browser_container.hash_label_missing",
severity: "warn",
title: "Sandbox browser container missing config hash label",
detail: `Containers: ${missingHash.join(", ")}. These browser containers predate hash-based drift checks and may miss security remediations until recreated.`,
remediation: `${formatCliCommand("openclaw sandbox recreate --browser --all")} (add --force to skip prompt).`
});
if (staleEpoch.length > 0) findings.push({
checkId: "sandbox.browser_container.hash_epoch_stale",
severity: "warn",
title: "Sandbox browser container hash epoch is stale",
detail: `Containers: ${staleEpoch.join(", ")}. Expected openclaw.browserConfigEpoch=${browserHashEpoch}.`,
remediation: `${formatCliCommand("openclaw sandbox recreate --browser --all")} (add --force to skip prompt).`
});
if (nonLoopbackPublished.length > 0) findings.push({
checkId: "sandbox.browser_container.non_loopback_publish",
severity: "critical",
title: "Sandbox browser container publishes ports on non-loopback interfaces",
detail: `Containers: ${nonLoopbackPublished.join(", ")}. Sandbox browser observer/control ports should stay loopback-only to avoid unintended remote access.`,
remediation: `${formatCliCommand("openclaw sandbox recreate --browser --all")} (add --force to skip prompt), then verify published ports are bound to 127.0.0.1.`
});
if (timedOut) findings.push(buildSandboxBrowserDockerProbeTimeoutFinding(timeoutMs));
return findings;
}
function buildSandboxBrowserDockerProbeTimeoutFinding(timeoutMs) {
return {
checkId: "sandbox.browser_container.docker_probe_timeout",
severity: "warn",
title: "Sandbox browser Docker audit probe timed out",
detail: `Docker did not answer within ${timeoutMs}ms while checking sandbox browser containers. OpenClaw skipped any remaining sandbox browser container drift checks for this status run.`,
remediation: "Retry after Docker is responsive, or recreate sandbox browser containers if drift is suspected."
};
}
async function collectIncludeFilePermFindings(params) {
const findings = [];
if (!params.configSnapshot.exists) return findings;
const configPath = params.configSnapshot.path;
const includePaths = await collectIncludePathsRecursive({
configPath,
parsed: params.configSnapshot.parsed,
env: params.env
});
if (includePaths.length === 0) return findings;
const { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions } = await loadAuditFsModule();
for (const p of includePaths) {
const perms = await inspectPathPermissions(p, {
env: params.env,
platform: params.platform,
exec: params.execIcacls
});
if (!perms.ok) continue;
if (perms.worldWritable || perms.groupWritable) findings.push({
checkId: "fs.config_include.perms_writable",
severity: "critical",
title: "Config include file is writable by others",
detail: `${formatPermissionDetail(p, perms)}; another user could influence your effective config.`,
remediation: formatPermissionRemediation({
targetPath: p,
perms,
isDir: false,
posixMode: 384,
env: params.env
})
});
else if (perms.worldReadable) findings.push({
checkId: "fs.config_include.perms_world_readable",
severity: "critical",
title: "Config include file is world-readable",
detail: `${formatPermissionDetail(p, perms)}; include files can contain tokens and private settings.`,
remediation: formatPermissionRemediation({
targetPath: p,
perms,
isDir: false,
posixMode: 384,
env: params.env
})
});
else if (perms.groupReadable) findings.push({
checkId: "fs.config_include.perms_group_readable",
severity: "warn",
title: "Config include file is group-readable",
detail: `${formatPermissionDetail(p, perms)}; include files can contain tokens and private settings.`,
remediation: formatPermissionRemediation({
targetPath: p,
perms,
isDir: false,
posixMode: 384,
env: params.env
})
});
}
return findings;
}
async function collectStateDeepFilesystemFindings(params) {
const findings = [];
const oauthDir = resolveOAuthDir(params.env, params.stateDir);
const { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions } = await loadAuditFsModule();
const oauthPerms = await inspectPathPermissions(oauthDir, {
env: params.env,
platform: params.platform,
exec: params.execIcacls
});
if (oauthPerms.ok && oauthPerms.isDir) {
if (oauthPerms.worldWritable || oauthPerms.groupWritable) findings.push({
checkId: "fs.credentials_dir.perms_writable",
severity: "critical",
title: "Credentials dir is writable by others",
detail: `${formatPermissionDetail(oauthDir, oauthPerms)}; another user could drop/modify credential files.`,
remediation: formatPermissionRemediation({
targetPath: oauthDir,
perms: oauthPerms,
isDir: true,
posixMode: 448,
env: params.env
})
});
else if (oauthPerms.groupReadable || oauthPerms.worldReadable) findings.push({
checkId: "fs.credentials_dir.perms_readable",
severity: "warn",
title: "Credentials dir is readable by others",
detail: `${formatPermissionDetail(oauthDir, oauthPerms)}; credentials and allowlists can be sensitive.`,
remediation: formatPermissionRemediation({
targetPath: oauthDir,
perms: oauthPerms,
isDir: true,
posixMode: 448,
env: params.env
})
});
}
const agentScope = await loadAgentScopeModule();
const agentIds = agentScope.listAgentEntries(params.cfg).map((agent) => agent.id);
let defaultAgentId;
if (agentIds.length > 0) try {
defaultAgentId = agentScope.resolveDefaultAgentId(params.cfg);
} catch {}
const ids = uniqueStrings([
LEGACY_IMPLICIT_AGENT_ID,
...defaultAgentId ? [defaultAgentId] : [],
...agentIds
]).map((id) => normalizeAgentId(id));
for (const agentId of ids) {
const agentDir = path.join(params.stateDir, "agents", agentId, "agent");
const authTargets = [{
path: path.join(agentDir, "auth-profiles.json"),
label: "legacy auth-profiles.json"
}, ...resolveAuthProfileDatabaseFilePaths(agentDir).map((targetPath) => ({
path: targetPath,
label: "auth profile SQLite store"
}))];
for (const authTarget of authTargets) {
const authPerms = await inspectPathPermissions(authTarget.path, {
env: params.env,
platform: params.platform,
exec: params.execIcacls
});
if (authPerms.ok) {
if (authPerms.worldWritable || authPerms.groupWritable) findings.push({
checkId: "fs.auth_profiles.perms_writable",
severity: "critical",
title: `${authTarget.label} is writable by others`,
detail: `${formatPermissionDetail(authTarget.path, authPerms)}; another user could inject credentials.`,
remediation: formatPermissionRemediation({
targetPath: authTarget.path,
perms: authPerms,
isDir: false,
posixMode: 384,
env: params.env
})
});
else if (authPerms.worldReadable || authPerms.groupReadable) findings.push({
checkId: "fs.auth_profiles.perms_readable",
severity: "warn",
title: `${authTarget.label} is readable by others`,
detail: `${formatPermissionDetail(authTarget.path, authPerms)}; auth profile storage contains API keys and OAuth tokens.`,
remediation: formatPermissionRemediation({
targetPath: authTarget.path,
perms: authPerms,
isDir: false,
posixMode: 384,
env: params.env
})
});
}
}
const storePath = path.join(params.stateDir, "agents", agentId, "sessions", "sessions.json");
const storePerms = await inspectPathPermissions(storePath, {
env: params.env,
platform: params.platform,
exec: params.execIcacls
});
if (storePerms.ok) {
if (storePerms.worldReadable || storePerms.groupReadable) findings.push({
checkId: "fs.sessions_store.perms_readable",
severity: "warn",
title: "sessions.json is readable by others",
detail: `${formatPermissionDetail(storePath, storePerms)}; routing and transcript metadata can be sensitive.`,
remediation: formatPermissionRemediation({
targetPath: storePath,
perms: storePerms,
isDir: false,
posixMode: 384,
env: params.env
})
});
}
}
const logFile = normalizeOptionalString(params.cfg.logging?.file) ?? "";
if (logFile) {
const expanded = logFile.startsWith("~") ? expandTilde(logFile, params.env) : logFile;
if (expanded) {
const logPath = path.resolve(expanded);
const logPerms = await inspectPathPermissions(logPath, {
env: params.env,
platform: params.platform,
exec: params.execIcacls
});
if (logPerms.ok) {
if (logPerms.worldReadable || logPerms.groupReadable) findings.push({
checkId: "fs.log_file.perms_readable",
severity: "warn",
title: "Log file is readable by others",
detail: `${formatPermissionDetail(logPath, logPerms)}; logs can contain private messages and tool output.`,
remediation: formatPermissionRemediation({
targetPath: logPath,
perms: logPerms,
isDir: false,
posixMode: 384,
env: params.env
})
});
}
}
}
return findings;
}
async function readConfigSnapshotForAudit(params) {
const { createConfigIO } = await loadConfigModule();
return await createConfigIO({
env: params.env,
configPath: params.configPath
}).readConfigFileSnapshot();
}
async function collectPluginsCodeSafetyFindings(params) {
const findings = [];
const { extensionsDir, pluginDirs } = await listInstalledPluginDirs({
stateDir: params.stateDir,
onReadError: (err) => {
findings.push({
checkId: "plugins.code_safety.scan_failed",
severity: "warn",
title: "Plugin extensions directory scan failed",
detail: `Static code scan could not list extensions directory: ${String(err)}`,
remediation: "Check file permissions and plugin layout, then rerun `openclaw security audit --deep`."
});
}
});
for (const pluginName of pluginDirs) {
const pluginPath = path.join(extensionsDir, pluginName);
let extensionEntries = [];
try {
extensionEntries = await readPluginManifestExtensions(pluginPath);
} catch (manifestErr) {
findings.push({
checkId: "plugins.code_safety.manifest_parse_error",
severity: "warn",
title: `Plugin "${pluginName}" has a malformed package.json`,
detail: `Could not parse plugin manifest: ${String(manifestErr)}.\nThe extension entrypoint list is unavailable. Deep scan will cover the plugin directory but may miss entries declared via \`openclaw.extensions\`.`,
remediation: "Inspect the plugin package.json for syntax errors. If the plugin is untrusted, remove it from your OpenClaw extensions state directory."
});
}
const forcedScanEntries = [];
const escapedEntries = [];
for (const entry of extensionEntries) {
const resolvedEntry = path.resolve(pluginPath, entry);
if (!isPathInside(pluginPath, resolvedEntry)) {
escapedEntries.push(entry);
continue;
}
if (extensionUsesSkippedScannerPath(entry)) findings.push({
checkId: "plugins.code_safety.entry_path",
severity: "warn",
title: `Plugin "${pluginName}" entry path is hidden or node_modules`,
detail: `Extension entry "${entry}" points to a hidden or node_modules path. Deep code scan will cover this entry explicitly, but review this path choice carefully.`,
remediation: "Prefer extension entrypoints under normal source paths like dist/ or src/."
});
forcedScanEntries.push(resolvedEntry);
}
if (escapedEntries.length > 0) findings.push({
checkId: "plugins.code_safety.entry_escape",
severity: "critical",
title: `Plugin "${pluginName}" has extension entry path traversal`,
detail: `Found extension entries that escape the plugin directory:\n${escapedEntries.map((entry) => ` - ${entry}`).join("\n")}`,
remediation: "Update the plugin manifest so all openclaw.extensions entries stay inside the plugin directory."
});
const summary = await getCodeSafetySummary({
dirPath: pluginPath,
includeFiles: forcedScanEntries,
summaryCache: params.summaryCache
}).catch((err) => {
findings.push({
checkId: "plugins.code_safety.scan_failed",
severity: "warn",
title: `Plugin "${pluginName}" code scan failed`,
detail: `Static code scan could not complete: ${String(err)}`,
remediation: "Check file permissions and plugin layout, then rerun `openclaw security audit --deep`."
});
return null;
});
if (!summary) continue;
if (summary.critical > 0) {
const details = formatCodeSafetyDetails(summary.findings.filter((f) => f.severity === "critical"), pluginPath);
findings.push({
checkId: "plugins.code_safety",
severity: "critical",
title: `Plugin "${pluginName}" contains dangerous code patterns`,
detail: `Found ${summary.critical} critical issue(s) in ${summary.scannedFiles} scanned file(s):\n${details}`,
remediation: "Review the plugin source code carefully before use. If untrusted, remove the plugin from your OpenClaw extensions state directory."
});
} else if (summary.warn > 0) {
const details = formatCodeSafetyDetails(summary.findings.filter((f) => f.severity === "warn"), pluginPath);
findings.push({
checkId: "plugins.code_safety",
severity: "warn",
title: `Plugin "${pluginName}" contains suspicious code patterns`,
detail: `Found ${summary.warn} warning(s) in ${summary.scannedFiles} scanned file(s):\n${details}`,
remediation: `Review the flagged code to ensure it is intentional and safe.`
});
}
}
return findings;
}
async function collectInstalledSkillsCodeSafetyFindings(params) {
const findings = [];
const pluginExtensionsDir = path.join(params.stateDir, "extensions");
const scannedSkillDirs = /* @__PURE__ */ new Set();
const [{ listAgentWorkspaceDirs, listExplicitAgentWorkspaceDirs }, { resolveSkillSource }] = await Promise.all([loadAgentWorkspaceDirsModule(), loadSkillSourceModule()]);
const workspaceDirs = new Set(params.workspaceDir ? [params.workspaceDir] : []);
try {
for (const workspaceDir of listAgentWorkspaceDirs(params.cfg)) workspaceDirs.add(workspaceDir);
} catch {
for (const workspaceDir of listExplicitAgentWorkspaceDirs(params.cfg)) workspaceDirs.add(workspaceDir);
}
for (const workspaceDir of workspaceDirs) {
const entries = loadWorkspaceSkills(workspaceDir, { config: params.cfg });
for (const entry of entries) {
if (resolveSkillSource(entry.skill) === "openclaw-bundled") continue;
const skillDir = path.resolve(entry.skill.baseDir);
if (isPathInside(pluginExtensionsDir, skillDir)) continue;
if (scannedSkillDirs.has(skillDir)) continue;
scannedSkillDirs.add(skillDir);
const skillName = entry.skill.name;
const summary = await getSkillCodeSafetySummary({
dirPath: skillDir,
skillFilePath: entry.skill.filePath,
summaryCache: params.summaryCache
}).catch((err) => {
findings.push({
checkId: "skills.code_safety.scan_failed",
severity: "warn",
title: `Skill "${skillName}" code scan failed`,
detail: `Static code scan could not complete for ${skillDir}: ${String(err)}`,
remediation: "Check file permissions and skill layout, then rerun `openclaw security audit --deep`."
});
return null;
});
if (!summary) continue;
if (summary.critical > 0) {
const details = formatCodeSafetyDetails(summary.findings.filter((finding) => finding.severity === "critical"), skillDir);
findings.push({
checkId: "skills.code_safety",
severity: "critical",
title: `Skill "${skillName}" contains dangerous code patterns`,
detail: `Found ${summary.critical} critical issue(s) in ${summary.scannedFiles} scanned file(s) under ${skillDir}:\n${details}`,
remediation: `Review the skill source code before use. If untrusted, remove "${skillDir}".`
});
} else if (summary.warn > 0) {
const details = formatCodeSafetyDetails(summary.findings.filter((finding) => finding.severity === "warn"), skillDir);
findings.push({
checkId: "skills.code_safety",
severity: "warn",
title: `Skill "${skillName}" contains suspicious code patterns`,
detail: `Found ${summary.warn} warning(s) in ${summary.scannedFiles} scanned file(s) under ${skillDir}:\n${details}`,
remediation: "Review flagged lines to ensure the behavior is intentional and safe."
});
}
}
}
return findings;
}
//#endregion
export { collectStateDeepFilesystemFindings as a, collectSandboxBrowserHashLabelFindings as i, collectInstalledSkillsCodeSafetyFindings as n, readConfigSnapshotForAudit as o, collectPluginsCodeSafetyFindings as r, listInstalledPluginDirs as s, collectIncludeFilePermFindings as t };