diff --git a/src/infra/exec-approvals-allowlist.ts b/src/infra/exec-approvals-allowlist.ts index 687ce3039ba..55c06f78df1 100644 --- a/src/infra/exec-approvals-allowlist.ts +++ b/src/infra/exec-approvals-allowlist.ts @@ -109,6 +109,29 @@ export type SkillBinTrustEntry = { name: string; resolvedPath: string; }; +type ExecAllowlistContext = { + allowlist: ExecAllowlistEntry[]; + safeBins: Set; + safeBinProfiles?: Readonly>; + cwd?: string; + platform?: string | null; + trustedSafeBinDirs?: ReadonlySet; + skillBins?: readonly SkillBinTrustEntry[]; + autoAllowSkills?: boolean; +}; + +function pickExecAllowlistContext(params: ExecAllowlistContext): ExecAllowlistContext { + return { + allowlist: params.allowlist, + safeBins: params.safeBins, + safeBinProfiles: params.safeBinProfiles, + cwd: params.cwd, + platform: params.platform, + trustedSafeBinDirs: params.trustedSafeBinDirs, + skillBins: params.skillBins, + autoAllowSkills: params.autoAllowSkills, + }; +} function normalizeSkillBinName(value: string | undefined): string | null { const trimmed = value?.trim().toLowerCase(); @@ -173,16 +196,7 @@ function isSkillAutoAllowedSegment(params: { function evaluateSegments( segments: ExecCommandSegment[], - params: { - allowlist: ExecAllowlistEntry[]; - safeBins: Set; - safeBinProfiles?: Readonly>; - cwd?: string; - platform?: string | null; - trustedSafeBinDirs?: ReadonlySet; - skillBins?: readonly SkillBinTrustEntry[]; - autoAllowSkills?: boolean; - }, + params: ExecAllowlistContext, ): { satisfied: boolean; matches: ExecAllowlistEntry[]; @@ -245,35 +259,21 @@ function resolveAnalysisSegmentGroups(analysis: ExecCommandAnalysis): ExecComman return [analysis.segments]; } -export function evaluateExecAllowlist(params: { - analysis: ExecCommandAnalysis; - allowlist: ExecAllowlistEntry[]; - safeBins: Set; - safeBinProfiles?: Readonly>; - cwd?: string; - platform?: string | null; - trustedSafeBinDirs?: ReadonlySet; - skillBins?: readonly SkillBinTrustEntry[]; - autoAllowSkills?: boolean; -}): ExecAllowlistEvaluation { +export function evaluateExecAllowlist( + params: { + analysis: ExecCommandAnalysis; + } & ExecAllowlistContext, +): ExecAllowlistEvaluation { const allowlistMatches: ExecAllowlistEntry[] = []; const segmentSatisfiedBy: ExecSegmentSatisfiedBy[] = []; if (!params.analysis.ok || params.analysis.segments.length === 0) { return { allowlistSatisfied: false, allowlistMatches, segmentSatisfiedBy }; } + const allowlistContext = pickExecAllowlistContext(params); const hasChains = Boolean(params.analysis.chains); for (const group of resolveAnalysisSegmentGroups(params.analysis)) { - const result = evaluateSegments(group, { - allowlist: params.allowlist, - safeBins: params.safeBins, - safeBinProfiles: params.safeBinProfiles, - cwd: params.cwd, - platform: params.platform, - trustedSafeBinDirs: params.trustedSafeBinDirs, - skillBins: params.skillBins, - autoAllowSkills: params.autoAllowSkills, - }); + const result = evaluateSegments(group, allowlistContext); if (!result.satisfied) { if (!hasChains) { return { @@ -339,16 +339,12 @@ function collectAllowAlwaysPatterns(params: { return; } - if (isDispatchWrapperSegment(params.segment)) { - const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(params.segment.argv); - if (dispatchUnwrap.kind !== "unwrapped" || dispatchUnwrap.argv.length === 0) { - return; - } + const recurseWithArgv = (argv: string[]): void => { collectAllowAlwaysPatterns({ segment: { - raw: dispatchUnwrap.argv.join(" "), - argv: dispatchUnwrap.argv, - resolution: resolveCommandResolutionFromArgv(dispatchUnwrap.argv, params.cwd, params.env), + raw: argv.join(" "), + argv, + resolution: resolveCommandResolutionFromArgv(argv, params.cwd, params.env), }, cwd: params.cwd, env: params.env, @@ -356,6 +352,14 @@ function collectAllowAlwaysPatterns(params: { depth: params.depth + 1, out: params.out, }); + }; + + if (isDispatchWrapperSegment(params.segment)) { + const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(params.segment.argv); + if (dispatchUnwrap.kind !== "unwrapped" || dispatchUnwrap.argv.length === 0) { + return; + } + recurseWithArgv(dispatchUnwrap.argv); return; } @@ -364,22 +368,7 @@ function collectAllowAlwaysPatterns(params: { return; } if (shellMultiplexerUnwrap.kind === "unwrapped") { - collectAllowAlwaysPatterns({ - segment: { - raw: shellMultiplexerUnwrap.argv.join(" "), - argv: shellMultiplexerUnwrap.argv, - resolution: resolveCommandResolutionFromArgv( - shellMultiplexerUnwrap.argv, - params.cwd, - params.env, - ), - }, - cwd: params.cwd, - env: params.env, - platform: params.platform, - depth: params.depth + 1, - out: params.out, - }); + recurseWithArgv(shellMultiplexerUnwrap.argv); return; } @@ -444,18 +433,13 @@ export function resolveAllowAlwaysPatterns(params: { /** * Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata. */ -export function evaluateShellAllowlist(params: { - command: string; - allowlist: ExecAllowlistEntry[]; - safeBins: Set; - safeBinProfiles?: Readonly>; - cwd?: string; - env?: NodeJS.ProcessEnv; - trustedSafeBinDirs?: ReadonlySet; - skillBins?: readonly SkillBinTrustEntry[]; - autoAllowSkills?: boolean; - platform?: string | null; -}): ExecAllowlistAnalysis { +export function evaluateShellAllowlist( + params: { + command: string; + env?: NodeJS.ProcessEnv; + } & ExecAllowlistContext, +): ExecAllowlistAnalysis { + const allowlistContext = pickExecAllowlistContext(params); const analysisFailure = (): ExecAllowlistAnalysis => ({ analysisOk: false, allowlistSatisfied: false, @@ -481,17 +465,7 @@ export function evaluateShellAllowlist(params: { if (!analysis.ok) { return analysisFailure(); } - const evaluation = evaluateExecAllowlist({ - analysis, - allowlist: params.allowlist, - safeBins: params.safeBins, - safeBinProfiles: params.safeBinProfiles, - cwd: params.cwd, - platform: params.platform, - trustedSafeBinDirs: params.trustedSafeBinDirs, - skillBins: params.skillBins, - autoAllowSkills: params.autoAllowSkills, - }); + const evaluation = evaluateExecAllowlist({ analysis, ...allowlistContext }); return { analysisOk: true, allowlistSatisfied: evaluation.allowlistSatisfied, @@ -517,17 +491,7 @@ export function evaluateShellAllowlist(params: { } segments.push(...analysis.segments); - const evaluation = evaluateExecAllowlist({ - analysis, - allowlist: params.allowlist, - safeBins: params.safeBins, - safeBinProfiles: params.safeBinProfiles, - cwd: params.cwd, - platform: params.platform, - trustedSafeBinDirs: params.trustedSafeBinDirs, - skillBins: params.skillBins, - autoAllowSkills: params.autoAllowSkills, - }); + const evaluation = evaluateExecAllowlist({ analysis, ...allowlistContext }); allowlistMatches.push(...evaluation.allowlistMatches); segmentSatisfiedBy.push(...evaluation.segmentSatisfiedBy); if (!evaluation.allowlistSatisfied) {