UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

466 lines 18.9 kB
/** * Query Complexity Firewall - Final Protection Layer * * CRITICAL COMPONENT: Last line of defense against query explosions by: * 1. Scoring query complexity across multiple dimensions * 2. Implementing circuit breakers for dangerous queries * 3. Providing intelligent fallback strategies * 4. Learning from query execution patterns * * Created: July 5, 2025 * Purpose: Third line of defense against analytics system failure */ import { getLogger } from '../logging/Logger.js'; const logger = getLogger(); /** * Query Complexity Firewall Implementation */ export class QueryComplexityFirewall { config; queryHistory = new Map(); // Pattern → executions circuitBreaker = new Map(); constructor(config = {}) { this.config = { enabled: true, thresholds: { simple: { maxScore: 35, maxJoins: 2, maxRows: 10000 }, // Allow up to 2 JOINs for simple queries moderate: { maxScore: 60, maxJoins: 3, maxRows: 50000 }, // Allow 3 JOINs for moderate complex: { maxScore: 85, maxJoins: 4, maxRows: 200000 }, // Allow 4 JOINs for complex dangerous: { maxScore: 100, maxJoins: 5, maxRows: 1000000 } // 5+ JOINs are dangerous }, enableFallbacks: true, enableLearning: true, timeoutMs: 10000, // 10 second timeout debugMode: true, ...config }; logger.info(`QueryComplexityFirewall initialized with thresholds: ${JSON.stringify(this.config.thresholds)}`); } /** * Analyze query complexity and determine if it should be allowed */ analyzeQuery(queryInfo) { if (!this.config.enabled) { return { complexity: this.createSimpleComplexity(), allowed: true, recommendations: [] }; } // Store query info for smart allowance logic this.queryInfo = queryInfo; // Calculate complexity score const complexity = this.calculateComplexity(queryInfo); // Check circuit breaker const circuitOpen = this.checkCircuitBreaker(complexity.complexity); if (circuitOpen) { logger.warn(`Circuit breaker open for ${complexity.complexity} queries`); return { complexity, allowed: false, fallback: this.generateFallbackStrategy(queryInfo, 'circuit_breaker'), recommendations: ['Circuit breaker is open due to repeated failures', 'Using simplified fallback query'] }; } // Determine if query is allowed const allowed = this.isQueryAllowed(complexity); const recommendations = this.generateRecommendations(complexity); // Generate fallback if needed let fallback; if (!allowed && this.config.enableFallbacks) { fallback = this.generateFallbackStrategy(queryInfo, complexity.complexity); } if (this.config.debugMode) { logger.info(`Query complexity analysis: ${complexity.complexity} (score: ${complexity.score}), allowed: ${allowed}`); if (complexity.riskFactors.length > 0) { logger.debug(`Risk factors: ${complexity.riskFactors.join(', ')}`); } } return { complexity, allowed, fallback, recommendations }; } /** * Calculate comprehensive complexity score */ calculateComplexity(queryInfo) { let score = 0; const riskFactors = []; // JOIN complexity (major factor) const joinCount = queryInfo.joins.length; // L1-3 ENHANCEMENT: Smarter JOIN scoring // First JOIN is often necessary (e.g., flags → flag_environments) // Additional JOINs increase complexity more if (joinCount === 1) { score += 5; // Minimal penalty for single JOIN } else if (joinCount === 2) { score += 15; // Moderate penalty for 2 JOINs } else if (joinCount >= 3) { score += joinCount * 12; // Higher penalty for 3+ JOINs riskFactors.push(`High JOIN count (${joinCount})`); } // Check for dangerous JOIN patterns let dangerousJoinCount = 0; for (const join of queryInfo.joins) { if (this.isDangerousJoinPattern(join)) { dangerousJoinCount++; score += 25; // Reduced from 30 riskFactors.push(`Dangerous JOIN: ${join.fromTable} → ${join.toTable}`); } } // If ALL JOINs are dangerous, that's a bigger problem if (dangerousJoinCount === joinCount && joinCount > 0) { score += 20; riskFactors.push('All JOINs are dangerous patterns'); } // Estimated result set size const estimatedRows = queryInfo.estimatedRows; if (estimatedRows > 100000) { score += 25; riskFactors.push(`Large result set (${estimatedRows} rows)`); } else if (estimatedRows > 10000) { score += 15; } else if (estimatedRows > 1000) { score += 5; } // Aggregation complexity const aggregationCount = queryInfo.aggregations.length; score += aggregationCount * 5; // 5 points per aggregation // Special penalty for multiple aggregations with JOINs if (aggregationCount > 1 && joinCount > 0) { score += 20; riskFactors.push('Multiple aggregations with JOINs'); } // GROUP BY complexity const groupByCount = queryInfo.groupByFields.length; score += groupByCount * 8; // 8 points per GROUP BY field if (groupByCount > 2) { riskFactors.push(`Complex grouping (${groupByCount} fields)`); } // Cross-entity complexity const crossEntityCount = queryInfo.crossEntities.length; if (crossEntityCount > 2) { score += (crossEntityCount - 2) * 10; riskFactors.push(`Cross-entity query (${crossEntityCount} entities)`); } // JSON path complexity const jsonPathCount = this.countJSONPaths(queryInfo.sql); score += jsonPathCount * 8; // 8 points per JSON path if (jsonPathCount > 2) { riskFactors.push(`Complex JSON access (${jsonPathCount} paths)`); } // SQL structure complexity const nestedQueryDepth = this.calculateNestedQueryDepth(queryInfo.sql); score += nestedQueryDepth * 15; // 15 points per nesting level if (nestedQueryDepth > 1) { riskFactors.push(`Nested queries (depth: ${nestedQueryDepth})`); } // Determine complexity level const complexity = this.scoreToComplexityLevel(score); return { joinCount, estimatedRows, aggregationFunctions: queryInfo.aggregations, groupByFields: groupByCount, crossEntityCount, jsonPathCount, nestedQueryDepth, complexity, score: Math.round(score), riskFactors }; } /** * Check if a JOIN pattern is known to be dangerous */ isDangerousJoinPattern(join) { const dangerousPatterns = [ // The root cause patterns that created the 700x inflation { from: 'flag_environments', to: 'environments' }, { from: 'experiment_pages', to: 'pages' }, { from: 'experiment_audiences', to: 'audiences' }, // Other high-risk patterns { from: 'flags', to: 'projects' }, { from: 'experiments', to: 'projects' }, { from: 'audiences', to: 'attributes' } ]; return dangerousPatterns.some(pattern => join.fromTable === pattern.from && join.toTable === pattern.to); } /** * Count JSON path operations in SQL */ countJSONPaths(sql) { const jsonFunctions = ['JSON_EXTRACT', 'JSON_VALUE', 'JSON_QUERY', 'JSON_ARRAY', 'JSON_OBJECT']; let count = 0; for (const func of jsonFunctions) { const regex = new RegExp(func, 'gi'); const matches = sql.match(regex); count += matches ? matches.length : 0; } return count; } /** * Calculate nested query depth */ calculateNestedQueryDepth(sql) { let depth = 0; let maxDepth = 0; for (const char of sql) { if (char === '(') { depth++; maxDepth = Math.max(maxDepth, depth); } else if (char === ')') { depth--; } } return maxDepth; } /** * Convert score to complexity level */ scoreToComplexityLevel(score) { if (score <= this.config.thresholds.simple.maxScore) return 'simple'; if (score <= this.config.thresholds.moderate.maxScore) return 'moderate'; if (score <= this.config.thresholds.complex.maxScore) return 'complex'; return 'dangerous'; } /** * Determine if query should be allowed based on complexity */ isQueryAllowed(complexity) { const threshold = this.config.thresholds[complexity.complexity]; // SMART ALLOWANCE: Special cases for necessary queries // Allow queries with specific field selection (not SELECT *) more leniency const hasSpecificFields = this.queryInfo?.sql && !this.queryInfo.sql.includes('SELECT *') && !this.queryInfo.sql.includes('SELECT COUNT(*)'); // If query has specific fields and reasonable JOIN count, be more lenient if (hasSpecificFields && complexity.joinCount <= 3 && complexity.estimatedRows < 50000) { // Allow up to score 70 for targeted queries if (complexity.score <= 70) { logger.info(`Allowing targeted query with score ${complexity.score} due to specific field selection`); return true; } } // Check multiple criteria if (complexity.joinCount > threshold.maxJoins) return false; if (complexity.estimatedRows > threshold.maxRows) return false; if (complexity.score > threshold.maxScore) return false; // Special blocks for dangerous patterns if (complexity.complexity === 'dangerous') { logger.warn(`Blocking dangerous query (score: ${complexity.score})`); return false; } return true; } // Store query info for use in isQueryAllowed queryInfo; /** * Generate recommendations for query optimization */ generateRecommendations(complexity) { const recommendations = []; if (complexity.joinCount > 2) { recommendations.push('Consider reducing the number of JOINs by using junction tables directly'); } if (complexity.estimatedRows > 50000) { recommendations.push('Add more selective WHERE conditions to reduce result set size'); } if (complexity.groupByFields > 2) { recommendations.push('Consider simplifying GROUP BY criteria or running separate queries'); } if (complexity.crossEntityCount > 2) { recommendations.push('Break down cross-entity query into simpler single-entity queries'); } if (complexity.jsonPathCount > 3) { recommendations.push('Consider materializing JSON fields into regular columns for better performance'); } if (complexity.riskFactors.length > 3) { recommendations.push('Query has multiple risk factors - consider using a simpler approach'); } return recommendations; } /** * Generate fallback strategy for complex queries */ generateFallbackStrategy(queryInfo, complexityLevel) { // Determine primary entity const primaryTable = queryInfo.joins[0]?.fromTable || 'flags'; // Build WHERE clause from conditions if available let whereClause = ''; if (queryInfo.whereConditions && queryInfo.whereConditions.length > 0) { const conditions = queryInfo.whereConditions.map(condition => { // Extract field name without table prefix for simple fallback queries let fieldName = condition.field; if (fieldName.includes('.')) { fieldName = fieldName.split('.').pop() || fieldName; } // Only include conditions that apply to the primary table if (fieldName === 'project_id') { if (typeof condition.value === 'string') { return `${fieldName} ${condition.operator} '${condition.value}'`; } else { return `${fieldName} ${condition.operator} ${condition.value}`; } } return null; }).filter(Boolean); if (conditions.length > 0) { whereClause = `WHERE ${conditions.join(' AND ')}`; } } // Generate simple fallback based on query type if (queryInfo.aggregations.includes('COUNT')) { return { name: 'Simple Count Fallback', description: 'Basic COUNT query without JOINs', sql: `SELECT COUNT(*) as count FROM ${primaryTable} ${whereClause}`.trim(), estimatedAccuracy: 0.8, executionTime: 500 }; } if (queryInfo.groupByFields.length > 0) { // L2-1 FIX: Handle GROUP BY fields that reference joined tables let groupField = queryInfo.groupByFields[0] || 'status'; let selectField = groupField; // If the GROUP BY field references another table, we can't use it in a simple query if (groupField.includes('.')) { logger.warn(`L2-1 DEBUG: GROUP BY field '${groupField}' references joined table, falling back to simple count`); // Return a simple count instead of trying to GROUP BY a field we don't have return { name: 'Simple Count Fallback', description: 'Cannot GROUP BY joined fields without JOINs', sql: `SELECT COUNT(*) as count FROM ${primaryTable} ${whereClause}`.trim(), estimatedAccuracy: 0.5, executionTime: 500 }; } return { name: 'Simplified Group By', description: 'Basic GROUP BY query without complex JOINs', sql: `SELECT ${selectField}, COUNT(*) as count FROM ${primaryTable} ${whereClause} GROUP BY ${groupField}`.trim(), estimatedAccuracy: 0.7, executionTime: 1000 }; } // Default list fallback return { name: 'Simple List Query', description: 'Basic SELECT query without JOINs', sql: `SELECT * FROM ${primaryTable} ${whereClause} LIMIT 100`.trim(), estimatedAccuracy: 0.9, executionTime: 200 }; } /** * Check circuit breaker for query type */ checkCircuitBreaker(complexityLevel) { const breaker = this.circuitBreaker.get(complexityLevel); if (!breaker) return false; // Circuit breaker logic: open if 3+ failures in last 5 minutes const now = Date.now(); const fiveMinutesAgo = now - (5 * 60 * 1000); if (breaker.failures >= 3 && breaker.lastFailure > fiveMinutesAgo) { return true; // Circuit is open } // Reset circuit breaker if enough time has passed if (breaker.lastFailure < fiveMinutesAgo) { this.circuitBreaker.delete(complexityLevel); } return false; } /** * Record query execution result for learning */ recordQueryExecution(complexity, success, executionTimeMs, resultCount) { if (!this.config.enableLearning) return; // Update circuit breaker if (!success) { const breaker = this.circuitBreaker.get(complexity.complexity) || { failures: 0, lastFailure: 0 }; breaker.failures++; breaker.lastFailure = Date.now(); this.circuitBreaker.set(complexity.complexity, breaker); } // Update query history for learning const pattern = this.createQueryPattern(complexity); const history = this.queryHistory.get(pattern) || []; history.push({ ...complexity, score: complexity.score + (success ? 0 : 20) // Penalty for failures }); // Keep only last 10 executions for each pattern if (history.length > 10) { history.shift(); } this.queryHistory.set(pattern, history); if (this.config.debugMode) { logger.debug(`Recorded query execution: ${complexity.complexity}, success: ${success}, time: ${executionTimeMs}ms`); } } /** * Create a pattern key for query learning */ createQueryPattern(complexity) { return `${complexity.complexity}_j${complexity.joinCount}_g${complexity.groupByFields}_a${complexity.aggregationFunctions.length}`; } /** * Create simple complexity for disabled firewall */ createSimpleComplexity() { return { joinCount: 0, estimatedRows: 100, aggregationFunctions: [], groupByFields: 0, crossEntityCount: 1, jsonPathCount: 0, nestedQueryDepth: 0, complexity: 'simple', score: 10, riskFactors: [] }; } /** * Get firewall statistics for monitoring */ getStatistics() { const stats = { totalQueries: 0, blockedQueries: 0, complexityDistribution: { simple: 0, moderate: 0, complex: 0, dangerous: 0 }, circuitBreakerStatus: {}, averageScore: 0 }; // Analyze circuit breaker status for (const [level, breaker] of this.circuitBreaker) { stats.circuitBreakerStatus[level] = this.checkCircuitBreaker(level); } return stats; } /** * Update firewall configuration at runtime */ updateConfig(updates) { this.config = { ...this.config, ...updates }; logger.info(`QueryComplexityFirewall configuration updated`); } } //# sourceMappingURL=QueryComplexityFirewall.js.map