fix: bind package-manager exec approvals to inner commands [AI] (#102035)

* fix: bind package-manager exec approvals to inner commands

* fix: fail closed package manager exec approval gaps

* chore: refresh pr branch metadata

* fix: cover npm exec alias approvals

* fix: fail closed hidden package manager exec aliases

* fix: unwrap chained package manager approvals

* fix: cover yarn package manager approvals

* fix: preserve yarn run approval compatibility

* fix: bind package manager script argv checks

* test: fix package manager script argv type check

* fix: fail closed pnpm shorthand approvals

* fix: preserve pnpm built-in approvals

* fix: preserve pnpm script shortcuts

* fix: preserve npm cwd exec unwrapping

* fix: fail closed cwd and yarn package shorthands
This commit is contained in:
Pavan Kumar Gondhi
2026-07-08 21:29:02 +05:30
committed by GitHub
parent c69724ef3b
commit 534ace4d8a
10 changed files with 1651 additions and 232 deletions

View File

@@ -590,6 +590,13 @@ describe("hardenApprovedExecutionPaths", () => {
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 5,
},
{
name: "pnpm cwd exec tsx file",
argv: ["pnpm", "-C", "./package", "exec", "tsx", "./run.ts"],
scriptName: "run.ts",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 5,
},
{
name: "pnpm js shim exec tsx file",
argv: ["./pnpm.js", "exec", "tsx", "./run.ts"],
@@ -627,6 +634,27 @@ describe("hardenApprovedExecutionPaths", () => {
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 4,
},
{
name: "npm x tsx file",
argv: ["npm", "x", "--", "tsx", "./run.ts"],
scriptName: "run.ts",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 4,
},
{
name: "npm loglevel exec tsx file",
argv: ["npm", "--loglevel=silent", "exec", "--", "tsx", "./run.ts"],
scriptName: "run.ts",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 5,
},
{
name: "npm cwd exec tsx file",
argv: ["npm", "-C", "./package", "exec", "--", "tsx", "./run.ts"],
scriptName: "run.ts",
initialBody: 'console.log("SAFE");\n',
expectedArgvIndex: 6,
},
];
it("captures mutable runtime operands in approval plans", () => {

View File

@@ -21,6 +21,14 @@ import {
} from "../infra/exec-wrapper-resolution.js";
import { sameFileIdentity } from "../infra/fs-safe-advanced.js";
import { parseInlineOptionToken } from "../infra/inline-option-token.js";
import {
normalizePackageManagerExecToken,
PNPM_CASE_SENSITIVE_OPTIONS_WITH_VALUE,
PNPM_DLX_OPTIONS_WITH_VALUE,
PNPM_FLAG_OPTIONS,
PNPM_OPTIONS_WITH_VALUE,
unwrapKnownPackageManagerExecInvocation,
} from "../infra/package-manager-exec-wrapper.js";
import {
advancePosixInlineOptionScan,
POSIX_INLINE_COMMAND_FLAGS,
@@ -172,52 +180,6 @@ function isPosixShellOptionToken(token: string, supportsPlusOptions: boolean): b
return token.startsWith("-") || (supportsPlusOptions && token.startsWith("+"));
}
const NPM_EXEC_OPTIONS_WITH_VALUE = new Set([
"--cache",
"--package",
"--prefix",
"--script-shell",
"--userconfig",
"--workspace",
"-p",
"-w",
]);
const NPM_EXEC_FLAG_OPTIONS = new Set([
"--no",
"--quiet",
"--ws",
"--workspaces",
"--yes",
"-q",
"-y",
]);
const PNPM_OPTIONS_WITH_VALUE = new Set([
"--config",
"--dir",
"--filter",
"--reporter",
"--stream",
"--test-pattern",
"--workspace-concurrency",
"-C",
]);
const PNPM_FLAG_OPTIONS = new Set([
"--aggregate-output",
"--color",
"--parallel",
"--recursive",
"--silent",
"--workspace-root",
"-r",
"-s",
"-w",
]);
const PNPM_DLX_OPTIONS_WITH_VALUE = new Set(["--allow-build", "--package", "-p"]);
type FileOperandCollection = {
hits: number[];
sawOptionValueFile: boolean;
@@ -428,171 +390,6 @@ function unwrapArgvForMutableOperand(argv: string[]): {
}
}
function unwrapKnownPackageManagerExecInvocation(argv: string[]): string[] | null {
const executable = normalizePackageManagerExecToken(argv[0] ?? "");
switch (executable) {
case "npm":
return unwrapNpmExecInvocation(argv);
case "npx":
case "bunx":
return unwrapDirectPackageExecInvocation(argv);
case "pnpm":
return unwrapPnpmExecInvocation(argv);
default:
return null;
}
}
function normalizePackageManagerExecToken(token: string): string {
const normalized = normalizeExecutableToken(token);
if (!normalized) {
return normalized;
}
// Approval binding only promises best-effort recovery of the effective runtime
// command for common package-manager shims; it is not full package-manager semantics.
return normalized.replace(/\.(?:c|m)?js$/i, "");
}
function unwrapPnpmExecInvocation(argv: string[]): string[] | null {
let idx = 1;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (token === "--") {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
if (token === "exec") {
if (idx + 1 >= argv.length) {
return null;
}
const tail = argv.slice(idx + 1);
return tail[0] === "--" ? (tail.length > 1 ? tail.slice(1) : null) : tail;
}
if (token === "dlx") {
return unwrapPnpmDlxInvocation(argv.slice(idx + 1));
}
if (token === "node") {
const tail = argv.slice(idx + 1);
const normalizedTail = tail[0] === "--" ? tail.slice(1) : tail;
return ["node", ...normalizedTail];
}
return null;
}
const flag = normalizeOptionFlag(token);
if (PNPM_OPTIONS_WITH_VALUE.has(flag) || PNPM_DLX_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapPnpmDlxInvocation(argv: string[]): string[] | null {
let idx = 0;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (token === "--") {
const tail = argv.slice(idx + 1);
return tail.length > 0 ? tail : null;
}
if (!token.startsWith("-")) {
// Once dlx-specific flags are stripped, the first positional token is the
// package binary pnpm will execute inside the temporary environment.
return argv.slice(idx);
}
const flag = normalizeOptionFlag(token);
if (flag === "-c" || flag === "--shell-mode") {
return null;
}
if (PNPM_OPTIONS_WITH_VALUE.has(flag) || PNPM_DLX_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapDirectPackageExecInvocation(argv: string[]): string[] | null {
let idx = 1;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
return argv.slice(idx);
}
const flag = normalizeOptionFlag(token);
if (flag === "-c" || flag === "--call") {
return null;
}
if (NPM_EXEC_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (NPM_EXEC_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapNpmExecInvocation(argv: string[]): string[] | null {
let idx = 1;
while (idx < argv.length) {
const token = readTrimmedArgToken(argv, idx);
if (!token) {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
if (token !== "exec") {
return null;
}
idx += 1;
break;
}
if (
(token === "-C" || token === "--prefix" || token === "--userconfig") &&
!token.includes("=")
) {
idx += 2;
continue;
}
idx += 1;
}
if (idx >= argv.length) {
return null;
}
const tail = argv.slice(idx);
if (tail[0] === "--") {
return tail.length > 1 ? tail.slice(1) : null;
}
return unwrapDirectPackageExecInvocation(["npx", ...tail]);
}
function resolvePosixShellScriptOperandIndex(argv: string[], executable: string): number | null {
const supportsPlusOptions = POSIX_SHELLS_WITH_PLUS_OPTIONS.has(executable);
if (
@@ -1007,11 +804,16 @@ function pnpmDlxInvocationNeedsFailClosedBinding(argv: string[], cwd: string | u
}
return pnpmDlxTailNeedsFailClosedBinding(argv.slice(idx + 1), cwd);
}
const flag = normalizeOptionFlag(token);
const parsedOption = parseInlineOptionToken(token);
const flag = normalizeLowercaseStringOrEmpty(parsedOption.name);
if (PNPM_OPTIONS_WITH_VALUE.has(flag) || PNPM_DLX_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_CASE_SENSITIVE_OPTIONS_WITH_VALUE.has(parsedOption.name)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
@@ -1036,7 +838,8 @@ function pnpmDlxTailNeedsFailClosedBinding(argv: string[], cwd: string | undefin
if (!token.startsWith("-")) {
return pnpmDlxTailMayNeedStableBinding(argv.slice(idx), cwd);
}
const flag = normalizeOptionFlag(token);
const parsedOption = parseInlineOptionToken(token);
const flag = normalizeLowercaseStringOrEmpty(parsedOption.name);
if (flag === "-c" || flag === "--shell-mode") {
return false;
}
@@ -1044,6 +847,10 @@ function pnpmDlxTailNeedsFailClosedBinding(argv: string[], cwd: string | undefin
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_CASE_SENSITIVE_OPTIONS_WITH_VALUE.has(parsedOption.name)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;