420 lines
11 KiB
JavaScript
420 lines
11 KiB
JavaScript
#!/usr/bin/env bun
|
|
|
|
/**
|
|
* Claude Code "Before Tools" Hook - Command Validation Script
|
|
*
|
|
* This script validates commands before execution to prevent harmful operations.
|
|
* It receives command data via stdin and returns exit code 0 (allow) or 1 (block).
|
|
*
|
|
* Usage: Called automatically by Claude Code PreToolUse hook
|
|
* Manual test: echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bun validate-command.js
|
|
*/
|
|
|
|
// Comprehensive dangerous command patterns database
|
|
const SECURITY_RULES = {
|
|
// Critical system destruction commands
|
|
CRITICAL_COMMANDS: [
|
|
'del',
|
|
'format',
|
|
'mkfs',
|
|
'shred',
|
|
'dd',
|
|
'fdisk',
|
|
'parted',
|
|
'gparted',
|
|
'cfdisk',
|
|
],
|
|
|
|
// Privilege escalation and system access
|
|
PRIVILEGE_COMMANDS: [
|
|
'sudo',
|
|
'su',
|
|
'passwd',
|
|
'chpasswd',
|
|
'usermod',
|
|
'chmod',
|
|
'chown',
|
|
'chgrp',
|
|
'setuid',
|
|
'setgid',
|
|
],
|
|
|
|
// Network and remote access tools
|
|
NETWORK_COMMANDS: [
|
|
'nc',
|
|
'netcat',
|
|
'nmap',
|
|
'telnet',
|
|
'ssh-keygen',
|
|
'iptables',
|
|
'ufw',
|
|
'firewall-cmd',
|
|
'ipfw',
|
|
],
|
|
|
|
// System service and process manipulation
|
|
SYSTEM_COMMANDS: [
|
|
'systemctl',
|
|
'service',
|
|
'kill',
|
|
'killall',
|
|
'pkill',
|
|
'mount',
|
|
'umount',
|
|
'swapon',
|
|
'swapoff',
|
|
],
|
|
|
|
// Dangerous regex patterns
|
|
DANGEROUS_PATTERNS: [
|
|
// File system destruction - block rm -rf with absolute paths
|
|
/rm\s+.*-rf\s*\/\s*$/i, // rm -rf ending at root directory
|
|
/rm\s+.*-rf\s*\/\w+/i, // rm -rf with any absolute path
|
|
/rm\s+.*-rf\s*\/etc/i, // rm -rf in /etc
|
|
/rm\s+.*-rf\s*\/usr/i, // rm -rf in /usr
|
|
/rm\s+.*-rf\s*\/bin/i, // rm -rf in /bin
|
|
/rm\s+.*-rf\s*\/sys/i, // rm -rf in /sys
|
|
/rm\s+.*-rf\s*\/proc/i, // rm -rf in /proc
|
|
/rm\s+.*-rf\s*\/boot/i, // rm -rf in /boot
|
|
/rm\s+.*-rf\s*\/home\/[^\/]*\s*$/i, // rm -rf entire home directory
|
|
/rm\s+.*-rf\s*\.\.+\//i, // rm -rf with parent directory traversal
|
|
/rm\s+.*-rf\s*\*.*\*/i, // rm -rf with multiple wildcards
|
|
/rm\s+.*-rf\s*\$\w+/i, // rm -rf with variables (could be dangerous)
|
|
/>\s*\/dev\/(sda|hda|nvme)/i,
|
|
/dd\s+.*of=\/dev\//i,
|
|
/shred\s+.*\/dev\//i,
|
|
/mkfs\.\w+\s+\/dev\//i,
|
|
|
|
// Fork bomb and resource exhaustion
|
|
/:\(\)\{\s*:\|:&\s*\};:/,
|
|
/while\s+true\s*;\s*do.*done/i,
|
|
/for\s*\(\(\s*;\s*;\s*\)\)/i,
|
|
|
|
// Command injection and chaining
|
|
/;\s*(rm|dd|mkfs|format)/i,
|
|
/&&\s*(rm|dd|mkfs|format)/i,
|
|
/\|\|\s*(rm|dd|mkfs|format)/i,
|
|
|
|
// Remote code execution
|
|
/\|\s*(sh|bash|zsh|fish)$/i,
|
|
/(wget|curl)\s+.*\|\s*(sh|bash)/i,
|
|
/(wget|curl)\s+.*-O-.*\|\s*(sh|bash)/i,
|
|
|
|
// Command substitution with dangerous commands
|
|
/`.*rm.*`/i,
|
|
/\$\(.*rm.*\)/i,
|
|
/`.*dd.*`/i,
|
|
/\$\(.*dd.*\)/i,
|
|
|
|
// Sensitive file access
|
|
/cat\s+\/etc\/(passwd|shadow|sudoers)/i,
|
|
/>\s*\/etc\/(passwd|shadow|sudoers)/i,
|
|
/echo\s+.*>>\s*\/etc\/(passwd|shadow|sudoers)/i,
|
|
|
|
// Network exfiltration
|
|
/\|\s*nc\s+\S+\s+\d+/i,
|
|
/curl\s+.*-d.*\$\(/i,
|
|
/wget\s+.*--post-data.*\$\(/i,
|
|
|
|
// Log manipulation
|
|
/>\s*\/var\/log\//i,
|
|
/rm\s+\/var\/log\//i,
|
|
/echo\s+.*>\s*~?\/?\.bash_history/i,
|
|
|
|
// Backdoor creation
|
|
/nc\s+.*-l.*-e/i,
|
|
/nc\s+.*-e.*-l/i,
|
|
/ncat\s+.*--exec/i,
|
|
/ssh-keygen.*authorized_keys/i,
|
|
|
|
// Crypto mining and malicious downloads
|
|
/(wget|curl).*\.(sh|py|pl|exe|bin).*\|\s*(sh|bash|python)/i,
|
|
/(xmrig|ccminer|cgminer|bfgminer)/i,
|
|
|
|
// Hardware direct access
|
|
/cat\s+\/dev\/(mem|kmem)/i,
|
|
/echo\s+.*>\s*\/dev\/(mem|kmem)/i,
|
|
|
|
// Kernel module manipulation
|
|
/(insmod|rmmod|modprobe)\s+/i,
|
|
|
|
// Cron job manipulation
|
|
/crontab\s+-e/i,
|
|
/echo\s+.*>>\s*\/var\/spool\/cron/i,
|
|
|
|
// Environment variable exposure
|
|
/env\s*\|\s*grep.*PASSWORD/i,
|
|
/printenv.*PASSWORD/i,
|
|
],
|
|
|
|
// Paths that should never be written to
|
|
PROTECTED_PATHS: [
|
|
'/etc/',
|
|
'/usr/',
|
|
'/bin/',
|
|
'/sbin/',
|
|
'/boot/',
|
|
'/sys/',
|
|
'/proc/',
|
|
'/dev/',
|
|
'/root/',
|
|
],
|
|
};
|
|
|
|
// Allowlist of safe commands (when used appropriately)
|
|
const SAFE_COMMANDS = [
|
|
'ls',
|
|
'dir',
|
|
'pwd',
|
|
'whoami',
|
|
'date',
|
|
'echo',
|
|
'cat',
|
|
'head',
|
|
'tail',
|
|
'grep',
|
|
'find',
|
|
'wc',
|
|
'sort',
|
|
'uniq',
|
|
'cut',
|
|
'awk',
|
|
'sed',
|
|
'git',
|
|
'npm',
|
|
'pnpm',
|
|
'node',
|
|
'bun',
|
|
'python',
|
|
'pip',
|
|
'cd',
|
|
'cp',
|
|
'mv',
|
|
'mkdir',
|
|
'touch',
|
|
'ln',
|
|
];
|
|
|
|
class CommandValidator {
|
|
constructor() {
|
|
this.logFile = '/Users/david/.claude/security.log';
|
|
}
|
|
|
|
/**
|
|
* Main validation function
|
|
*/
|
|
validate(command, toolName = 'Unknown') {
|
|
const result = {
|
|
isValid: true,
|
|
severity: 'LOW',
|
|
violations: [],
|
|
sanitizedCommand: command,
|
|
};
|
|
|
|
if (!command || typeof command !== 'string') {
|
|
result.isValid = false;
|
|
result.violations.push('Invalid command format');
|
|
return result;
|
|
}
|
|
|
|
// Normalize command for analysis
|
|
const normalizedCmd = command.trim().toLowerCase();
|
|
const cmdParts = normalizedCmd.split(/\s+/);
|
|
const mainCommand = cmdParts[0];
|
|
|
|
// Check against critical commands
|
|
if (SECURITY_RULES.CRITICAL_COMMANDS.includes(mainCommand)) {
|
|
result.isValid = false;
|
|
result.severity = 'CRITICAL';
|
|
result.violations.push(`Critical dangerous command: ${mainCommand}`);
|
|
}
|
|
|
|
// Check privilege escalation commands
|
|
if (SECURITY_RULES.PRIVILEGE_COMMANDS.includes(mainCommand)) {
|
|
result.isValid = false;
|
|
result.severity = 'HIGH';
|
|
result.violations.push(`Privilege escalation command: ${mainCommand}`);
|
|
}
|
|
|
|
// Check network commands
|
|
if (SECURITY_RULES.NETWORK_COMMANDS.includes(mainCommand)) {
|
|
result.isValid = false;
|
|
result.severity = 'HIGH';
|
|
result.violations.push(`Network/remote access command: ${mainCommand}`);
|
|
}
|
|
|
|
// Check system commands
|
|
if (SECURITY_RULES.SYSTEM_COMMANDS.includes(mainCommand)) {
|
|
result.isValid = false;
|
|
result.severity = 'HIGH';
|
|
result.violations.push(`System manipulation command: ${mainCommand}`);
|
|
}
|
|
|
|
// Check dangerous patterns
|
|
for (const pattern of SECURITY_RULES.DANGEROUS_PATTERNS) {
|
|
if (pattern.test(command)) {
|
|
result.isValid = false;
|
|
result.severity = 'CRITICAL';
|
|
result.violations.push(`Dangerous pattern detected: ${pattern.source}`);
|
|
}
|
|
}
|
|
|
|
// Check for protected path access (but allow common redirections like /dev/null)
|
|
for (const path of SECURITY_RULES.PROTECTED_PATHS) {
|
|
if (command.includes(path)) {
|
|
// Allow common safe redirections
|
|
if (
|
|
path === '/dev/' &&
|
|
(command.includes('/dev/null') ||
|
|
command.includes('/dev/stderr') ||
|
|
command.includes('/dev/stdout'))
|
|
) {
|
|
continue;
|
|
}
|
|
result.isValid = false;
|
|
result.severity = 'HIGH';
|
|
result.violations.push(`Access to protected path: ${path}`);
|
|
}
|
|
}
|
|
|
|
// Additional safety checks
|
|
if (command.length > 2000) {
|
|
result.isValid = false;
|
|
result.severity = 'MEDIUM';
|
|
result.violations.push('Command too long (potential buffer overflow)');
|
|
}
|
|
|
|
// Check for binary/encoded content
|
|
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(command)) {
|
|
result.isValid = false;
|
|
result.severity = 'HIGH';
|
|
result.violations.push('Binary or encoded content detected');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Log security events
|
|
*/
|
|
async logSecurityEvent(command, toolName, result, sessionId = null) {
|
|
const timestamp = new Date().toISOString();
|
|
const logEntry = {
|
|
timestamp,
|
|
sessionId,
|
|
toolName,
|
|
command: command.substring(0, 500), // Truncate for logs
|
|
blocked: !result.isValid,
|
|
severity: result.severity,
|
|
violations: result.violations,
|
|
source: 'claude-code-hook',
|
|
};
|
|
|
|
try {
|
|
// Write to log file
|
|
const logLine = JSON.stringify(logEntry) + '\n';
|
|
await Bun.write(this.logFile, logLine, { createPath: true, flag: 'a' });
|
|
|
|
// Also output to stderr for immediate visibility
|
|
console.error(
|
|
`[SECURITY] ${result.isValid ? 'ALLOWED' : 'BLOCKED'}: ${command.substring(0, 100)}`
|
|
);
|
|
} catch (error) {
|
|
console.error('Failed to write security log:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if command matches any allowed patterns from settings
|
|
*/
|
|
isExplicitlyAllowed(command, allowedPatterns = []) {
|
|
for (const pattern of allowedPatterns) {
|
|
// Convert Claude Code permission pattern to regex
|
|
// e.g., "Bash(git *)" becomes /^git\s+.*$/
|
|
if (pattern.startsWith('Bash(') && pattern.endsWith(')')) {
|
|
const cmdPattern = pattern.slice(5, -1); // Remove "Bash(" and ")"
|
|
const regex = new RegExp('^' + cmdPattern.replace(/\*/g, '.*') + '$', 'i');
|
|
if (regex.test(command)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main execution function
|
|
*/
|
|
async function main() {
|
|
const validator = new CommandValidator();
|
|
|
|
try {
|
|
// Read hook input from stdin
|
|
const stdin = process.stdin;
|
|
const chunks = [];
|
|
|
|
for await (const chunk of stdin) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
const input = Buffer.concat(chunks).toString();
|
|
|
|
if (!input.trim()) {
|
|
console.error('No input received from stdin');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Parse Claude Code hook JSON format
|
|
let hookData;
|
|
try {
|
|
hookData = JSON.parse(input);
|
|
} catch (error) {
|
|
console.error('Invalid JSON input:', error.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
const toolName = hookData.tool_name || 'Unknown';
|
|
const toolInput = hookData.tool_input || {};
|
|
const sessionId = hookData.session_id || null;
|
|
|
|
// Only validate Bash commands for now
|
|
if (toolName !== 'Bash') {
|
|
console.log(`Skipping validation for tool: ${toolName}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const command = toolInput.command;
|
|
if (!command) {
|
|
console.error('No command found in tool input');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Validate the command
|
|
const result = validator.validate(command, toolName);
|
|
|
|
// Log the security event
|
|
await validator.logSecurityEvent(command, toolName, result, sessionId);
|
|
|
|
// Output result and exit with appropriate code
|
|
if (result.isValid) {
|
|
console.log('Command validation passed');
|
|
process.exit(0); // Allow execution
|
|
} else {
|
|
console.error(`Command validation failed: ${result.violations.join(', ')}`);
|
|
console.error(`Severity: ${result.severity}`);
|
|
process.exit(2); // Block execution (Claude Code requires exit code 2)
|
|
}
|
|
} catch (error) {
|
|
console.error('Validation script error:', error);
|
|
// Fail safe - block execution on any script error
|
|
process.exit(2);
|
|
}
|
|
}
|
|
|
|
// Execute main function
|
|
main().catch(error => {
|
|
console.error('Fatal error:', error);
|
|
process.exit(2);
|
|
});
|