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
267 lines • 9.37 kB
JavaScript
/**
* Firewalla-specific query syntax validation
* Validates query syntax and provides helpful error messages
*/
/**
* Firewalla query syntax patterns
*/
const FIELD_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
const OPERATOR_PATTERN = /^(:|=|!=|>|<|>=|<=)$/;
const LOGICAL_OPERATORS = ['AND', 'OR', 'NOT'];
/**
* Tokenize a Firewalla query string
*/
function tokenizeQuery(query) {
const tokens = [];
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) {
if (!query || typeof query !== 'string') {
return {
isValid: true,
errors: [],
sanitizedValue: '',
};
}
const trimmedQuery = query.trim();
if (!trimmedQuery) {
return {
isValid: true,
errors: [],
sanitizedValue: '',
};
}
const errors = [];
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) {
const examples = {
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] || [];
}
//# sourceMappingURL=query-validator.js.map