- Add Pink Purple theme (hot pink/purple/magenta on dark plum background) - Fix race condition where clearPastedImages() in input-area ran before the async message handler could read the images, silently dropping them - Allow @ file picker and / command menu to trigger at any cursor position, not just when the input is empty - Update CHANGELOG and README with new changes
154 lines
4.2 KiB
TypeScript
154 lines
4.2 KiB
TypeScript
/**
|
|
* Dangerous Command Blocker Service
|
|
*
|
|
* Detects and blocks dangerous bash commands before execution.
|
|
* This is a safety feature that cannot be bypassed, even with auto-approve.
|
|
*/
|
|
|
|
import {
|
|
BLOCKED_PATTERNS,
|
|
BLOCKED_COMMAND_MESSAGES,
|
|
type BlockedPattern,
|
|
type DangerCategory,
|
|
} from "@constants/dangerous-commands";
|
|
|
|
/**
|
|
* Result of checking a command for danger
|
|
*/
|
|
export interface DangerCheckResult {
|
|
blocked: boolean;
|
|
pattern?: BlockedPattern;
|
|
message?: string;
|
|
}
|
|
|
|
/**
|
|
* Split a shell command into individual sub-commands on chaining operators.
|
|
* Handles &&, ||, ;, and | (pipe). Respects quoted strings.
|
|
*/
|
|
const splitChainedCommands = (command: string): string[] => {
|
|
const parts: string[] = [];
|
|
let current = "";
|
|
let inSingle = false;
|
|
let inDouble = false;
|
|
|
|
for (let i = 0; i < command.length; i++) {
|
|
const ch = command[i];
|
|
const next = command[i + 1];
|
|
|
|
if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; continue; }
|
|
if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; continue; }
|
|
if (inSingle || inDouble) { current += ch; continue; }
|
|
|
|
if (ch === "&" && next === "&") { parts.push(current); current = ""; i++; continue; }
|
|
if (ch === "|" && next === "|") { parts.push(current); current = ""; i++; continue; }
|
|
if (ch === ";") { parts.push(current); current = ""; continue; }
|
|
if (ch === "|") { parts.push(current); current = ""; continue; }
|
|
|
|
current += ch;
|
|
}
|
|
|
|
if (current.trim()) parts.push(current);
|
|
return parts.map((p) => p.trim()).filter(Boolean);
|
|
};
|
|
|
|
/**
|
|
* Check if a command matches any blocked pattern.
|
|
* For chained commands (&&, ||, ;, |), each sub-command is checked individually
|
|
* to prevent dangerous commands hidden behind benign ones (e.g. cd /safe && rm -rf /).
|
|
*/
|
|
export const checkDangerousCommand = (command: string): DangerCheckResult => {
|
|
const subCommands = splitChainedCommands(command);
|
|
|
|
for (const subCmd of subCommands) {
|
|
const normalized = subCmd.trim();
|
|
for (const pattern of BLOCKED_PATTERNS) {
|
|
if (pattern.pattern.test(normalized)) {
|
|
return {
|
|
blocked: true,
|
|
pattern,
|
|
message: formatBlockedMessage(pattern),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check the full command in case a pattern targets the chaining itself
|
|
const normalizedCommand = command.trim();
|
|
for (const pattern of BLOCKED_PATTERNS) {
|
|
if (pattern.pattern.test(normalizedCommand)) {
|
|
return {
|
|
blocked: true,
|
|
pattern,
|
|
message: formatBlockedMessage(pattern),
|
|
};
|
|
}
|
|
}
|
|
|
|
return { blocked: false };
|
|
};
|
|
|
|
/**
|
|
* Format a user-friendly blocked message
|
|
*/
|
|
const formatBlockedMessage = (pattern: BlockedPattern): string => {
|
|
const categoryDescription =
|
|
BLOCKED_COMMAND_MESSAGES.CATEGORY_DESCRIPTIONS[pattern.category];
|
|
|
|
const lines = [
|
|
`[${BLOCKED_COMMAND_MESSAGES.BLOCKED_PREFIX}] ${pattern.name}: ${pattern.description}`,
|
|
"",
|
|
`Category: ${formatCategoryName(pattern.category)}`,
|
|
`Severity: ${pattern.severity.toUpperCase()}`,
|
|
"",
|
|
categoryDescription,
|
|
"",
|
|
BLOCKED_COMMAND_MESSAGES.CANNOT_BYPASS,
|
|
BLOCKED_COMMAND_MESSAGES.SUGGESTION,
|
|
];
|
|
|
|
return lines.join("\n");
|
|
};
|
|
|
|
/**
|
|
* Format category name for display
|
|
*/
|
|
const formatCategoryName = (category: DangerCategory): string => {
|
|
const names: Record<DangerCategory, string> = {
|
|
destructive_delete: "Destructive Delete",
|
|
privilege_escalation: "Privilege Escalation",
|
|
system_damage: "System Damage",
|
|
network_attack: "Network Attack",
|
|
git_destructive: "Git Destructive",
|
|
credential_exposure: "Credential Exposure",
|
|
};
|
|
return names[category];
|
|
};
|
|
|
|
/**
|
|
* Get all blocked patterns (for configuration UI)
|
|
*/
|
|
export const getBlockedPatterns = (): readonly BlockedPattern[] => {
|
|
return BLOCKED_PATTERNS;
|
|
};
|
|
|
|
/**
|
|
* Check if a command is safe (inverse of dangerous check)
|
|
*/
|
|
export const isCommandSafe = (command: string): boolean => {
|
|
return !checkDangerousCommand(command).blocked;
|
|
};
|
|
|
|
/**
|
|
* Get categories of dangerous commands
|
|
*/
|
|
export const getDangerCategories = (): DangerCategory[] => {
|
|
return [
|
|
"destructive_delete",
|
|
"privilege_escalation",
|
|
"system_damage",
|
|
"network_attack",
|
|
"git_destructive",
|
|
"credential_exposure",
|
|
];
|
|
};
|