UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

595 lines (499 loc) 15.2 kB
/** * Security Module * * Provides security utilities for plugin development. * Implements best practices for input validation, sanitization, and safe operations. */ import * as path from 'path'; import * as fs from 'fs/promises'; import * as crypto from 'crypto'; // ============================================================================ // Input Validation // ============================================================================ /** * Validate and sanitize a string input. */ export function validateString( input: unknown, options?: { minLength?: number; maxLength?: number; pattern?: RegExp; trim?: boolean; lowercase?: boolean; uppercase?: boolean; } ): string | null { if (typeof input !== 'string') return null; let value = input; if (options?.trim) value = value.trim(); if (options?.lowercase) value = value.toLowerCase(); if (options?.uppercase) value = value.toUpperCase(); if (options?.minLength !== undefined && value.length < options.minLength) return null; if (options?.maxLength !== undefined && value.length > options.maxLength) return null; if (options?.pattern && !options.pattern.test(value)) return null; return value; } /** * Validate a number input. */ export function validateNumber( input: unknown, options?: { min?: number; max?: number; integer?: boolean; } ): number | null { const num = typeof input === 'number' ? input : parseFloat(String(input)); if (isNaN(num) || !isFinite(num)) return null; if (options?.min !== undefined && num < options.min) return null; if (options?.max !== undefined && num > options.max) return null; if (options?.integer && !Number.isInteger(num)) return null; return num; } /** * Validate a boolean input. */ export function validateBoolean(input: unknown): boolean | null { if (typeof input === 'boolean') return input; if (input === 'true' || input === '1' || input === 1) return true; if (input === 'false' || input === '0' || input === 0) return false; return null; } /** * Validate an array input. */ export function validateArray<T>( input: unknown, itemValidator: (item: unknown) => T | null, options?: { minLength?: number; maxLength?: number; unique?: boolean; } ): T[] | null { if (!Array.isArray(input)) return null; if (options?.minLength !== undefined && input.length < options.minLength) return null; if (options?.maxLength !== undefined && input.length > options.maxLength) return null; const result: T[] = []; for (const item of input) { const validated = itemValidator(item); if (validated === null) return null; result.push(validated); } if (options?.unique) { const uniqueSet = new Set(result.map(String)); if (uniqueSet.size !== result.length) return null; } return result; } /** * Validate an enum value. */ export function validateEnum<T extends string>( input: unknown, allowedValues: readonly T[] ): T | null { if (typeof input !== 'string') return null; if (!allowedValues.includes(input as T)) return null; return input as T; } // ============================================================================ // Path Security // ============================================================================ const MAX_PATH_LENGTH = 4096; const BLOCKED_PATH_PATTERNS = [ /\.\./, // Parent directory traversal /^~/, // Home directory expansion /^\/etc\//i, /^\/var\//i, /^\/tmp\//i, /^\/proc\//i, /^\/sys\//i, /^\/dev\//i, /^C:\\Windows/i, /^C:\\Program Files/i, ]; /** * Validate a file path for safety. */ export function validatePath( inputPath: unknown, options?: { allowedExtensions?: string[]; blockedPatterns?: RegExp[]; mustExist?: boolean; allowAbsolute?: boolean; } ): string | null { if (typeof inputPath !== 'string') return null; if (inputPath.length === 0 || inputPath.length > MAX_PATH_LENGTH) return null; // Normalize the path const normalized = path.normalize(inputPath); // Check blocked patterns const blockedPatterns = [...BLOCKED_PATH_PATTERNS, ...(options?.blockedPatterns ?? [])]; for (const pattern of blockedPatterns) { if (pattern.test(normalized)) return null; } // Check absolute path restriction if (!options?.allowAbsolute && path.isAbsolute(normalized)) return null; // Check extension if (options?.allowedExtensions) { const ext = path.extname(normalized).toLowerCase(); if (!options.allowedExtensions.includes(ext)) return null; } return normalized; } /** * Create a safe path relative to a base directory. * Prevents path traversal attacks. */ export function safePath(baseDir: string, ...segments: string[]): string { const resolved = path.resolve(baseDir, ...segments); const normalizedBase = path.normalize(baseDir); if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) { throw new Error(`Path traversal blocked: ${resolved}`); } return resolved; } /** * Async version of safePath that uses realpath. * More secure as it resolves symlinks. */ export async function safePathAsync(baseDir: string, ...segments: string[]): Promise<string> { const resolved = path.resolve(baseDir, ...segments); try { const realResolved = await fs.realpath(resolved).catch(() => resolved); const realBase = await fs.realpath(baseDir).catch(() => baseDir); if (!realResolved.startsWith(realBase + path.sep) && realResolved !== realBase) { throw new Error(`Path traversal blocked: ${realResolved}`); } return realResolved; } catch (error) { // Handle non-existent files const normalizedBase = path.normalize(baseDir); if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) { throw new Error(`Path traversal blocked: ${resolved}`); } return resolved; } } // ============================================================================ // JSON Security // ============================================================================ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']); /** * Parse JSON safely, stripping dangerous keys. */ export function safeJsonParse<T = unknown>(content: string): T { return JSON.parse(content, (key, value) => { if (DANGEROUS_KEYS.has(key)) { return undefined; } return value; }) as T; } /** * Stringify JSON with circular reference detection. */ export function safeJsonStringify( value: unknown, options?: { space?: number; maxDepth?: number; replacer?: (key: string, value: unknown) => unknown; } ): string { const seen = new WeakSet(); const maxDepth = options?.maxDepth ?? 100; let currentDepth = 0; const replacer = (key: string, val: unknown): unknown => { // Apply custom replacer first if (options?.replacer) { val = options.replacer(key, val); } // Strip dangerous keys if (DANGEROUS_KEYS.has(key)) { return undefined; } // Handle circular references if (val !== null && typeof val === 'object') { if (seen.has(val)) { return '[Circular]'; } seen.add(val); } // Depth limiting if (key !== '') { currentDepth++; if (currentDepth > maxDepth) { return '[Max Depth Exceeded]'; } } return val; }; return JSON.stringify(value, replacer, options?.space); } // ============================================================================ // Command Security // ============================================================================ const ALLOWED_COMMANDS = new Set([ 'npm', 'npx', 'node', 'git', 'tsc', 'vitest', 'jest', 'prettier', 'eslint', 'ls', 'cat', 'grep', 'find', ]); const BLOCKED_COMMANDS = new Set([ 'rm', 'del', 'format', 'dd', 'mkfs', 'fdisk', 'shutdown', 'reboot', 'poweroff', 'halt', 'passwd', 'sudo', 'su', 'chmod', 'chown', 'curl', 'wget', 'nc', 'netcat', ]); const SHELL_METACHARACTERS = /[|;&$`<>(){}[\]!\\]/; /** * Validate a command for safe execution. */ export function validateCommand( command: unknown, options?: { allowedCommands?: Set<string>; blockedCommands?: Set<string>; allowShellMetachars?: boolean; } ): { command: string; args: string[] } | null { if (typeof command !== 'string') return null; const trimmed = command.trim(); if (trimmed.length === 0) return null; // Check for shell metacharacters if (!options?.allowShellMetachars && SHELL_METACHARACTERS.test(trimmed)) { return null; } // Parse command and args const parts = trimmed.split(/\s+/); const cmd = parts[0].toLowerCase(); const args = parts.slice(1); // Check allowed/blocked lists const allowed = options?.allowedCommands ?? ALLOWED_COMMANDS; const blocked = options?.blockedCommands ?? BLOCKED_COMMANDS; if (blocked.has(cmd)) return null; if (!allowed.has(cmd) && allowed.size > 0) return null; return { command: cmd, args }; } /** * Escape a string for safe shell argument use. */ export function escapeShellArg(arg: string): string { // Empty string if (arg.length === 0) return "''"; // If no special characters, return as-is if (!/[^a-zA-Z0-9_\-=./:@]/.test(arg)) return arg; // Single-quote the argument and escape any single quotes return "'" + arg.replace(/'/g, "'\"'\"'") + "'"; } // ============================================================================ // Error Sanitization // ============================================================================ const SENSITIVE_PATTERNS = [ /password[=:]\s*\S+/gi, /api[_-]?key[=:]\s*\S+/gi, /secret[=:]\s*\S+/gi, /token[=:]\s*\S+/gi, /auth[=:]\s*\S+/gi, /bearer\s+\S+/gi, /\/\/[^:]+:[^@]+@/g, // Credentials in URLs ]; /** * Sanitize error messages to remove sensitive data. */ export function sanitizeErrorMessage(error: unknown): string { const message = error instanceof Error ? error.message : String(error); let sanitized = message; for (const pattern of SENSITIVE_PATTERNS) { sanitized = sanitized.replace(pattern, '[REDACTED]'); } // Truncate very long messages if (sanitized.length > 1000) { sanitized = sanitized.substring(0, 1000) + '... [truncated]'; } return sanitized; } /** * Create a safe error object for logging/transmission. */ export function sanitizeError(error: unknown): { name: string; message: string; code?: string; } { if (error instanceof Error) { return { name: error.name, message: sanitizeErrorMessage(error), code: (error as NodeJS.ErrnoException).code, }; } return { name: 'Error', message: sanitizeErrorMessage(error), }; } // ============================================================================ // Rate Limiting // ============================================================================ export interface RateLimiter { tryAcquire(): boolean; getRemaining(): number; reset(): void; } /** * Create a token bucket rate limiter. */ export function createRateLimiter(options: { maxTokens: number; refillRate: number; refillInterval: number; }): RateLimiter { let tokens = options.maxTokens; let lastRefill = Date.now(); const refill = () => { const now = Date.now(); const elapsed = now - lastRefill; const refillCount = Math.floor(elapsed / options.refillInterval) * options.refillRate; if (refillCount > 0) { tokens = Math.min(options.maxTokens, tokens + refillCount); lastRefill = now; } }; return { tryAcquire(): boolean { refill(); if (tokens > 0) { tokens--; return true; } return false; }, getRemaining(): number { refill(); return tokens; }, reset(): void { tokens = options.maxTokens; lastRefill = Date.now(); }, }; } // ============================================================================ // Crypto Utilities // ============================================================================ /** * Generate a secure random ID. */ export function generateSecureId(length: number = 32): string { return crypto.randomBytes(length).toString('hex'); } /** * Generate a secure random token (URL-safe). */ export function generateSecureToken(length: number = 32): string { return crypto.randomBytes(length).toString('base64url'); } /** * Hash a string securely. */ export function hashString(input: string, algorithm: string = 'sha256'): string { return crypto.createHash(algorithm).update(input).digest('hex'); } /** * Compare two strings in constant time. */ export function constantTimeCompare(a: string, b: string): boolean { if (a.length !== b.length) return false; const bufA = Buffer.from(a); const bufB = Buffer.from(b); return crypto.timingSafeEqual(bufA, bufB); } // ============================================================================ // Resource Limits // ============================================================================ export interface ResourceLimits { maxMemoryMB: number; maxCPUPercent: number; maxFileSize: number; maxOpenFiles: number; maxExecutionTime: number; } const DEFAULT_LIMITS: ResourceLimits = { maxMemoryMB: 512, maxCPUPercent: 80, maxFileSize: 10 * 1024 * 1024, // 10MB maxOpenFiles: 100, maxExecutionTime: 30000, // 30s }; /** * Create a resource limiter. */ export function createResourceLimiter(limits?: Partial<ResourceLimits>): { check(): { ok: boolean; violations: string[] }; enforce<T>(fn: () => Promise<T>): Promise<T>; } { const config = { ...DEFAULT_LIMITS, ...limits }; return { check(): { ok: boolean; violations: string[] } { const violations: string[] = []; const memUsage = process.memoryUsage(); const memMB = memUsage.heapUsed / 1024 / 1024; if (memMB > config.maxMemoryMB) { violations.push(`Memory usage ${memMB.toFixed(1)}MB exceeds limit ${config.maxMemoryMB}MB`); } return { ok: violations.length === 0, violations, }; }, async enforce<T>(fn: () => Promise<T>): Promise<T> { const check = this.check(); if (!check.ok) { throw new Error(`Resource limits exceeded: ${check.violations.join(', ')}`); } return Promise.race([ fn(), new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Execution time limit exceeded')), config.maxExecutionTime) ), ]); }, }; } // ============================================================================ // Export All // ============================================================================ export const Security = { // Validation validateString, validateNumber, validateBoolean, validateArray, validateEnum, validatePath, validateCommand, // Path security safePath, safePathAsync, // JSON security safeJsonParse, safeJsonStringify, // Command security escapeShellArg, // Error sanitization sanitizeError, sanitizeErrorMessage, // Rate limiting createRateLimiter, // Crypto generateSecureId, generateSecureToken, hashString, constantTimeCompare, // Resource limits createResourceLimiter, };