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 { 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; }