Skip to main content
Glama

MCP Shamash

custom-rule-engine-redos.test.ts14.7 kB
import { CustomRuleEngine, CustomRule } from '../../src/rules/custom-rule-engine'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; describe('Custom Rule Engine - ReDoS Security Tests', () => { let ruleEngine: CustomRuleEngine; let tempDir: string; let testFile: string; beforeAll(async () => { // Create temporary test directory tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'shamash-redos-test-')); testFile = path.join(tempDir, 'test.js'); // Initialize rule engine ruleEngine = new CustomRuleEngine(tempDir); await ruleEngine.loadRules(); }); afterAll(async () => { // Clean up temporary directory if (tempDir) { await fs.rm(tempDir, { recursive: true, force: true }); } }); beforeEach(async () => { // Create a test file with content for each test await fs.writeFile(testFile, ` const apiKey = "sk_test_12345678901234567890"; console.log("Debug message"); const query = "SELECT * FROM users WHERE id = " + userId; const hash = md5(password); const randomNum = Math.random(); `); }); describe('ReDoS Pattern Detection', () => { it('should reject nested quantifier patterns (a+)+', async () => { const dangerousRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Dangerous Nested Quantifier', description: 'Test ReDoS pattern', severity: 'high', category: 'security', pattern: '(a+)+', filePatterns: ['*.js'], messageTemplate: 'Dangerous pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(dangerousRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Potentially dangerous ReDoS pattern detected - pattern may cause performance issues'); }); it('should reject nested quantifier patterns (a*)+', async () => { const dangerousRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Dangerous Star Plus', description: 'Test ReDoS pattern', severity: 'high', category: 'security', pattern: '(a*)+', filePatterns: ['*.js'], messageTemplate: 'Dangerous pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(dangerousRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Potentially dangerous ReDoS pattern detected - pattern may cause performance issues'); }); it('should reject alternation with quantifiers (a|b)+', async () => { const dangerousRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Dangerous Alternation', description: 'Test ReDoS pattern', severity: 'high', category: 'security', pattern: '(hello|world)+', filePatterns: ['*.js'], messageTemplate: 'Dangerous pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(dangerousRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Potentially dangerous ReDoS pattern detected - pattern may cause performance issues'); }); it('should reject triple wildcards *.*. patterns', async () => { const dangerousRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Triple Wildcard', description: 'Test ReDoS pattern', severity: 'high', category: 'security', pattern: 'start.*.*.*end', // This has triple wildcards which is dangerous filePatterns: ['*.js'], messageTemplate: 'Dangerous pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(dangerousRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Potentially dangerous ReDoS pattern detected - pattern may cause performance issues'); }); it('should reject patterns with too many quantifiers', async () => { const dangerousRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Too Many Quantifiers', description: 'Test ReDoS pattern', severity: 'high', category: 'security', pattern: 'a+b*c?d{2,}e+f*g?h{3,}i+j*k?l+m+n*o?p+', // 16 quantifiers (over our limit of 15) filePatterns: ['*.js'], messageTemplate: 'Dangerous pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(dangerousRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Potentially dangerous ReDoS pattern detected - pattern may cause performance issues'); }); it('should reject patterns with too many groups', async () => { const dangerousRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Too Many Groups', description: 'Test ReDoS pattern', severity: 'high', category: 'security', pattern: '(a)(b)(c)(d)(e)(f)(g)(h)(i)', // 9 groups (over our limit of 8) filePatterns: ['*.js'], messageTemplate: 'Dangerous pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(dangerousRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Potentially dangerous ReDoS pattern detected - pattern may cause performance issues'); }); it('should reject extremely long patterns', async () => { const longPattern = 'a'.repeat(1001); // Over 1000 characters const dangerousRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Extremely Long Pattern', description: 'Test ReDoS pattern', severity: 'high', category: 'security', pattern: longPattern, filePatterns: ['*.js'], messageTemplate: 'Dangerous pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(dangerousRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Potentially dangerous ReDoS pattern detected - pattern may cause performance issues'); }); it('should accept safe patterns', async () => { const safeRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Safe Pattern', description: 'Test safe pattern', severity: 'medium', category: 'security', pattern: 'api[_-]?key\\s*[=:]\\s*["\'][a-zA-Z0-9]{20,}["\']', filePatterns: ['*.js'], messageTemplate: 'Safe pattern: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(safeRule); expect(validation.valid).toBe(true); expect(validation.errors).toHaveLength(0); }); }); describe('Runtime Protection Against ReDoS', () => { it('should timeout on catastrophic backtracking pattern', async () => { // Create a rule that would cause catastrophic backtracking // but will be caught by our safety checks const testContent = 'a'.repeat(100); // Long string of 'a's await fs.writeFile(testFile, testContent); // This pattern would normally cause ReDoS but should be blocked const maliciousRuleId = await ruleEngine.addRule({ name: 'Potential ReDoS', description: 'This should be blocked', severity: 'high', category: 'security', pattern: 'a+a+a+a+a+', // Would cause catastrophic backtracking filePatterns: ['*.js'], messageTemplate: 'Found: {matchedText}', enabled: false, // Disabled so it won't run }); // Verify the rule was added but disabled due to safety const rule = ruleEngine.getRule(maliciousRuleId); expect(rule).toBeDefined(); expect(rule!.pattern).toBe('a+a+a+a+a+'); // The rule should not run because it's disabled await ruleEngine.scanWithCustomRules(tempDir); }); it('should enforce timeout protection during scanning', async () => { // Create content designed to trigger timeout const problematicContent = 'x' + 'a'.repeat(10000) + 'x'; await fs.writeFile(testFile, problematicContent); // Add a potentially slow but safe rule const ruleId = await ruleEngine.addRule({ name: 'Slow Pattern Test', description: 'Test timeout protection', severity: 'low', category: 'maintainability', pattern: 'x[a]{1000,}x', // Could be slow but is safe filePatterns: ['*.js'], messageTemplate: 'Found: {matchedText}', enabled: true, }); const startTime = Date.now(); await ruleEngine.scanWithCustomRules(tempDir); const duration = Date.now() - startTime; // Should complete in reasonable time (our timeout is 5 seconds + overhead) expect(duration).toBeLessThan(10000); // Clean up await ruleEngine.removeRule(ruleId); }); it('should enforce iteration limits to prevent infinite loops', async () => { // Create content with many matches to test iteration limits const manyMatches = 'console.log("test"); '.repeat(15000); // Over iteration limit await fs.writeFile(testFile, manyMatches); const results = await ruleEngine.scanWithCustomRules(tempDir); // Should find matches but be limited by iteration count // The default console.log rule should trigger this const consoleFindings = results.findings.filter(f => f.title.includes('Console Log in Production') ); // Should have some findings but not exceed iteration limit if (consoleFindings.length > 0) { expect(consoleFindings.length).toBeLessThanOrEqual(10000); } }); }); describe('Safe Rule Processing', () => { it('should successfully scan with default safe rules', async () => { const results = await ruleEngine.scanWithCustomRules(tempDir); expect(results.findings.length).toBeGreaterThan(0); expect(results.tokenUsage).toBeGreaterThan(0); // Should find some security issues in our test file const securityFindings = results.findings.filter(f => f.title.includes('Console Log') || f.title.includes('Insecure Random') ); expect(securityFindings.length).toBeGreaterThan(0); // Debug: Log all findings to see what's being detected console.log('Found findings:', results.findings.map(f => f.title)); }); it('should process multiple files safely', async () => { // Create multiple test files const files = [ { name: 'file1.js', content: 'const apiKey = "sk_test_12345678901234567890";' }, { name: 'file2.ts', content: 'console.log("debug");' }, { name: 'file3.py', content: 'hash = md5(password)' } ]; for (const file of files) { await fs.writeFile(path.join(tempDir, file.name), file.content); } const results = await ruleEngine.scanWithCustomRules(tempDir); expect(results.findings.length).toBeGreaterThan(0); // Should find issues in multiple files const uniqueFiles = new Set(results.findings.map(f => f.location?.file).filter(Boolean)); expect(uniqueFiles.size).toBeGreaterThan(1); }); it('should handle regex compilation errors gracefully', async () => { const invalidRule: Omit<CustomRule, 'id' | 'createdAt' | 'lastModified'> = { name: 'Invalid Regex', description: 'Test invalid regex handling', severity: 'medium', category: 'security', pattern: '[invalid', // Unclosed bracket filePatterns: ['*.js'], messageTemplate: 'Invalid: {matchedText}', enabled: true, }; const validation = await ruleEngine.validateRule(invalidRule); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Invalid regex pattern'); }); it('should validate rule structure properly', async () => { const incompleteRule: Partial<CustomRule> = { name: '', // Empty name should fail pattern: 'test', severity: 'invalid' as any, // Invalid severity category: 'unknown' as any, // Invalid category }; const validation = await ruleEngine.validateRule(incompleteRule); expect(validation.valid).toBe(false); expect(validation.errors.length).toBeGreaterThan(0); expect(validation.errors).toContain('Rule name is required'); expect(validation.errors).toContain('Invalid severity level'); expect(validation.errors).toContain('Invalid category'); expect(validation.errors).toContain('Message template is required'); }); }); describe('Engine Statistics and Management', () => { it('should provide accurate statistics', () => { const stats = ruleEngine.getStats(); expect(stats.totalRules).toBeGreaterThan(0); expect(stats.enabledRules).toBeGreaterThan(0); expect(stats.totalRules).toBeGreaterThanOrEqual(stats.enabledRules); expect(stats.categoryCounts.security).toBeGreaterThan(0); expect(stats.severityCounts.high).toBeGreaterThan(0); }); it('should enable and disable rules properly', async () => { const rules = ruleEngine.getRules(); const testRule = rules[0]; // Disable rule const disabled = await ruleEngine.disableRule(testRule.id); expect(disabled).toBe(true); const disabledRule = ruleEngine.getRule(testRule.id); expect(disabledRule!.enabled).toBe(false); // Re-enable rule const enabled = await ruleEngine.enableRule(testRule.id); expect(enabled).toBe(true); const enabledRule = ruleEngine.getRule(testRule.id); expect(enabledRule!.enabled).toBe(true); }); it('should update rules with validation', async () => { const rules = ruleEngine.getRules(); const testRule = rules[0]; const originalModified = testRule.lastModified; // Wait a small amount to ensure timestamp difference await new Promise(resolve => setTimeout(resolve, 10)); // Valid update const updated = await ruleEngine.updateRule(testRule.id, { description: 'Updated description' }); expect(updated).toBe(true); const updatedRule = ruleEngine.getRule(testRule.id); expect(updatedRule!.description).toBe('Updated description'); expect(updatedRule!.lastModified).not.toBe(originalModified); }); }); });

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/NeoTecDigital/mcp_shamash'

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