tasq/node_modules/@claude-flow/shared/dist/hooks/safety/bash-safety.js

481 lines
15 KiB
JavaScript

/**
* V3 Bash Safety Hook
*
* TypeScript conversion of V2 bash-hook.sh.
* Provides command safety analysis, dangerous command detection,
* secret detection, and safe alternatives.
*
* @module v3/shared/hooks/safety/bash-safety
*/
import { HookEvent, HookPriority, } from '../types.js';
/**
* Dangerous command patterns
*/
const DANGEROUS_PATTERNS = [
// Critical - Always block
{
pattern: /rm\s+(-[rRf]+\s+)*\//,
type: 'destructive',
severity: 'critical',
description: 'Recursive deletion from root directory',
block: true,
},
{
pattern: /rm\s+-rf\s+\/\*/,
type: 'destructive',
severity: 'critical',
description: 'Recursive deletion of all root files',
block: true,
},
{
pattern: /dd\s+if=.*of=\/dev\/(sd|hd|nvme)/,
type: 'destructive',
severity: 'critical',
description: 'Direct disk write that can destroy data',
block: true,
},
{
pattern: /mkfs\./,
type: 'destructive',
severity: 'critical',
description: 'Filesystem formatting command',
block: true,
},
{
pattern: />\s*\/dev\/sd[a-z]/,
type: 'destructive',
severity: 'critical',
description: 'Direct write to disk device',
block: true,
},
{
// Fork bomb patterns - various formats (with flexible spacing)
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:|bomb\s*\(\)|while\s+true.*fork/,
type: 'resource',
severity: 'critical',
description: 'Fork bomb detected',
block: true,
},
{
pattern: /chmod\s+(-R\s+)?777\s+\//,
type: 'privilege',
severity: 'critical',
description: 'Setting dangerous permissions on root',
block: true,
},
// High - Block but offer alternatives
{
pattern: /rm\s+-rf\s+\*/,
type: 'destructive',
severity: 'high',
description: 'Recursive deletion of all files in directory',
block: true,
},
{
pattern: /rm\s+-rf\s+\.\//,
type: 'destructive',
severity: 'high',
description: 'Recursive deletion of current directory',
block: true,
},
{
pattern: /rm\s+-rf\s+~/,
type: 'destructive',
severity: 'high',
description: 'Recursive deletion of home directory',
block: true,
},
{
pattern: /curl.*\|\s*(bash|sh|zsh)/,
type: 'dangerous',
severity: 'high',
description: 'Piping remote content directly to shell',
block: true,
},
{
pattern: /wget.*-O-\s*\|\s*(bash|sh|zsh)/,
type: 'dangerous',
severity: 'high',
description: 'Piping remote content directly to shell',
block: true,
},
{
pattern: /eval\s+.*\$\(/,
type: 'dangerous',
severity: 'high',
description: 'Dynamic code execution with command substitution',
block: true,
},
// Medium - Warn
{
pattern: /rm\s+(?!.*-i)/,
type: 'destructive',
severity: 'medium',
description: 'Remove command without interactive flag',
block: false,
},
{
pattern: /sudo\s+rm/,
type: 'privilege',
severity: 'medium',
description: 'Privileged file deletion',
block: false,
},
{
pattern: /sudo\s+chmod/,
type: 'privilege',
severity: 'medium',
description: 'Privileged permission change',
block: false,
},
{
pattern: /git\s+push\s+.*--force/,
type: 'destructive',
severity: 'medium',
description: 'Force push can overwrite remote history',
block: false,
},
{
pattern: /git\s+reset\s+--hard/,
type: 'destructive',
severity: 'medium',
description: 'Hard reset discards uncommitted changes',
block: false,
},
{
pattern: /DROP\s+(DATABASE|TABLE)/i,
type: 'destructive',
severity: 'high',
description: 'Database/table deletion command',
block: false,
},
{
pattern: /TRUNCATE\s+TABLE/i,
type: 'destructive',
severity: 'medium',
description: 'Table truncation command',
block: false,
},
// Low - Informational
{
pattern: /kill\s+-9/,
type: 'dangerous',
severity: 'low',
description: 'Force kill signal prevents graceful shutdown',
block: false,
},
{
pattern: /killall/,
type: 'dangerous',
severity: 'low',
description: 'Kills all processes by name',
block: false,
},
];
/**
* Secret patterns to detect and redact
*/
const SECRET_PATTERNS = [
{ pattern: /(password|passwd|pwd)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'password', redactGroup: 2 },
{ pattern: /(api[_-]?key)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'API key', redactGroup: 2 },
{ pattern: /(secret[_-]?key)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'secret key', redactGroup: 2 },
{ pattern: /(access[_-]?token)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'access token', redactGroup: 2 },
{ pattern: /(auth[_-]?token)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'auth token', redactGroup: 2 },
{ pattern: /(bearer)\s+([a-zA-Z0-9._-]+)/i, name: 'bearer token', redactGroup: 2 },
{ pattern: /(private[_-]?key)\s*[=:]\s*['"]?([^\s'"]+)/i, name: 'private key', redactGroup: 2 },
{ pattern: /(\bsk-[a-zA-Z0-9]{20,})/i, name: 'OpenAI API key' },
{ pattern: /(\bghp_[a-zA-Z0-9]{36,})/i, name: 'GitHub token' },
{ pattern: /(\bnpm_[a-zA-Z0-9]{36,})/i, name: 'npm token' },
{ pattern: /(AKIA[0-9A-Z]{16})/i, name: 'AWS access key' },
];
/**
* Common dependencies to check
*/
const DEPENDENCY_CHECKS = [
{ command: /\bjq\b/, dependency: 'jq' },
{ command: /\byq\b/, dependency: 'yq' },
{ command: /\bawk\b/, dependency: 'awk' },
{ command: /\bsed\b/, dependency: 'sed' },
{ command: /\bcurl\b/, dependency: 'curl' },
{ command: /\bwget\b/, dependency: 'wget' },
{ command: /\bgit\b/, dependency: 'git' },
{ command: /\bdocker\b/, dependency: 'docker' },
{ command: /\bkubectl\b/, dependency: 'kubectl' },
{ command: /\bpython3?\b/, dependency: 'python' },
{ command: /\bnode\b/, dependency: 'node' },
{ command: /\bnpm\b/, dependency: 'npm' },
{ command: /\byarn\b/, dependency: 'yarn' },
{ command: /\bpnpm\b/, dependency: 'pnpm' },
];
/**
* Safe alternatives for dangerous commands (with patterns for matching)
*/
const SAFE_ALTERNATIVES = [
{
pattern: /rm\s+-rf\s+\*/,
alternatives: [
'rm -ri * (interactive mode)',
'find . -maxdepth 1 -type f -delete (only files)',
'git clean -fd (for git repositories)',
],
},
{
pattern: /rm\s+-rf/,
alternatives: [
'rm -ri (interactive mode)',
'trash-cli (move to trash instead)',
'mv to backup directory first',
],
},
{
pattern: /kill\s+-9/,
alternatives: [
'kill (graceful termination first)',
'kill -15 (SIGTERM)',
'systemctl stop (for services)',
],
},
{
pattern: /curl.*\|\s*(bash|sh|zsh)/,
alternatives: [
'Download script first, review, then execute',
'Use package managers when available',
'Verify script hash before execution',
],
},
{
pattern: /wget.*\|\s*(bash|sh|zsh)/,
alternatives: [
'Download script first, review, then execute',
'Use package managers when available',
'Verify script hash before execution',
],
},
{
pattern: /git\s+push.*--force/,
alternatives: [
'git push --force-with-lease (safer)',
'Create backup branch first',
'git push --force-if-includes',
],
},
{
pattern: /git\s+reset\s+--hard/,
alternatives: [
'git stash (save changes first)',
'git reset --soft (keep changes staged)',
'Create backup branch first',
],
},
];
/**
* Bash Safety Hook Manager
*/
export class BashSafetyHook {
registry;
blockedCommands = new Set();
availableDependencies = new Set();
constructor(registry) {
this.registry = registry;
this.registerHooks();
this.detectAvailableDependencies();
}
/**
* Register bash safety hooks
*/
registerHooks() {
this.registry.register(HookEvent.PreCommand, this.analyzeCommand.bind(this), HookPriority.Critical, { name: 'bash-safety:pre-command' });
}
/**
* Detect available dependencies
*/
async detectAvailableDependencies() {
// In a real implementation, this would check which commands are available
// For now, assume common ones are available
const commonDeps = ['git', 'node', 'npm', 'curl', 'sed', 'awk'];
commonDeps.forEach(dep => this.availableDependencies.add(dep));
}
/**
* Analyze a command for safety
*/
async analyzeCommand(context) {
const commandInfo = context.command;
if (!commandInfo) {
return this.createResult('low', false, []);
}
const command = commandInfo.command;
const risks = [];
const warnings = [];
let blocked = false;
let blockReason;
let modifiedCommand;
let safeAlternatives;
// Check for dangerous patterns
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.pattern.test(command)) {
risks.push({
type: pattern.type,
severity: pattern.severity,
description: pattern.description,
pattern: pattern.pattern.toString(),
});
if (pattern.block) {
blocked = true;
blockReason = pattern.description;
}
// Find safe alternatives using pattern matching
for (const { pattern: altPattern, alternatives } of SAFE_ALTERNATIVES) {
if (altPattern.test(command)) {
safeAlternatives = alternatives;
break;
}
}
}
}
// Check for secrets
const { secrets, redactedCommand } = this.detectSecrets(command);
for (const secret of secrets) {
risks.push({
type: 'secret',
severity: 'high',
description: `Potential ${secret.name} detected in command`,
});
warnings.push(`Detected potential secret: ${secret.name}`);
}
// Check for missing dependencies
const missingDependencies = this.checkDependencies(command);
// Add -i flag to rm commands if not present
if (/\brm\s+/.test(command) && !/-i\b/.test(command) && !blocked) {
modifiedCommand = command.replace(/\brm\s+/, 'rm -i ');
warnings.push('Added -i flag for interactive confirmation');
}
// Calculate overall risk level
const riskLevel = this.calculateRiskLevel(risks);
// Determine if we should proceed
const shouldProceed = !blocked;
return {
success: true,
riskLevel,
blocked,
blockReason,
modifiedCommand,
risks,
safeAlternatives,
warnings: warnings.length > 0 ? warnings : undefined,
missingDependencies: missingDependencies.length > 0 ? missingDependencies : undefined,
redactedCommand: secrets.length > 0 ? redactedCommand : undefined,
abort: blocked,
data: blocked ? undefined : {
command: {
...commandInfo,
command: modifiedCommand || command,
isDestructive: risks.some(r => r.type === 'destructive'),
},
},
};
}
/**
* Detect secrets in command
*/
detectSecrets(command) {
const secrets = [];
let redactedCommand = command;
for (const { pattern, name, redactGroup } of SECRET_PATTERNS) {
const match = pattern.exec(command);
if (match) {
secrets.push({ name, position: match.index });
// Redact the secret value
if (redactGroup && match[redactGroup]) {
redactedCommand = redactedCommand.replace(match[redactGroup], '[REDACTED]');
}
else {
redactedCommand = redactedCommand.replace(match[0], `[REDACTED_${name.toUpperCase().replace(/\s/g, '_')}]`);
}
}
}
return { secrets, redactedCommand };
}
/**
* Check for missing dependencies
*/
checkDependencies(command) {
const missing = [];
for (const { command: pattern, dependency } of DEPENDENCY_CHECKS) {
if (pattern.test(command) && !this.availableDependencies.has(dependency)) {
missing.push(dependency);
}
}
return missing;
}
/**
* Calculate overall risk level
*/
calculateRiskLevel(risks) {
if (risks.length === 0) {
return 'low';
}
const severities = risks.map(r => r.severity);
if (severities.includes('critical')) {
return 'critical';
}
if (severities.includes('high')) {
return 'high';
}
if (severities.includes('medium')) {
return 'medium';
}
return 'low';
}
/**
* Create a result object
*/
createResult(riskLevel, blocked, risks) {
return {
success: true,
riskLevel,
blocked,
risks,
};
}
/**
* Manually analyze a command
*/
async analyze(command) {
const context = {
event: HookEvent.PreCommand,
timestamp: new Date(),
command: { command },
};
return this.analyzeCommand(context);
}
/**
* Add a custom dangerous pattern
*/
addDangerousPattern(pattern, type, severity, description, block = true) {
DANGEROUS_PATTERNS.push({ pattern, type, severity, description, block });
}
/**
* Mark a dependency as available
*/
markDependencyAvailable(dependency) {
this.availableDependencies.add(dependency);
}
/**
* Check if a command would be blocked
*/
wouldBlock(command) {
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.block && pattern.pattern.test(command)) {
return true;
}
}
return false;
}
}
/**
* Create bash safety hook
*/
export function createBashSafetyHook(registry) {
return new BashSafetyHook(registry);
}
//# sourceMappingURL=bash-safety.js.map