mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 07:57:40 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user