Files
openclaw/src/security/audit-fs.ts
the sun gif man 114736ed1a Doctor/Security: fix telegram numeric ID + symlink config permission warnings (#19844)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: e42bf1e48d
Co-authored-by: joshp123 <1497361+joshp123@users.noreply.github.com>
Co-authored-by: joshp123 <1497361+joshp123@users.noreply.github.com>
Reviewed-by: @joshp123
2026-02-18 00:09:51 -08:00

207 lines
5.0 KiB
TypeScript

import fs from "node:fs/promises";
import {
formatIcaclsResetCommand,
formatWindowsAclSummary,
inspectWindowsAcl,
type ExecFn,
} from "./windows-acl.js";
export type PermissionCheck = {
ok: boolean;
isSymlink: boolean;
isDir: boolean;
mode: number | null;
bits: number | null;
source: "posix" | "windows-acl" | "unknown";
worldWritable: boolean;
groupWritable: boolean;
worldReadable: boolean;
groupReadable: boolean;
aclSummary?: string;
error?: string;
};
export type PermissionCheckOptions = {
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
exec?: ExecFn;
};
export async function safeStat(targetPath: string): Promise<{
ok: boolean;
isSymlink: boolean;
isDir: boolean;
mode: number | null;
uid: number | null;
gid: number | null;
error?: string;
}> {
try {
const lst = await fs.lstat(targetPath);
return {
ok: true,
isSymlink: lst.isSymbolicLink(),
isDir: lst.isDirectory(),
mode: typeof lst.mode === "number" ? lst.mode : null,
uid: typeof lst.uid === "number" ? lst.uid : null,
gid: typeof lst.gid === "number" ? lst.gid : null,
};
} catch (err) {
return {
ok: false,
isSymlink: false,
isDir: false,
mode: null,
uid: null,
gid: null,
error: String(err),
};
}
}
export async function inspectPathPermissions(
targetPath: string,
opts?: PermissionCheckOptions,
): Promise<PermissionCheck> {
const st = await safeStat(targetPath);
if (!st.ok) {
return {
ok: false,
isSymlink: false,
isDir: false,
mode: null,
bits: null,
source: "unknown",
worldWritable: false,
groupWritable: false,
worldReadable: false,
groupReadable: false,
error: st.error,
};
}
let effectiveMode = st.mode;
let effectiveIsDir = st.isDir;
if (st.isSymlink) {
try {
const target = await fs.stat(targetPath);
effectiveMode = typeof target.mode === "number" ? target.mode : st.mode;
effectiveIsDir = target.isDirectory();
} catch {
// Keep lstat-derived metadata when target lookup fails.
}
}
const bits = modeBits(effectiveMode);
const platform = opts?.platform ?? process.platform;
if (platform === "win32") {
const acl = await inspectWindowsAcl(targetPath, { env: opts?.env, exec: opts?.exec });
if (!acl.ok) {
return {
ok: true,
isSymlink: st.isSymlink,
isDir: effectiveIsDir,
mode: effectiveMode,
bits,
source: "unknown",
worldWritable: false,
groupWritable: false,
worldReadable: false,
groupReadable: false,
error: acl.error,
};
}
return {
ok: true,
isSymlink: st.isSymlink,
isDir: effectiveIsDir,
mode: effectiveMode,
bits,
source: "windows-acl",
worldWritable: acl.untrustedWorld.some((entry) => entry.canWrite),
groupWritable: acl.untrustedGroup.some((entry) => entry.canWrite),
worldReadable: acl.untrustedWorld.some((entry) => entry.canRead),
groupReadable: acl.untrustedGroup.some((entry) => entry.canRead),
aclSummary: formatWindowsAclSummary(acl),
};
}
return {
ok: true,
isSymlink: st.isSymlink,
isDir: effectiveIsDir,
mode: effectiveMode,
bits,
source: "posix",
worldWritable: isWorldWritable(bits),
groupWritable: isGroupWritable(bits),
worldReadable: isWorldReadable(bits),
groupReadable: isGroupReadable(bits),
};
}
export function formatPermissionDetail(targetPath: string, perms: PermissionCheck): string {
if (perms.source === "windows-acl") {
const summary = perms.aclSummary ?? "unknown";
return `${targetPath} acl=${summary}`;
}
return `${targetPath} mode=${formatOctal(perms.bits)}`;
}
export function formatPermissionRemediation(params: {
targetPath: string;
perms: PermissionCheck;
isDir: boolean;
posixMode: number;
env?: NodeJS.ProcessEnv;
}): string {
if (params.perms.source === "windows-acl") {
return formatIcaclsResetCommand(params.targetPath, { isDir: params.isDir, env: params.env });
}
const mode = params.posixMode.toString(8).padStart(3, "0");
return `chmod ${mode} ${params.targetPath}`;
}
export function modeBits(mode: number | null): number | null {
if (mode == null) {
return null;
}
return mode & 0o777;
}
export function formatOctal(bits: number | null): string {
if (bits == null) {
return "unknown";
}
return bits.toString(8).padStart(3, "0");
}
export function isWorldWritable(bits: number | null): boolean {
if (bits == null) {
return false;
}
return (bits & 0o002) !== 0;
}
export function isGroupWritable(bits: number | null): boolean {
if (bits == null) {
return false;
}
return (bits & 0o020) !== 0;
}
export function isWorldReadable(bits: number | null): boolean {
if (bits == null) {
return false;
}
return (bits & 0o004) !== 0;
}
export function isGroupReadable(bits: number | null): boolean {
if (bits == null) {
return false;
}
return (bits & 0o040) !== 0;
}