fix(exec-approvals): match simple command name patterns in allowlist

Previously, matchAllowlist() skipped all patterns without path separators
(like 'pwd', 'ls', 'date'), making it impossible to allowlist commands by
their simple executable name. The bug was caused by 'if (!hasPath) continue;'
which short-circuited before matching against executableName.

Now simple patterns match against executableName, and path-based patterns
fallback to rawExecutable when resolvedPath is undefined.
This commit is contained in:
Aaron Ring 2026-01-25 20:09:39 -08:00
parent 9daa846457
commit 4bef31e009
2 changed files with 13 additions and 7 deletions

View File

@ -33,7 +33,7 @@ function makeTempDir() {
}
describe("exec approvals allowlist matching", () => {
it("ignores basename-only patterns", () => {
it("matches basename-only patterns case-insensitively", () => {
const resolution = {
rawExecutable: "rg",
resolvedPath: "/opt/homebrew/bin/rg",
@ -41,7 +41,7 @@ describe("exec approvals allowlist matching", () => {
};
const entries: ExecAllowlistEntry[] = [{ pattern: "RG" }];
const match = matchAllowlist(entries, resolution);
expect(match).toBeNull();
expect(match?.pattern).toBe("RG");
});
it("matches by resolved path with **", () => {
@ -66,7 +66,7 @@ describe("exec approvals allowlist matching", () => {
expect(match).toBeNull();
});
it("requires a resolved path", () => {
it("falls back to rawExecutable when resolvedPath is undefined", () => {
const resolution = {
rawExecutable: "bin/rg",
resolvedPath: undefined,
@ -74,7 +74,7 @@ describe("exec approvals allowlist matching", () => {
};
const entries: ExecAllowlistEntry[] = [{ pattern: "bin/rg" }];
const match = matchAllowlist(entries, resolution);
expect(match).toBeNull();
expect(match?.pattern).toBe("bin/rg");
});
});

View File

@ -508,14 +508,20 @@ export function matchAllowlist(
entries: ExecAllowlistEntry[],
resolution: CommandResolution | null,
): ExecAllowlistEntry | null {
if (!entries.length || !resolution?.resolvedPath) return null;
if (!entries.length || !resolution) return null;
const rawExecutable = resolution.rawExecutable;
const resolvedPath = resolution.resolvedPath;
const executableName = resolution.executableName;
for (const entry of entries) {
const pattern = entry.pattern?.trim();
if (!pattern) continue;
const hasPath = pattern.includes("/") || pattern.includes("\\") || pattern.includes("~");
if (!hasPath) continue;
if (matchesPattern(pattern, resolvedPath)) return entry;
if (hasPath) {
const target = resolvedPath ?? rawExecutable;
if (target && matchesPattern(pattern, target)) return entry;
continue;
}
if (executableName && matchesPattern(pattern, executableName)) return entry;
}
return null;
}