UNPKG

@felixgeelhaar/cclint

Version:

A comprehensive linter for CLAUDE.md files with multi-language code block validation

171 lines 6.22 kB
import { ContextFile } from '../../domain/ContextFile.js'; /** * Provides secure, sandboxed execution environment for plugins */ export class PluginSandbox { defaultConfig = { timeout: 5000, // 5 seconds maxMemory: 128, // 128 MB allowNetwork: false, allowFileSystem: false, allowedModules: ['path', 'url', 'util', 'string_decoder'], }; config; constructor(config) { this.config = { ...this.defaultConfig, ...config }; } /** * Execute a plugin in a sandboxed environment * @param plugin The plugin to execute * @param file The file to lint * @returns Sandbox execution result */ async executePlugin(plugin, file) { const startTime = Date.now(); const startMemory = process.memoryUsage().heapUsed; try { // For now, we'll use a timeout-based approach // In production, consider using VM2 or isolated-vm for better sandboxing const violations = await this.executeWithTimeout(() => this.runPluginRules(plugin, file), this.config.timeout); const executionTime = Date.now() - startTime; const memoryUsed = (process.memoryUsage().heapUsed - startMemory) / 1024 / 1024; // Check memory usage if (memoryUsed > this.config.maxMemory) { throw new Error(`Plugin exceeded memory limit: ${memoryUsed.toFixed(2)}MB > ${this.config.maxMemory}MB`); } return { success: true, violations, executionTime, memoryUsed, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error', executionTime: Date.now() - startTime, memoryUsed: (process.memoryUsage().heapUsed - startMemory) / 1024 / 1024, }; } } /** * Run plugin rules with basic isolation * @param plugin The plugin to run * @param file The file to lint * @returns Array of violations */ async runPluginRules(plugin, file) { const violations = []; for (const rule of plugin.rules) { try { // Create a safe copy of the file to prevent mutations const safeFile = this.createSafeFileCopy(file); // Execute the rule with error handling const ruleViolations = await Promise.resolve(rule.lint(safeFile)); // Validate the returned violations if (Array.isArray(ruleViolations)) { for (const violation of ruleViolations) { if (this.isValidViolation(violation)) { violations.push(violation); } } } } catch (error) { // Log but don't throw - one rule failure shouldn't stop others console.error(`Rule ${rule.id} failed:`, error); } } return violations; } /** * Execute a function with a timeout * @param fn The function to execute * @param timeout Timeout in milliseconds * @returns The function result */ executeWithTimeout(fn, timeout) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Plugin execution timed out after ${timeout}ms`)); }, timeout); Promise.resolve(fn()) .then(result => { clearTimeout(timer); resolve(result); }) .catch(error => { clearTimeout(timer); reject(error); }); }); } /** * Create a safe, immutable copy of a ContextFile * @param file The original file * @returns A safe copy */ createSafeFileCopy(file) { // Create a new instance with frozen properties const safeCopy = new ContextFile(file.path, file.content); // Prevent modifications return Object.freeze(safeCopy); } /** * Validate that a violation object is properly formed * @param violation The violation to validate * @returns True if valid */ isValidViolation(violation) { if (!violation || typeof violation !== 'object') { return false; } const v = violation; return (typeof v.ruleId === 'string' && typeof v.message === 'string' && v.severity != null && v.location != null); } /** * Validate plugin signature (for future implementation) * @param plugin The plugin to validate * @param signature The plugin signature * @returns True if signature is valid */ validatePluginSignature(plugin, signature) { if (!signature) { // No signature provided - consider this based on security policy return false; } // TODO: Implement actual signature verification // This would involve: // 1. Computing hash of plugin code // 2. Verifying signature against trusted public key // 3. Checking certificate chain if applicable // For now, we'll just check if a signature exists return signature.length > 0; } /** * Check if a module is allowed to be imported * @param moduleName The module name to check * @returns True if allowed */ isModuleAllowed(moduleName) { // Check against allowed list if (this.config.allowedModules.includes(moduleName)) { return true; } // Check for relative imports (could be dangerous) if (moduleName.startsWith('.') || moduleName.startsWith('/')) { return false; } // Check for node: protocol if (moduleName.startsWith('node:')) { const coreModule = moduleName.slice(5); return this.config.allowedModules.includes(coreModule); } return false; } } //# sourceMappingURL=PluginSandbox.js.map