Files
openclaw/src/security/installed-plugin-dirs.ts
Peter Steinberger 49cc59b1e8 refactor: consolidate markdown code fences, error coercion, and byte-identical helper pairs (#99932)
* refactor(shared): add markdown code span/fence helpers and migrate seven call sites

* refactor(normalization-core): add canonical toErrorObject and migrate six copies

* refactor: consolidate byte-identical helper pairs onto owner modules

* refactor: reuse canonical path and token helpers in session tools and credentials

* refactor(packages): dedupe compaction summarization tail and session dir parsing

* fix(security): keep missing extensions dir silent in shared plugin dir lister

* refactor: reuse media-core chunk reader and shared dreaming session key helper

* fix(plugins): register error-coercion subpath in sdk alias table

* chore(plugin-sdk): re-pin callable export count after rebase

* chore(plugin-sdk): re-pin callable export count after rebase
2026-07-04 08:40:41 -04:00

68 lines
2.3 KiB
TypeScript

// Resolves installed plugin directories for security trust audits.
import fs from "node:fs/promises";
import path from "node:path";
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
const IGNORED_INSTALLED_PLUGIN_DIR_NAMES = 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.
*/
export function shouldIgnoreInstalledPluginDirName(name: string): boolean {
const normalized = normalizeOptionalLowercaseString(name);
if (!normalized) {
return true;
}
if (IGNORED_INSTALLED_PLUGIN_DIR_NAMES.has(normalized)) {
return true;
}
if (normalized.startsWith(".")) {
return true;
}
// Failed installs and rollback copies can contain stale plugin code; audit the live
// root once and ignore these generated backups so findings stay actionable.
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.
*/
export async function listInstalledPluginDirs(params: {
stateDir: string;
onReadError?: (error: unknown) => void;
}): Promise<{ extensionsDir: string; pluginDirs: string[] }> {
const extensionsDir = path.join(params.stateDir, "extensions");
const st = await fs.stat(extensionsDir).catch((err: unknown) => {
const code = (err as NodeJS.ErrnoException | null)?.code;
if (code !== "ENOENT" && code !== "ENOTDIR") {
params.onReadError?.(err);
}
return null;
});
if (!st?.isDirectory()) {
return { extensionsDir, pluginDirs: [] };
}
const entries = await fs.readdir(extensionsDir, { withFileTypes: true }).catch((err: unknown) => {
params.onReadError?.(err);
return [];
});
const pluginDirs = entries
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter((name) => !shouldIgnoreInstalledPluginDirName(name))
.filter(Boolean);
return { extensionsDir, pluginDirs };
}