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