refactor(core): delegate sandbox denial parsing to SandboxManager (#23928)

This commit is contained in:
Tommaso Sciortino
2026-03-26 15:10:15 -07:00
committed by GitHub
parent 73dd7328df
commit 8868b34c75
10 changed files with 272 additions and 148 deletions
@@ -16,7 +16,9 @@ import {
GOVERNANCE_FILES,
getSecretFileFindArgs,
sanitizePaths,
type ParsedSandboxDenial,
} from '../../services/sandboxManager.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
import {
sanitizeEnvironment,
getSecureSanitizationConfig,
@@ -38,6 +40,7 @@ import {
isKnownSafeCommand,
isDangerousCommand,
} from '../utils/commandSafety.js';
import { parsePosixSandboxDenials } from '../utils/sandboxDenialUtils.js';
let cachedBpfPath: string | undefined;
@@ -154,6 +157,10 @@ export class LinuxSandboxManager implements SandboxManager {
return isDangerousCommand(args);
}
parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined {
return parsePosixSandboxDenials(result);
}
private getMaskFilePath(): string {
if (
LinuxSandboxManager.maskFilePath &&
@@ -10,7 +10,9 @@ import {
type SandboxedCommand,
type SandboxPermissions,
type GlobalSandboxOptions,
type ParsedSandboxDenial,
} from '../../services/sandboxManager.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
import {
sanitizeEnvironment,
getSecureSanitizationConfig,
@@ -27,6 +29,7 @@ import {
} from '../utils/commandSafety.js';
import { type SandboxPolicyManager } from '../../policy/sandboxPolicyManager.js';
import { verifySandboxOverrides } from '../utils/commandUtils.js';
import { parsePosixSandboxDenials } from '../utils/sandboxDenialUtils.js';
export interface MacOsSandboxOptions extends GlobalSandboxOptions {
/** The current sandbox mode behavior from config. */
@@ -59,6 +62,10 @@ export class MacOsSandboxManager implements SandboxManager {
return isDangerousCommand(args);
}
parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined {
return parsePosixSandboxDenials(result);
}
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
await initializeShellParsers();
const sanitizationConfig = getSecureSanitizationConfig(
@@ -0,0 +1,43 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { parsePosixSandboxDenials } from './sandboxDenialUtils.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
describe('parsePosixSandboxDenials', () => {
it('should detect file system denial and extract paths', () => {
const parsed = parsePosixSandboxDenials({
output: 'ls: /root: Operation not permitted',
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.filePaths).toContain('/root');
});
it('should detect network denial', () => {
const parsed = parsePosixSandboxDenials({
output: 'curl: (6) Could not resolve host: google.com',
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.network).toBe(true);
});
it('should use fallback heuristic for absolute paths', () => {
const parsed = parsePosixSandboxDenials({
output:
'operation not permitted\nsome error happened with /some/path/to/file',
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.filePaths).toContain('/some/path/to/file');
});
it('should return undefined if no denial detected', () => {
const parsed = parsePosixSandboxDenials({
output: 'hello world',
} as unknown as ShellExecutionResult);
expect(parsed).toBeUndefined();
});
});
@@ -0,0 +1,81 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { type ParsedSandboxDenial } from '../../services/sandboxManager.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
/**
* Common POSIX-style sandbox denial detection.
* Used by macOS and Linux sandbox managers.
*/
export function parsePosixSandboxDenials(
result: ShellExecutionResult,
): ParsedSandboxDenial | undefined {
const output = result.output || '';
const errorOutput = result.error?.message;
const combined = (output + ' ' + (errorOutput || '')).toLowerCase();
const isFileDenial = [
'operation not permitted',
'vim:e303',
'should be read/write',
'sandbox_apply',
'sandbox: ',
].some((keyword) => combined.includes(keyword));
const isNetworkDenial = [
'error connecting to',
'network is unreachable',
'could not resolve host',
'connection refused',
'no address associated with hostname',
].some((keyword) => combined.includes(keyword));
if (!isFileDenial && !isNetworkDenial) {
return undefined;
}
const filePaths = new Set<string>();
// Extract denied paths (POSIX absolute paths)
const regex =
/(?:^|\s)['"]?(\/[\w.-/]+)['"]?:\s*[Oo]peration not permitted/gi;
let match;
while ((match = regex.exec(output)) !== null) {
filePaths.add(match[1]);
}
if (errorOutput) {
while ((match = regex.exec(errorOutput)) !== null) {
filePaths.add(match[1]);
}
}
// Fallback heuristic: look for any absolute path in the output if it was a file denial
if (isFileDenial && filePaths.size === 0) {
const fallbackRegex =
/(?:^|[\s"'[\]])(\/[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)+)(?:$|[\s"'[\]:])/gi;
let m;
while ((m = fallbackRegex.exec(output)) !== null) {
const p = m[1];
if (p && !p.startsWith('/bin/') && !p.startsWith('/usr/bin/')) {
filePaths.add(p);
}
}
if (errorOutput) {
while ((m = fallbackRegex.exec(errorOutput)) !== null) {
const p = m[1];
if (p && !p.startsWith('/bin/') && !p.startsWith('/usr/bin/')) {
filePaths.add(p);
}
}
}
}
return {
network: isNetworkDenial || undefined,
filePaths: filePaths.size > 0 ? Array.from(filePaths) : undefined,
};
}
@@ -18,7 +18,9 @@ import {
sanitizePaths,
tryRealpath,
type SandboxPermissions,
type ParsedSandboxDenial,
} from '../../services/sandboxManager.js';
import type { ShellExecutionResult } from '../../services/shellExecutionService.js';
import {
sanitizeEnvironment,
getSecureSanitizationConfig,
@@ -77,6 +79,10 @@ export class WindowsSandboxManager implements SandboxManager {
return isDangerousCommand(args);
}
parseDenials(_result: ShellExecutionResult): ParsedSandboxDenial | undefined {
return undefined; // TODO: Implement Windows-specific denial parsing
}
/**
* Ensures a file or directory exists.
*/