Skip to main content
Glama
unified-security-config.test.ts26 kB
/** * Comprehensive Tests for Unified Security Configuration * Tests centralized security boundary validation across all path utilities */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import path from 'path'; import { UnifiedSecurityConfigManager, validatePathSecurity, createSecurePath, normalizePath, isPathWithin, isPathAllowed, ValidationOptions } from '../../security/unified-security-config.js'; import { OpenRouterConfig } from '../../../../types/workflow.js'; // Mock the logger vi.mock('../../../../logger.js', () => ({ default: { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn() } })); // Mock the config loader to return consistent test configuration vi.mock('../../utils/config-loader.js', () => ({ extractVibeTaskManagerSecurityConfig: vi.fn(() => ({ allowedReadDirectory: '/test/read-dir', allowedWriteDirectory: '/test/write-dir', securityMode: 'strict' as const })) })); describe('Unified Security Configuration Tests', () => { let securityManager: UnifiedSecurityConfigManager; let mockMCPConfig: OpenRouterConfig; // Test directories const testReadDir = '/test/read-dir'; const testWriteDir = '/test/write-dir'; const originalNodeEnv = process.env.NODE_ENV; beforeEach(() => { // Reset singleton instance (UnifiedSecurityConfigManager as unknown as { instance: UnifiedSecurityConfigManager | null }).instance = null; // Setup mock MCP config mockMCPConfig = { apiKey: 'test-key', baseUrl: 'test-url', geminiModel: 'test-gemini-model', perplexityModel: 'test-perplexity-model' } as OpenRouterConfig; // Get fresh instance and initialize securityManager = UnifiedSecurityConfigManager.getInstance(); securityManager.initializeFromMCPConfig(mockMCPConfig); // Reset NODE_ENV to known state process.env.NODE_ENV = 'test'; }); afterEach(() => { // Reset the configuration securityManager.reset(); // Restore NODE_ENV process.env.NODE_ENV = originalNodeEnv; // Clear mocks vi.clearAllMocks(); }); describe('Configuration Initialization', () => { it('should initialize configuration from MCP config correctly', () => { const config = securityManager.getConfig(); expect(config.allowedReadDirectory).toBe(testReadDir); expect(config.allowedWriteDirectory).toBe(testWriteDir); expect(config.securityMode).toBe('strict'); expect(config.allowedDirectories).toEqual([testReadDir, testWriteDir]); }); it('should throw error when accessing config before initialization', () => { const freshInstance = new (UnifiedSecurityConfigManager as unknown as new () => UnifiedSecurityConfigManager)(); expect(() => freshInstance.getConfig()).toThrow( 'Unified security configuration not initialized' ); }); it('should provide correct service-specific configurations', () => { const vibeConfig = securityManager.getVibeTaskManagerSecurityValidatorConfig(); expect(vibeConfig.readDir).toBe(testReadDir); expect(vibeConfig.writeDir).toBe(testWriteDir); const codeMapConfig = securityManager.getCodeMapGeneratorConfig(); expect(codeMapConfig.allowedDir).toBe(testReadDir); expect(codeMapConfig.outputDir).toBe(testWriteDir); const contextConfig = securityManager.getContextCuratorConfig(); expect(contextConfig.readDir).toBe(testReadDir); expect(contextConfig.outputDir).toBe(testWriteDir); }); }); describe('Path Normalization', () => { it('should normalize relative paths to absolute paths', () => { const relativePath = './test/file.txt'; const normalized = securityManager.normalizePath(relativePath); expect(path.isAbsolute(normalized)).toBe(true); expect(normalized).toContain('test/file.txt'); }); it('should handle absolute paths correctly', () => { const absolutePath = '/usr/local/test.txt'; const normalized = securityManager.normalizePath(absolutePath); expect(normalized).toBe(absolutePath); }); it('should sanitize dangerous characters from paths', () => { const dangerousPath = '/test/file<script>.txt'; const normalized = securityManager.normalizePath(dangerousPath); expect(normalized).not.toContain('<script>'); expect(normalized).toBe('/test/filescript.txt'); }); it('should handle test mode paths correctly', () => { const testPath = '/tmp/test-output/file.txt'; const normalized = securityManager.normalizePath(testPath); expect(normalized).toBe(testPath); }); it('should throw error for empty or invalid paths', () => { expect(() => securityManager.normalizePath('')).toThrow('Path cannot be empty'); expect(() => securityManager.normalizePath(null as unknown as string)).toThrow('Path cannot be empty'); expect(() => securityManager.normalizePath(undefined as unknown as string)).toThrow('Path cannot be empty'); }); }); describe('Path Containment Validation', () => { it('should correctly identify when child path is within parent', () => { const parentPath = '/test/parent'; const childPath = '/test/parent/child/file.txt'; const result = securityManager.isPathWithin(childPath, parentPath); expect(result).toBe(true); }); it('should correctly identify when child path equals parent', () => { const samePath = '/test/same'; const result = securityManager.isPathWithin(samePath, samePath); expect(result).toBe(true); }); it('should correctly identify when child path is outside parent', () => { const parentPath = '/test/parent'; const outsidePath = '/test/other/file.txt'; const result = securityManager.isPathWithin(outsidePath, parentPath); expect(result).toBe(false); }); it('should handle path traversal attempts correctly', () => { const parentPath = '/test/parent'; const traversalPath = '/test/parent/../sibling/file.txt'; const result = securityManager.isPathWithin(traversalPath, parentPath); expect(result).toBe(false); }); it('should handle cross-platform path separators', () => { const parentPath = '/test/parent'; const childPath = '/test/parent/child/file.txt'; // Use forward slashes for cross-platform compatibility const result = securityManager.isPathWithin(childPath, parentPath); expect(result).toBe(true); }); }); describe('Security Boundary Validation', () => { it('should validate read paths within allowed read directory', () => { const validReadPath = path.join(testReadDir, 'valid-file.txt'); const result = securityManager.validatePathSecurity(validReadPath, { operation: 'read' }); expect(result.isValid).toBe(true); expect(result.normalizedPath).toBeDefined(); expect(result.error).toBeUndefined(); }); it('should validate write paths within allowed write directory', () => { const validWritePath = path.join(testWriteDir, 'output-file.txt'); const result = securityManager.validatePathSecurity(validWritePath, { operation: 'write' }); expect(result.isValid).toBe(true); expect(result.normalizedPath).toBeDefined(); expect(result.error).toBeUndefined(); }); it('should reject read paths outside allowed read directory', () => { const invalidPath = '/unauthorized/file.txt'; const result = securityManager.validatePathSecurity(invalidPath, { operation: 'read' }); expect(result.isValid).toBe(false); expect(result.error).toContain('outside the allowed read directory'); expect(result.violationType).toBe('outside_boundary'); }); it('should reject write paths outside allowed write directory', () => { const invalidPath = '/unauthorized/output.txt'; const result = securityManager.validatePathSecurity(invalidPath, { operation: 'write' }); expect(result.isValid).toBe(false); expect(result.error).toContain('outside the allowed write directory'); expect(result.violationType).toBe('outside_boundary'); }); it('should detect path traversal attempts', () => { const traversalPath = `${testReadDir}/subdir/../../../etc/passwd`; const result = securityManager.validatePathSecurity(traversalPath, { operation: 'read', strictMode: true }); expect(result.isValid).toBe(false); // This could be either path_traversal or outside_boundary depending on the normalization expect(['path_traversal', 'outside_boundary']).toContain(result.violationType); }); it('should detect dangerous characters in strict mode', () => { const dangerousPath = `${testReadDir}/file<script>alert('xss')</script>.txt`; const result = securityManager.validatePathSecurity(dangerousPath, { operation: 'read', strictMode: true }); expect(result.isValid).toBe(false); expect(result.violationType).toBe('dangerous_characters'); }); it('should validate file extensions when requested', () => { // Temporarily set NODE_ENV to production to disable test mode relaxations const originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { const txtFile = path.join(testReadDir, 'file.txt'); const jsFile = path.join(testReadDir, 'file.exe'); const options: ValidationOptions = { operation: 'read', checkExtensions: true, allowedExtensions: ['.txt', '.md', '.json'], allowTestMode: false }; const txtResult = securityManager.validatePathSecurity(txtFile, options); expect(txtResult.isValid).toBe(true); const exeResult = securityManager.validatePathSecurity(jsFile, options); expect(exeResult.isValid).toBe(false); expect(exeResult.violationType).toBe('invalid_extension'); } finally { // Restore NODE_ENV process.env.NODE_ENV = originalNodeEnv; } }); it('should enforce path length limits', () => { // Temporarily set NODE_ENV to production to disable test mode relaxations const originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { const config = securityManager.getConfig(); const longPath = path.join(testReadDir, 'a'.repeat(config.maxPathLength + 100)); const result = securityManager.validatePathSecurity(longPath, { operation: 'read', allowTestMode: false }); expect(result.isValid).toBe(false); expect(result.error).toContain('exceeds maximum'); } finally { // Restore NODE_ENV process.env.NODE_ENV = originalNodeEnv; } }); }); describe('Test Mode Behavior', () => { beforeEach(() => { process.env.NODE_ENV = 'test'; }); it('should allow test paths in test mode', () => { const testPath = '/tmp/test-data/file.txt'; const result = securityManager.validatePathSecurity(testPath, { operation: 'read', allowTestMode: true }); expect(result.isValid).toBe(true); expect(result.warnings).toBeDefined(); expect(result.warnings![0]).toContain('Test mode'); }); it('should relax path length limits in test mode', () => { const config = securityManager.getConfig(); const longTestPath = '/tmp/' + 'a'.repeat(config.maxPathLength + 50); const result = securityManager.validatePathSecurity(longTestPath, { operation: 'read', allowTestMode: true }); // Should pass because test mode allows 2x path length expect(result.isValid).toBe(true); }); it('should relax extension validation in test mode', () => { const testFile = '/tmp/test.unknown-extension'; const result = securityManager.validatePathSecurity(testFile, { operation: 'read', checkExtensions: true, allowedExtensions: ['.txt'], allowTestMode: true }); expect(result.isValid).toBe(true); }); }); describe('Secure Path Creation', () => { it('should create secure paths for valid inputs', () => { const validPath = path.join(testReadDir, 'secure-file.txt'); const securePath = securityManager.createSecurePath(validPath, 'read'); expect(securePath).toBe(path.resolve(validPath)); }); it('should throw error for invalid paths', () => { const invalidPath = '/unauthorized/file.txt'; expect(() => { securityManager.createSecurePath(invalidPath, 'read'); }).toThrow('Security violation'); }); it('should handle different operation types', () => { const readPath = path.join(testReadDir, 'read-file.txt'); const writePath = path.join(testWriteDir, 'write-file.txt'); const secureReadPath = securityManager.createSecurePath(readPath, 'read'); const secureWritePath = securityManager.createSecurePath(writePath, 'write'); expect(secureReadPath).toBeDefined(); expect(secureWritePath).toBeDefined(); }); }); describe('Batch Validation', () => { it('should validate multiple paths efficiently', () => { const paths = [ path.join(testReadDir, 'file1.txt'), path.join(testReadDir, 'file2.txt'), '/unauthorized/file3.txt', path.join(testReadDir, 'file4.txt') ]; const results = securityManager.validateMultiplePaths(paths, 'read'); expect(results.size).toBe(4); expect(results.get(paths[0])?.isValid).toBe(true); expect(results.get(paths[1])?.isValid).toBe(true); expect(results.get(paths[2])?.isValid).toBe(false); expect(results.get(paths[3])?.isValid).toBe(true); }); it('should handle errors in batch validation gracefully', () => { const paths = [ path.join(testReadDir, 'valid.txt'), '', // Invalid empty path path.join(testReadDir, 'another-valid.txt') ]; const results = securityManager.validateMultiplePaths(paths, 'read'); expect(results.size).toBe(3); expect(results.get(paths[0])?.isValid).toBe(true); expect(results.get(paths[1])?.isValid).toBe(false); expect(results.get(paths[2])?.isValid).toBe(true); }); }); describe('Backward Compatibility', () => { it('should support legacy validatePathSecurityCompat method', () => { const validPath = path.join(testReadDir, 'legacy-file.txt'); const result = securityManager.validatePathSecurityCompat(validPath); expect(result.isValid).toBe(true); expect(result.normalizedPath).toBeDefined(); }); it('should support legacy createSecureReadPath method', () => { const validPath = path.join(testReadDir, 'legacy-read.txt'); const securePath = securityManager.createSecureReadPath(validPath); expect(securePath).toBe(path.resolve(validPath)); }); it('should support legacy createSecureWritePath method', () => { const validPath = path.join(testWriteDir, 'legacy-write.txt'); const securePath = securityManager.createSecureWritePath(validPath); expect(securePath).toBe(path.resolve(validPath)); }); it('should support legacy directory checking methods', () => { const readPath = path.join(testReadDir, 'check-read.txt'); const writePath = path.join(testWriteDir, 'check-write.txt'); const invalidPath = '/unauthorized/file.txt'; expect(securityManager.isPathWithinReadDirectory(readPath)).toBe(true); expect(securityManager.isPathWithinWriteDirectory(writePath)).toBe(true); expect(securityManager.isPathWithinReadDirectory(invalidPath)).toBe(false); expect(securityManager.isPathWithinWriteDirectory(invalidPath)).toBe(false); }); }); describe('Convenience Functions', () => { it('should provide working convenience functions', () => { const validPath = path.join(testReadDir, 'convenience.txt'); // Test convenience functions const normalized = normalizePath(validPath); expect(normalized).toBe(path.resolve(validPath)); const isWithin = isPathWithin(validPath, testReadDir); expect(isWithin).toBe(true); const isAllowed = isPathAllowed(validPath, 'read'); expect(isAllowed).toBe(true); const validation = validatePathSecurity(validPath, { operation: 'read' }); expect(validation.isValid).toBe(true); const securePath = createSecurePath(validPath, 'read'); expect(securePath).toBe(path.resolve(validPath)); }); it('should handle convenience function errors correctly', () => { const invalidPath = '/unauthorized/file.txt'; expect(isPathAllowed(invalidPath, 'read')).toBe(false); expect(() => { createSecurePath(invalidPath, 'read'); }).toThrow('Security violation'); }); }); describe('Performance Requirements', () => { it('should meet <50ms validation performance target', () => { const testPaths = Array.from({ length: 100 }, (_, i) => path.join(testReadDir, `perf-test-${i}.txt`) ); const startTime = Date.now(); testPaths.forEach(testPath => { securityManager.validatePathSecurity(testPath, { operation: 'read' }); }); const endTime = Date.now(); const totalTime = endTime - startTime; const averageTime = totalTime / testPaths.length; // Each validation should be well under 50ms expect(averageTime).toBeLessThan(5); }); it('should efficiently handle batch validation', () => { const largeBatch = Array.from({ length: 1000 }, (_, i) => path.join(testReadDir, `batch-${i}.txt`) ); const startTime = Date.now(); const results = securityManager.validateMultiplePaths(largeBatch, 'read'); const endTime = Date.now(); expect(results.size).toBe(1000); expect(endTime - startTime).toBeLessThan(500); // 500ms for 1000 validations }); }); describe('Error Handling', () => { it('should handle malformed configuration gracefully', () => { const freshManager = new (UnifiedSecurityConfigManager as unknown as new () => UnifiedSecurityConfigManager)(); expect(() => { freshManager.getConfig(); }).toThrow('Unified security configuration not initialized'); }); it('should handle path normalization errors', () => { const result = securityManager.validatePathSecurity('', { operation: 'read' }); expect(result.isValid).toBe(false); expect(result.violationType).toBe('invalid_path'); }); it('should handle containment check errors gracefully', () => { // Mock path.resolve to throw an error const originalResolve = path.resolve; vi.spyOn(path, 'resolve').mockImplementation(() => { throw new Error('Path resolution failed'); }); const result = securityManager.isPathWithin('test', 'parent'); expect(result).toBe(false); // Restore original implementation path.resolve = originalResolve; }); }); describe('Configuration Status', () => { it('should provide accurate configuration status', () => { const status = securityManager.getConfigStatus(); expect(status.initialized).toBe(true); expect(status.mcpConfigPresent).toBe(true); expect(status.allowedReadDirectory).toBe(testReadDir); expect(status.allowedWriteDirectory).toBe(testWriteDir); expect(status.securityMode).toBe('strict'); }); it('should show uninitialized status correctly', () => { const freshManager = new (UnifiedSecurityConfigManager as unknown as new () => UnifiedSecurityConfigManager)(); const status = freshManager.getConfigStatus(); expect(status.initialized).toBe(false); expect(status.mcpConfigPresent).toBe(false); }); }); describe('Integration with Existing Pattern', () => { it('should maintain compatibility with existing security validators', () => { // Test that our centralized config can be used by existing patterns const config = securityManager.getFilesystemSecurityConfig(); expect(config.allowedDirectories).toEqual([testReadDir, testWriteDir]); expect(config.securityMode).toBe('strict'); expect(config.enablePermissionChecking).toBe(true); expect(config.maxPathLength).toBe(4096); }); it('should provide correct path validator configuration', () => { const config = securityManager.getPathValidatorConfig(); expect(config.allowedDirectories).toEqual([testReadDir, testWriteDir]); expect(config.maxPathLength).toBe(4096); }); it('should provide correct security manager configuration', () => { const config = securityManager.getSecurityManagerConfig(); expect(config.pathSecurity.allowedDirectories).toEqual([testReadDir, testWriteDir]); expect(config.strictMode).toBe(true); expect(config.performanceThresholdMs).toBe(50); }); }); describe('Tool-Specific Convenience Methods', () => { beforeEach(() => { // Reset singleton instance (UnifiedSecurityConfigManager as unknown as { instance: UnifiedSecurityConfigManager | null }).instance = null; // Setup mock MCP config mockMCPConfig = { apiKey: 'test-key', baseUrl: 'test-url', geminiModel: 'test-gemini-model', perplexityModel: 'test-perplexity-model' } as OpenRouterConfig; securityManager = UnifiedSecurityConfigManager.getInstance(); securityManager.initializeFromMCPConfig(mockMCPConfig); }); describe('getToolOutputDirectory', () => { it('should return the configured write directory', () => { const outputDir = securityManager.getToolOutputDirectory(); expect(outputDir).toBe(testWriteDir); }); }); describe('createSecureToolOutputPath', () => { it('should create secure paths within write directory', () => { const relativePath = 'my-tool/output.json'; const securePath = securityManager.createSecureToolOutputPath(relativePath); expect(securePath).toBe(path.join(testWriteDir, relativePath)); }); it('should reject paths outside write directory', () => { const maliciousPath = '../../../etc/passwd'; expect(() => { securityManager.createSecureToolOutputPath(maliciousPath); }).toThrow('Security violation'); }); it('should handle absolute paths correctly', () => { const absolutePath = '/some/other/path/file.txt'; expect(() => { securityManager.createSecureToolOutputPath(absolutePath); }).toThrow('Security violation'); }); }); describe('ensureToolOutputDirectory', () => { // Mock fs-extra let mockEnsureDir: ReturnType<typeof vi.fn>; beforeEach(() => { mockEnsureDir = vi.fn(); vi.doMock('fs-extra', () => ({ default: { ensureDir: mockEnsureDir }, ensureDir: mockEnsureDir })); }); it('should create tool directory within write boundary', async () => { mockEnsureDir.mockResolvedValue(undefined); const toolName = 'my-test-tool'; const toolDir = await securityManager.ensureToolOutputDirectory(toolName); expect(toolDir).toBe(path.join(testWriteDir, toolName)); expect(mockEnsureDir).toHaveBeenCalledWith(path.join(testWriteDir, toolName)); }); it('should reject tool names with path traversal', async () => { const maliciousToolName = '../../../etc'; await expect( securityManager.ensureToolOutputDirectory(maliciousToolName) ).rejects.toThrow('Invalid tool directory'); }); it('should handle fs errors gracefully', async () => { // Reset the mock to throw an error vi.resetModules(); vi.doMock('fs-extra', () => ({ default: { ensureDir: vi.fn().mockRejectedValue(new Error('Permission denied')) }, ensureDir: vi.fn().mockRejectedValue(new Error('Permission denied')) })); await expect( securityManager.ensureToolOutputDirectory('test-tool') ).rejects.toThrow('Failed to create tool output directory'); }); }); describe('getEnvironmentVariable', () => { const originalEnv = process.env; beforeEach(() => { process.env = { ...originalEnv }; }); afterEach(() => { process.env = originalEnv; }); it('should return environment variable value when set', () => { process.env.TEST_VAR = 'test-value'; const value = securityManager.getEnvironmentVariable('TEST_VAR'); expect(value).toBe('test-value'); }); it('should return fallback when environment variable not set', () => { delete process.env.TEST_VAR; const value = securityManager.getEnvironmentVariable('TEST_VAR', 'fallback-value'); expect(value).toBe('fallback-value'); }); it('should return undefined when no fallback provided', () => { delete process.env.TEST_VAR; const value = securityManager.getEnvironmentVariable('TEST_VAR'); expect(value).toBeUndefined(); }); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server