@felixgeelhaar/cclint
Version:
A comprehensive linter for CLAUDE.md files with multi-language code block validation
171 lines • 6.22 kB
JavaScript
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