UNPKG

create-sparc

Version:

NPX package to scaffold new projects with SPARC methodology structure

522 lines (449 loc) 18.6 kB
/** * MCP Security Module * * Provides security features for the MCP Configuration Wizard: * - Secure credential management * - Environment variable reference handling * - Permission scope validation * - Security auditing of configurations * - Warning system for potentially insecure configurations */ const fs = require('fs-extra'); const path = require('path'); const crypto = require('crypto'); const { logger } = require('../../utils'); /** * MCP Security Module */ const mcpSecurity = { /** * Audit a configuration for security issues * @param {Object} config - MCP configuration object * @returns {Object} Audit results with issues and recommendations */ auditConfiguration(config) { logger.debug('Auditing MCP configuration for security issues'); const results = { secure: true, issues: [], recommendations: [] }; if (!config || !config.mcpServers) { results.secure = false; results.issues.push({ severity: 'error', message: 'Invalid or empty configuration' }); return results; } // Check each server configuration for (const [serverId, serverConfig] of Object.entries(config.mcpServers)) { // Check for hardcoded credentials const credentialIssues = this.detectHardcodedCredentials(serverConfig, serverId); if (credentialIssues.length > 0) { results.secure = false; results.issues.push(...credentialIssues); } // Check for excessive permissions const permissionIssues = this.validatePermissionScope(serverConfig, serverId); if (permissionIssues.length > 0) { results.secure = false; results.issues.push(...permissionIssues); } // Check for insecure command configuration const commandIssues = this.validateCommandSecurity(serverConfig, serverId); if (commandIssues.length > 0) { results.secure = false; results.issues.push(...commandIssues); } } // Generate recommendations based on issues results.recommendations = this.generateRecommendations(results.issues); return results; }, /** * Detect hardcoded credentials in server configuration * @param {Object} serverConfig - Server configuration * @param {string} serverId - Server ID * @returns {Array} Array of credential issues */ detectHardcodedCredentials(serverConfig, serverId) { const issues = []; if (!serverConfig.args || !Array.isArray(serverConfig.args)) { return issues; } // Patterns that might indicate sensitive information const sensitivePatterns = [ { pattern: /^[A-Za-z0-9-_]{20,}$/, type: 'API key' }, { pattern: /^sk-[A-Za-z0-9]{20,}$/, type: 'OpenAI API key' }, { pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, type: 'UUID/token' }, { pattern: /^Bearer\s+[A-Za-z0-9-_]+$/, type: 'Bearer token' }, { pattern: /^[A-Za-z0-9+/]{40,}={0,2}$/, type: 'Base64 encoded data' } ]; // Parameter names that typically contain sensitive information const sensitiveParamNames = [ 'key', 'apikey', 'api-key', 'token', 'secret', 'password', 'credential', 'auth', 'authorization', 'access-token', 'refresh-token' ]; for (let i = 0; i < serverConfig.args.length; i++) { const arg = serverConfig.args[i]; // Check if this is a parameter name (starts with --) if (typeof arg === 'string' && arg.startsWith('--')) { const paramName = arg.substring(2).toLowerCase(); // Check if the next argument might be a hardcoded secret if (i + 1 < serverConfig.args.length) { const paramValue = serverConfig.args[i + 1]; // Check if parameter name is sensitive and value is not an environment variable reference const isSensitiveParam = sensitiveParamNames.some(name => paramName.includes(name)); const isHardcoded = isSensitiveParam && typeof paramValue === 'string' && !paramValue.includes('${env:'); if (isHardcoded) { issues.push({ severity: 'critical', message: `Hardcoded ${paramName} detected in server "${serverId}"`, location: `mcpServers.${serverId}.args`, recommendation: `Replace with environment variable reference: \${env:${serverId.toUpperCase()}_${paramName.toUpperCase().replace(/-/g, '_')}}` }); } } } else if (typeof arg === 'string') { // Check for values that match sensitive patterns for (const { pattern, type } of sensitivePatterns) { if (pattern.test(arg) && !arg.includes('${env:')) { issues.push({ severity: 'critical', message: `Potential ${type} detected in server "${serverId}" arguments`, location: `mcpServers.${serverId}.args`, recommendation: 'Replace with environment variable reference' }); break; } } } } return issues; }, /** * Validate permission scope of server configuration * @param {Object} serverConfig - Server configuration * @param {string} serverId - Server ID * @returns {Array} Array of permission issues */ validatePermissionScope(serverConfig, serverId) { const issues = []; if (!serverConfig.alwaysAllow || !Array.isArray(serverConfig.alwaysAllow)) { return issues; } // High-risk permissions that should be carefully reviewed const highRiskPermissions = [ 'admin', 'delete', 'write', 'execute', 'deploy', 'manage', 'create', 'update', 'remove', 'modify', 'execute_sql', 'execute_query' ]; // Check for high-risk permissions const grantedHighRiskPermissions = serverConfig.alwaysAllow.filter( permission => highRiskPermissions.some(risk => permission.includes(risk)) ); if (grantedHighRiskPermissions.length > 0) { issues.push({ severity: 'warning', message: `High-risk permissions granted to server "${serverId}": ${grantedHighRiskPermissions.join(', ')}`, location: `mcpServers.${serverId}.alwaysAllow`, recommendation: 'Review and limit permissions to only what is necessary' }); } // Check for wildcard permissions if (serverConfig.alwaysAllow.includes('*')) { issues.push({ severity: 'critical', message: `Wildcard permission granted to server "${serverId}"`, location: `mcpServers.${serverId}.alwaysAllow`, recommendation: 'Replace wildcard with specific required permissions' }); } // Check for excessive number of permissions if (serverConfig.alwaysAllow.length > 10) { issues.push({ severity: 'info', message: `Server "${serverId}" has a large number of permissions (${serverConfig.alwaysAllow.length})`, location: `mcpServers.${serverId}.alwaysAllow`, recommendation: 'Review and consolidate permissions where possible' }); } return issues; }, /** * Validate command security of server configuration * @param {Object} serverConfig - Server configuration * @param {string} serverId - Server ID * @returns {Array} Array of command security issues */ validateCommandSecurity(serverConfig, serverId) { const issues = []; if (!serverConfig.command || typeof serverConfig.command !== 'string') { issues.push({ severity: 'error', message: `Missing or invalid command for server "${serverId}"`, location: `mcpServers.${serverId}.command`, recommendation: 'Specify a valid command string' }); return issues; } // Check for potentially dangerous commands const dangerousCommands = ['rm', 'sudo', 'chmod', 'chown', 'eval']; if (dangerousCommands.some(cmd => serverConfig.command.includes(cmd))) { issues.push({ severity: 'critical', message: `Potentially dangerous command for server "${serverId}": ${serverConfig.command}`, location: `mcpServers.${serverId}.command`, recommendation: 'Avoid using system-level commands that could modify the system' }); } // Check for command injection vulnerabilities if (serverConfig.command.includes('$') || serverConfig.command.includes('`') || serverConfig.command.includes(';')) { issues.push({ severity: 'critical', message: `Potential command injection vulnerability in server "${serverId}"`, location: `mcpServers.${serverId}.command`, recommendation: 'Avoid using shell metacharacters in commands' }); } return issues; }, /** * Generate recommendations based on issues * @param {Array} issues - Array of security issues * @returns {Array} Array of recommendations */ generateRecommendations(issues) { const recommendations = []; // Group issues by type const credentialIssues = issues.filter(issue => issue.message.includes('Hardcoded') || issue.message.includes('API key')); const permissionIssues = issues.filter(issue => issue.location && issue.location.includes('alwaysAllow')); const commandIssues = issues.filter(issue => issue.location && issue.location.includes('command')); // Generate recommendations for credential issues if (credentialIssues.length > 0) { recommendations.push({ title: 'Secure Credential Management', steps: [ 'Use environment variable references (${env:VARIABLE_NAME}) for all sensitive information', 'Never hardcode API keys, tokens, or passwords in configuration files', 'Consider using a secrets management solution for production environments' ] }); } // Generate recommendations for permission issues if (permissionIssues.length > 0) { recommendations.push({ title: 'Permission Scope Management', steps: [ 'Follow the principle of least privilege - grant only permissions that are necessary', 'Avoid using wildcard (*) permissions', 'Regularly audit and review granted permissions' ] }); } // Generate recommendations for command issues if (commandIssues.length > 0) { recommendations.push({ title: 'Command Security', steps: [ 'Use specific package versions instead of "latest" to prevent supply chain attacks', 'Avoid commands that could modify the system or execute arbitrary code', 'Validate all inputs to prevent command injection' ] }); } return recommendations; }, /** * Secure an existing configuration by fixing common security issues * @param {Object} config - MCP configuration object * @returns {Object} Secured configuration and applied fixes */ secureConfiguration(config) { logger.debug('Securing MCP configuration'); const securedConfig = JSON.parse(JSON.stringify(config)); const appliedFixes = []; if (!securedConfig || !securedConfig.mcpServers) { return { securedConfig, appliedFixes }; } // Process each server configuration for (const [serverId, serverConfig] of Object.entries(securedConfig.mcpServers)) { // Fix hardcoded credentials if (serverConfig.args && Array.isArray(serverConfig.args)) { for (let i = 0; i < serverConfig.args.length; i++) { const arg = serverConfig.args[i]; // Check if this is a parameter name (starts with --) if (typeof arg === 'string' && arg.startsWith('--')) { const paramName = arg.substring(2).toLowerCase(); // Check if the next argument might be a hardcoded secret if (i + 1 < serverConfig.args.length) { const paramValue = serverConfig.args[i + 1]; // Check if parameter name is sensitive and value is not an environment variable reference const isSensitiveParam = this._isSensitiveParamName(paramName); const isHardcoded = isSensitiveParam && typeof paramValue === 'string' && !paramValue.includes('${env:'); if (isHardcoded) { // Generate environment variable name const envVarName = `${serverId.toUpperCase()}_${paramName.toUpperCase().replace(/-/g, '_')}`; // Replace hardcoded value with environment variable reference serverConfig.args[i + 1] = `\${env:${envVarName}}`; appliedFixes.push({ type: 'credential', message: `Replaced hardcoded ${paramName} with environment variable reference \${env:${envVarName}}`, location: `mcpServers.${serverId}.args` }); } } } } } // Fix permission issues if (serverConfig.alwaysAllow && Array.isArray(serverConfig.alwaysAllow)) { // Remove wildcard permissions if (serverConfig.alwaysAllow.includes('*')) { const index = serverConfig.alwaysAllow.indexOf('*'); serverConfig.alwaysAllow.splice(index, 1); appliedFixes.push({ type: 'permission', message: `Removed wildcard permission from server "${serverId}"`, location: `mcpServers.${serverId}.alwaysAllow` }); } } } return { securedConfig, appliedFixes }; }, /** * Validate environment variable references in a configuration * @param {Object} config - MCP configuration object * @returns {Object} Validation results */ validateEnvVarReferences(config) { logger.debug('Validating environment variable references'); const results = { valid: true, missingVariables: [], references: [] }; if (!config || !config.mcpServers) { results.valid = false; return results; } // Process each server configuration for (const [serverId, serverConfig] of Object.entries(config.mcpServers)) { if (serverConfig.args && Array.isArray(serverConfig.args)) { for (const arg of serverConfig.args) { if (typeof arg === 'string' && arg.includes('${env:')) { // Extract environment variable name const matches = arg.match(/\${env:([A-Za-z0-9_]+)}/g); if (matches) { for (const match of matches) { const envVarName = match.substring(6, match.length - 1); // Check if environment variable is set const isSet = process.env[envVarName] !== undefined; results.references.push({ name: envVarName, isSet, serverId, value: arg }); if (!isSet) { results.missingVariables.push(envVarName); results.valid = false; } } } } } } } return results; }, /** * Generate a secure configuration template with best practices * @param {string} serverId - Server ID * @param {string} serverType - Type of server (database, ai, cloud, etc.) * @returns {Object} Secure configuration template */ generateSecureTemplate(serverId, serverType = 'generic') { logger.debug(`Generating secure template for ${serverType} server: ${serverId}`); // Base secure configuration const secureConfig = { command: 'npx', args: [ '-y', `@${serverId}/mcp-server@1.0.0` // Specific version instead of latest ], alwaysAllow: [] }; // Add type-specific configurations switch (serverType.toLowerCase()) { case 'database': secureConfig.args.push('--connection-string'); secureConfig.args.push(`\${env:${serverId.toUpperCase()}_CONNECTION_STRING}`); secureConfig.alwaysAllow = ['list_tables', 'execute_query', 'describe_table']; break; case 'ai': secureConfig.args.push('--api-key'); secureConfig.args.push(`\${env:${serverId.toUpperCase()}_API_KEY}`); secureConfig.alwaysAllow = ['generate_text', 'generate_image', 'embed_text']; break; case 'cloud': secureConfig.args.push('--access-key'); secureConfig.args.push(`\${env:${serverId.toUpperCase()}_ACCESS_KEY}`); secureConfig.args.push('--secret-key'); secureConfig.args.push(`\${env:${serverId.toUpperCase()}_SECRET_KEY}`); secureConfig.alwaysAllow = ['list_resources', 'describe_resource', 'get_resource']; break; default: secureConfig.args.push('--token'); secureConfig.args.push(`\${env:${serverId.toUpperCase()}_TOKEN}`); secureConfig.alwaysAllow = ['read', 'list']; break; } return secureConfig; }, /** * Calculate integrity hash for a configuration * @param {Object} config - Configuration object * @returns {string} Integrity hash */ calculateIntegrityHash(config) { const configString = JSON.stringify(config); return crypto.createHash('sha256').update(configString).digest('hex'); }, /** * Verify configuration integrity * @param {Object} config - Configuration object * @param {string} expectedHash - Expected integrity hash * @returns {boolean} Whether the configuration is intact */ verifyIntegrity(config, expectedHash) { const actualHash = this.calculateIntegrityHash(config); return actualHash === expectedHash; }, /** * Check if a parameter name is sensitive * @param {string} paramName - Parameter name * @returns {boolean} Whether the parameter is sensitive * @private */ _isSensitiveParamName(paramName) { const sensitivePatterns = [ /token/i, /key/i, /secret/i, /password/i, /credential/i, /auth/i, /access/i ]; return sensitivePatterns.some(pattern => pattern.test(paramName)); } }; module.exports = { mcpSecurity };