UNPKG

firewalla-mcp-server

Version:

Model Context Protocol (MCP) server for Firewalla MSP API - Provides real-time network monitoring, security analysis, and firewall management through 28 specialized tools compatible with any MCP client

540 lines 17.5 kB
/** * Advanced Query Parser for Firewalla Search API * Implements recursive descent parser for complex search queries */ import { TokenType, SEARCH_FIELDS, } from './types.js'; export class QueryParser { constructor() { this.tokens = []; this.current = 0; this.errors = []; } /** * Parse a search query string into an AST */ parse(query, entityType) { this.reset(); // Input validation if (!query || typeof query !== 'string') { this.errors.push('Query must be a non-empty string'); return { isValid: false, errors: this.errors, warnings: [], suggestions: [], ast: undefined, }; } try { this.tokens = this.tokenize(query); const ast = this.parseExpression(); // Validate fields if entity type is provided if (entityType && ast) { this.validateFields(ast, entityType); } return { isValid: this.errors.length === 0, errors: this.errors, warnings: [], suggestions: this.generateSuggestions(query, entityType), ast: this.errors.length === 0 ? ast : undefined, }; } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown parsing error'; this.errors.push(errorMsg); return { isValid: false, errors: this.errors, warnings: [], suggestions: this.generateSuggestions(query, entityType), }; } } reset() { this.tokens = []; this.current = 0; this.errors = []; } /** * Tokenize the input query string */ tokenize(input) { const tokens = []; let i = 0; // Additional safety check if (!input || typeof input !== 'string') { return tokens; } const safeInput = input.trim(); if (!safeInput) { return tokens; } while (i < safeInput.length) { const char = safeInput[i]; // Skip whitespace if (/\s/.test(char)) { i++; continue; } // Parentheses if (char === '(') { tokens.push({ type: TokenType.LPAREN, value: char, position: i, length: 1, }); i++; continue; } if (char === ')') { tokens.push({ type: TokenType.RPAREN, value: char, position: i, length: 1, }); i++; continue; } // Brackets for ranges if (char === '[') { tokens.push({ type: TokenType.LBRACKET, value: char, position: i, length: 1, }); i++; continue; } if (char === ']') { tokens.push({ type: TokenType.RBRACKET, value: char, position: i, length: 1, }); i++; continue; } // Colon for field:value if (char === ':') { tokens.push({ type: TokenType.COLON, value: char, position: i, length: 1, }); i++; continue; } // Quoted strings if (char === '"' || char === "'") { const quote = char; let value = ''; i++; // Skip opening quote const start = i - 1; while (i < safeInput.length && safeInput[i] !== quote) { if (safeInput[i] === '\\' && i + 1 < safeInput.length) { // Handle escaped characters i++; value += safeInput[i]; } else { value += safeInput[i]; } i++; } if (i >= safeInput.length) { throw new Error(`Unclosed quoted string starting at position ${start}`); } i++; // Skip closing quote tokens.push({ type: TokenType.QUOTED_VALUE, value, position: start, length: i - start, }); continue; } // Operators if (char === '>' || char === '<') { let operator = char; i++; if (i < safeInput.length && safeInput[i] === '=') { operator += '='; i++; } tokens.push({ type: TokenType.OPERATOR, value: operator, position: i - operator.length, length: operator.length, }); continue; } if (char === '!' && i + 1 < safeInput.length && safeInput[i + 1] === '=') { tokens.push({ type: TokenType.OPERATOR, value: '!=', position: i, length: 2, }); i += 2; continue; } // Words (fields, values, logical operators) if (/[a-zA-Z_]/.test(char)) { let word = ''; const start = i; while (i < safeInput.length && /[a-zA-Z0-9_.-]/.test(safeInput[i])) { word += safeInput[i]; i++; } const upperWord = word.toUpperCase(); if (upperWord === 'AND' || upperWord === 'OR' || upperWord === 'NOT') { tokens.push({ type: TokenType.LOGICAL, value: upperWord, position: start, length: word.length, }); } else if (upperWord === 'TO') { tokens.push({ type: TokenType.TO, value: upperWord, position: start, length: word.length, }); } else { tokens.push({ type: TokenType.FIELD, value: word, position: start, length: word.length, }); } continue; } // Numbers and values with wildcards if (/[0-9*?]/.test(char) || char === '.') { let value = ''; const start = i; let hasWildcard = false; while (i < safeInput.length && /[0-9*?.-]/.test(safeInput[i])) { if (safeInput[i] === '*' || safeInput[i] === '?') { hasWildcard = true; } value += safeInput[i]; i++; } tokens.push({ type: hasWildcard ? TokenType.WILDCARD : TokenType.VALUE, value, position: start, length: value.length, }); continue; } // Unknown character throw new Error(`Unexpected character '${char}' at position ${i}`); } tokens.push({ type: TokenType.EOF, value: '', position: safeInput.length, length: 0, }); return tokens; } /** * Parse expression with logical operators (lowest precedence) */ parseExpression() { let left = this.parseAndExpression(); while (this.match(TokenType.LOGICAL) && this.previous().value === 'OR') { const right = this.parseAndExpression(); if (!right) { break; } left = { type: 'logical', operator: 'OR', left, right, }; } return left; } /** * Parse AND expressions (higher precedence than OR) */ parseAndExpression() { let left = this.parseNotExpression(); while (this.match(TokenType.LOGICAL) && this.previous().value === 'AND') { const right = this.parseNotExpression(); if (!right) { break; } left = { type: 'logical', operator: 'AND', left, right, }; } return left; } /** * Parse NOT expressions (highest precedence) */ parseNotExpression() { if (this.match(TokenType.LOGICAL) && this.previous().value === 'NOT') { const operand = this.parsePrimary(); if (!operand) { this.errors.push('Expected expression after NOT operator'); return undefined; } return { type: 'logical', operator: 'NOT', operand, }; } return this.parsePrimary(); } /** * Parse primary expressions (field queries, groups, etc.) */ parsePrimary() { // Grouped expression if (this.match(TokenType.LPAREN)) { const expr = this.parseExpression(); if (!this.match(TokenType.RPAREN)) { this.errors.push('Expected closing parenthesis'); return undefined; } return expr ? { type: 'group', query: expr } : undefined; } // Field query if (this.check(TokenType.FIELD)) { return this.parseFieldQuery(); } // Wildcard query (standalone *) - treat as match-all if (this.match(TokenType.WILDCARD) && this.previous().value === '*') { // Return a special match-all query that bypasses field validation return { type: 'field', field: '*', value: '*', }; } this.errors.push(`Unexpected token: ${this.peek().value}`); return undefined; } /** * Parse field-based queries (field:value, field:>value, etc.) */ parseFieldQuery() { const fieldToken = this.advance(); const field = fieldToken.value; if (!this.match(TokenType.COLON)) { this.errors.push(`Expected ':' after field '${field}'`); return undefined; } // Check for operators if (this.match(TokenType.OPERATOR)) { const operator = this.previous().value; if (this.match(TokenType.VALUE, TokenType.QUOTED_VALUE)) { const { value } = this.previous(); if (operator === '>=' || operator === '<=' || operator === '>' || operator === '<') { return { type: 'comparison', field, operator, value: this.parseValue(value), }; } return { type: 'field', field, value, operator: operator, }; } } // Range query [min TO max] if (this.match(TokenType.LBRACKET)) { return this.parseRangeQuery(field); } // Wildcard or regular value if (this.match(TokenType.WILDCARD)) { const pattern = this.previous().value; return { type: 'wildcard', field, pattern, }; } if (this.match(TokenType.VALUE, TokenType.QUOTED_VALUE, TokenType.FIELD)) { const { value } = this.previous(); return { type: 'field', field, value, operator: '=', }; } this.errors.push(`Expected value after field '${field}:'`); return undefined; } /** * Parse range queries [min TO max] */ parseRangeQuery(field) { let min; let max; // Parse minimum value if (this.match(TokenType.VALUE, TokenType.QUOTED_VALUE)) { min = this.parseValue(this.previous().value); } if (!this.match(TokenType.TO)) { this.errors.push('Expected TO in range query'); return undefined; } // Parse maximum value if (this.match(TokenType.VALUE, TokenType.QUOTED_VALUE)) { max = this.parseValue(this.previous().value); } if (!this.match(TokenType.RBRACKET)) { this.errors.push('Expected closing bracket in range query'); return undefined; } return { type: 'range', field, min, max, inclusive: true, }; } /** * Parse and convert values to appropriate types */ parseValue(value) { // Check if value is an integer if (/^-?\d+$/.test(value)) { const int = parseInt(value, 10); if (Number.isSafeInteger(int)) { return int; } } else if (/^-?\d+\.\d+$/.test(value)) { // Handle float values const float = parseFloat(value); if (!isNaN(float) && Number.isFinite(float)) { return float; } } return value; } /** * Validate fields against entity schema */ validateFields(node, entityType) { const validFields = SEARCH_FIELDS[entityType]; const validateNode = (n) => { switch (n.type) { case 'field': case 'wildcard': case 'range': case 'comparison': { const fieldNode = n; // Skip validation for special match-all field '*' if (fieldNode.field !== '*' && !validFields.includes(fieldNode.field)) { this.errors.push(`Invalid field '${fieldNode.field}' for ${entityType}. Valid fields: ${validFields.join(', ')}`); } break; } case 'logical': if (n.left) { validateNode(n.left); } if (n.right) { validateNode(n.right); } if (n.operand) { validateNode(n.operand); } break; case 'group': validateNode(n.query); break; } }; validateNode(node); } /** * Generate helpful suggestions for invalid queries */ generateSuggestions(query, entityType) { const suggestions = []; if (entityType && this.errors.some(e => e.includes('Invalid field'))) { suggestions.push(`Available fields for ${entityType}: ${SEARCH_FIELDS[entityType].join(', ')}`); } if (query.includes('(') && !query.includes(')')) { suggestions.push('Check for missing closing parenthesis'); } if (query.includes('[') && !query.includes(']')) { suggestions.push('Check for missing closing bracket in range query'); } if (query.includes(':') && !query.split(':').every(part => part.trim())) { suggestions.push('Ensure field:value pairs are properly formatted'); } return suggestions; } // Utility methods for token management match(...types) { for (const type of types) { if (this.check(type)) { this.advance(); return true; } } return false; } check(type) { if (this.isAtEnd()) { return false; } return this.peek().type === type; } advance() { if (!this.isAtEnd()) { this.current++; } return this.previous(); } isAtEnd() { return this.peek().type === TokenType.EOF; } peek() { return this.tokens[this.current]; } previous() { return this.tokens[this.current - 1]; } } // Export singleton instance export const queryParser = new QueryParser(); //# sourceMappingURL=parser.js.map