Initial commit : docs (specs, data-model, architecture, roadmap)
This commit is contained in:
426
.claude/validate-commands.js
Normal file
426
.claude/validate-commands.js
Normal file
@@ -0,0 +1,426 @@
|
||||
#!/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/melvynx/.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);
|
||||
});
|
||||
Reference in New Issue
Block a user