Skip to main content
Glama

firewalla-mcp-server

query-validator.ts8.52 kB
/** * Firewalla-specific query syntax validation * Validates query syntax and provides helpful error messages */ import type { ValidationResult } from '../types.js'; /** * Firewalla query syntax patterns */ const FIELD_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_.]*$/; const OPERATOR_PATTERN = /^(:|=|!=|>|<|>=|<=)$/; const LOGICAL_OPERATORS = ['AND', 'OR', 'NOT']; interface QueryToken { type: 'field' | 'operator' | 'value' | 'logical' | 'parenthesis'; value: string; position: number; } /** * Tokenize a Firewalla query string */ function tokenizeQuery(query: string): QueryToken[] { const tokens: QueryToken[] = []; let current = 0; while (current < query.length) { // Skip whitespace if (/\s/.test(query[current])) { current++; continue; } // Check for parentheses if (query[current] === '(' || query[current] === ')') { tokens.push({ type: 'parenthesis', value: query[current], position: current, }); current++; continue; } // Check for quoted strings if (query[current] === '"' || query[current] === "'") { const quote = query[current]; let value = ''; current++; // Skip opening quote while (current < query.length && query[current] !== quote) { if (query[current] === '\\' && current + 1 < query.length) { // Handle escaped characters current++; } value += query[current]; current++; } if (current >= query.length) { // Unclosed quote tokens.push({ type: 'value', value: quote + value, position: current - value.length - 1, }); } else { current++; // Skip closing quote tokens.push({ type: 'value', value, position: current - value.length - 2, }); } continue; } // Check for operators let operator = ''; const operatorStart = current; while (current < query.length && /[:<>=!]/.test(query[current])) { operator += query[current]; current++; } if (operator && OPERATOR_PATTERN.test(operator)) { tokens.push({ type: 'operator', value: operator, position: operatorStart, }); continue; } else if (operator) { // Invalid operator, treat as value tokens.push({ type: 'value', value: operator, position: operatorStart, }); continue; } // Read word (field, logical operator, or value) let word = ''; const wordStart = current; while (current < query.length && !/[\s():<>=!]/.test(query[current])) { word += query[current]; current++; } if (LOGICAL_OPERATORS.includes(word.toUpperCase())) { tokens.push({ type: 'logical', value: word.toUpperCase(), position: wordStart, }); } else if ( tokens.length === 0 || tokens[tokens.length - 1].type === 'logical' || tokens[tokens.length - 1].value === '(' ) { // This should be a field name tokens.push({ type: 'field', value: word, position: wordStart, }); } else { // This is a value tokens.push({ type: 'value', value: word, position: wordStart, }); } } return tokens; } /** * Validate Firewalla query syntax */ export function validateFirewallaQuerySyntax(query: string): ValidationResult { if (!query || typeof query !== 'string') { return { isValid: true, errors: [], sanitizedValue: '', }; } const trimmedQuery = query.trim(); if (!trimmedQuery) { return { isValid: true, errors: [], sanitizedValue: '', }; } const errors: string[] = []; const tokens = tokenizeQuery(trimmedQuery); // Check for balanced parentheses let parenCount = 0; for (const token of tokens) { if (token.value === '(') { parenCount++; } if (token.value === ')') { parenCount--; } if (parenCount < 0) { errors.push( `Unmatched closing parenthesis at position ${token.position}` ); } } if (parenCount > 0) { errors.push(`Unclosed parenthesis in query`); } // Validate token sequence for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; const nextToken = tokens[i + 1]; const prevToken = tokens[i - 1]; switch (token.type) { case 'field': // Validate field name format if (!FIELD_PATTERN.test(token.value)) { errors.push( `Invalid field name '${token.value}' at position ${token.position}. Field names must start with a letter and contain only letters, numbers, underscores, and dots.` ); } // Field must be followed by operator if (nextToken && nextToken.type !== 'operator') { errors.push( `Field '${token.value}' at position ${token.position} must be followed by an operator (: = != > < >= <=)` ); } break; case 'operator': // Operator must be between field and value if (!prevToken || prevToken.type !== 'field') { errors.push( `Operator '${token.value}' at position ${token.position} must be preceded by a field name` ); } if ( !nextToken || (nextToken.type !== 'value' && nextToken.value !== '(') ) { errors.push( `Operator '${token.value}' at position ${token.position} must be followed by a value` ); } break; case 'value': // Value must follow operator if (!prevToken || prevToken.type !== 'operator') { errors.push( `Value '${token.value}' at position ${token.position} must be preceded by an operator` ); } // Check for common syntax errors if (token.value.includes('*') && !token.value.match(/^[*\w.-]+$/)) { errors.push( `Invalid wildcard pattern '${token.value}' at position ${token.position}` ); } break; case 'logical': // Logical operators must be between complete expressions if (i === 0 || i === tokens.length - 1) { errors.push( `Logical operator '${token.value}' at position ${token.position} cannot be at the beginning or end of query` ); } break; case 'parenthesis': // Parentheses are handled in the balanced parentheses check above break; } } // Check for empty parentheses for (let i = 0; i < tokens.length - 1; i++) { if (tokens[i].value === '(' && tokens[i + 1].value === ')') { errors.push(`Empty parentheses at position ${tokens[i].position}`); } } // Provide helpful suggestions for common mistakes if ( trimmedQuery.includes('@') || trimmedQuery.includes('#') || trimmedQuery.includes('$') ) { errors.push( `Query contains invalid special characters. Use field:value syntax (e.g., severity:high, source_ip:192.168.*)` ); } return { isValid: errors.length === 0, errors, sanitizedValue: trimmedQuery, }; } /** * Get example queries for a specific entity type */ export function getExampleQueries(entityType: string): string[] { const examples: Record<string, string[]> = { flows: [ 'protocol:tcp AND blocked:true', 'region:US AND bytes:>1000000', 'domain:*.facebook.com', 'category:social OR category:games', 'source_ip:192.168.1.* AND direction:outbound', ], alarms: [ 'severity:high AND status:1', 'region:CN AND type:1', 'source_ip:192.168.* AND NOT resolved:true', 'message:"suspicious activity"', 'device.name:*laptop* AND severity:>=medium', ], rules: [ 'action:block AND target.value:*.social.com', 'status:paused', 'target.type:domain AND action:block', 'scope.type:device AND protocol:tcp', 'notes:"temporary rule"', ], devices: [ 'online:false AND vendor:Apple', 'ip:192.168.1.* AND name:*phone*', 'mac:AA:BB:*', 'network.name:"Guest Network"', 'online:true AND group.name:*kids*', ], target_lists: [ 'category:social', 'owner:global AND name:*Block*', 'targets:*.gaming.com', 'notes:"custom blocklist"', ], }; return examples[entityType] || []; }

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