Add security allowlist for command execution and update copilot instructions

- Add security.ts with allowlist configuration for shell commands
- Update command-executor.ts to enforce security policy (exit code 126 for blocked commands)
- Update copilot instructions to clarify builtin tools vs shell commands
- Document that builtin tools (deleteFile, createFile, etc.) bypass security filtering
- Only executeCommand (shell commands) requires security.json allowlist entries
This commit is contained in:
tusharmagar 2025-11-18 20:42:11 +05:30
parent 570543e1c7
commit 28488d5fd1
4 changed files with 183 additions and 1 deletions

View file

@ -1,7 +1,73 @@
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
import { getSecurityAllowList, SECURITY_CONFIG_PATH } from '../config/security.js';
const execPromise = promisify(exec);
const COMMAND_SPLIT_REGEX = /(?:\|\||&&|;|\||\n)/;
const ENV_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=.*/;
const WRAPPER_COMMANDS = new Set(['sudo', 'env', 'time', 'command']);
function sanitizeToken(token: string): string {
return token.trim().replace(/^['"]+|['"]+$/g, '');
}
function extractCommandNames(command: string): string[] {
const discovered = new Set<string>();
const segments = command.split(COMMAND_SPLIT_REGEX);
for (const segment of segments) {
const tokens = segment.trim().split(/\s+/).filter(Boolean);
if (!tokens.length) continue;
let index = 0;
while (index < tokens.length && ENV_ASSIGNMENT_REGEX.test(tokens[index])) {
index++;
}
if (index >= tokens.length) continue;
const primary = sanitizeToken(tokens[index]).toLowerCase();
if (!primary) continue;
discovered.add(primary);
if (WRAPPER_COMMANDS.has(primary) && index + 1 < tokens.length) {
const wrapped = sanitizeToken(tokens[index + 1]).toLowerCase();
if (wrapped) {
discovered.add(wrapped);
}
}
}
return Array.from(discovered);
}
function findBlockedCommands(command: string): string[] {
const invoked = extractCommandNames(command);
if (!invoked.length) return [];
const allowList = getSecurityAllowList();
if (!allowList.length) return invoked;
const allowSet = new Set(allowList);
if (allowSet.has('*')) return [];
return invoked.filter((cmd) => !allowSet.has(cmd));
}
function enforceSecurity(command: string): CommandResult | null {
const blocked = findBlockedCommands(command);
if (!blocked.length) {
return null;
}
return {
stdout: '',
stderr: `Command blocked by security policy. Blocked command(s): ${blocked.join(', ')}. Update ${SECURITY_CONFIG_PATH} to allow them before retrying.`,
exitCode: 126,
};
}
export interface CommandResult {
stdout: string;
@ -23,6 +89,11 @@ export async function executeCommand(
maxBuffer?: number; // max buffer size in bytes
}
): Promise<CommandResult> {
const securityResult = enforceSecurity(command);
if (securityResult) {
return securityResult;
}
try {
const { stdout, stderr } = await execPromise(command, {
cwd: options?.cwd,
@ -57,6 +128,11 @@ export function executeCommandSync(
timeout?: number;
}
): CommandResult {
const securityResult = enforceSecurity(command);
if (securityResult) {
return securityResult;
}
try {
const stdout = execSync(command, {
cwd: options?.cwd,