Skip to main content
Glama

firewalla-mcp-server

operator-validator.ts15.2 kB
/** * Operator Validator for Firewalla MCP Server * Provides operator compatibility checking with field types */ import { SEARCH_FIELDS } from '../search/types.js'; type EntityType = keyof typeof SEARCH_FIELDS; /** * Field data types for operator compatibility */ export type FieldType = 'string' | 'number' | 'boolean' | 'enum' | 'ip' | 'timestamp' | 'array'; /** * Query operators */ export type QueryOperator = ':' | '=' | '!=' | '>' | '<' | '>=' | '<=' | '~' | 'contains' | 'startswith' | 'endswith' | 'in' | 'not_in' | 'range'; /** * Operator validation result */ export interface OperatorValidation { isValid: boolean; error?: string; suggestion?: string; validOperators?: QueryOperator[]; fieldType?: FieldType; } /** * Operator compatibility matrix */ type OperatorCompatibility = Record<string, QueryOperator[]>; export class OperatorValidator { private static readonly FIELD_TYPE_MAP: Record<string, FieldType> = { // String fields 'source_ip': 'ip', 'destination_ip': 'ip', 'device_ip': 'ip', 'protocol': 'enum', 'direction': 'enum', 'action': 'enum', 'status': 'enum', 'type': 'enum', 'severity': 'enum', 'name': 'string', 'description': 'string', 'target_value': 'string', 'target_type': 'enum', 'network_name': 'string', 'group_name': 'string', 'mac_vendor': 'string', 'category': 'string', 'user_agent': 'string', 'application': 'string', 'domain_category': 'enum', 'ssl_subject': 'string', 'ssl_issuer': 'string', 'country': 'string', 'city': 'string', 'isp': 'string', 'organization': 'string', // Numeric fields 'bytes': 'number', 'hit_count': 'number', 'total_download': 'number', 'total_upload': 'number', 'session_duration': 'number', 'frequency_score': 'number', 'geographic_risk_score': 'number', 'target_count': 'number', // Boolean fields 'blocked': 'boolean', 'online': 'boolean', 'is_cloud_provider': 'boolean', 'is_proxy': 'boolean', 'is_vpn': 'boolean', // Timestamp fields 'timestamp': 'timestamp', 'created_at': 'timestamp', 'updated_at': 'timestamp', 'last_updated': 'timestamp', // Array/list fields 'owner': 'array' }; private static readonly OPERATOR_COMPATIBILITY: OperatorCompatibility = { 'string': [':', '=', '!=', '~', 'contains', 'startswith', 'endswith'], 'number': [':', '=', '!=', '>', '<', '>=', '<=', 'range'], 'boolean': [':', '='], 'enum': [':', '=', '!=', 'in', 'not_in'], 'ip': [':', '=', '!=', '~', 'contains'], 'timestamp': [':', '=', '!=', '>', '<', '>=', '<=', 'range'], 'array': [':', '=', 'contains', 'in'] }; private static readonly OPERATOR_DESCRIPTIONS: Record<QueryOperator, string> = { ':': 'Equals (standard field query)', '=': 'Equals (alternative syntax)', '!=': 'Not equals', '>': 'Greater than', '<': 'Less than', '>=': 'Greater than or equal', '<=': 'Less than or equal', '~': 'Contains/matches pattern', 'contains': 'Contains substring', 'startswith': 'Starts with string', 'endswith': 'Ends with string', 'in': 'Value in list', 'not_in': 'Value not in list', 'range': 'Value in range [min TO max]' }; /** * Validate operator compatibility with field */ static validateOperator( field: string, operator: string, entityType: EntityType ): OperatorValidation { if (!field || typeof field !== 'string') { return { isValid: false, error: 'Field name is required for operator validation' }; } if (!operator || typeof operator !== 'string') { return { isValid: false, error: 'Operator is required for validation' }; } const fieldType = this.getFieldType(field, entityType); if (!fieldType) { return { isValid: false, error: `Unknown field '${field}' for entity type '${entityType}'`, suggestion: `Check field name spelling or refer to ${entityType} field documentation` }; } const normalizedOperator = this.normalizeOperator(operator); if (!normalizedOperator) { return { isValid: false, error: `Unknown operator '${operator}'`, suggestion: 'Valid operators include: :, =, !=, >, <, >=, <=, ~, contains, range', validOperators: this.getAllValidOperators(), fieldType }; } const compatibleOperators = this.OPERATOR_COMPATIBILITY[fieldType]; if (!compatibleOperators || !compatibleOperators.includes(normalizedOperator)) { return { isValid: false, error: this.createCompatibilityError(field, operator, fieldType), suggestion: this.suggestAlternativeOperators(fieldType), validOperators: compatibleOperators, fieldType }; } return { isValid: true, fieldType }; } /** * Get field type for validation */ private static getFieldType(field: string, entityType: EntityType): FieldType | null { const validFields = SEARCH_FIELDS[entityType]; if (!validFields || !validFields.includes(field)) { return null; } return this.FIELD_TYPE_MAP[field] || 'string'; } /** * Normalize operator to standard form */ private static normalizeOperator(operator: string): QueryOperator | null { const normalized = operator.trim().toLowerCase(); // Direct mappings const operatorMap: Record<string, QueryOperator> = { ':': ':', '=': '=', '!=': '!=', '<>': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=', '~': '~', 'contains': 'contains', 'like': 'contains', 'startswith': 'startswith', 'starts_with': 'startswith', 'endswith': 'endswith', 'ends_with': 'endswith', 'in': 'in', 'not_in': 'not_in', 'not in': 'not_in', 'range': 'range' }; return operatorMap[normalized] || null; } /** * Create detailed compatibility error message */ private static createCompatibilityError( field: string, operator: string, fieldType: FieldType ): string { switch (fieldType) { case 'string': if (['>', '<', '>=', '<='].includes(operator)) { return `Comparison operator '${operator}' cannot be used with string field '${field}'. Use equality operators (:, =, !=) or pattern matching (~, contains) instead.`; } break; case 'boolean': if (operator !== ':' && operator !== '=') { return `Operator '${operator}' is not valid for boolean field '${field}'. Use exact values only (field:true or field:false).`; } break; case 'enum': if (['>', '<', '>=', '<=', '~'].includes(operator)) { return `Operator '${operator}' is not valid for enum field '${field}'. Use exact values (:, =) or list matching (in, not_in) instead.`; } break; case 'number': if (['~', 'contains', 'startswith', 'endswith'].includes(operator)) { return `Text operator '${operator}' cannot be used with numeric field '${field}'. Use comparison operators (>, <, >=, <=) or equality (:, =) instead.`; } break; case 'timestamp': if (['~', 'contains', 'startswith', 'endswith'].includes(operator)) { return `Text operator '${operator}' cannot be used with timestamp field '${field}'. Use comparison operators for date ranges or exact matching (:, =).`; } break; case 'array': if (['>', '<', '>=', '<=', '~', 'startswith', 'endswith'].includes(operator)) { return `Operator '${operator}' is not valid for array field '${field}'. Use contains or in operators for array matching.`; } break; case 'ip': if (['startswith', 'endswith'].includes(operator)) { return `Text operator '${operator}' cannot be used with IP field '${field}'. Use CIDR notation, equality (:, =), or pattern matching (~) instead.`; } break; default: return `Operator '${operator}' is not compatible with field '${field}' of type ${fieldType}.`; } return `Operator '${operator}' is not compatible with field '${field}' of type ${fieldType}.`; } /** * Suggest alternative operators for field type */ private static suggestAlternativeOperators(fieldType: FieldType): string { const operators = this.OPERATOR_COMPATIBILITY[fieldType]; if (!operators || operators.length === 0) { return 'No valid operators available for this field type'; } const examples = operators.slice(0, 4).map(op => { const desc = this.OPERATOR_DESCRIPTIONS[op]; return `${op} (${desc})`; }).join(', '); return `Try using: ${examples}${operators.length > 4 ? '...' : ''}`; } /** * Get all valid operators */ private static getAllValidOperators(): QueryOperator[] { const allOperators = new Set<QueryOperator>(); Object.values(this.OPERATOR_COMPATIBILITY).forEach(operators => { operators.forEach(op => allOperators.add(op)); }); return Array.from(allOperators); } /** * Get detailed operator information */ static getOperatorInfo(operator: string): { description: string; examples: string[]; compatibleTypes: FieldType[]; } | null { const normalizedOperator = this.normalizeOperator(operator); if (!normalizedOperator) { return null; } const description = this.OPERATOR_DESCRIPTIONS[normalizedOperator]; const compatibleTypes: FieldType[] = []; Object.entries(this.OPERATOR_COMPATIBILITY).forEach(([type, operators]) => { if (operators.includes(normalizedOperator)) { compatibleTypes.push(type as FieldType); } }); const examples = this.generateOperatorExamples(normalizedOperator); return { description, examples, compatibleTypes }; } /** * Generate usage examples for operator */ private static generateOperatorExamples(operator: QueryOperator): string[] { const examples: Record<QueryOperator, string[]> = { ':': ['protocol:tcp', 'severity:high', 'blocked:true'], '=': ['protocol=tcp', 'severity=high', 'blocked=true'], '!=': ['protocol!=udp', 'severity!=low', 'blocked!=false'], '>': ['bytes>1000000', 'hit_count>10', 'session_duration>300'], '<': ['bytes<50000', 'hit_count<5', 'session_duration<60'], '>=': ['bytes>=1000000', 'hit_count>=10', 'severity>=medium'], '<=': ['bytes<=50000', 'hit_count<=5', 'severity<=medium'], '~': ['description~"suspicious"', 'target_value~"*.com"', 'source_ip~"192.168.*"'], 'contains': ['description:contains:"malware"', 'user_agent:contains:"Chrome"'], 'startswith': ['target_value:startswith:"www."', 'name:startswith:"dev"'], 'endswith': ['target_value:endswith:".com"', 'name:endswith:"_backup"'], 'in': ['severity:in:["high","critical"]', 'protocol:in:["tcp","udp"]'], 'not_in': ['action:not_in:["allow","permit"]', 'type:not_in:["info"]'], 'range': ['bytes:[1000 TO 50000]', 'timestamp:[2024-01-01 TO 2024-01-31]'] }; return examples[operator] || []; } /** * Validate complex operator expressions */ static validateComplexExpression( expression: string, entityType: EntityType ): { isValid: boolean; errors: string[]; suggestions: string[]; } { const errors: string[] = []; const suggestions: string[] = []; // Extract field:operator:value patterns const fieldOperatorPattern = /(\w+)\s*([!<>=~:]+)\s*([^)\s]+)/g; let match; while ((match = fieldOperatorPattern.exec(expression)) !== null) { const [, field, operator, value] = match; const validation = this.validateOperator(field, operator, entityType); if (!validation.isValid && validation.error) { errors.push(validation.error); if (validation.suggestion) { suggestions.push(validation.suggestion); } } // Additional value validation based on field type if (validation.fieldType) { const valueValidation = this.validateOperatorValue(value, operator, validation.fieldType); if (!valueValidation.isValid) { errors.push(valueValidation.error || 'Invalid value for operator'); if (valueValidation.suggestion) { suggestions.push(valueValidation.suggestion); } } } } return { isValid: errors.length === 0, errors, suggestions }; } /** * Validate operator value compatibility */ private static validateOperatorValue( value: string, operator: string, fieldType: FieldType ): { isValid: boolean; error?: string; suggestion?: string } { // Remove quotes if present const cleanValue = value.replace(/^["']|["']$/g, ''); switch (fieldType) { case 'number': if (['>', '<', '>=', '<='].includes(operator)) { if (isNaN(Number(cleanValue))) { return { isValid: false, error: `Numeric comparison requires a valid number, got '${cleanValue}'`, suggestion: `Use a numeric value like: bytes>${operator === '>' ? '1000000' : '50000'}` }; } } break; case 'boolean': if (!['true', 'false'].includes(cleanValue.toLowerCase())) { return { isValid: false, error: `Boolean field requires 'true' or 'false', got '${cleanValue}'`, suggestion: 'Use: field:true or field:false' }; } break; case 'timestamp': if (['>', '<', '>=', '<='].includes(operator)) { // Basic timestamp format validation if (!/^\d{4}-\d{2}-\d{2}/.test(cleanValue)) { return { isValid: false, error: `Timestamp comparison requires date format, got '${cleanValue}'`, suggestion: 'Use format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS' }; } } break; case 'string': // String fields are generally flexible with values break; case 'ip': if (operator === ':' || operator === '=') { // Basic IP validation for exact matches if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.test(cleanValue) && !cleanValue.includes('*') && !cleanValue.includes('/')) { return { isValid: false, error: `IP field requires valid IP address or pattern, got '${cleanValue}'`, suggestion: 'Use: 192.168.1.1, 192.168.*, or 192.168.1.0/24' }; } } break; case 'array': // Array fields are generally flexible with values break; case 'enum': // Enum validation would require field-specific allowed values break; } return { isValid: true }; } }

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