Files
Moltbot/src/security/audit-fs.ts

195 lines
4.6 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,
};
}
const bits = modeBits(st.mode);
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: st.isDir,
mode: st.mode,
bits,
source: "unknown",
worldWritable: false,
groupWritable: false,
worldReadable: false,
groupReadable: false,
error: acl.error,
};
}
return {
ok: true,
isSymlink: st.isSymlink,
isDir: st.isDir,
mode: st.mode,
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: st.isDir,
mode: st.mode,
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;
}