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 & 0 o777;
}
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 & 0 o002) !== 0 ;
}
export function isGroupWritable(bits: number | null ): boolean {
if (bits == null ) {
return false ;
}
return (bits & 0 o020) !== 0 ;
}
export function isWorldReadable(bits: number | null ): boolean {
if (bits == null ) {
return false ;
}
return (bits & 0 o004) !== 0 ;
}
export function isGroupReadable(bits: number | null ): boolean {
if (bits == null ) {
return false ;
}
return (bits & 0 o040) !== 0 ;
}
Messung V0.5 in Prozent C=98 H=96 G=96
¤ Dauer der Verarbeitung: 0.9 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland