mirror of https://github.com/openclaw/openclaw.git
229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
import os from "node:os";
|
|
import { runExec } from "../process/exec.js";
|
|
|
|
export type ExecFn = typeof runExec;
|
|
|
|
export type WindowsAclEntry = {
|
|
principal: string;
|
|
rights: string[];
|
|
rawRights: string;
|
|
canRead: boolean;
|
|
canWrite: boolean;
|
|
};
|
|
|
|
export type WindowsAclSummary = {
|
|
ok: boolean;
|
|
entries: WindowsAclEntry[];
|
|
untrustedWorld: WindowsAclEntry[];
|
|
untrustedGroup: WindowsAclEntry[];
|
|
trusted: WindowsAclEntry[];
|
|
error?: string;
|
|
};
|
|
|
|
const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
|
|
const WORLD_PRINCIPALS = new Set([
|
|
"everyone",
|
|
"users",
|
|
"builtin\\users",
|
|
"authenticated users",
|
|
"nt authority\\authenticated users",
|
|
]);
|
|
const TRUSTED_BASE = new Set([
|
|
"nt authority\\system",
|
|
"system",
|
|
"builtin\\administrators",
|
|
"creator owner",
|
|
]);
|
|
const WORLD_SUFFIXES = ["\\users", "\\authenticated users"];
|
|
const TRUSTED_SUFFIXES = ["\\administrators", "\\system"];
|
|
|
|
const normalize = (value: string) => value.trim().toLowerCase();
|
|
|
|
export function resolveWindowsUserPrincipal(env?: NodeJS.ProcessEnv): string | null {
|
|
const username = env?.USERNAME?.trim() || os.userInfo().username?.trim();
|
|
if (!username) {
|
|
return null;
|
|
}
|
|
const domain = env?.USERDOMAIN?.trim();
|
|
return domain ? `${domain}\\${username}` : username;
|
|
}
|
|
|
|
function buildTrustedPrincipals(env?: NodeJS.ProcessEnv): Set<string> {
|
|
const trusted = new Set<string>(TRUSTED_BASE);
|
|
const principal = resolveWindowsUserPrincipal(env);
|
|
if (principal) {
|
|
trusted.add(normalize(principal));
|
|
const parts = principal.split("\\");
|
|
const userOnly = parts.at(-1);
|
|
if (userOnly) {
|
|
trusted.add(normalize(userOnly));
|
|
}
|
|
}
|
|
return trusted;
|
|
}
|
|
|
|
function classifyPrincipal(
|
|
principal: string,
|
|
env?: NodeJS.ProcessEnv,
|
|
): "trusted" | "world" | "group" {
|
|
const normalized = normalize(principal);
|
|
const trusted = buildTrustedPrincipals(env);
|
|
if (trusted.has(normalized) || TRUSTED_SUFFIXES.some((s) => normalized.endsWith(s))) {
|
|
return "trusted";
|
|
}
|
|
if (WORLD_PRINCIPALS.has(normalized) || WORLD_SUFFIXES.some((s) => normalized.endsWith(s))) {
|
|
return "world";
|
|
}
|
|
return "group";
|
|
}
|
|
|
|
function rightsFromTokens(tokens: string[]): { canRead: boolean; canWrite: boolean } {
|
|
const upper = tokens.join("").toUpperCase();
|
|
const canWrite =
|
|
upper.includes("F") || upper.includes("M") || upper.includes("W") || upper.includes("D");
|
|
const canRead = upper.includes("F") || upper.includes("M") || upper.includes("R");
|
|
return { canRead, canWrite };
|
|
}
|
|
|
|
export function parseIcaclsOutput(output: string, targetPath: string): WindowsAclEntry[] {
|
|
const entries: WindowsAclEntry[] = [];
|
|
const normalizedTarget = targetPath.trim();
|
|
const lowerTarget = normalizedTarget.toLowerCase();
|
|
const quotedTarget = `"${normalizedTarget}"`;
|
|
const quotedLower = quotedTarget.toLowerCase();
|
|
|
|
for (const rawLine of output.split(/\r?\n/)) {
|
|
const line = rawLine.trimEnd();
|
|
if (!line.trim()) {
|
|
continue;
|
|
}
|
|
const trimmed = line.trim();
|
|
const lower = trimmed.toLowerCase();
|
|
if (
|
|
lower.startsWith("successfully processed") ||
|
|
lower.startsWith("processed") ||
|
|
lower.startsWith("failed processing") ||
|
|
lower.startsWith("no mapping between account names")
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let entry = trimmed;
|
|
if (lower.startsWith(lowerTarget)) {
|
|
entry = trimmed.slice(normalizedTarget.length).trim();
|
|
} else if (lower.startsWith(quotedLower)) {
|
|
entry = trimmed.slice(quotedTarget.length).trim();
|
|
}
|
|
if (!entry) {
|
|
continue;
|
|
}
|
|
|
|
const idx = entry.indexOf(":");
|
|
if (idx === -1) {
|
|
continue;
|
|
}
|
|
|
|
const principal = entry.slice(0, idx).trim();
|
|
const rawRights = entry.slice(idx + 1).trim();
|
|
const tokens =
|
|
rawRights
|
|
.match(/\(([^)]+)\)/g)
|
|
?.map((token) => token.slice(1, -1).trim())
|
|
.filter(Boolean) ?? [];
|
|
if (tokens.some((token) => token.toUpperCase() === "DENY")) {
|
|
continue;
|
|
}
|
|
const rights = tokens.filter((token) => !INHERIT_FLAGS.has(token.toUpperCase()));
|
|
if (rights.length === 0) {
|
|
continue;
|
|
}
|
|
const { canRead, canWrite } = rightsFromTokens(rights);
|
|
entries.push({ principal, rights, rawRights, canRead, canWrite });
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
export function summarizeWindowsAcl(
|
|
entries: WindowsAclEntry[],
|
|
env?: NodeJS.ProcessEnv,
|
|
): Pick<WindowsAclSummary, "trusted" | "untrustedWorld" | "untrustedGroup"> {
|
|
const trusted: WindowsAclEntry[] = [];
|
|
const untrustedWorld: WindowsAclEntry[] = [];
|
|
const untrustedGroup: WindowsAclEntry[] = [];
|
|
for (const entry of entries) {
|
|
const classification = classifyPrincipal(entry.principal, env);
|
|
if (classification === "trusted") {
|
|
trusted.push(entry);
|
|
} else if (classification === "world") {
|
|
untrustedWorld.push(entry);
|
|
} else {
|
|
untrustedGroup.push(entry);
|
|
}
|
|
}
|
|
return { trusted, untrustedWorld, untrustedGroup };
|
|
}
|
|
|
|
export async function inspectWindowsAcl(
|
|
targetPath: string,
|
|
opts?: { env?: NodeJS.ProcessEnv; exec?: ExecFn },
|
|
): Promise<WindowsAclSummary> {
|
|
const exec = opts?.exec ?? runExec;
|
|
try {
|
|
const { stdout, stderr } = await exec("icacls", [targetPath]);
|
|
const output = `${stdout}\n${stderr}`.trim();
|
|
const entries = parseIcaclsOutput(output, targetPath);
|
|
const { trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, opts?.env);
|
|
return { ok: true, entries, trusted, untrustedWorld, untrustedGroup };
|
|
} catch (err) {
|
|
return {
|
|
ok: false,
|
|
entries: [],
|
|
trusted: [],
|
|
untrustedWorld: [],
|
|
untrustedGroup: [],
|
|
error: String(err),
|
|
};
|
|
}
|
|
}
|
|
|
|
export function formatWindowsAclSummary(summary: WindowsAclSummary): string {
|
|
if (!summary.ok) {
|
|
return "unknown";
|
|
}
|
|
const untrusted = [...summary.untrustedWorld, ...summary.untrustedGroup];
|
|
if (untrusted.length === 0) {
|
|
return "trusted-only";
|
|
}
|
|
return untrusted.map((entry) => `${entry.principal}:${entry.rawRights}`).join(", ");
|
|
}
|
|
|
|
export function formatIcaclsResetCommand(
|
|
targetPath: string,
|
|
opts: { isDir: boolean; env?: NodeJS.ProcessEnv },
|
|
): string {
|
|
const user = resolveWindowsUserPrincipal(opts.env) ?? "%USERNAME%";
|
|
const grant = opts.isDir ? "(OI)(CI)F" : "F";
|
|
return `icacls "${targetPath}" /inheritance:r /grant:r "${user}:${grant}" /grant:r "SYSTEM:${grant}"`;
|
|
}
|
|
|
|
export function createIcaclsResetCommand(
|
|
targetPath: string,
|
|
opts: { isDir: boolean; env?: NodeJS.ProcessEnv },
|
|
): { command: string; args: string[]; display: string } | null {
|
|
const user = resolveWindowsUserPrincipal(opts.env);
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
const grant = opts.isDir ? "(OI)(CI)F" : "F";
|
|
const args = [
|
|
targetPath,
|
|
"/inheritance:r",
|
|
"/grant:r",
|
|
`${user}:${grant}`,
|
|
"/grant:r",
|
|
`SYSTEM:${grant}`,
|
|
];
|
|
return { command: "icacls", args, display: formatIcaclsResetCommand(targetPath, opts) };
|
|
}
|