diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 3d9a414a59..f290518cf6 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -45,6 +45,7 @@ export const globalSettingsSchema = z.object({ alwaysAllowSubtasks: z.boolean().optional(), alwaysAllowExecute: z.boolean().optional(), allowedCommands: z.array(z.string()).optional(), + blacklistedCommands: z.array(z.string()).optional(), allowedMaxRequests: z.number().nullish(), autoCondenseContext: z.boolean().optional(), autoCondenseContextPercent: z.number().optional(), @@ -131,6 +132,7 @@ export const GLOBAL_SETTINGS_KEYS = keysOf()([ "alwaysAllowSubtasks", "alwaysAllowExecute", "allowedCommands", + "blacklistedCommands", "allowedMaxRequests", "autoCondenseContext", "autoCondenseContextPercent", diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e8e63cb3c6..44b6d73dc6 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -444,6 +444,15 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We .getConfiguration(Package.name) .update("allowedCommands", message.commands, vscode.ConfigurationTarget.Global) + break + case "blacklistedCommands": + await provider.context.globalState.update("blacklistedCommands", message.commands) + + // Also update workspace settings. + await vscode.workspace + .getConfiguration(Package.name) + .update("blacklistedCommands", message.commands, vscode.ConfigurationTarget.Global) + break case "openCustomModesSettings": { const customModesFilePath = await provider.customModesManager.getCustomModesFilePath() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 31ac0611d7..355d1121b3 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -138,6 +138,7 @@ export type ExtensionState = Pick< | "alwaysAllowSubtasks" | "alwaysAllowExecute" | "allowedCommands" + | "blacklistedCommands" | "allowedMaxRequests" | "browserToolEnabled" | "browserViewportSize" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 9655d773b6..7fda14d421 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -23,6 +23,7 @@ export interface WebviewMessage { | "getListApiConfiguration" | "customInstructions" | "allowedCommands" + | "blacklistedCommands" | "alwaysAllowReadOnly" | "alwaysAllowReadOnlyOutsideWorkspace" | "alwaysAllowWrite" diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index ea60024d4e..ad133682ed 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -81,6 +81,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { if (message?.type !== "ask") return false - return validateCommand(message.text || "", allowedCommands || []) + return validateCommand(message.text || "", allowedCommands || [], blacklistedCommands || []) }, - [allowedCommands], + [allowedCommands, blacklistedCommands], ) const isAutoApproved = useCallback( diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index 4f44ada43c..fe999d61bc 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -25,6 +25,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { alwaysAllowSubtasks?: boolean alwaysAllowExecute?: boolean allowedCommands?: string[] + blacklistedCommands?: string[] setCachedStateField: SetCachedStateField< | "alwaysAllowReadOnly" | "alwaysAllowReadOnlyOutsideWorkspace" @@ -39,6 +40,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "alwaysAllowSubtasks" | "alwaysAllowExecute" | "allowedCommands" + | "blacklistedCommands" > } @@ -56,11 +58,13 @@ export const AutoApproveSettings = ({ alwaysAllowSubtasks, alwaysAllowExecute, allowedCommands, + blacklistedCommands, setCachedStateField, ...props }: AutoApproveSettingsProps) => { const { t } = useAppTranslation() const [commandInput, setCommandInput] = useState("") + const [blacklistCommandInput, setBlacklistCommandInput] = useState("") const handleAddCommand = () => { const currentCommands = allowedCommands ?? [] @@ -73,6 +77,17 @@ export const AutoApproveSettings = ({ } } + const handleAddBlacklistCommand = () => { + const currentBlacklistedCommands = blacklistedCommands ?? [] + + if (blacklistCommandInput && !currentBlacklistedCommands.includes(blacklistCommandInput)) { + const newBlacklistedCommands = [...currentBlacklistedCommands, blacklistCommandInput] + setCachedStateField("blacklistedCommands", newBlacklistedCommands) + setBlacklistCommandInput("") + vscode.postMessage({ type: "blacklistedCommands", commands: newBlacklistedCommands }) + } + } + return (
@@ -239,6 +254,61 @@ export const AutoApproveSettings = ({ ))}
+ +
+ +
+ {t("settings:autoApprove.execute.blacklistedCommandsDescription")} +
+
+ +
+ setBlacklistCommandInput(e.target.value)} + onKeyDown={(e: any) => { + if (e.key === "Enter") { + e.preventDefault() + handleAddBlacklistCommand() + } + }} + placeholder={t("settings:autoApprove.execute.blacklistCommandPlaceholder")} + className="grow" + data-testid="blacklist-command-input" + /> + +
+ +
+ {(blacklistedCommands ?? []).map((cmd, index) => ( + + ))} +
)} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 44b7393618..3d9d74528a 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -122,6 +122,7 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowReadOnly, alwaysAllowReadOnlyOutsideWorkspace, allowedCommands, + blacklistedCommands, allowedMaxRequests, language, alwaysAllowBrowser, @@ -256,6 +257,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser }) vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp }) vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] }) + vscode.postMessage({ type: "blacklistedCommands", commands: blacklistedCommands ?? [] }) vscode.postMessage({ type: "allowedMaxRequests", value: allowedMaxRequests ?? undefined }) vscode.postMessage({ type: "autoCondenseContext", bool: autoCondenseContext }) vscode.postMessage({ type: "autoCondenseContextPercent", value: autoCondenseContextPercent }) @@ -578,6 +580,7 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowSubtasks={alwaysAllowSubtasks} alwaysAllowExecute={alwaysAllowExecute} allowedCommands={allowedCommands} + blacklistedCommands={blacklistedCommands} setCachedStateField={setCachedStateField} /> )} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 0709ec0ad6..119901d60b 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -54,6 +54,7 @@ export interface ExtensionStateContextType extends ExtensionState { setShowRooIgnoredFiles: (value: boolean) => void setShowAnnouncement: (value: boolean) => void setAllowedCommands: (value: string[]) => void + setBlacklistedCommands: (value: string[]) => void setAllowedMaxRequests: (value: number | undefined) => void setSoundEnabled: (value: boolean) => void setSoundVolume: (value: number) => void @@ -154,6 +155,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode taskHistory: [], shouldShowAnnouncement: false, allowedCommands: [], + blacklistedCommands: [], soundEnabled: false, soundVolume: 0.5, ttsEnabled: false, @@ -332,6 +334,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowSubtasks: (value) => setState((prevState) => ({ ...prevState, alwaysAllowSubtasks: value })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })), setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), + setBlacklistedCommands: (value) => setState((prevState) => ({ ...prevState, blacklistedCommands: value })), setAllowedMaxRequests: (value) => setState((prevState) => ({ ...prevState, allowedMaxRequests: value })), setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })), setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })), diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 7986c27883..077c063e44 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Comandes d'auto-execució permeses", "allowedCommandsDescription": "Prefixos de comandes que poden ser executats automàticament quan \"Aprovar sempre operacions d'execució\" està habilitat. Afegeix * per permetre totes les comandes (usar amb precaució).", "commandPlaceholder": "Introduïu prefix de comanda (ex. 'git ')", - "addButton": "Afegir" + "addButton": "Afegir", + "blacklistedCommands": "Comandes Bloquejades", + "blacklistedCommandsDescription": "Prefixos de comandes que seran bloquejats de l'execució fins i tot quan \"Aprovar sempre operacions d'execució\" estigui habilitat. Aquestes comandes sempre requeriran aprovació manual.", + "blacklistCommandPlaceholder": "Introduïu prefix de comanda a bloquejar (ex. 'rm ')", + "addBlacklistButton": "Afegir a la Llista Negra" }, "apiRequestLimit": { "title": "Màximes Sol·licituds", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 3bb81837f6..d68b7d1a02 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Erlaubte Auto-Ausführungsbefehle", "allowedCommandsDescription": "Befehlspräfixe, die automatisch ausgeführt werden können, wenn 'Ausführungsoperationen immer genehmigen' aktiviert ist. Fügen Sie * hinzu, um alle Befehle zu erlauben (mit Vorsicht verwenden).", "commandPlaceholder": "Befehlspräfix eingeben (z.B. 'git ')", - "addButton": "Hinzufügen" + "addButton": "Hinzufügen", + "blacklistedCommands": "Gesperrte Befehle", + "blacklistedCommandsDescription": "Befehlspräfixe, die auch bei aktivierter Option \"Ausführungsoperationen immer genehmigen\" blockiert werden. Diese Befehle erfordern immer eine manuelle Genehmigung.", + "blacklistCommandPlaceholder": "Befehlspräfix zum Blockieren eingeben (z.B. 'rm ')", + "addBlacklistButton": "Zur Sperrliste hinzufügen" }, "apiRequestLimit": { "title": "Maximale Anfragen", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 752b034228..72bb98ec6d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Allowed Auto-Execute Commands", "allowedCommandsDescription": "Command prefixes that can be auto-executed when \"Always approve execute operations\" is enabled. Add * to allow all commands (use with caution).", "commandPlaceholder": "Enter command prefix (e.g., 'git ')", - "addButton": "Add" + "addButton": "Add", + "blacklistedCommands": "Blacklisted Commands", + "blacklistedCommandsDescription": "Command prefixes that will be blocked from execution even when \"Always approve execute operations\" is enabled. These commands will always require manual approval.", + "blacklistCommandPlaceholder": "Enter command prefix to block (e.g., 'rm ')", + "addBlacklistButton": "Add to Blacklist" }, "apiRequestLimit": { "title": "Max Requests", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 983d2df266..fe8cb9f011 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Comandos de auto-ejecución permitidos", "allowedCommandsDescription": "Prefijos de comandos que pueden ser ejecutados automáticamente cuando \"Aprobar siempre operaciones de ejecución\" está habilitado. Añade * para permitir todos los comandos (usar con precaución).", "commandPlaceholder": "Ingrese prefijo de comando (ej. 'git ')", - "addButton": "Añadir" + "addButton": "Añadir", + "blacklistedCommands": "Comandos Bloqueados", + "blacklistedCommandsDescription": "Prefijos de comandos que serán bloqueados de la ejecución incluso cuando \"Aprobar siempre operaciones de ejecución\" esté habilitado. Estos comandos siempre requerirán aprobación manual.", + "blacklistCommandPlaceholder": "Ingrese prefijo de comando a bloquear (ej. 'rm ')", + "addBlacklistButton": "Agregar a Lista de Bloqueo" }, "apiRequestLimit": { "title": "Solicitudes máximas", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 24f1aa4e9d..90680b582d 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Commandes auto-exécutables autorisées", "allowedCommandsDescription": "Préfixes de commandes qui peuvent être auto-exécutés lorsque \"Toujours approuver les opérations d'exécution\" est activé. Ajoutez * pour autoriser toutes les commandes (à utiliser avec précaution).", "commandPlaceholder": "Entrez le préfixe de commande (ex. 'git ')", - "addButton": "Ajouter" + "addButton": "Ajouter", + "blacklistedCommands": "Commandes Bloquées", + "blacklistedCommandsDescription": "Préfixes de commandes qui seront bloqués de l'exécution même lorsque \"Toujours approuver les opérations d'exécution\" est activé. Ces commandes nécessiteront toujours une approbation manuelle.", + "blacklistCommandPlaceholder": "Entrez le préfixe de commande à bloquer (ex. 'rm ')", + "addBlacklistButton": "Ajouter à la Liste de Blocage" }, "apiRequestLimit": { "title": "Requêtes maximales", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 04476ece10..1d98f744b7 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "अनुमत स्वतः-निष्पादन कमांड", "allowedCommandsDescription": "कमांड प्रीफिक्स जो स्वचालित रूप से निष्पादित किए जा सकते हैं जब \"निष्पादन ऑपरेशन हमेशा अनुमोदित करें\" सक्षम है। सभी कमांड की अनुमति देने के लिए * जोड़ें (सावधानी से उपयोग करें)।", "commandPlaceholder": "कमांड प्रीफिक्स दर्ज करें (उदा. 'git ')", - "addButton": "जोड़ें" + "addButton": "जोड़ें", + "blacklistedCommands": "ब्लॉक की गई कमांड्स", + "blacklistedCommandsDescription": "कमांड प्रीफिक्स जो \"हमेशा execute ऑपरेशन्स को approve करें\" सक्षम होने पर भी execution से ब्लॉक हो जाएंगे। इन कमांड्स के लिए हमेशा मैन्युअल approval की आवश्यकता होगी।", + "blacklistCommandPlaceholder": "ब्लॉक करने के लिए कमांड प्रीफिक्स दर्ज करें (उदा. 'rm ')", + "addBlacklistButton": "ब्लैकलिस्ट में जोड़ें" }, "apiRequestLimit": { "title": "अधिकतम अनुरोध", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index cbfc9cdfac..df4cc8b8b5 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Comandi di auto-esecuzione consentiti", "allowedCommandsDescription": "Prefissi di comando che possono essere auto-eseguiti quando \"Approva sempre operazioni di esecuzione\" è abilitato. Aggiungi * per consentire tutti i comandi (usare con cautela).", "commandPlaceholder": "Inserisci prefisso comando (es. 'git ')", - "addButton": "Aggiungi" + "addButton": "Aggiungi", + "blacklistedCommands": "Comandi Bloccati", + "blacklistedCommandsDescription": "Prefissi di comandi che saranno bloccati dall'esecuzione anche quando \"Approva sempre operazioni di esecuzione\" è abilitato. Questi comandi richiederanno sempre approvazione manuale.", + "blacklistCommandPlaceholder": "Inserisci prefisso comando da bloccare (es. 'rm ')", + "addBlacklistButton": "Aggiungi alla Lista Nera" }, "apiRequestLimit": { "title": "Richieste massime", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index c8ac18fd01..d0937b7c60 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "許可された自動実行コマンド", "allowedCommandsDescription": "「実行操作を常に承認」が有効な場合に自動実行できるコマンドプレフィックス。すべてのコマンドを許可するには * を追加します(注意して使用してください)。", "commandPlaceholder": "コマンドプレフィックスを入力(例:'git ')", - "addButton": "追加" + "addButton": "追加", + "blacklistedCommands": "ブロックされたコマンド", + "blacklistedCommandsDescription": "「実行操作を常に承認する」が有効になっている場合でも、実行がブロックされるコマンドプレフィックス。これらのコマンドは常に手動承認が必要です。", + "blacklistCommandPlaceholder": "ブロックするコマンドプレフィックスを入力(例:'rm ')", + "addBlacklistButton": "ブラックリストに追加" }, "apiRequestLimit": { "title": "最大リクエスト数", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index ac8069a184..8e3d04fd58 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "허용된 자동 실행 명령", "allowedCommandsDescription": "\"실행 작업 항상 승인\"이 활성화되었을 때 자동 실행될 수 있는 명령 접두사. 모든 명령을 허용하려면 * 추가(주의해서 사용)", "commandPlaceholder": "명령 접두사 입력(예: 'git ')", - "addButton": "추가" + "addButton": "추가", + "blacklistedCommands": "차단된 명령어", + "blacklistedCommandsDescription": "\"실행 작업 항상 승인\"이 활성화되어 있어도 실행이 차단될 명령 접두사입니다. 이러한 명령어는 항상 수동 승인이 필요합니다.", + "blacklistCommandPlaceholder": "차단할 명령 접두사 입력(예: 'rm ')", + "addBlacklistButton": "블랙리스트에 추가" }, "apiRequestLimit": { "title": "최대 요청 수", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index c4ee8e58e3..3dcb388bae 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Toegestane automatisch uit te voeren commando's", "allowedCommandsDescription": "Commando-prefixen die automatisch kunnen worden uitgevoerd als 'Altijd goedkeuren voor uitvoeren' is ingeschakeld. Voeg * toe om alle commando's toe te staan (gebruik met voorzichtigheid).", "commandPlaceholder": "Voer commando-prefix in (bijv. 'git ')", - "addButton": "Toevoegen" + "addButton": "Toevoegen", + "blacklistedCommands": "Geblokkeerde Commando's", + "blacklistedCommandsDescription": "Commando-prefixen die geblokkeerd worden van uitvoering, zelfs wanneer \"Altijd uitvoeringsoperaties goedkeuren\" is ingeschakeld. Deze commando's vereisen altijd handmatige goedkeuring.", + "blacklistCommandPlaceholder": "Voer commando-prefix in om te blokkeren (bijv. 'rm ')", + "addBlacklistButton": "Toevoegen aan Zwarte Lijst" }, "apiRequestLimit": { "title": "Maximale verzoeken", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index c499976cc3..bcfd37bc74 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Dozwolone polecenia auto-wykonania", "allowedCommandsDescription": "Prefiksy poleceń, które mogą być automatycznie wykonywane, gdy \"Zawsze zatwierdzaj operacje wykonania\" jest włączone. Dodaj * aby zezwolić na wszystkie polecenia (używaj z ostrożnością).", "commandPlaceholder": "Wprowadź prefiks polecenia (np. 'git ')", - "addButton": "Dodaj" + "addButton": "Dodaj", + "blacklistedCommands": "Zablokowane Polecenia", + "blacklistedCommandsDescription": "Prefiksy poleceń, które będą zablokowane przed wykonaniem, nawet gdy \"Zawsze zatwierdzaj operacje wykonywania\" jest włączone. Te polecenia zawsze będą wymagać ręcznego zatwierdzenia.", + "blacklistCommandPlaceholder": "Wprowadź prefiks polecenia do zablokowania (np. 'rm ')", + "addBlacklistButton": "Dodaj do Czarnej Listy" }, "apiRequestLimit": { "title": "Maksymalna liczba żądań", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 893e52d402..5a6e0a1652 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Comandos de auto-execução permitidos", "allowedCommandsDescription": "Prefixos de comando que podem ser auto-executados quando \"Aprovar sempre operações de execução\" está ativado. Adicione * para permitir todos os comandos (use com cautela).", "commandPlaceholder": "Digite o prefixo do comando (ex. 'git ')", - "addButton": "Adicionar" + "addButton": "Adicionar", + "blacklistedCommands": "Comandos Bloqueados", + "blacklistedCommandsDescription": "Prefixos de comandos que serão bloqueados da execução mesmo quando \"Sempre aprovar operações de execução\" estiver habilitado. Estes comandos sempre exigirão aprovação manual.", + "blacklistCommandPlaceholder": "Digite o prefixo do comando para bloquear (ex. 'rm ')", + "addBlacklistButton": "Adicionar à Lista Negra" }, "apiRequestLimit": { "title": "Máximo de Solicitações", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 5c611d8a45..3e5a1401c0 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Разрешённые авто-выполняемые команды", "allowedCommandsDescription": "Префиксы команд, которые могут быть автоматически выполнены при включённом параметре \"Всегда одобрять выполнение операций\". Добавьте * для разрешения всех команд (используйте с осторожностью).", "commandPlaceholder": "Введите префикс команды (например, 'git ')", - "addButton": "Добавить" + "addButton": "Добавить", + "blacklistedCommands": "Заблокированные Команды", + "blacklistedCommandsDescription": "Префиксы команд, которые будут заблокированы от выполнения, даже когда включена опция \"Всегда одобрять операции выполнения\". Эти команды всегда будут требовать ручного одобрения.", + "blacklistCommandPlaceholder": "Введите префикс команды для блокировки (например, 'rm ')", + "addBlacklistButton": "Добавить в Черный Список" }, "apiRequestLimit": { "title": "Максимум запросов", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 25ec36780f..2f120d2423 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "İzin Verilen Otomatik Yürütme Komutları", "allowedCommandsDescription": "\"Yürütme işlemlerini her zaman onayla\" etkinleştirildiğinde otomatik olarak yürütülebilen komut önekleri. Tüm komutlara izin vermek için * ekleyin (dikkatli kullanın).", "commandPlaceholder": "Komut öneki girin (örn. 'git ')", - "addButton": "Ekle" + "addButton": "Ekle", + "blacklistedCommands": "Engellenen Komutlar", + "blacklistedCommandsDescription": "\"Yürütme işlemlerini her zaman onayla\" etkinleştirildiğinde bile yürütülmesi engellenecek komut önekleri. Bu komutlar her zaman manuel onay gerektirecektir.", + "blacklistCommandPlaceholder": "Engellenecek komut öneki girin (örn. 'rm ')", + "addBlacklistButton": "Kara Listeye Ekle" }, "apiRequestLimit": { "title": "Maksimum İstek", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 2871f73f3e..fac625e743 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "Các lệnh tự động thực thi được phép", "allowedCommandsDescription": "Tiền tố lệnh có thể được tự động thực thi khi \"Luôn phê duyệt các hoạt động thực thi\" được bật. Thêm * để cho phép tất cả các lệnh (sử dụng cẩn thận).", "commandPlaceholder": "Nhập tiền tố lệnh (ví dụ: 'git ')", - "addButton": "Thêm" + "addButton": "Thêm", + "blacklistedCommands": "Lệnh Bị Chặn", + "blacklistedCommandsDescription": "Các tiền tố lệnh sẽ bị chặn thực thi ngay cả khi \"Luôn phê duyệt các thao tác thực thi\" được bật. Những lệnh này sẽ luôn yêu cầu phê duyệt thủ công.", + "blacklistCommandPlaceholder": "Nhập tiền tố lệnh để chặn (ví dụ: 'rm ')", + "addBlacklistButton": "Thêm vào Danh Sách Đen" }, "apiRequestLimit": { "title": "Số lượng yêu cầu tối đa", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 51f247fd0d..bce06cc2bb 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "命令白名单", "allowedCommandsDescription": "当\"自动批准命令行操作\"启用时可以自动执行的命令前缀。添加 * 以允许所有命令(谨慎使用)。", "commandPlaceholder": "输入命令前缀(例如 'git ')", - "addButton": "添加" + "addButton": "添加", + "blacklistedCommands": "命令黑名单", + "blacklistedCommandsDescription": "即使启用\"自动批准执行操作\",这些命令前缀也会被阻止执行。这些命令始终需要手动批准。", + "blacklistCommandPlaceholder": "输入要阻止的命令前缀(例如 'rm ')", + "addBlacklistButton": "添加到黑名单" }, "apiRequestLimit": { "title": "最大请求数", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 595194f97c..a96832c7aa 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -106,7 +106,11 @@ "allowedCommands": "允許自動執行的命令", "allowedCommandsDescription": "當「始終核准執行操作」啟用時可以自動執行的命令前綴。新增 * 以允許所有命令(請謹慎使用)。", "commandPlaceholder": "輸入命令前綴(例如 'git ')", - "addButton": "新增" + "addButton": "新增", + "blacklistedCommands": "封鎖指令", + "blacklistedCommandsDescription": "即使啟用「總是核准執行操作」,這些指令前綴也會被封鎖執行。這些指令總是需要手動核准。", + "blacklistCommandPlaceholder": "輸入要封鎖的指令前綴(例如 'rm ')", + "addBlacklistButton": "新增至封鎖清單" }, "apiRequestLimit": { "title": "最大請求數", diff --git a/webview-ui/src/utils/__tests__/command-validation.test.ts b/webview-ui/src/utils/__tests__/command-validation.test.ts index e73ec31dd1..441ad40679 100644 --- a/webview-ui/src/utils/__tests__/command-validation.test.ts +++ b/webview-ui/src/utils/__tests__/command-validation.test.ts @@ -2,7 +2,12 @@ // npx jest src/utils/__tests__/command-validation.test.ts -import { parseCommand, isAllowedSingleCommand, validateCommand } from "../command-validation" +import { + parseCommand, + isAllowedSingleCommand, + isBlacklistedSingleCommand, + validateCommand, +} from "../command-validation" describe("Command Validation", () => { describe("parseCommand", () => { @@ -68,6 +73,33 @@ describe("Command Validation", () => { }) }) + describe("isBlacklistedSingleCommand", () => { + const blacklistedCommands = ["rm ", "sudo ", "del "] + + it("matches blacklisted commands case-insensitively", () => { + expect(isBlacklistedSingleCommand("RM -rf /", blacklistedCommands)).toBe(true) + expect(isBlacklistedSingleCommand("sudo apt install", blacklistedCommands)).toBe(true) + expect(isBlacklistedSingleCommand("DEL file.txt", blacklistedCommands)).toBe(true) + }) + + it("matches blacklisted command prefixes", () => { + expect(isBlacklistedSingleCommand("rm -rf /home", blacklistedCommands)).toBe(true) + expect(isBlacklistedSingleCommand("sudo rm file", blacklistedCommands)).toBe(true) + expect(isBlacklistedSingleCommand("del *.txt", blacklistedCommands)).toBe(true) + }) + + it("allows non-blacklisted commands", () => { + expect(isBlacklistedSingleCommand("npm test", blacklistedCommands)).toBe(false) + expect(isBlacklistedSingleCommand("echo hello", blacklistedCommands)).toBe(false) + expect(isBlacklistedSingleCommand("git status", blacklistedCommands)).toBe(false) + }) + + it("handles empty inputs", () => { + expect(isBlacklistedSingleCommand("", blacklistedCommands)).toBe(false) + expect(isBlacklistedSingleCommand("rm file", [])).toBe(false) + }) + }) + describe("validateCommand", () => { const allowedCommands = ["npm test", "npm run", "echo", "Select-String"] @@ -121,6 +153,33 @@ describe("Command Validation", () => { expect(validateCommand("npm test $(echo dangerous)", wildcardAllowedCommands)).toBe(true) expect(validateCommand("npm test `rm -rf /`", wildcardAllowedCommands)).toBe(true) }) + + it("blocks blacklisted commands even if allowed", () => { + const allowedWithBlacklisted = ["npm test", "npm run", "echo", "rm "] + const blacklistedCommands = ["rm ", "sudo ", "del "] + expect(validateCommand("rm -rf /", allowedWithBlacklisted, blacklistedCommands)).toBe(false) + expect(validateCommand("sudo apt install", allowedWithBlacklisted, blacklistedCommands)).toBe(false) + expect(validateCommand("npm test", allowedWithBlacklisted, blacklistedCommands)).toBe(true) + }) + + it("validates chained commands with blacklist", () => { + const blacklistedCommands = ["rm ", "sudo ", "del "] + expect(validateCommand("npm test && npm run build", allowedCommands, blacklistedCommands)).toBe(true) + expect(validateCommand("npm test && rm file", allowedCommands, blacklistedCommands)).toBe(false) + expect(validateCommand("npm test && dangerous", allowedCommands, blacklistedCommands)).toBe(false) + }) + + it("handles wildcard allowed commands with blacklist", () => { + const blacklistedCommands = ["rm ", "sudo ", "del "] + expect(validateCommand("any command", ["*"], blacklistedCommands)).toBe(true) + expect(validateCommand("rm -rf /", ["*"], blacklistedCommands)).toBe(false) // Still blocked by blacklist + expect(validateCommand("sudo apt install", ["*"], blacklistedCommands)).toBe(false) // Still blocked by blacklist + }) + + it("works without blacklist parameter (backward compatibility)", () => { + expect(validateCommand("npm test", allowedCommands)).toBe(true) + expect(validateCommand("dangerous", allowedCommands)).toBe(false) + }) }) }) diff --git a/webview-ui/src/utils/command-validation.ts b/webview-ui/src/utils/command-validation.ts index 7ad21b1675..ba1bcabbb5 100644 --- a/webview-ui/src/utils/command-validation.ts +++ b/webview-ui/src/utils/command-validation.ts @@ -109,16 +109,40 @@ export function isAllowedSingleCommand(command: string, allowedCommands: string[ } /** - * Check if a command string is allowed based on the allowed command prefixes. + * Check if a single command is blacklisted based on prefix matching. + */ +export function isBlacklistedSingleCommand(command: string, blacklistedCommands: string[]): boolean { + if (!command || !blacklistedCommands?.length) return false + const trimmedCommand = command.trim().toLowerCase() + return blacklistedCommands.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase())) +} + +/** + * Check if a command string is allowed based on the allowed command prefixes and blacklisted commands. * This version also blocks subshell attempts by checking for `$(` or `` ` ``. */ -export function validateCommand(command: string, allowedCommands: string[]): boolean { +export function validateCommand( + command: string, + allowedCommands: string[], + blacklistedCommands: string[] = [], +): boolean { if (!command?.trim()) return true - // If '*' is in allowed commands, everything is allowed - if (allowedCommands?.includes("*")) return true + // If '*' is in allowed commands, check blacklist but allow everything else (including subshells) + if (allowedCommands?.includes("*")) { + // Parse into sub-commands (split by &&, ||, ;, |) + const subCommands = parseCommand(command) + + // Check if any sub-command is blacklisted + const hasBlacklistedCommand = subCommands.some((cmd) => { + const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, "").trim() + return isBlacklistedSingleCommand(cmdWithoutRedirection, blacklistedCommands) + }) - // Block subshell execution attempts + return !hasBlacklistedCommand + } + + // Block subshell execution attempts when not using wildcard if (command.includes("$(") || command.includes("`")) { return false } @@ -126,6 +150,16 @@ export function validateCommand(command: string, allowedCommands: string[]): boo // Parse into sub-commands (split by &&, ||, ;, |) const subCommands = parseCommand(command) + // Check if any sub-command is blacklisted first + const hasBlacklistedCommand = subCommands.some((cmd) => { + const cmdWithoutRedirection = cmd.replace(/\d*>&\d*/, "").trim() + return isBlacklistedSingleCommand(cmdWithoutRedirection, blacklistedCommands) + }) + + if (hasBlacklistedCommand) { + return false + } + // Then ensure every sub-command starts with an allowed prefix return subCommands.every((cmd) => { // Remove simple PowerShell-like redirections (e.g. 2>&1) before checking