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
457 lines • 17.7 kB
JavaScript
/**
* Progressive Validator for Firewalla MCP Server
* Provides step-by-step validation guidance for complex queries
*/
import { EnhancedQueryValidator } from './enhanced-query-validator.js';
import { FieldValidator } from './field-validator.js';
import { OperatorValidator } from './operator-validator.js';
import { ErrorFormatter } from './error-formatter.js';
export class ProgressiveValidator {
/**
* Validate query with progressive guidance
*/
static validateWithProgression(query, entityType) {
const results = {
basicSyntax: this.validateBasicSyntax(query),
fieldExistence: this.validateFields(query, entityType),
operatorCompatibility: this.validateOperators(query, entityType),
semanticCorrectness: this.validateSemantics(query, entityType),
performanceOptimization: this.validatePerformance(query, entityType)
};
// Find first critical failure
const firstFailure = this.findFirstCriticalFailure(results);
if (firstFailure) {
const errorReport = this.createStepErrorReport(firstFailure, results);
return {
isValid: false,
currentStep: firstFailure.step,
error: errorReport,
nextSteps: this.getNextSteps(firstFailure.step),
progress: this.calculateProgress(results),
allValidations: results,
overallGuidance: this.generateOverallGuidance(firstFailure.step, results)
};
}
// Check for non-critical issues
const hasWarnings = Object.values(results).some(r => r.warnings.length > 0);
const progress = this.calculateProgress(results);
return {
isValid: true,
progress,
allValidations: results,
overallGuidance: hasWarnings
? this.generateOptimizationGuidance(results)
: 'Query is valid and optimized!'
};
}
/**
* Validate basic syntax
*/
static validateBasicSyntax(query) {
const validator = new EnhancedQueryValidator();
const result = validator.validateQuery(query);
const syntaxErrors = result.detailedErrors.filter(e => e.errorType === 'syntax');
return {
step: 'basicSyntax',
isValid: syntaxErrors.length === 0,
errors: syntaxErrors.map(e => e.message),
warnings: [],
suggestions: syntaxErrors.map(e => e.suggestion).filter(Boolean),
progress: syntaxErrors.length === 0 ? 100 : 0
};
}
/**
* Validate field existence and compatibility
*/
static validateFields(query, entityType) {
const errors = [];
const warnings = [];
const suggestions = [];
// Extract field names from query
const fieldPattern = /(\w+)\s*[:=]/g;
const fields = new Set();
let match;
while ((match = fieldPattern.exec(query)) !== null) {
fields.add(match[1]);
}
let validFields = 0;
const totalFields = fields.size;
fields.forEach(field => {
const validation = FieldValidator.validateField(field, entityType);
if (validation.isValid) {
validFields++;
}
else {
errors.push(validation.error || `Invalid field: ${field}`);
if (validation.suggestion) {
suggestions.push(validation.suggestion);
}
}
});
const progress = totalFields > 0 ? Math.round((validFields / totalFields) * 100) : 100;
return {
step: 'fieldExistence',
isValid: errors.length === 0,
errors,
warnings,
suggestions,
progress
};
}
/**
* Validate operator compatibility
*/
static validateOperators(query, entityType) {
const errors = [];
const warnings = [];
const suggestions = [];
// Extract field:operator patterns - updated to handle >= and other operators
const operatorPattern = /(\w+)\s*([!<>=:~]+|contains|startswith|endswith)/g;
const operators = new Map();
let match;
while ((match = operatorPattern.exec(query)) !== null) {
operators.set(match[1], match[2]);
}
let validOperators = 0;
const totalOperators = operators.size;
operators.forEach((operator, field) => {
const validation = OperatorValidator.validateOperator(field, operator, entityType);
if (validation.isValid) {
validOperators++;
}
else {
errors.push(validation.error || `Invalid operator: ${operator} for field ${field}`);
if (validation.suggestion) {
suggestions.push(validation.suggestion);
}
}
});
const progress = totalOperators > 0 ? Math.round((validOperators / totalOperators) * 100) : 100;
return {
step: 'operatorCompatibility',
isValid: errors.length === 0,
errors,
warnings,
suggestions,
progress
};
}
/**
* Validate semantic correctness
*/
static validateSemantics(query, entityType) {
const errors = [];
const warnings = [];
const suggestions = [];
// Check for contradictory conditions
const contradictions = this.findContradictions(query);
errors.push(...contradictions);
// Check for redundant conditions
const redundancies = this.findRedundancies(query);
warnings.push(...redundancies);
// Check for impossible ranges
const impossibleRanges = this.findImpossibleRanges(query);
errors.push(...impossibleRanges);
// Generate semantic suggestions
if (errors.length === 0 && warnings.length === 0) {
const optimizations = this.suggestSemanticOptimizations(query, entityType);
suggestions.push(...optimizations);
}
return {
step: 'semanticCorrectness',
isValid: errors.length === 0,
errors,
warnings,
suggestions,
progress: errors.length === 0 ? 100 : Math.max(0, 100 - (errors.length * 25))
};
}
/**
* Validate performance characteristics
*/
static validatePerformance(query, entityType) {
const warnings = [];
const suggestions = [];
// Check query complexity
const complexity = this.calculateQueryComplexity(query);
if (complexity > 10) {
warnings.push('Query complexity is high, consider simplifying');
suggestions.push('Break complex queries into smaller parts or use more specific filters');
}
// Check for inefficient patterns
if (query.includes('*') && query.split('*').length > 3) {
warnings.push('Multiple wildcards may impact performance');
suggestions.push('Use more specific patterns instead of multiple wildcards');
}
// Check for missing limit considerations
if (!query.includes('limit') && entityType === 'flows') {
suggestions.push('Consider adding a limit parameter for better performance with flow queries');
}
return {
step: 'performanceOptimization',
isValid: true, // Performance issues are not critical
errors: [],
warnings,
suggestions,
progress: warnings.length === 0 ? 100 : Math.max(70, 100 - (warnings.length * 10))
};
}
/**
* Find first critical failure
*/
static findFirstCriticalFailure(results) {
// Check all steps in order, but only fail on critical ones
const orderedSteps = [
'basicSyntax',
'fieldExistence',
'operatorCompatibility',
'semanticCorrectness',
'performanceOptimization'
];
for (const step of orderedSteps) {
const result = results[step];
if (!result.isValid && this.STEP_CONFIGS[step].critical) {
return result;
}
}
return null;
}
/**
* Calculate overall progress
*/
static calculateProgress(results) {
let totalProgress = 0;
let totalWeight = 0;
Object.entries(results).forEach(([step, result]) => {
const config = this.STEP_CONFIGS[step];
const stepProgress = result.progress / 100;
totalProgress += stepProgress * config.weight;
totalWeight += config.weight;
});
return Math.round((totalProgress / totalWeight) * 100);
}
/**
* Get next steps guidance
*/
static getNextSteps(failedStep) {
switch (failedStep) {
case 'basicSyntax':
return [
'Fix syntax errors first before proceeding',
'Check parentheses and quotes are properly matched',
'Verify operator placement and field:value syntax',
'Review query syntax documentation'
];
case 'fieldExistence':
return [
'Correct field names to match entity schema',
'Check field spelling and capitalization',
'Refer to field documentation for valid options',
'Consider field aliases if using common abbreviations'
];
case 'operatorCompatibility':
return [
'Use appropriate operators for each field type',
'Check operator documentation for compatibility',
'Consider alternative operators that match field types',
'Review examples of correct operator usage'
];
case 'semanticCorrectness':
return [
'Resolve logical contradictions in query conditions',
'Remove redundant or conflicting filters',
'Check date ranges and numeric comparisons',
'Simplify complex logical expressions'
];
case 'performanceOptimization':
return [
'Consider query complexity and performance impact',
'Use more specific filters to reduce result sets',
'Avoid excessive wildcard usage',
'Add appropriate limits for large result sets'
];
default:
return ['Review query documentation for guidance'];
}
}
/**
* Generate overall guidance message
*/
static generateOverallGuidance(failedStep, results) {
const config = this.STEP_CONFIGS[failedStep];
const progress = this.calculateProgress(results);
let guidance = `Query validation failed at step: ${config.title}. `;
guidance += `Overall progress: ${progress}%. `;
guidance += `Focus on: ${config.description}`;
return guidance;
}
/**
* Generate optimization guidance for valid queries
*/
static generateOptimizationGuidance(results) {
const warnings = Object.values(results).flatMap(r => r.warnings);
const suggestions = Object.values(results).flatMap(r => r.suggestions);
if (warnings.length > 0) {
return `Query is valid but has ${warnings.length} optimization opportunities. ${suggestions[0] || 'Consider reviewing performance suggestions.'}`;
}
return 'Query is valid and well-optimized!';
}
/**
* Create error report for failed step
*/
static createStepErrorReport(failedStep, _allResults) {
const detailedErrors = failedStep.errors.map(error => ({
message: error,
errorType: this.getErrorTypeForStep(failedStep.step),
suggestion: failedStep.suggestions[0]
}));
return ErrorFormatter.formatMultipleErrors(detailedErrors);
}
/**
* Get error type for validation step
*/
static getErrorTypeForStep(step) {
switch (step) {
case 'basicSyntax':
return 'syntax';
case 'fieldExistence':
return 'field';
case 'operatorCompatibility':
return 'operator';
case 'semanticCorrectness':
return 'semantic';
case 'performanceOptimization':
return 'semantic'; // Performance optimizations are semantic-level suggestions
default:
return 'syntax';
}
}
/**
* Find contradictory conditions in query
*/
static findContradictions(query) {
const contradictions = [];
// Check for field:true AND field:false patterns
const booleanFields = ['blocked', 'online'];
booleanFields.forEach(field => {
if (query.includes(`${field}:true`) && query.includes(`${field}:false`)) {
contradictions.push(`Contradictory condition: ${field} cannot be both true and false`);
}
});
// Check for mutually exclusive protocol values
const protocolPattern = /protocol:(\w+)/g;
const protocols = [];
let match;
while ((match = protocolPattern.exec(query)) !== null) {
protocols.push(match[1].toLowerCase());
}
if (protocols.length > 1) {
const uniqueProtocols = [...new Set(protocols)];
if (uniqueProtocols.length > 1) {
contradictions.push(`Contradictory condition: protocol cannot be multiple values simultaneously (${uniqueProtocols.join(', ')})`);
}
}
return contradictions;
}
/**
* Find redundant conditions
*/
static findRedundancies(query) {
const redundancies = [];
// Check for duplicate field conditions
const fieldPattern = /(\w+):[^)\s]+/g;
const fieldCounts = new Map();
let match;
while ((match = fieldPattern.exec(query)) !== null) {
const field = match[1];
fieldCounts.set(field, (fieldCounts.get(field) || 0) + 1);
}
fieldCounts.forEach((count, field) => {
if (count > 1) {
redundancies.push(`Field '${field}' appears ${count} times - consider combining conditions`);
}
});
return redundancies;
}
/**
* Find impossible range conditions
*/
static findImpossibleRanges(query) {
const impossible = [];
// Check for impossible numeric ranges
const rangePattern = /(\w+):\[(\d+)\s+TO\s+(\d+)\]/g;
let match;
while ((match = rangePattern.exec(query)) !== null) {
const [, field, min, max] = match;
if (parseInt(min) > parseInt(max)) {
impossible.push(`Impossible range for ${field}: minimum (${min}) is greater than maximum (${max})`);
}
}
return impossible;
}
/**
* Suggest semantic optimizations
*/
static suggestSemanticOptimizations(query, entityType) {
const suggestions = [];
// Suggest more specific filters
if (query.length < 20 && !query.includes('AND') && !query.includes('OR')) {
suggestions.push('Consider adding additional filters for more specific results');
}
// Suggest field combinations
if (entityType === 'flows' && query.includes('protocol:tcp') && !query.includes('blocked')) {
suggestions.push('Consider adding blocked:true/false filter for TCP flows');
}
return suggestions;
}
/**
* Calculate query complexity score
*/
static calculateQueryComplexity(query) {
let complexity = 0;
// Count logical operators
complexity += (query.match(/\b(AND|OR|NOT)\b/gi) || []).length * 2;
// Count field conditions
complexity += (query.match(/\w+\s*[:=]/g) || []).length;
// Count wildcards
complexity += (query.match(/\*/g) || []).length * 1.5;
// Count parentheses groups
complexity += (query.match(/\(/g) || []).length;
// Count range queries
complexity += (query.match(/\[.*TO.*\]/g) || []).length * 2;
return complexity;
}
}
ProgressiveValidator.STEP_CONFIGS = {
basicSyntax: {
title: 'Basic Syntax Check',
description: 'Validates parentheses, quotes, and basic query structure',
weight: 0.3,
critical: true
},
fieldExistence: {
title: 'Field Validation',
description: 'Checks that all field names are valid for the entity type',
weight: 0.25,
critical: true
},
operatorCompatibility: {
title: 'Operator Compatibility',
description: 'Validates operators are compatible with field types',
weight: 0.25,
critical: true
},
semanticCorrectness: {
title: 'Semantic Analysis',
description: 'Checks for logical consistency and conflicts',
weight: 0.15,
critical: true
},
performanceOptimization: {
title: 'Performance Check',
description: 'Suggests optimizations for better query performance',
weight: 0.05,
critical: false
}
};
//# sourceMappingURL=progressive-validator.js.map