Skip to main content
Glama

firewalla-mcp-server

enhanced-query-validator.ts33.2 kB
/** * Enhanced Query Validator for Firewalla MCP Server * Provides comprehensive syntax, semantic, and field validation for search queries */ import { queryParser } from '../search/parser.js'; import { SEARCH_FIELDS, type QueryNode, type FieldQuery, type ComparisonQuery, type RangeQuery } from '../search/types.js'; import { FIELD_MAPPINGS, type EntityType, type CorrelationFieldName } from './field-mapper.js'; import { QuerySanitizer } from './error-handler.js'; import type { ValidationResult } from '../types.js'; /** * Detailed error information with position and suggestions */ export interface DetailedError { message: string; position?: number; suggestion?: string; validOptions?: string[]; errorType: 'syntax' | 'semantic' | 'field' | 'operator'; context?: string; } /** * Quick fix suggestion for common errors */ export interface QuickFix { description: string; action: 'replace_field' | 'fix_syntax' | 'change_operator' | 'add_quotes'; original?: string; replacement?: string; position?: number; } /** * Enhanced validation configuration for different entity types */ interface EntityValidationConfig { numericFields: string[]; dateFields: string[]; booleanFields: string[]; requiredFields?: string[]; deprecatedFields?: string[]; fieldAliases?: Record<string, string>; } /** * Validation configurations for each entity type */ const ENTITY_VALIDATION_CONFIGS: Record<EntityType, EntityValidationConfig> = { flows: { numericFields: ['bytes', 'download', 'upload', 'ts', 'timestamp', 'duration', 'port', 'session_duration', 'frequency_score', 'geographic_risk_score'], dateFields: ['ts', 'timestamp', 'time_window'], booleanFields: ['block', 'blocked', 'is_cloud_provider', 'is_proxy', 'is_vpn'], deprecatedFields: ['srcIP', 'dstIP'], // Legacy field names fieldAliases: { 'src_ip': 'source_ip', 'dst_ip': 'destination_ip', 'size': 'bytes' } }, alarms: { numericFields: ['ts', 'timestamp', 'severity_level', 'priority', 'geographic_risk_score'], dateFields: ['ts', 'timestamp', 'created_at', 'updated_at'], booleanFields: ['resolved', 'acknowledged', 'is_cloud_provider', 'is_proxy', 'is_vpn'], requiredFields: ['type', 'severity'], deprecatedFields: ['alarmType'], fieldAliases: { 'alarm_type': 'type', 'level': 'severity' } }, rules: { numericFields: ['ts', 'timestamp', 'hit_count', 'priority', 'port'], dateFields: ['ts', 'timestamp', 'created_at', 'last_hit'], booleanFields: ['active', 'enabled', 'disabled'], requiredFields: ['action', 'target_value'], fieldAliases: { 'target': 'target_value', 'target.type': 'target_type', 'target.value': 'target_value', 'rule_action': 'action' } }, devices: { numericFields: ['last_seen', 'first_seen', 'activity_score', 'bandwidth_usage'], dateFields: ['last_seen', 'first_seen', 'created_at'], booleanFields: ['online', 'managed', 'monitored'], requiredFields: ['ip', 'mac'], fieldAliases: { 'device_ip': 'ip', 'mac_address': 'mac', 'is_online': 'online' } }, target_lists: { numericFields: ['target_count', 'created_at', 'updated_at'], dateFields: ['created_at', 'updated_at', 'last_updated'], booleanFields: ['active', 'enabled'], requiredFields: ['name', 'category'], fieldAliases: { 'list_name': 'name', 'list_category': 'category' } } }; /** * Semantic validation results with detailed feedback */ interface SemanticValidationResult extends ValidationResult { suggestions?: string[]; correctedQuery?: string; fieldIssues?: Array<{ field: string; issue: 'invalid' | 'deprecated' | 'type_mismatch' | 'not_supported'; suggestion?: string; }>; detailedErrors?: DetailedError[]; quickFixes?: QuickFix[]; } /** * Enhanced Query Validator with comprehensive validation capabilities */ export class EnhancedQueryValidator { /** * Validate query with comprehensive syntax, semantic, and field validation */ static validateQuery(query: string, entityType: EntityType): SemanticValidationResult { // Step 1: Basic sanitization (security) const sanitizationResult = QuerySanitizer.sanitizeSearchQuery(query); if (!sanitizationResult.isValid) { return { isValid: false, errors: sanitizationResult.errors, suggestions: ['Check for malicious patterns or invalid characters'] }; } // Step 2: Syntax validation using parser const parseResult = queryParser.parse(sanitizationResult.sanitizedValue as string, entityType); if (!parseResult.isValid || !parseResult.ast) { return { isValid: false, errors: parseResult.errors, suggestions: parseResult.suggestions || [], correctedQuery: this.attemptQueryCorrection(query, entityType) }; } // Step 3: Semantic validation const semanticResult = this.validateSemantics(parseResult.ast, entityType); if (!semanticResult.isValid) { return semanticResult; } // Step 4: Field validation and optimization const fieldResult = this.validateAndOptimizeFields(parseResult.ast, entityType); return { isValid: fieldResult.isValid, errors: fieldResult.errors, sanitizedValue: fieldResult.correctedQuery || sanitizationResult.sanitizedValue, suggestions: fieldResult.suggestions, correctedQuery: fieldResult.correctedQuery, fieldIssues: fieldResult.fieldIssues }; } /** * Legacy validateQuery method for backward compatibility with position tracking */ validateQuery(query: string, entityType?: EntityType): SemanticValidationResult & { detailedErrors: DetailedError[]; quickFixes: QuickFix[] } { const errors: DetailedError[] = []; const quickFixes: QuickFix[] = []; if (!query || typeof query !== 'string') { errors.push({ message: 'Query must be a non-empty string', errorType: 'syntax', suggestion: 'Provide a valid search query string' }); return this.buildResult(false, errors, quickFixes); } const trimmedQuery = query.trim(); if (trimmedQuery.length === 0) { errors.push({ message: 'Query cannot be empty', errorType: 'syntax', suggestion: 'Enter a search query like "protocol:tcp" or "severity:high"' }); return this.buildResult(false, errors, quickFixes); } try { // If entity type provided, use static method for full validation then add position tracking if (entityType) { const staticResult = EnhancedQueryValidator.validateQuery(query, entityType); // If static validation failed, add position tracking details if (!staticResult.isValid) { const parseResult = this.parseWithPositionTracking(trimmedQuery); return { ...staticResult, detailedErrors: parseResult.errors, quickFixes: parseResult.quickFixes }; } return { ...staticResult, detailedErrors: staticResult.detailedErrors || [], quickFixes: staticResult.quickFixes || [] }; } // Parse query and catch detailed syntax errors const parseResult = this.parseWithPositionTracking(trimmedQuery); errors.push(...parseResult.errors); quickFixes.push(...parseResult.quickFixes); } catch (error) { if (error instanceof SyntaxError) { const detailedError = this.createDetailedSyntaxError(error, trimmedQuery); errors.push(detailedError); const quickFix = this.suggestSyntaxFix(error, trimmedQuery); if (quickFix) { quickFixes.push(quickFix); } } else { errors.push({ message: `Unexpected parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, errorType: 'syntax' }); } } return this.buildResult(errors.length === 0, errors, quickFixes); } /** * Parse query with detailed position tracking */ private parseWithPositionTracking(query: string): { errors: DetailedError[]; quickFixes: QuickFix[] } { const errors: DetailedError[] = []; const quickFixes: QuickFix[] = []; // Check for unmatched parentheses const parenthesesResult = this.validateParentheses(query); if (!parenthesesResult.isValid) { errors.push(...parenthesesResult.errors); quickFixes.push(...parenthesesResult.quickFixes); } // Check for unmatched quotes const quotesResult = this.validateQuotes(query); if (!quotesResult.isValid) { errors.push(...quotesResult.errors); quickFixes.push(...quotesResult.quickFixes); } // Check for malformed field syntax const fieldsResult = this.validateFieldSyntax(query); if (!fieldsResult.isValid) { errors.push(...fieldsResult.errors); quickFixes.push(...fieldsResult.quickFixes); } // Check for operator placement const operatorsResult = this.validateOperatorPlacement(query); if (!operatorsResult.isValid) { errors.push(...operatorsResult.errors); quickFixes.push(...operatorsResult.quickFixes); } return { errors, quickFixes }; } /** * Validate parentheses matching */ private validateParentheses(query: string): { isValid: boolean; errors: DetailedError[]; quickFixes: QuickFix[] } { const errors: DetailedError[] = []; const quickFixes: QuickFix[] = []; const stack: Array<{ char: string; position: number }> = []; for (let i = 0; i < query.length; i++) { const char = query[i]; if (char === '(') { stack.push({ char, position: i }); } else if (char === ')') { if (stack.length === 0) { const context = this.getErrorContext(query, i); errors.push({ message: `Unexpected closing parenthesis at position ${i}`, position: i, errorType: 'syntax', context, suggestion: 'Add matching opening parenthesis or remove this closing parenthesis' }); quickFixes.push({ description: 'Remove unexpected closing parenthesis', action: 'fix_syntax', position: i, original: ')', replacement: '' }); } else { stack.pop(); } } } // Check for unmatched opening parentheses if (stack.length > 0) { const unmatched = stack[stack.length - 1]; const context = this.getErrorContext(query, unmatched.position); errors.push({ message: `Unclosed opening parenthesis at position ${unmatched.position}`, position: unmatched.position, errorType: 'syntax', context, suggestion: 'Add matching closing parenthesis' }); quickFixes.push({ description: 'Add matching closing parenthesis', action: 'fix_syntax', position: query.length, original: '', replacement: ')' }); } return { isValid: errors.length === 0, errors, quickFixes }; } /** * Validate quote matching */ private validateQuotes(query: string): { isValid: boolean; errors: DetailedError[]; quickFixes: QuickFix[] } { const errors: DetailedError[] = []; const quickFixes: QuickFix[] = []; let inQuotes = false; let quoteStart = -1; let quoteChar = ''; for (let i = 0; i < query.length; i++) { const char = query[i]; if ((char === '"' || char === "'") && (i === 0 || query[i-1] !== '\\')) { if (!inQuotes) { inQuotes = true; quoteStart = i; quoteChar = char; } else if (char === quoteChar) { inQuotes = false; quoteStart = -1; } } } if (inQuotes && quoteStart >= 0) { const context = this.getErrorContext(query, quoteStart); errors.push({ message: `Unclosed quoted string starting at position ${quoteStart}`, position: quoteStart, errorType: 'syntax', context, suggestion: `Add matching ${quoteChar} to close the quoted string` }); quickFixes.push({ description: `Add matching ${quoteChar} quote`, action: 'fix_syntax', position: query.length, original: '', replacement: quoteChar }); } return { isValid: errors.length === 0, errors, quickFixes }; } /** * Validate field syntax (field:value patterns) */ private validateFieldSyntax(query: string): { isValid: boolean; errors: DetailedError[]; quickFixes: QuickFix[] } { const errors: DetailedError[] = []; const quickFixes: QuickFix[] = []; // Match potential field patterns that might be malformed // Updated regex to better handle logical operators and range syntax const fieldPattern = /(\w+)\s*([=:]?)\s*([^)\s]*)/g; let match; // Skip validation for logical operators and range keywords const logicalOperators = ['AND', 'OR', 'NOT']; const rangeKeywords = ['TO']; while ((match = fieldPattern.exec(query)) !== null) { const [, field, operator, value] = match; const position = match.index; // Skip logical operators and range keywords - they have different syntax rules if (logicalOperators.includes(field.toUpperCase()) || rangeKeywords.includes(field.toUpperCase())) { continue; } // Skip if this appears to be part of a range syntax (field:[value TO value]) if (this.isPartOfRangeSyntax(query, position)) { continue; } // Skip if this appears to be part of a quoted geographic name if (this.isPartOfQuotedValue(query, position)) { continue; } // Check for missing colon if (operator === '' && value !== '') { const context = this.getErrorContext(query, position); errors.push({ message: `Expected ':' after field '${field}' at position ${position + field.length}`, position: position + field.length, errorType: 'syntax', context, suggestion: `Use '${field}:${value}' instead of '${field} ${value}'` }); quickFixes.push({ description: `Add colon after field '${field}'`, action: 'fix_syntax', position: position + field.length, original: `${field } ${ value}`, replacement: `${field }:${ value}` }); } // Check for invalid equals operator if (operator === '=') { const context = this.getErrorContext(query, position); errors.push({ message: `Use ':' instead of '=' for field queries at position ${position + field.length}`, position: position + field.length, errorType: 'syntax', context, suggestion: `Use '${field}:${value}' instead of '${field}=${value}'` }); quickFixes.push({ description: `Replace '=' with ':' for field query`, action: 'fix_syntax', position: position + field.length, original: '=', replacement: ':' }); } } return { isValid: errors.length === 0, errors, quickFixes }; } /** * Check if the current position is part of range syntax like [value TO value] */ private isPartOfRangeSyntax(query: string, position: number): boolean { // Look for surrounding brackets and TO keyword const rangePattern = /\[[^\]]*TO[^\]]*\]/g; let match; while ((match = rangePattern.exec(query)) !== null) { if (position >= match.index && position <= match.index + match[0].length) { return true; } } return false; } /** * Check if the current position is part of a quoted value */ private isPartOfQuotedValue(query: string, position: number): boolean { // Find if we're inside quotes let inQuotes = false; let quoteStart = -1; for (let i = 0; i < position; i++) { const char = query[i]; if ((char === '"' || char === "'") && (i === 0 || query[i-1] !== '\\')) { if (!inQuotes) { inQuotes = true; quoteStart = i; } else { inQuotes = false; quoteStart = -1; } } } return inQuotes && quoteStart >= 0; } /** * Validate operator placement */ private validateOperatorPlacement(query: string): { isValid: boolean; errors: DetailedError[]; quickFixes: QuickFix[] } { const errors: DetailedError[] = []; const quickFixes: QuickFix[] = []; // Check for malformed logical operators, but be more lenient about placement const logicalPattern = /\b(AND|OR|NOT)\b/gi; let match; while ((match = logicalPattern.exec(query)) !== null) { const operator = match[0]; const position = match.index; // NOT at the beginning is actually valid syntax (e.g., "NOT blocked:true") // Only flag if it's truly malformed (e.g., multiple consecutive operators) // Check if operator is at the very end (after trimming) const trimmedQuery = query.trim(); if (position + operator.length === trimmedQuery.length) { errors.push({ message: `Logical operator '${operator}' cannot appear at the end of the query`, position, errorType: 'syntax', suggestion: `Add a condition after '${operator}' or remove it` }); } } return { isValid: errors.length === 0, errors, quickFixes }; } /** * Create detailed syntax error with context */ private createDetailedSyntaxError(error: SyntaxError, query: string): DetailedError { const position = this.findErrorPosition(error, query); const context = this.getErrorContext(query, position); return { message: `Syntax error at position ${position}: ${error.message}`, position, errorType: 'syntax', context, suggestion: this.getSyntaxFixSuggestion(error.message) }; } /** * Get context around error position */ private getErrorContext(query: string, position: number, contextLength = 10): string { const start = Math.max(0, position - contextLength); const end = Math.min(query.length, position + contextLength); const before = query.substring(start, position); const at = query[position] || ''; const after = query.substring(position + 1, end); return `"${before}[${at}]${after}"`; } /** * Find error position from error message */ private findErrorPosition(error: SyntaxError, _query: string): number { // Try to extract position from error message const positionMatch = error.message.match(/position (\d+)/); if (positionMatch) { return parseInt(positionMatch[1], 10); } // Fallback: estimate based on error content return 0; } /** * Get syntax fix suggestion based on error message */ private getSyntaxFixSuggestion(errorMessage: string): string { if (errorMessage.includes('parenthesis')) { return 'Check that all opening parentheses have matching closing parentheses'; } if (errorMessage.includes('quote')) { return 'Check that all quoted strings are properly closed'; } if (errorMessage.includes('operator')) { return 'Check that operators (AND, OR, NOT) are properly placed between terms'; } if (errorMessage.includes('colon')) { return 'Use field:value syntax for field queries'; } return 'Review query syntax documentation for proper formatting'; } /** * Suggest syntax fix as quick fix */ private suggestSyntaxFix(error: SyntaxError, query: string): QuickFix | null { if (error.message.includes('parenthesis')) { return { description: 'Add missing closing parenthesis', action: 'fix_syntax', position: query.length, original: '', replacement: ')' }; } if (error.message.includes('quote')) { return { description: 'Add missing quote', action: 'fix_syntax', position: query.length, original: '', replacement: '"' }; } return null; } /** * Build enhanced validation result */ private buildResult( isValid: boolean, detailedErrors: DetailedError[], quickFixes: QuickFix[] ): SemanticValidationResult & { detailedErrors: DetailedError[]; quickFixes: QuickFix[] } { return { isValid, errors: detailedErrors.map(e => e.message), suggestions: detailedErrors.map(e => e.suggestion).filter(Boolean) as string[], detailedErrors, quickFixes }; } /** * Validate semantic correctness of the query AST */ private static validateSemantics(ast: QueryNode, entityType: EntityType): SemanticValidationResult { const errors: string[] = []; const suggestions: string[] = []; const fieldIssues: SemanticValidationResult['fieldIssues'] = []; const config = ENTITY_VALIDATION_CONFIGS[entityType]; const validateNode = (node: QueryNode): void => { switch (node.type) { case 'field': { const fieldNode = node; this.validateFieldQuery(fieldNode, config, errors, suggestions, fieldIssues); break; } case 'comparison': { const compNode = node; this.validateComparisonQuery(compNode, config, errors, suggestions, fieldIssues); break; } case 'range': { const rangeNode = node; this.validateRangeQuery(rangeNode, config, errors, suggestions, fieldIssues); break; } case 'wildcard': { // Handle wildcard queries - they don't need field validation break; } case 'logical': if (node.left) {validateNode(node.left);} if (node.right) {validateNode(node.right);} if (node.operand) {validateNode(node.operand);} break; case 'group': validateNode(node.query); break; } }; validateNode(ast); return { isValid: errors.length === 0, errors, suggestions, fieldIssues }; } /** * Validate field query semantics */ private static validateFieldQuery( node: FieldQuery, config: EntityValidationConfig, errors: string[], suggestions: string[], fieldIssues: SemanticValidationResult['fieldIssues'] ): void { // Skip validation for match-all queries if (node.field === '*') {return;} // Check for deprecated fields if (config.deprecatedFields?.includes(node.field)) { const alias = config.fieldAliases?.[node.field]; fieldIssues?.push({ field: node.field, issue: 'deprecated', suggestion: alias ? `Use '${alias}' instead` : 'Field is deprecated' }); if (alias) { suggestions.push(`Replace deprecated field '${node.field}' with '${alias}'`); } } // Validate boolean field usage if (config.booleanFields.includes(node.field)) { const value = String(node.value).toLowerCase(); if (!['true', 'false', '1', '0', 'yes', 'no'].includes(value)) { errors.push(`Field '${node.field}' expects a boolean value (true/false), got '${node.value}'`); suggestions.push(`Use 'true' or 'false' for boolean field '${node.field}'`); } } // Validate date field format (basic check) if (config.dateFields.includes(node.field) && typeof node.value === 'string') { if (!/^\d{4}-\d{2}-\d{2}|^\d{10,13}$/.test(node.value)) { errors.push(`Field '${node.field}' expects a date format (YYYY-MM-DD or timestamp), got '${node.value}'`); suggestions.push(`Use ISO date format (YYYY-MM-DD) or Unix timestamp for '${node.field}'`); } } } /** * Validate comparison query semantics */ private static validateComparisonQuery( node: ComparisonQuery, config: EntityValidationConfig, errors: string[], suggestions: string[], fieldIssues: SemanticValidationResult['fieldIssues'] ): void { // Comparison operators should only be used with numeric or date fields if (!config.numericFields.includes(node.field) && !config.dateFields.includes(node.field)) { errors.push(`Comparison operator '${node.operator}' cannot be used with non-numeric field '${node.field}'`); fieldIssues?.push({ field: node.field, issue: 'type_mismatch', suggestion: `Use '=' operator for string fields or choose a numeric field` }); suggestions.push(`Field '${node.field}' requires '=' operator for string matching`); } // Validate numeric values for numeric fields if (config.numericFields.includes(node.field) && typeof node.value === 'string') { if (!/^-?\d+(\.\d+)?$/.test(node.value)) { errors.push(`Field '${node.field}' expects a numeric value, got '${node.value}'`); suggestions.push(`Provide a numeric value for field '${node.field}'`); } } } /** * Validate range query semantics */ private static validateRangeQuery( node: RangeQuery, config: EntityValidationConfig, errors: string[], suggestions: string[], fieldIssues: SemanticValidationResult['fieldIssues'] ): void { // Range queries should only be used with numeric or date fields if (!config.numericFields.includes(node.field) && !config.dateFields.includes(node.field)) { errors.push(`Range query cannot be used with non-numeric field '${node.field}'`); fieldIssues?.push({ field: node.field, issue: 'type_mismatch', suggestion: `Use wildcard patterns for string field ranges` }); } // Validate range bounds if (typeof node.min === 'number' && typeof node.max === 'number' && node.min >= node.max) { errors.push(`Range minimum (${node.min}) must be less than maximum (${node.max})`); suggestions.push(`Ensure range minimum is less than maximum`); } } /** * Validate and optimize field usage */ private static validateAndOptimizeFields(ast: QueryNode, entityType: EntityType): SemanticValidationResult { const errors: string[] = []; const suggestions: string[] = []; const fieldIssues: SemanticValidationResult['fieldIssues'] = []; const validFields = SEARCH_FIELDS[entityType] || []; const config = ENTITY_VALIDATION_CONFIGS[entityType]; let hasOptimizations = false; const validateAndOptimize = (node: QueryNode): QueryNode => { if (node.type === 'field' || node.type === 'comparison' || node.type === 'range') { const fieldNode = node as FieldQuery; // Skip validation for match-all if (fieldNode.field === '*') {return node;} // Check if field exists in valid fields for entity type if (!validFields.includes(fieldNode.field)) { // Check if it's an alias const alias = config.fieldAliases?.[fieldNode.field]; if (alias && validFields.includes(alias)) { hasOptimizations = true; suggestions.push(`Optimized field '${fieldNode.field}' to '${alias}'`); return { ...node, field: alias }; } // Check field mappings for alternative names const fieldMapping = FIELD_MAPPINGS[entityType]; const mappedField = Object.entries(fieldMapping).find(([, mappings]) => mappings.includes(fieldNode.field) )?.[0]; if (mappedField && validFields.includes(mappedField)) { hasOptimizations = true; suggestions.push(`Mapped field '${fieldNode.field}' to '${mappedField}'`); return { ...node, field: mappedField }; } errors.push(`Invalid field '${fieldNode.field}' for entity type '${entityType}'`); fieldIssues.push({ field: fieldNode.field, issue: 'invalid', suggestion: `Valid fields: ${validFields.slice(0, 5).join(', ')}${validFields.length > 5 ? '...' : ''}` }); } } // Recursively process logical operations if (node.type === 'logical') { return { ...node, left: node.left ? validateAndOptimize(node.left) : undefined, right: node.right ? validateAndOptimize(node.right) : undefined, operand: node.operand ? validateAndOptimize(node.operand) : undefined }; } if (node.type === 'group') { return { ...node, query: validateAndOptimize(node.query) }; } return node; }; const optimizedAst = validateAndOptimize(ast); return { isValid: errors.length === 0, errors, suggestions, fieldIssues, correctedQuery: hasOptimizations ? this.astToQueryString(optimizedAst) : undefined }; } /** * Attempt to correct common query syntax errors */ private static attemptQueryCorrection(query: string, _entityType: EntityType): string | undefined { let corrected = query; let hasCorrections = false; // Fix common syntax issues const corrections = [ // Fix missing quotes around values with spaces { pattern: /(\w+):([^"\s]+\s+[^"\s]+)/g, replacement: '$1:"$2"', description: 'Add quotes around spaced values' }, // Fix incorrect operators { pattern: /(\w+)\s*==\s*/g, replacement: '$1:', description: 'Replace == with :' }, { pattern: /(\w+)\s*=\s*/g, replacement: '$1:', description: 'Replace = with :' }, // Fix missing AND/OR between terms { pattern: /(\w+:\S+)\s+(\w+:\S+)/g, replacement: '$1 AND $2', description: 'Add AND between terms' }, // Fix common field aliases { pattern: /\bsrc_ip\b/g, replacement: 'source_ip', description: 'Replace src_ip with source_ip' }, { pattern: /\bdst_ip\b/g, replacement: 'destination_ip', description: 'Replace dst_ip with destination_ip' }, ]; for (const correction of corrections) { if (correction.pattern.test(corrected)) { corrected = corrected.replace(correction.pattern, correction.replacement); hasCorrections = true; } } return hasCorrections ? corrected : undefined; } /** * Convert AST back to query string (simplified) */ private static astToQueryString(ast: QueryNode): string { switch (ast.type) { case 'field': return `${ast.field}:${ast.value}`; case 'comparison': return `${ast.field}:${ast.operator}${ast.value}`; case 'range': return `${ast.field}:[${ast.min} TO ${ast.max}]`; case 'wildcard': return `${ast.field}:${ast.pattern}`; case 'logical': if (ast.operator === 'NOT' && ast.operand) { return `NOT ${this.astToQueryString(ast.operand)}`; } if (ast.left && ast.right) { return `${this.astToQueryString(ast.left)} ${ast.operator} ${this.astToQueryString(ast.right)}`; } break; case 'group': return `(${this.astToQueryString(ast.query)})`; } return ''; } /** * Validate correlation field compatibility between entity types */ static validateCorrelationFields( fields: CorrelationFieldName[], primaryEntityType: EntityType, secondaryEntityTypes: EntityType[] ): ValidationResult & { compatibleFields?: CorrelationFieldName[]; suggestions?: string[] } { const errors: string[] = []; const suggestions: string[] = []; const compatibleFields: CorrelationFieldName[] = []; const primaryMapping = FIELD_MAPPINGS[primaryEntityType]; for (const field of fields) { // Check if field exists in primary entity if (primaryMapping[field]) { // Check if field exists in all secondary entities const existsInAllSecondary = secondaryEntityTypes.every(entityType => FIELD_MAPPINGS[entityType][field] ); if (existsInAllSecondary) { compatibleFields.push(field); } else { const incompatibleTypes = secondaryEntityTypes.filter(entityType => !FIELD_MAPPINGS[entityType][field] ); errors.push(`Field '${field}' is not available in entity types: ${incompatibleTypes.join(', ')}`); } } else { errors.push(`Field '${field}' is not available in primary entity type '${primaryEntityType}'`); } } // Generate suggestions for alternative fields if (compatibleFields.length === 0 && fields.length > 0) { const commonFields = Object.keys(primaryMapping).filter(field => secondaryEntityTypes.every(entityType => FIELD_MAPPINGS[entityType][field]) ); if (commonFields.length > 0) { suggestions.push(`Consider using these compatible fields: ${commonFields.slice(0, 5).join(', ')}`); } } return { isValid: errors.length === 0, errors, compatibleFields, suggestions }; } }

Latest Blog Posts

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/amittell/firewalla-mcp-server'

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