## Major Enhancements ### 🏗️ Architecture & Infrastructure - Implement comprehensive Docker testing infrastructure with hermetic environment - Add Forgejo Actions CI/CD pipeline for automated deployments - Create Page Object Model (POM) testing architecture reducing test duplication by 90% - Establish security-first development patterns with input validation and output escaping ### 🧪 Testing Framework Modernization - Migrate 146+ tests from 80 duplicate files to centralized architecture - Add comprehensive E2E test suites for all user roles and workflows - Implement WordPress error detection with automatic site health monitoring - Create robust browser lifecycle management with proper cleanup ### 📚 Documentation & Guides - Add comprehensive development best practices guide - Create detailed administrator setup documentation - Establish user guides for trainers and master trainers - Document security incident reports and migration guides ### 🔧 Core Plugin Features - Enhance trainer profile management with certification system - Improve find trainer functionality with advanced filtering - Strengthen master trainer area with content management - Add comprehensive venue and organizer management ### 🛡️ Security & Reliability - Implement security-first patterns throughout codebase - Add comprehensive input validation and output escaping - Create secure credential management system - Establish proper WordPress role-based access control ### 🎯 WordPress Integration - Strengthen singleton pattern implementation across all classes - Enhance template hierarchy with proper WordPress integration - Improve page manager with hierarchical URL structure - Add comprehensive shortcode and menu system ### 🔍 Developer Experience - Add extensive debugging and troubleshooting tools - Create comprehensive test data seeding scripts - Implement proper error handling and logging - Establish consistent code patterns and standards ### 📊 Performance & Optimization - Optimize database queries and caching strategies - Improve asset loading and script management - Enhance template rendering performance - Streamline user experience across all interfaces 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
432 lines
No EOL
14 KiB
JavaScript
432 lines
No EOL
14 KiB
JavaScript
/**
|
|
* HVAC Testing Framework - Secure Command Execution
|
|
*
|
|
* Provides secure command execution with input validation, parameterization,
|
|
* and command allowlisting to prevent injection vulnerabilities.
|
|
*
|
|
* Security Features:
|
|
* - Command allowlisting with parameter validation
|
|
* - Input sanitization and validation
|
|
* - Subprocess execution with proper isolation
|
|
* - Audit logging of all command executions
|
|
* - Timeout and resource limits
|
|
*
|
|
* @author Claude Code - Emergency Security Response
|
|
* @version 1.0.0
|
|
* @security CRITICAL - Prevents command injection vulnerabilities
|
|
*/
|
|
|
|
const { spawn, execFile } = require('child_process');
|
|
const path = require('path');
|
|
const { getCredentialManager } = require('./SecureCredentialManager');
|
|
|
|
class SecureCommandExecutor {
|
|
constructor() {
|
|
this.credentialManager = getCredentialManager();
|
|
this.allowedCommands = this.initializeCommandAllowlist();
|
|
this.executionLog = [];
|
|
}
|
|
|
|
/**
|
|
* Initialize allowlist of safe commands with their parameter patterns
|
|
* @returns {Map} Command allowlist configuration
|
|
*/
|
|
initializeCommandAllowlist() {
|
|
return new Map([
|
|
// WordPress CLI commands
|
|
['wp', {
|
|
binary: 'wp',
|
|
allowedSubcommands: [
|
|
'rewrite flush',
|
|
'eval',
|
|
'user list',
|
|
'user get',
|
|
'option get',
|
|
'option update',
|
|
'db query',
|
|
'plugin list',
|
|
'theme list'
|
|
],
|
|
parameterValidation: {
|
|
'eval': /^['"][^'";&|`$()]*['"]$/,
|
|
'user get': /^[a-zA-Z0-9@._-]+$/,
|
|
'option get': /^[a-zA-Z0-9_-]+$/,
|
|
'option update': /^[a-zA-Z0-9_-]+\s+['"][^'";&|`$()]*['"]$/,
|
|
'db query': /^['"]SELECT\s+[^'";&|`$()]*['"]$/
|
|
},
|
|
timeout: 30000,
|
|
maxOutputSize: 1024 * 1024 // 1MB
|
|
}],
|
|
|
|
// Node.js and npm commands
|
|
['node', {
|
|
binary: 'node',
|
|
allowedSubcommands: [
|
|
'--version',
|
|
'-e'
|
|
],
|
|
parameterValidation: {
|
|
'-e': /^['"]console\.log\([^'";&|`$()]*\)['"]$/
|
|
},
|
|
timeout: 10000,
|
|
maxOutputSize: 64 * 1024 // 64KB
|
|
}],
|
|
|
|
// Playwright commands
|
|
['npx', {
|
|
binary: 'npx',
|
|
allowedSubcommands: [
|
|
'playwright install',
|
|
'playwright test'
|
|
],
|
|
parameterValidation: {
|
|
'playwright test': /^[a-zA-Z0-9/_.-]+\.js$/
|
|
},
|
|
timeout: 120000,
|
|
maxOutputSize: 10 * 1024 * 1024 // 10MB
|
|
}],
|
|
|
|
// System information commands (read-only)
|
|
['ls', {
|
|
binary: 'ls',
|
|
allowedSubcommands: ['-la', '-l', ''],
|
|
parameterValidation: {
|
|
'': /^[a-zA-Z0-9/_.-]+$/,
|
|
'-l': /^[a-zA-Z0-9/_.-]+$/,
|
|
'-la': /^[a-zA-Z0-9/_.-]+$/
|
|
},
|
|
timeout: 5000,
|
|
maxOutputSize: 64 * 1024
|
|
}],
|
|
|
|
['cat', {
|
|
binary: 'cat',
|
|
allowedSubcommands: [''],
|
|
parameterValidation: {
|
|
'': /^[a-zA-Z0-9/_.-]+\.(log|txt|json)$/
|
|
},
|
|
timeout: 5000,
|
|
maxOutputSize: 1024 * 1024
|
|
}]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Execute a command securely with validation and isolation
|
|
* @param {string} command - Command to execute
|
|
* @param {Array} args - Command arguments
|
|
* @param {Object} options - Execution options
|
|
* @returns {Promise<Object>} Execution result
|
|
*/
|
|
async executeCommand(command, args = [], options = {}) {
|
|
// Validate command is in allowlist
|
|
const commandConfig = this.allowedCommands.get(command);
|
|
if (!commandConfig) {
|
|
throw new Error(`Command not allowed: ${command}`);
|
|
}
|
|
|
|
// Validate subcommand and parameters
|
|
await this.validateCommandParameters(command, args, commandConfig);
|
|
|
|
// Prepare execution environment
|
|
const executionOptions = this.prepareExecutionOptions(commandConfig, options);
|
|
|
|
// Log command execution attempt
|
|
const executionId = this.logCommandExecution(command, args, options);
|
|
|
|
try {
|
|
// Execute command with isolation
|
|
const result = await this.executeWithIsolation(
|
|
commandConfig.binary,
|
|
args,
|
|
executionOptions
|
|
);
|
|
|
|
// Log successful execution
|
|
this.logCommandResult(executionId, 'SUCCESS', result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
// Log failed execution
|
|
this.logCommandResult(executionId, 'FAILED', { error: error.message });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate command parameters against allowlist patterns
|
|
* @param {string} command - Base command
|
|
* @param {Array} args - Command arguments
|
|
* @param {Object} commandConfig - Command configuration
|
|
*/
|
|
async validateCommandParameters(command, args, commandConfig) {
|
|
if (args.length === 0) return;
|
|
|
|
const subcommand = args[0];
|
|
const fullSubcommand = args.join(' ');
|
|
|
|
// Check if subcommand is allowed
|
|
const isAllowed = commandConfig.allowedSubcommands.some(allowed => {
|
|
if (allowed === '') return true;
|
|
if (allowed === subcommand) return true;
|
|
if (fullSubcommand.startsWith(allowed)) return true;
|
|
return false;
|
|
});
|
|
|
|
if (!isAllowed) {
|
|
throw new Error(`Subcommand not allowed: ${command} ${fullSubcommand}`);
|
|
}
|
|
|
|
// Validate parameters against patterns
|
|
if (commandConfig.parameterValidation) {
|
|
for (const [pattern, regex] of Object.entries(commandConfig.parameterValidation)) {
|
|
if (fullSubcommand.startsWith(pattern)) {
|
|
const parameterPart = fullSubcommand.substring(pattern.length).trim();
|
|
if (parameterPart && !regex.test(parameterPart)) {
|
|
throw new Error(`Invalid parameters for ${command} ${pattern}: ${parameterPart}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Additional security checks
|
|
await this.performSecurityChecks(args);
|
|
}
|
|
|
|
/**
|
|
* Perform additional security checks on command arguments
|
|
* @param {Array} args - Command arguments
|
|
*/
|
|
async performSecurityChecks(args) {
|
|
const dangerousPatterns = [
|
|
/[;&|`$(){}]/, // Shell metacharacters
|
|
/\.\./, // Directory traversal
|
|
/\/etc\/passwd/, // System file access
|
|
/rm\s+-rf/, // Destructive commands
|
|
/sudo/, // Privilege escalation
|
|
/chmod/, // Permission changes
|
|
/curl.*http/, // External network access
|
|
/wget.*http/ // External downloads
|
|
];
|
|
|
|
const fullCommand = args.join(' ');
|
|
for (const pattern of dangerousPatterns) {
|
|
if (pattern.test(fullCommand)) {
|
|
throw new Error(`Security violation: dangerous pattern detected in command`);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare secure execution options
|
|
* @param {Object} commandConfig - Command configuration
|
|
* @param {Object} userOptions - User-provided options
|
|
* @returns {Object} Execution options
|
|
*/
|
|
prepareExecutionOptions(commandConfig, userOptions) {
|
|
return {
|
|
timeout: commandConfig.timeout,
|
|
maxBuffer: commandConfig.maxOutputSize,
|
|
env: this.createSecureEnvironment(),
|
|
shell: false, // Disable shell to prevent injection
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
...userOptions
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create secure execution environment
|
|
* @returns {Object} Environment variables
|
|
*/
|
|
createSecureEnvironment() {
|
|
// Start with minimal environment
|
|
const secureEnv = {
|
|
PATH: '/usr/local/bin:/usr/bin:/bin',
|
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
HOME: process.env.HOME
|
|
};
|
|
|
|
// Add WordPress CLI path if needed
|
|
if (process.env.WP_CLI_PATH) {
|
|
secureEnv.WP_CLI_PATH = process.env.WP_CLI_PATH;
|
|
}
|
|
|
|
return secureEnv;
|
|
}
|
|
|
|
/**
|
|
* Execute command with proper isolation
|
|
* @param {string} binary - Command binary
|
|
* @param {Array} args - Arguments
|
|
* @param {Object} options - Execution options
|
|
* @returns {Promise<Object>} Execution result
|
|
*/
|
|
async executeWithIsolation(binary, args, options) {
|
|
return new Promise((resolve, reject) => {
|
|
const startTime = Date.now();
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
const child = spawn(binary, args, options);
|
|
|
|
// Set up timeout
|
|
const timeoutId = setTimeout(() => {
|
|
child.kill('SIGTERM');
|
|
reject(new Error(`Command timeout after ${options.timeout}ms`));
|
|
}, options.timeout);
|
|
|
|
// Collect output with size limits
|
|
child.stdout.on('data', (data) => {
|
|
stdout += data.toString();
|
|
if (stdout.length > options.maxBuffer) {
|
|
child.kill('SIGTERM');
|
|
reject(new Error('Output size limit exceeded'));
|
|
}
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
stderr += data.toString();
|
|
if (stderr.length > options.maxBuffer) {
|
|
child.kill('SIGTERM');
|
|
reject(new Error('Error output size limit exceeded'));
|
|
}
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
clearTimeout(timeoutId);
|
|
const duration = Date.now() - startTime;
|
|
|
|
if (code === 0) {
|
|
resolve({
|
|
stdout: stdout.trim(),
|
|
stderr: stderr.trim(),
|
|
exitCode: code,
|
|
duration
|
|
});
|
|
} else {
|
|
reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
|
|
}
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
clearTimeout(timeoutId);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log command execution for audit trail
|
|
* @param {string} command - Command name
|
|
* @param {Array} args - Arguments
|
|
* @param {Object} options - Options
|
|
* @returns {string} Execution ID
|
|
*/
|
|
logCommandExecution(command, args, options) {
|
|
const executionId = require('crypto').randomUUID();
|
|
const logEntry = {
|
|
executionId,
|
|
timestamp: new Date().toISOString(),
|
|
command,
|
|
args: args.map(arg => this.sanitizeForLogging(arg)),
|
|
options: this.sanitizeOptionsForLogging(options),
|
|
status: 'STARTED'
|
|
};
|
|
|
|
this.executionLog.push(logEntry);
|
|
this.credentialManager.auditLogger('COMMAND_EXECUTION_STARTED', logEntry);
|
|
|
|
return executionId;
|
|
}
|
|
|
|
/**
|
|
* Log command execution result
|
|
* @param {string} executionId - Execution ID
|
|
* @param {string} status - Execution status
|
|
* @param {Object} result - Execution result
|
|
*/
|
|
logCommandResult(executionId, status, result) {
|
|
const logEntry = {
|
|
executionId,
|
|
timestamp: new Date().toISOString(),
|
|
status,
|
|
duration: result.duration,
|
|
exitCode: result.exitCode,
|
|
outputSize: result.stdout ? result.stdout.length : 0,
|
|
errorSize: result.stderr ? result.stderr.length : 0
|
|
};
|
|
|
|
this.executionLog.push(logEntry);
|
|
this.credentialManager.auditLogger('COMMAND_EXECUTION_COMPLETED', logEntry);
|
|
}
|
|
|
|
/**
|
|
* Sanitize arguments for logging (remove sensitive data)
|
|
* @param {string} arg - Argument to sanitize
|
|
* @returns {string} Sanitized argument
|
|
*/
|
|
sanitizeForLogging(arg) {
|
|
// Remove potential passwords and sensitive data
|
|
return arg.replace(/password[=:]\s*[^\s]+/gi, 'password=***')
|
|
.replace(/token[=:]\s*[^\s]+/gi, 'token=***')
|
|
.replace(/key[=:]\s*[^\s]+/gi, 'key=***');
|
|
}
|
|
|
|
/**
|
|
* Sanitize execution options for logging
|
|
* @param {Object} options - Options to sanitize
|
|
* @returns {Object} Sanitized options
|
|
*/
|
|
sanitizeOptionsForLogging(options) {
|
|
const sanitized = { ...options };
|
|
if (sanitized.env) {
|
|
sanitized.env = Object.keys(sanitized.env);
|
|
}
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* WordPress-specific secure command execution
|
|
* @param {string} wpCommand - WP-CLI command
|
|
* @param {Array} args - Additional arguments
|
|
* @returns {Promise<Object>} Command result
|
|
*/
|
|
async executeWordPressCommand(wpCommand, args = []) {
|
|
const wpArgs = wpCommand.split(' ').concat(args);
|
|
return await this.executeCommand('wp', wpArgs);
|
|
}
|
|
|
|
/**
|
|
* Get execution audit log
|
|
* @returns {Array} Complete execution log
|
|
*/
|
|
getExecutionLog() {
|
|
return [...this.executionLog];
|
|
}
|
|
|
|
/**
|
|
* Clear execution log (for cleanup)
|
|
*/
|
|
clearExecutionLog() {
|
|
this.executionLog = [];
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let commandExecutorInstance = null;
|
|
|
|
/**
|
|
* Get singleton instance of SecureCommandExecutor
|
|
* @returns {SecureCommandExecutor}
|
|
*/
|
|
function getCommandExecutor() {
|
|
if (!commandExecutorInstance) {
|
|
commandExecutorInstance = new SecureCommandExecutor();
|
|
}
|
|
return commandExecutorInstance;
|
|
}
|
|
|
|
module.exports = {
|
|
SecureCommandExecutor,
|
|
getCommandExecutor
|
|
}; |