vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
456 lines (455 loc) • 19.3 kB
JavaScript
import path from 'path';
import { extractVibeTaskManagerSecurityConfig } from '../utils/config-loader.js';
import logger from '../../../logger.js';
export class UnifiedSecurityConfigManager {
static instance = null;
config = null;
mcpConfig = null;
constructor() {
logger.info('Unified Security Configuration Manager initialized');
}
static getInstance() {
if (!UnifiedSecurityConfigManager.instance) {
UnifiedSecurityConfigManager.instance = new UnifiedSecurityConfigManager();
}
return UnifiedSecurityConfigManager.instance;
}
initializeFromMCPConfig(mcpConfig) {
this.mcpConfig = mcpConfig;
try {
const securityConfig = extractVibeTaskManagerSecurityConfig(mcpConfig);
this.config = {
allowedReadDirectory: securityConfig.allowedReadDirectory,
allowedWriteDirectory: securityConfig.allowedWriteDirectory,
securityMode: securityConfig.securityMode,
allowedDirectories: [
securityConfig.allowedReadDirectory,
securityConfig.allowedWriteDirectory
],
performanceThresholdMs: 50,
enablePermissionChecking: true,
enableBlacklist: true,
enableExtensionFiltering: securityConfig.securityMode === 'strict',
maxPathLength: 4096,
allowedDir: securityConfig.allowedReadDirectory,
outputDir: securityConfig.allowedWriteDirectory,
serviceBoundaries: {
vibeTaskManager: {
readDir: securityConfig.allowedReadDirectory,
writeDir: securityConfig.allowedWriteDirectory
},
codeMapGenerator: {
allowedDir: securityConfig.allowedReadDirectory,
outputDir: securityConfig.allowedWriteDirectory
},
contextCurator: {
readDir: securityConfig.allowedReadDirectory,
outputDir: securityConfig.allowedWriteDirectory
}
}
};
logger.info({
allowedReadDirectory: this.config.allowedReadDirectory,
allowedWriteDirectory: this.config.allowedWriteDirectory,
securityMode: this.config.securityMode,
allowedDirectories: this.config.allowedDirectories
}, 'Unified security configuration initialized from MCP client config');
}
catch (error) {
logger.error({ err: error }, 'Failed to initialize security configuration from MCP client config');
throw error;
}
}
isInitialized() {
return this.config !== null;
}
getConfig() {
if (!this.config) {
throw new Error('Unified security configuration not initialized. Call initializeFromMCPConfig() first.');
}
return { ...this.config };
}
getFilesystemSecurityConfig() {
const config = this.getConfig();
return {
allowedDirectories: config.allowedDirectories,
securityMode: config.securityMode,
enablePermissionChecking: config.enablePermissionChecking,
enableBlacklist: config.enableBlacklist,
enableExtensionFiltering: config.enableExtensionFiltering,
maxPathLength: config.maxPathLength,
performanceThresholdMs: config.performanceThresholdMs
};
}
getPathValidatorConfig() {
const config = this.getConfig();
return {
allowedDirectories: config.allowedDirectories,
maxPathLength: config.maxPathLength
};
}
getSecurityManagerConfig() {
const config = this.getConfig();
return {
pathSecurity: {
allowedDirectories: config.allowedDirectories
},
strictMode: config.securityMode === 'strict',
performanceThresholdMs: config.performanceThresholdMs
};
}
getCodeMapGeneratorConfig() {
const config = this.getConfig();
return {
allowedDir: config.allowedReadDirectory,
outputDir: config.allowedWriteDirectory,
securityMode: config.securityMode
};
}
getContextCuratorConfig() {
const config = this.getConfig();
return {
readDir: config.allowedReadDirectory,
outputDir: config.allowedWriteDirectory,
allowedDirectories: config.allowedDirectories,
securityMode: config.securityMode
};
}
getVibeTaskManagerSecurityValidatorConfig() {
const config = this.getConfig();
return {
readDir: config.allowedReadDirectory,
writeDir: config.allowedWriteDirectory,
securityMode: config.securityMode
};
}
getServiceBoundaries(serviceName) {
const config = this.getConfig();
const boundaries = config.serviceBoundaries[serviceName];
if (!boundaries) {
throw new Error(`Service boundaries not found for service: ${serviceName}`);
}
return boundaries;
}
isPathAllowed(filePath, operation = 'read') {
const config = this.getConfig();
try {
const resolvedPath = path.resolve(filePath);
if (operation === 'read') {
return resolvedPath.startsWith(config.allowedReadDirectory);
}
else {
return resolvedPath.startsWith(config.allowedWriteDirectory);
}
}
catch (error) {
logger.error({ err: error, filePath, operation }, 'Error validating path');
return false;
}
}
normalizePath(inputPath) {
if (!inputPath || typeof inputPath !== 'string') {
throw new Error('Path cannot be empty, undefined, or non-string');
}
try {
const sanitized = inputPath.replace(/[<>:"|?*\x00-\x1f]/g, '');
const normalizedPath = path.normalize(sanitized);
const isTestMode = process.env.NODE_ENV === 'test';
if (isTestMode && (normalizedPath.includes('/tmp/') || normalizedPath.includes('temp/'))) {
if (path.isAbsolute(normalizedPath)) {
logger.debug(`Using test path as-is: ${normalizedPath}`);
return normalizedPath;
}
}
return path.isAbsolute(normalizedPath)
? normalizedPath
: path.resolve(normalizedPath);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Path normalization failed: ${errorMessage}`);
}
}
isPathWithin(childPath, parentPath) {
try {
const normalizedChild = this.normalizePath(childPath);
const normalizedParent = this.normalizePath(parentPath);
if (normalizedChild === normalizedParent) {
return true;
}
const separator = path.sep;
const parentWithSep = normalizedParent.endsWith(separator)
? normalizedParent
: normalizedParent + separator;
return normalizedChild.startsWith(parentWithSep);
}
catch (error) {
logger.error({ err: error, childPath, parentPath }, 'Error checking path containment');
return false;
}
}
validatePathSecurity(inputPath, options = {}) {
const { operation = 'read', allowTestMode = true, checkExtensions = false, allowedExtensions = ['.md', '.json', '.txt', '.yaml', '.yml', '.js', '.ts'], strictMode = true } = options;
try {
const config = this.getConfig();
const isTestMode = process.env.NODE_ENV === 'test';
if (!inputPath || typeof inputPath !== 'string' || inputPath.trim() === '') {
return {
isValid: false,
error: 'Path cannot be empty, undefined, or non-string',
violationType: 'invalid_path'
};
}
const dangerousChars = /[<>:"|?*\x00-\x1f]/;
if (strictMode && dangerousChars.test(inputPath)) {
return {
isValid: false,
error: `Path contains dangerous characters: ${inputPath}`,
violationType: 'dangerous_characters'
};
}
if (inputPath.includes('..') && strictMode) {
const normalizedTest = path.normalize(inputPath);
if (normalizedTest.includes('..')) {
return {
isValid: false,
error: `Path traversal detected: ${inputPath}`,
violationType: 'path_traversal'
};
}
}
let normalizedPath;
try {
normalizedPath = this.normalizePath(inputPath);
}
catch (error) {
return {
isValid: false,
error: `Path normalization failed: ${error instanceof Error ? error.message : String(error)}`,
violationType: 'invalid_path'
};
}
if (normalizedPath.length > config.maxPathLength) {
const multiplier = isTestMode && allowTestMode ? 2 : 1;
if (normalizedPath.length > config.maxPathLength * multiplier) {
return {
isValid: false,
error: `Path length ${normalizedPath.length} exceeds maximum ${config.maxPathLength * multiplier}`,
violationType: 'invalid_path'
};
}
}
if (checkExtensions) {
const ext = path.extname(normalizedPath).toLowerCase();
if (ext && !allowedExtensions.includes(ext)) {
if (!(isTestMode && allowTestMode)) {
return {
isValid: false,
error: `File extension '${ext}' not allowed. Allowed extensions: ${allowedExtensions.join(', ')}`,
violationType: 'invalid_extension'
};
}
}
}
const allowedDirectory = operation === 'read'
? config.allowedReadDirectory
: config.allowedWriteDirectory;
if (!allowedDirectory) {
return {
isValid: false,
error: `Security boundary not configured for ${operation} operations`,
violationType: 'outside_boundary'
};
}
if (!this.isPathWithin(normalizedPath, allowedDirectory)) {
if (isTestMode && allowTestMode) {
const testPaths = ['/tmp', path.join(process.cwd(), '__tests__'), path.join(process.cwd(), 'test')];
const isTestPath = testPaths.some(testPath => this.isPathWithin(normalizedPath, testPath));
if (isTestPath) {
logger.debug(`Allowing test path: ${normalizedPath}`);
return {
isValid: true,
normalizedPath,
warnings: [`Test mode: Path outside normal boundaries but within test paths`]
};
}
}
return {
isValid: false,
error: `Access denied: Path '${inputPath}' is outside the allowed ${operation} directory '${allowedDirectory}'`,
violationType: 'outside_boundary'
};
}
return {
isValid: true,
normalizedPath
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ err: error, inputPath, options }, 'Unexpected error during path validation');
return {
isValid: false,
error: `Validation error: ${errorMessage}`,
violationType: 'invalid_path'
};
}
}
createSecurePath(inputPath, operation = 'read', options = {}) {
const validation = this.validatePathSecurity(inputPath, { ...options, operation });
if (!validation.isValid) {
const errorMsg = `Security violation: ${validation.error}`;
logger.error({ inputPath, operation, validation }, errorMsg);
throw new Error(errorMsg);
}
return validation.normalizedPath;
}
isPathAllowedForOperation(inputPath, operation, options = {}) {
return this.validatePathSecurity(inputPath, { ...options, operation });
}
validateMultiplePaths(paths, operation = 'read', options = {}) {
const results = new Map();
for (const inputPath of paths) {
try {
const result = this.validatePathSecurity(inputPath, { ...options, operation });
results.set(inputPath, result);
}
catch (error) {
results.set(inputPath, {
isValid: false,
error: `Batch validation error: ${error instanceof Error ? error.message : String(error)}`,
violationType: 'invalid_path'
});
}
}
return results;
}
getConfigStatus() {
return {
initialized: this.config !== null,
mcpConfigPresent: this.mcpConfig !== null,
allowedReadDirectory: this.config?.allowedReadDirectory,
allowedWriteDirectory: this.config?.allowedWriteDirectory,
securityMode: this.config?.securityMode
};
}
validatePathSecurityCompat(inputPath, allowedDirectory) {
if (allowedDirectory) {
logger.warn({
inputPath,
providedAllowedDirectory: allowedDirectory,
centralizedDirectory: this.getConfig().allowedReadDirectory
}, 'Path validation using deprecated allowedDirectory parameter. Consider using centralized configuration.');
}
const result = this.validatePathSecurity(inputPath, { operation: 'read' });
return {
isValid: result.isValid,
normalizedPath: result.normalizedPath,
error: result.error
};
}
createSecureReadPath(filePath) {
return this.createSecurePath(filePath, 'read');
}
createSecureWritePath(filePath) {
return this.createSecurePath(filePath, 'write');
}
isPathWithinReadDirectory(filePath) {
const result = this.isPathAllowedForOperation(filePath, 'read');
return result.isValid;
}
isPathWithinWriteDirectory(filePath) {
const result = this.isPathAllowedForOperation(filePath, 'write');
return result.isValid;
}
validatePathWithConfig(inputPath, config) {
const options = {
operation: 'read',
checkExtensions: true,
allowedExtensions: config?.allowedExtensions,
strictMode: config?.strictMode ?? true
};
return this.validatePathSecurity(inputPath, options);
}
reset() {
this.config = null;
this.mcpConfig = null;
logger.debug('Unified security configuration reset');
}
getToolOutputDirectory() {
const config = this.getConfig();
return config.allowedWriteDirectory;
}
createSecureToolOutputPath(relativePath) {
const config = this.getConfig();
const outputPath = path.join(config.allowedWriteDirectory, relativePath);
return this.createSecurePath(outputPath, 'write');
}
async ensureToolOutputDirectory(toolName) {
const config = this.getConfig();
const toolDir = path.join(config.allowedWriteDirectory, toolName);
const validation = this.validatePathSecurity(toolDir, { operation: 'write' });
if (!validation.isValid) {
throw new Error(`Invalid tool directory: ${validation.error}`);
}
try {
const fs = await import('fs-extra');
await fs.ensureDir(toolDir);
logger.debug({ toolName, toolDir }, 'Ensured tool output directory exists');
return toolDir;
}
catch (error) {
logger.error({ err: error, toolName, toolDir }, 'Failed to ensure tool output directory');
throw new Error(`Failed to create tool output directory: ${error instanceof Error ? error.message : String(error)}`);
}
}
getEnvironmentVariable(varName, fallback) {
return process.env[varName] || fallback;
}
}
export function getUnifiedSecurityConfig() {
return UnifiedSecurityConfigManager.getInstance();
}
export function validatePathSecurity(inputPath, options = {}) {
return getUnifiedSecurityConfig().validatePathSecurity(inputPath, options);
}
export function createSecurePath(inputPath, operation = 'read', options = {}) {
return getUnifiedSecurityConfig().createSecurePath(inputPath, operation, options);
}
export function normalizePath(inputPath) {
return getUnifiedSecurityConfig().normalizePath(inputPath);
}
export function isPathWithin(childPath, parentPath) {
return getUnifiedSecurityConfig().isPathWithin(childPath, parentPath);
}
export function isPathAllowed(inputPath, operation = 'read', options = {}) {
const result = getUnifiedSecurityConfig().isPathAllowedForOperation(inputPath, operation, options);
return result.isValid;
}
export function validateMultiplePaths(paths, operation = 'read', options = {}) {
return getUnifiedSecurityConfig().validateMultiplePaths(paths, operation, options);
}
export function validatePathSecurityCompat(inputPath, allowedDirectory) {
return getUnifiedSecurityConfig().validatePathSecurityCompat(inputPath, allowedDirectory);
}
export function createSecureReadPath(filePath) {
return getUnifiedSecurityConfig().createSecureReadPath(filePath);
}
export function createSecureWritePath(filePath) {
return getUnifiedSecurityConfig().createSecureWritePath(filePath);
}
export function isPathWithinReadDirectory(filePath) {
return getUnifiedSecurityConfig().isPathWithinReadDirectory(filePath);
}
export function isPathWithinWriteDirectory(filePath) {
return getUnifiedSecurityConfig().isPathWithinWriteDirectory(filePath);
}
export function getToolOutputDirectory() {
return getUnifiedSecurityConfig().getToolOutputDirectory();
}
export function createSecureToolOutputPath(relativePath) {
return getUnifiedSecurityConfig().createSecureToolOutputPath(relativePath);
}
export async function ensureToolOutputDirectory(toolName) {
return getUnifiedSecurityConfig().ensureToolOutputDirectory(toolName);
}