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

246 lines 8.73 kB
/** * Filter Factory and Registry * Centralized management of all search filters */ import { TimeRangeFilter } from './time.js'; /** * Determines whether a query node is a field query. * * @param node - The query node to check * @returns True if the node is of type 'field' and contains both 'field' and 'value' properties. */ function isFieldQuery(node) { return node.type === 'field' && 'field' in node && 'value' in node; } /** * Determines whether a query node is a wildcard query. * * @param node - The query node to check * @returns True if the node is of type 'wildcard' and contains both 'field' and 'pattern' properties. */ function isWildcardQuery(node) { return node.type === 'wildcard' && 'field' in node && 'pattern' in node; } // IP address filtering with proper validation and subnet matching class IpAddressFilter { constructor() { this.name = 'ip_address'; } canHandle(node) { if (isFieldQuery(node) || isWildcardQuery(node)) { return ['source_ip', 'destination_ip', 'ip', 'device_ip'].includes(node.field); } return false; } apply(node, _context) { // Enhanced IP filtering with proper validation if (isWildcardQuery(node)) { return { apiParams: {}, postProcessing: (items) => items.filter(item => { const value = this.getNestedValue(item, node.field); const ipString = String(value || ''); // Validate IP address format first if (!this.isValidIpAddress(ipString)) { return false; } return this.matchWildcardIp(ipString, node.pattern); }), cacheKeyComponent: `${this.name}:${JSON.stringify(node)}`, }; } if (isFieldQuery(node)) { return { apiParams: {}, postProcessing: (items) => items.filter(item => { const value = this.getNestedValue(item, node.field); const ipString = String(value || ''); const queryString = String(node.value); // Handle CIDR notation for exact matching if (queryString.includes('/')) { return this.matchCidr(ipString, queryString); } // Validate both IPs for exact match if (!this.isValidIpAddress(ipString) || !this.isValidIpAddress(queryString)) { return false; } return ipString === queryString; }), cacheKeyComponent: `${this.name}:${JSON.stringify(node)}`, }; } return { apiParams: {} }; } getNestedValue(obj, path) { return path.split('.').reduce((current, key) => current?.[key], obj); } /** * Validates if a string is a valid IPv4 or IPv6 address */ isValidIpAddress(ip) { if (!ip || typeof ip !== 'string') { return false; } // IPv4 validation const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; if (ipv4Regex.test(ip)) { const parts = ip.split('.'); return parts.every(part => { const num = parseInt(part, 10); return num >= 0 && num <= 255; }); } // IPv6 validation (basic) const ipv6Regex = /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$/; return ipv6Regex.test(ip); } /** * Enhanced wildcard matching for IP addresses with subnet support */ matchWildcardIp(ip, pattern) { // Handle CIDR notation in pattern if (pattern.includes('/')) { return this.matchCidr(ip, pattern); } // Handle common IP wildcard patterns if (pattern.includes('*')) { // Convert IP wildcard to regex (e.g., 192.168.*.* or 10.0.0.*) const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '\\d{1,3}'); const regex = new RegExp(`^${regexPattern}$`); return regex.test(ip); } return ip === pattern; } /** * CIDR subnet matching */ matchCidr(ip, cidr) { if (!this.isValidIpAddress(ip)) { return false; } const [network, prefixStr] = cidr.split('/'); const prefix = parseInt(prefixStr, 10); if (!this.isValidIpAddress(network) || isNaN(prefix) || prefix < 0 || prefix > 32) { return false; } // Convert IPs to 32-bit integers for comparison const ipInt = this.ipToInt(ip); const networkInt = this.ipToInt(network); const mask = (0xffffffff << (32 - prefix)) >>> 0; return (ipInt & mask) === (networkInt & mask); } /** * Convert IPv4 address to 32-bit integer */ ipToInt(ip) { const parts = ip.split('.').map(part => parseInt(part, 10)); return (((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0); } } class SeverityFilter { constructor() { this.name = 'severity'; } canHandle(node) { return isFieldQuery(node) && node.field === 'severity'; } apply(node, _context) { if (isFieldQuery(node)) { return { apiParams: { severity: node.value }, cacheKeyComponent: `${this.name}:${node.value}`, }; } return { apiParams: {} }; } } class ProtocolFilter { constructor() { this.name = 'protocol'; } canHandle(node) { return isFieldQuery(node) && node.field === 'protocol'; } apply(node, _context) { if (isFieldQuery(node)) { return { apiParams: { protocol: node.value }, cacheKeyComponent: `${this.name}:${node.value}`, }; } return { apiParams: {} }; } } /** * Filter Factory for managing and applying filters */ export class FilterFactory { constructor() { this.filters = [ new TimeRangeFilter(), new IpAddressFilter(), new SeverityFilter(), new ProtocolFilter(), ]; } /** * Apply all relevant filters to a query node */ applyFilters(node, context) { const result = { apiParams: {}, postProcessing: undefined, cacheKeyComponent: '', }; const applicableFilters = this.filters.filter(filter => filter.canHandle(node)); for (const filter of applicableFilters) { const filterResult = filter.apply(node, context); // Merge API parameters Object.assign(result.apiParams, filterResult.apiParams); // Combine post-processing functions with debugging support if (filterResult.postProcessing) { const existingPostProcessing = result.postProcessing; if (existingPostProcessing) { result.postProcessing = (items) => { if (context.debug) { process.stderr.write(`Applying ${filter.name} after existing filters\n`); } const intermediate = existingPostProcessing(items); const final = filterResult.postProcessing(intermediate); if (context.debug) { process.stderr.write(`${filter.name}: ${items.length} → ${intermediate.length} → ${final.length} items\n`); } return final; }; } else { result.postProcessing = filterResult.postProcessing; } } // Combine cache keys with unique separator to avoid conflicts // Using '::' as separator instead of '|' to prevent conflicts with // field values that might contain pipe characters (e.g., regex patterns) if (filterResult.cacheKeyComponent) { result.cacheKeyComponent += (result.cacheKeyComponent ? '::' : '') + filterResult.cacheKeyComponent; } } return result; } /** * Register a new filter */ registerFilter(filter) { this.filters.push(filter); } } // Export singleton instance export const filterFactory = new FilterFactory(); //# sourceMappingURL=index.js.map