fix: permissions shell rule normalization (#1189)

This commit is contained in:
Charles Packer
2026-02-26 23:55:07 -08:00
committed by GitHub
parent 34ec2aaa4e
commit 5a3a76e333
11 changed files with 474 additions and 216 deletions

View File

@@ -0,0 +1,193 @@
const SHELL_EXECUTORS = new Set(["bash", "sh", "zsh", "dash", "ksh"]);
function trimMatchingQuotes(value: string): string {
const trimmed = value.trim();
if (trimmed.length < 2) {
return trimmed;
}
const first = trimmed[0];
const last = trimmed[trimmed.length - 1];
if ((first === '"' || first === "'") && last === first) {
return trimmed.slice(1, -1);
}
return trimmed;
}
function normalizeExecutableToken(token: string): string {
const normalized = trimMatchingQuotes(token).replace(/\\/g, "/");
const parts = normalized.split("/").filter(Boolean);
const executable = parts[parts.length - 1] ?? normalized;
return executable.toLowerCase();
}
function tokenizeShell(input: string): string[] {
const tokens: string[] = [];
let current = "";
let quote: "single" | "double" | null = null;
let escaping = false;
const flush = () => {
if (current.length > 0) {
tokens.push(current);
current = "";
}
};
for (let i = 0; i < input.length; i += 1) {
const ch = input[i];
if (ch === undefined) {
continue;
}
if (escaping) {
current += ch;
escaping = false;
continue;
}
if (ch === "\\" && quote !== "single") {
escaping = true;
continue;
}
if (quote === "single") {
if (ch === "'") {
quote = null;
} else {
current += ch;
}
continue;
}
if (quote === "double") {
if (ch === '"') {
quote = null;
} else {
current += ch;
}
continue;
}
if (ch === "'") {
quote = "single";
continue;
}
if (ch === '"') {
quote = "double";
continue;
}
if (/\s/.test(ch)) {
flush();
continue;
}
current += ch;
}
if (escaping) {
current += "\\";
}
flush();
return tokens;
}
function isDashCFlag(token: string): boolean {
return token === "-c" || /^-[a-zA-Z]*c[a-zA-Z]*$/.test(token);
}
function extractInnerShellCommand(tokens: string[]): string | null {
if (tokens.length === 0) {
return null;
}
let index = 0;
if (normalizeExecutableToken(tokens[0] ?? "") === "env") {
index += 1;
while (index < tokens.length) {
const token = tokens[index] ?? "";
if (!token) {
index += 1;
continue;
}
if (/^-[A-Za-z]+$/.test(token)) {
index += 1;
continue;
}
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
index += 1;
continue;
}
break;
}
}
const executableToken = tokens[index];
if (!executableToken) {
return null;
}
if (!SHELL_EXECUTORS.has(normalizeExecutableToken(executableToken))) {
return null;
}
for (let i = index + 1; i < tokens.length; i += 1) {
const token = tokens[i];
if (!token) {
continue;
}
if (!isDashCFlag(token)) {
continue;
}
const innerCommand = tokens[i + 1];
if (!innerCommand) {
return null;
}
return trimMatchingQuotes(innerCommand);
}
return null;
}
export function unwrapShellLauncherCommand(command: string): string {
let current = command.trim();
for (let depth = 0; depth < 5; depth += 1) {
if (!current) {
break;
}
const tokens = tokenizeShell(current);
const inner = extractInnerShellCommand(tokens);
if (!inner || inner === current) {
break;
}
current = inner.trim();
}
return current;
}
export function normalizeBashRulePayload(payload: string): string {
const trimmed = payload.trim();
if (!trimmed) {
return "";
}
const hasWildcardSuffix = trimmed.endsWith(":*");
const withoutWildcard = hasWildcardSuffix
? trimmed.slice(0, -2).trimEnd()
: trimmed;
const unwrapped = unwrapShellLauncherCommand(withoutWildcard);
if (hasWildcardSuffix) {
return `${unwrapped}:*`;
}
return unwrapped;
}