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

948 lines 55.6 kB
/** * Firewall rule management tool handlers */ import { BaseToolHandler } from './base.js'; import { ParameterValidator, SafeAccess, createErrorResponse, ErrorType, } from '../../validation/error-handler.js'; import { optimizeRuleResponse, DEFAULT_OPTIMIZATION_CONFIG, } from '../../optimization/index.js'; import { safeUnixToISOString, getCurrentTimestamp, } from '../../utils/timestamp.js'; import { getLimitValidationConfig, VALIDATION_CONFIG, } from '../../config/limits.js'; import { withToolTimeout, createTimeoutErrorResponse, TimeoutError, } from '../../utils/timeout-manager.js'; import { validateRuleExists } from '../../validation/resource-validator.js'; import { logger } from '../../monitoring/logger.js'; /** * Check the current status of a rule before performing operations */ async function checkRuleStatus(ruleId, toolName, firewalla) { try { // First check if the rule exists const existenceCheck = await validateRuleExists(ruleId, toolName, firewalla); if (!existenceCheck.exists) { return { exists: false, status: 'not_found', isPaused: false, isActive: false, errorResponse: existenceCheck.errorResponse, }; } // Get the specific rule details to check its status const rulesResponse = await firewalla.getNetworkRules(`id:${ruleId}`, 1); const rules = SafeAccess.getNestedValue(rulesResponse, 'results', []); if (rules.length === 0) { return { exists: false, status: 'not_found', isPaused: false, isActive: false, errorResponse: createErrorResponse(toolName, 'Rule not found in current rule set', ErrorType.API_ERROR, { rule_id: ruleId }), }; } const rule = rules[0]; const status = SafeAccess.getNestedValue(rule, 'status', 'unknown'); const resumeTs = SafeAccess.getNestedValue(rule, 'resumeTs', undefined); // Determine if rule is paused or active const isPaused = status === 'paused' || status === 'disabled' || Boolean(resumeTs && resumeTs > Date.now() / 1000); const isActive = status === 'active' || status === 'enabled'; return { exists: true, status, isPaused, isActive, resumeAt: resumeTs ? new Date(resumeTs * 1000).toISOString() : undefined, }; } catch (error) { return { exists: false, status: 'error', isPaused: false, isActive: false, errorResponse: createErrorResponse(toolName, `Failed to check rule status: ${error instanceof Error ? error.message : 'Unknown error'}`, ErrorType.API_ERROR, { rule_id: ruleId }), }; } } export class GetNetworkRulesHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, // No IP fields in network rules enableFieldNormalization: true, additionalMeta: { data_source: 'network_rules', entity_type: 'firewall_rules', supports_geographic_enrichment: false, supports_field_normalization: true, supports_pagination: true, supports_filtering: true, standardization_version: '2.0.0', }, }); this.name = 'get_network_rules'; this.description = 'Retrieve firewall rules and conditions including target domains, actions, and status. Requires limit parameter. Data is cached for 10 minutes for performance.'; this.category = 'rule'; } async execute(args, firewalla) { try { // Parameter validation with standardized limits const limitValidation = ParameterValidator.validateNumber(args?.limit, 'limit', { required: false, defaultValue: 200, ...getLimitValidationConfig(this.name), }); if (!limitValidation.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, limitValidation.errors); } const query = args?.query; const summaryOnly = args?.summary_only ?? false; const limit = limitValidation.sanitizedValue; const response = await withToolTimeout(async () => firewalla.getNetworkRules(query, limit), this.name); // Apply additional optimization if summary mode requested let optimizedResponse = response; if (summaryOnly) { optimizedResponse = optimizeRuleResponse(response, { ...DEFAULT_OPTIMIZATION_CONFIG, summaryMode: { maxItems: limit, includeFields: [ 'id', 'action', 'target', 'direction', 'status', 'hit', ], excludeFields: ['notes', 'schedule', 'timeUsage', 'scope'], }, }); } const startTime = Date.now(); const unifiedResponseData = { count: SafeAccess.getNestedValue(optimizedResponse, 'count', 0), summary_mode: summaryOnly, limit_applied: summaryOnly ? limit : undefined, rules: summaryOnly ? optimizedResponse.results : SafeAccess.safeArrayMap(response.results.slice(0, limit), (rule) => ({ id: SafeAccess.getNestedValue(rule, 'id', 'unknown'), action: SafeAccess.getNestedValue(rule, 'action', 'unknown'), target: rule.target ? { type: SafeAccess.getNestedValue(rule.target, 'type', 'unknown'), value: SafeAccess.getNestedValue(rule.target, 'value', 'unknown'), ...(rule.target?.dnsOnly && { dnsOnly: rule.target.dnsOnly, }), ...(rule.target?.port && { port: rule.target.port }), } : { type: 'unknown', value: 'unknown' }, direction: SafeAccess.getNestedValue(rule, 'direction', 'unknown'), gid: SafeAccess.getNestedValue(rule, 'gid', 'unknown'), group: SafeAccess.getNestedValue(rule, 'group', undefined), scope: SafeAccess.getNestedValue(rule, 'scope', undefined), notes: SafeAccess.getNestedValue(rule, 'notes', ''), status: SafeAccess.getNestedValue(rule, 'status', 'unknown'), hit: SafeAccess.getNestedValue(rule, 'hit', undefined), schedule: SafeAccess.getNestedValue(rule, 'schedule', undefined), timeUsage: SafeAccess.getNestedValue(rule, 'timeUsage', undefined), protocol: SafeAccess.getNestedValue(rule, 'protocol', undefined), created_at: safeUnixToISOString(SafeAccess.getNestedValue(rule, 'ts', undefined), undefined), updated_at: safeUnixToISOString(SafeAccess.getNestedValue(rule, 'updateTs', undefined), undefined), resume_at: safeUnixToISOString(SafeAccess.getNestedValue(rule, 'resumeTs', undefined), undefined), })), next_cursor: SafeAccess.getNestedValue(summaryOnly ? optimizedResponse : response, 'next_cursor', undefined), ...(summaryOnly && optimizedResponse.pagination_note && { pagination_note: optimizedResponse.pagination_note, }), }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout ); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return this.createErrorResponse(`Failed to get network rules: ${errorMessage}`); } } } export class PauseRuleHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, // No IP fields in rule operations enableFieldNormalization: true, additionalMeta: { data_source: 'rule_operations', entity_type: 'rule_pause_operation', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'pause_rule'; this.description = 'Temporarily disable a specific firewall rule. Requires rule_id parameter. Optional duration parameter (default 60 minutes).'; this.category = 'rule'; } async execute(args, firewalla) { try { // Parameter validation with enhanced rule ID format checking const ruleIdValidation = ParameterValidator.validateRuleId(args?.rule_id, 'rule_id'); const durationValidation = ParameterValidator.validateNumber(args?.duration, 'duration', { defaultValue: 60, ...VALIDATION_CONFIG.DURATION_MINUTES, }); const validationResult = ParameterValidator.combineValidationResults([ ruleIdValidation, durationValidation, ]); if (!validationResult.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, validationResult.errors); } const ruleId = ruleIdValidation.sanitizedValue; const duration = durationValidation.sanitizedValue; // Check rule status before attempting to pause it const statusCheck = await checkRuleStatus(ruleId, this.name, firewalla); if (!statusCheck.exists) { return statusCheck.errorResponse; } // Prevent redundant pause operations if (statusCheck.isPaused) { const resumeInfo = statusCheck.resumeAt ? ` (scheduled to resume at ${statusCheck.resumeAt})` : ''; return createErrorResponse(this.name, `Rule is already paused${resumeInfo}`, ErrorType.API_ERROR, { rule_id: ruleId, current_status: statusCheck.status, already_paused: true, resume_at: statusCheck.resumeAt, requested_duration_minutes: duration, }, [ 'Rule is already in a paused state', statusCheck.resumeAt ? `Rule will automatically resume at ${statusCheck.resumeAt}` : 'Use resume_rule to manually reactivate the rule', 'Use get_network_rules to check current rule status', 'If you want to extend the pause duration, resume first then pause again', ]); } // Warn if rule is not currently active if (!statusCheck.isActive) { logger.warn(`Rule ${ruleId} has status '${statusCheck.status}' - pausing may not have the expected effect`, { tool: 'pause_rule', rule_id: ruleId, current_status: statusCheck.status, warning: 'rule_not_active', }); } const result = await withToolTimeout(async () => firewalla.pauseRule(ruleId, duration), this.name); const startTime = Date.now(); const unifiedResponseData = { success: SafeAccess.getNestedValue(result, 'success', false), message: SafeAccess.getNestedValue(result, 'message', 'Rule pause completed'), rule_id: ruleId, duration_minutes: duration, action: 'pause_rule', }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout ); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; // Provide enhanced error context based on common failure scenarios let errorType = ErrorType.API_ERROR; const suggestions = []; const context = { rule_id: args?.rule_id, duration: args?.duration || 60, operation: 'pause_rule', }; // Analyze error message for specific guidance if (errorMessage.includes('not found') || errorMessage.includes('404')) { errorType = ErrorType.API_ERROR; suggestions.push('Verify the rule_id exists by searching rules first: search_rules query:"id:your_rule_id"', 'Check if the rule was recently deleted or modified', 'Ensure you have permission to access this rule'); } else if (errorMessage.includes('permission') || errorMessage.includes('401') || errorMessage.includes('403')) { errorType = ErrorType.AUTHENTICATION_ERROR; suggestions.push('Verify your Firewalla MSP API credentials are valid', 'Check if your API token has rule management permissions', 'Ensure the rule belongs to a box you have access to'); } else if (errorMessage.includes('already paused') || errorMessage.includes('inactive')) { errorType = ErrorType.API_ERROR; suggestions.push('Rule may already be paused - check rule status first', 'Use resume_rule if the rule needs to be reactivated', 'Check rule status with get_network_rules to verify current state'); } else { suggestions.push('Verify network connectivity to Firewalla API', 'Check if the Firewalla box is online and accessible', 'Try with a different rule_id to test functionality', 'See the Error Handling Guide: /docs/error-handling-guide.md'); } return createErrorResponse(this.name, `Failed to pause rule: ${errorMessage}`, errorType, context, suggestions); } } } export class ResumeRuleHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, // No IP fields in rule operations enableFieldNormalization: true, additionalMeta: { data_source: 'rule_operations', entity_type: 'rule_resume_operation', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'resume_rule'; this.description = 'Resume a previously paused firewall rule. Requires rule_id parameter.'; this.category = 'rule'; } async execute(args, firewalla) { try { // Parameter validation with enhanced rule ID format checking const ruleIdValidation = ParameterValidator.validateRuleId(args?.rule_id, 'rule_id'); if (!ruleIdValidation.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, ruleIdValidation.errors); } const ruleId = ruleIdValidation.sanitizedValue; // Check rule status before attempting to resume it const statusCheck = await checkRuleStatus(ruleId, this.name, firewalla); if (!statusCheck.exists) { return statusCheck.errorResponse; } // Prevent redundant resume operations if (statusCheck.isActive) { return createErrorResponse(this.name, 'Rule is already active and does not need to be resumed', ErrorType.API_ERROR, { rule_id: ruleId, current_status: statusCheck.status, already_active: true, }, [ 'Rule is already in an active state', 'Use get_network_rules to verify current rule status', 'If the rule is not working as expected, check rule configuration instead', 'Use pause_rule if you want to temporarily disable the rule', ]); } // Provide helpful context for non-paused rules if (!statusCheck.isPaused) { logger.warn(`Rule ${ruleId} has status '${statusCheck.status}' - resuming may not activate it as expected`, { tool: 'resume_rule', rule_id: ruleId, current_status: statusCheck.status, warning: 'rule_not_paused', }); } const result = await withToolTimeout(async () => firewalla.resumeRule(ruleId), this.name); const startTime = Date.now(); const unifiedResponseData = { success: SafeAccess.getNestedValue(result, 'success', false), message: SafeAccess.getNestedValue(result, 'message', 'Rule resume completed'), rule_id: ruleId, action: 'resume_rule', }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout ); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return this.createErrorResponse(`Failed to resume rule: ${errorMessage}`); } } } export class GetTargetListsHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, // No IP fields in target lists metadata enableFieldNormalization: true, additionalMeta: { data_source: 'target_lists', entity_type: 'security_target_lists', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'get_target_lists'; this.description = 'Access security target lists (CloudFlare, CrowdSec) with domains and IPs. Requires limit parameter. Data cached for 1 hour for performance.'; this.category = 'rule'; } async execute(args, firewalla) { // Pre-flight parameter validation - do this before timeout wrapper const limitValidation = ParameterValidator.validateNumber(args?.limit, 'limit', { required: true, ...getLimitValidationConfig(this.name), }); if (!limitValidation.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, limitValidation.errors); } const limit = limitValidation.sanitizedValue; const listType = args?.list_type; // Validate list_type parameter if provided if (listType !== undefined) { const validTypes = ['cloudflare', 'crowdsec', 'all']; if (!validTypes.includes(listType)) { return createErrorResponse(this.name, 'Invalid list_type parameter', ErrorType.VALIDATION_ERROR, undefined, [`list_type must be one of: ${validTypes.join(', ')}`]); } } // Use timeout wrapper only for the API call and response processing return withToolTimeout(async () => { const listsResponse = await firewalla.getTargetLists(listType, limit); const startTime = Date.now(); const unifiedResponseData = { total_lists: SafeAccess.safeArrayAccess(listsResponse.results, arr => arr.length, 0), limit_applied: limit, categories: Array.from(new Set(SafeAccess.safeArrayMap(listsResponse.results, (l) => SafeAccess.getNestedValue(l, 'category', undefined)).filter(Boolean))), target_lists: SafeAccess.safeArrayMap(listsResponse.results, (list) => ({ id: SafeAccess.getNestedValue(list, 'id', 'unknown'), name: SafeAccess.getNestedValue(list, 'name', 'Unknown List'), owner: SafeAccess.getNestedValue(list, 'owner', 'unknown'), category: SafeAccess.getNestedValue(list, 'category', 'unknown'), entry_count: SafeAccess.safeArrayAccess(SafeAccess.getNestedValue(list, 'targets', []), arr => arr.length, 0), // Target List Buffer Strategy: Per-list target limiting // // Problem: Some target lists (especially threat intelligence feeds) // can contain 10,000+ targets, leading to: // - Excessive response payload sizes // - JSON serialization performance issues // - Client-side rendering problems // // Solution: Limit to 500 targets per list while preserving total count. // This balances: // - Useful data visibility (500 targets shows patterns/types) // - Response performance (manageable payload size) // - Client usability (reasonable display limits) // // The 500 limit was chosen as 5x the original 100 limit to provide // better visibility into large lists while maintaining performance. targets: SafeAccess.safeArrayAccess(SafeAccess.getNestedValue(list, 'targets', []), arr => arr.slice(0, 500), // Per-list target buffer limit []), last_updated: safeUnixToISOString(SafeAccess.getNestedValue(list, 'lastUpdated', undefined), undefined), notes: SafeAccess.getNestedValue(list, 'notes', ''), })), }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); }, this.name); } } export class GetNetworkRulesSummaryHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, // No IP fields in rule summary statistics enableFieldNormalization: true, additionalMeta: { data_source: 'rule_summary', entity_type: 'rule_statistics', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'get_network_rules_summary'; this.description = 'Get overview statistics and counts of network rules by category. Requires limit parameter. Data cached for 10 minutes for performance.'; this.category = 'rule'; } async execute(args, firewalla) { try { // Parameter validation with standardized limits const limitValidation = ParameterValidator.validateNumber(args?.limit, 'limit', { required: false, defaultValue: 200, ...getLimitValidationConfig(this.name), }); const ruleTypeValidation = ParameterValidator.validateEnum(args?.rule_type, 'rule_type', ['block', 'allow', 'timelimit', 'all'], false, 'all'); const activeOnlyValidation = ParameterValidator.validateBoolean(args?.active_only, 'active_only', true); const validationResult = ParameterValidator.combineValidationResults([ limitValidation, ruleTypeValidation, activeOnlyValidation, ]); if (!validationResult.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, validationResult.errors); } const limit = limitValidation.sanitizedValue; const ruleType = ruleTypeValidation.sanitizedValue; const activeOnly = activeOnlyValidation.sanitizedValue; // Statistical Analysis Buffer Strategy: User-controlled limit for rule analysis // // Problem: Rule summary analysis requires processing potentially thousands // of rules to generate meaningful statistics. Without limits, this could: // - Consume excessive memory for large rule sets (10k+ rules) // - Cause slow API responses // - Risk timeout failures on resource-constrained systems // // Solution: Use user-specified limit (validated 1-10000) for statistical analysis. // This provides: // - User control over memory usage and response time // - Predictable memory usage based on user's choice // - Consistent with other rule tools' validation patterns // // The limit is validated to ensure reasonable bounds (1-10000) which allows // both lightweight queries and comprehensive enterprise-level analysis. const allRulesResponse = await withToolTimeout(async () => firewalla.getNetworkRules(undefined, limit), this.name); const allRules = SafeAccess.getNestedValue(allRulesResponse, 'results', []); // Group rules by various categories for overview const rulesByAction = allRules.reduce((acc, rule) => { const action = SafeAccess.getNestedValue(rule, 'action', 'unknown'); acc[action] = (acc[action] || 0) + 1; return acc; }, {}); const rulesByDirection = allRules.reduce((acc, rule) => { const direction = SafeAccess.getNestedValue(rule, 'direction', 'unknown'); acc[direction] = (acc[direction] || 0) + 1; return acc; }, {}); const rulesByStatus = allRules.reduce((acc, rule) => { const status = SafeAccess.getNestedValue(rule, 'status', 'active'); acc[status] = (acc[status] || 0) + 1; return acc; }, {}); const rulesByTargetType = allRules.reduce((acc, rule) => { const targetType = SafeAccess.getNestedValue(rule, 'target.type', 'unknown'); acc[targetType] = (acc[targetType] || 0) + 1; return acc; }, {}); // Calculate hit statistics const rulesWithHits = allRules.filter((rule) => { const hitCount = SafeAccess.getNestedValue(rule, 'hit.count', 0); return hitCount > 0; }); const totalHits = allRules.reduce((sum, rule) => sum + SafeAccess.getNestedValue(rule, 'hit.count', 0), 0); const avgHitsPerRule = allRules.length > 0 ? Math.round((totalHits / allRules.length) * 100) / 100 : 0; // Find most recent rule activity let mostRecentRuleTs = undefined; let oldestRuleTs = undefined; if (allRules.length > 0) { const validTimestamps = allRules .map((rule) => { const ts = SafeAccess.getNestedValue(rule, 'ts', 0); const updateTs = SafeAccess.getNestedValue(rule, 'updateTs', 0); return Math.max(ts, updateTs); }) .filter((ts) => ts > 0); const creationTimestamps = allRules .map((rule) => SafeAccess.getNestedValue(rule, 'ts', 0)) .filter((ts) => ts > 0); if (validTimestamps.length > 0) { mostRecentRuleTs = Math.max(...validTimestamps); } if (creationTimestamps.length > 0) { oldestRuleTs = Math.min(...creationTimestamps); } } const startTime = Date.now(); const unifiedResponseData = { total_rules: allRules.length, limit_applied: limit, summary_timestamp: getCurrentTimestamp(), breakdown: { by_action: rulesByAction, by_direction: rulesByDirection, by_status: rulesByStatus, by_target_type: rulesByTargetType, }, hit_statistics: { total_hits: totalHits, rules_with_hits: rulesWithHits.length, rules_with_no_hits: allRules.length - rulesWithHits.length, average_hits_per_rule: avgHitsPerRule, hit_rate_percentage: allRules.length > 0 ? Math.round((rulesWithHits.length / allRules.length) * 100) : 0, }, age_statistics: { most_recent_activity: safeUnixToISOString(mostRecentRuleTs, undefined), oldest_rule_created: safeUnixToISOString(oldestRuleTs, undefined), has_timestamp_data: mostRecentRuleTs !== undefined || oldestRuleTs !== undefined, }, filters_applied: { rule_type: ruleType || 'all', active_only: activeOnly, }, }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout ); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return this.createErrorResponse(`Failed to get network rules summary: ${errorMessage}`); } } } export class GetMostActiveRulesHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, // No IP fields in rule hit analysis enableFieldNormalization: true, additionalMeta: { data_source: 'rule_analysis', entity_type: 'active_rule_statistics', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'get_most_active_rules'; this.description = 'Get rules with highest hit counts for traffic analysis. Requires limit parameter. Optional min_hits parameter.'; this.category = 'rule'; } async execute(args, firewalla) { try { // Parameter validation with standardized limits const limitValidation = ParameterValidator.validateNumber(args?.limit, 'limit', { required: false, defaultValue: 200, ...getLimitValidationConfig(this.name), }); if (!limitValidation.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, limitValidation.errors); } const limit = limitValidation.sanitizedValue; const minHitsValidation = ParameterValidator.validateNumber(args?.min_hits, 'min_hits', { min: 0, max: 1000000, defaultValue: 1, integer: true, }); if (!minHitsValidation.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, minHitsValidation.errors); } const minHits = minHitsValidation.sanitizedValue; // Buffer Strategy: Over-fetch to compensate for hit-count filtering // // Problem: Rules are filtered by minimum hit count after retrieval. Since hit // counts vary widely (some rules have 0 hits, others have thousands), we can't // predict how many rules will pass the filter. // // Solution: Apply the same 3x buffer strategy as device filtering. This ensures // we typically have enough rules that meet the minimum hit threshold without // requiring multiple API calls or complex pagination logic. // // The 3000 cap prevents excessive API loads while still allowing reasonable // result sets for most use cases. const fetchLimit = Math.min(limit * 3, 3000); // 3x buffer with 3000 cap const allRulesResponse = await withToolTimeout(async () => firewalla.getNetworkRules(undefined, fetchLimit), this.name); // Filter and sort by hit count const activeRules = SafeAccess.safeArrayFilter(allRulesResponse.results, (rule) => { const hitCount = SafeAccess.getNestedValue(rule, 'hit.count', 0); return hitCount >= minHits; }) .sort((a, b) => { const aHits = SafeAccess.getNestedValue(a, 'hit.count', 0); const bHits = SafeAccess.getNestedValue(b, 'hit.count', 0); return bHits - aHits; }) .slice(0, limit); const startTime = Date.now(); const unifiedResponseData = { total_rules_analyzed: SafeAccess.safeArrayAccess(allRulesResponse.results, arr => arr.length, 0), rules_meeting_criteria: activeRules.length, min_hits_threshold: minHits, limit_applied: limit, rules: SafeAccess.safeArrayMap(activeRules, (rule) => { const targetValue = SafeAccess.getNestedValue(rule, 'target.value', ''); const notes = SafeAccess.getNestedValue(rule, 'notes', ''); return { id: SafeAccess.getNestedValue(rule, 'id', 'unknown'), action: SafeAccess.getNestedValue(rule, 'action', 'unknown'), target_type: SafeAccess.getNestedValue(rule, 'target.type', 'unknown'), target_value: targetValue.length > 60 ? `${targetValue.substring(0, 60)}...` : targetValue, direction: SafeAccess.getNestedValue(rule, 'direction', 'unknown'), hit_count: SafeAccess.getNestedValue(rule, 'hit.count', 0), last_hit: safeUnixToISOString(SafeAccess.getNestedValue(rule, 'hit.lastHitTs', undefined), 'Never'), created_at: safeUnixToISOString(SafeAccess.getNestedValue(rule, 'ts', undefined), undefined), notes: notes.length > 80 ? `${notes.substring(0, 80)}...` : notes, }; }), summary: { total_hits: activeRules.reduce((sum, rule) => sum + SafeAccess.getNestedValue(rule, 'hit.count', 0), 0), top_rule_hits: activeRules.length > 0 ? SafeAccess.getNestedValue(activeRules[0], 'hit.count', 0) : 0, analysis_timestamp: getCurrentTimestamp(), }, }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout ); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return this.createErrorResponse(`Failed to get most active rules: ${errorMessage}`); } } } export class GetRecentRulesHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, // No IP fields in rule timeline analysis enableFieldNormalization: true, additionalMeta: { data_source: 'rule_timeline', entity_type: 'recent_rule_activity', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'get_recent_rules'; this.description = 'Get recently created or modified firewall rules. Requires limit parameter. Optional hours parameter (default 24 hours lookback).'; this.category = 'rule'; } async execute(args, firewalla) { try { // Parameter validation with standardized limits const limitValidation = ParameterValidator.validateNumber(args?.limit, 'limit', { required: false, defaultValue: 200, ...getLimitValidationConfig(this.name), }); const hoursValidation = ParameterValidator.validateNumber(args?.hours, 'hours', { min: 0.1, max: 168, defaultValue: 24, integer: false, }); const validationResult = ParameterValidator.combineValidationResults([ limitValidation, hoursValidation, ]); if (!validationResult.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, validationResult.errors); } const hours = hoursValidation.sanitizedValue; const limit = limitValidation.sanitizedValue; const includeModified = args?.include_modified ?? true; // Adaptive Buffer Strategy: Dynamic fetch limit calculation // // Challenge: Time-based filtering has highly variable efficiency depending on: // - Time window size (1 hour vs 168 hours) // - Network activity levels during the period // - Historical rule creation/modification patterns // // Solution: Use an adaptive multiplier that scales with the requested limit: // - Small limits (≤50): Use higher multiplier (up to 10x) to ensure adequate results // - Large limits (≥500): Use conservative multiplier (3x) to avoid excessive API load // - Formula: max(3, min(10, 500/limit)) provides smooth scaling // // Example multipliers: // - limit=10: multiplier=10x (fetchLimit=100) - high buffer for small requests // - limit=50: multiplier=10x (fetchLimit=500) - still generous buffer // - limit=100: multiplier=5x (fetchLimit=500) - balanced approach // - limit=200: multiplier=3x (fetchLimit=600) - efficient for larger requests // - limit=500: multiplier=3x (fetchLimit=1500) - minimal overhead // // The 2000 cap prevents excessive API calls while still allowing reasonable // result sets for most time-based queries. const fetchMultiplier = Math.max(3, Math.min(10, 500 / limit)); // Adaptive multiplier: 3-10x based on limit size const fetchLimit = Math.min(limit * fetchMultiplier, 2000); // Cap at reasonable maximum const allRulesResponse = await withToolTimeout(async () => firewalla.getNetworkRules(undefined, fetchLimit), this.name); const hoursAgoTs = Math.floor(Date.now() / 1000) - hours * 3600; // Filter rules created or modified within the timeframe const recentRules = SafeAccess.safeArrayFilter(allRulesResponse.results, (rule) => { const ts = SafeAccess.getNestedValue(rule, 'ts', 0); const updateTs = SafeAccess.getNestedValue(rule, 'updateTs', 0); const created = ts >= hoursAgoTs; const modified = includeModified && updateTs >= hoursAgoTs && updateTs > ts; return created || modified; }) .sort((a, b) => { const aTs = SafeAccess.getNestedValue(a, 'ts', 0); const aUpdateTs = SafeAccess.getNestedValue(a, 'updateTs', 0); const bTs = SafeAccess.getNestedValue(b, 'ts', 0); const bUpdateTs = SafeAccess.getNestedValue(b, 'updateTs', 0); return Math.max(bTs, bUpdateTs) - Math.max(aTs, aUpdateTs); }) // Sort by most recent activity .slice(0, limit); const startTime = Date.now(); const unifiedResponseData = { total_rules_analyzed: SafeAccess.safeArrayAccess(allRulesResponse.results, arr => arr.length, 0), recent_rules_found: recentRules.length, lookback_hours: hours, include_modified: includeModified, cutoff_time: safeUnixToISOString(hoursAgoTs, undefined), rules: SafeAccess.safeArrayMap(recentRules, (rule) => { const ts = SafeAccess.getNestedValue(rule, 'ts', 0); const updateTs = SafeAccess.getNestedValue(rule, 'updateTs', 0); const wasModified = updateTs > ts && updateTs >= hoursAgoTs; const targetValue = SafeAccess.getNestedValue(rule, 'target.value', ''); const notes = SafeAccess.getNestedValue(rule, 'notes', ''); return { id: SafeAccess.getNestedValue(rule, 'id', 'unknown'), action: SafeAccess.getNestedValue(rule, 'action', 'unknown'), target_type: SafeAccess.getNestedValue(rule, 'target.type', 'unknown'), target_value: targetValue.length > 60 ? `${targetValue.substring(0, 60)}...` : targetValue, direction: SafeAccess.getNestedValue(rule, 'direction', 'unknown'), status: SafeAccess.getNestedValue(rule, 'status', 'active'), activity_type: wasModified ? 'modified' : 'created', created_at: safeUnixToISOString(ts, undefined), updated_at: safeUnixToISOString(updateTs, undefined), hit_count: SafeAccess.getNestedValue(rule, 'hit.count', 0), notes: notes.length > 80 ? `${notes.substring(0, 80)}...` : notes, }; }), summary: { newly_created: recentRules.filter((r) => { const ts = SafeAccess.getNestedValue(r, 'ts', 0); const updateTs = SafeAccess.getNestedValue(r, 'updateTs', 0); return (ts >= hoursAgoTs && (updateTs <= ts || updateTs < hoursAgoTs)); }).length, recently_modified: recentRules.filter((r) => { const ts = SafeAccess.getNestedValue(r, 'ts', 0); const updateTs = SafeAccess.getNestedValue(r, 'updateTs', 0); return updateTs > ts && updateTs >= hoursAgoTs; }).length, analysis_timestamp: getCurrentTimestamp(), }, }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout ); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return this.createErrorResponse(`Failed to get recent rules: ${errorMessage}`); } } } /** * Handler for retrieving a specific target list by ID */ export class GetSpecificTargetListHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, enableFieldNormalization: true, additionalMeta: { data_source: 'target_lists', entity_type: 'target_list', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'get_specific_target_list'; this.description = 'Retrieve a specific target list by ID from Firewalla'; this.category = 'rule'; } async execute(args, firewalla) { try { const idValidation = ParameterValidator.validateRequiredString(args?.id, 'id'); if (!idValidation.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, idValidation.errors); } const id = idValidation.sanitizedValue; const response = await withToolTimeout(async () => firewalla.getSpecificTargetList(id), this.name); return this.createUnifiedResponse(response); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return createErrorResponse(this.name, `Failed to get target list: ${errorMessage}`, ErrorType.API_ERROR, { id: args?.id }); } } } /** * Handler for creating a new target list */ export class CreateTargetListHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: false, enableFieldNormalization: true, additionalMeta: { data_source: 'target_lists', entity_type: 'target_list_creation', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'create_target_list'; this.description = 'Create a new target list in Firewalla'; this.category = 'rule'; } async execute(args, firewalla) { try { const nameValidation = ParameterValidator.validateRequiredString(args?.name, 'name'); const ownerValidation = ParameterValidator.validateRequiredString(args?.owner, 'owner'); const targetsValidation = ParameterValidator.validateArray(args?.targets, 'targets', { required: true }); const categoryValidation = ParameterValidator.validateEnum(args?.category, 'category', [ 'ad', 'edu', 'games', 'gamble', 'intel', 'p2p', 'porn', 'private', 'social', 'shopping', 'video', 'vpn', ], false); const notesValidation = ParameterValidator.validateOptionalString(args?.notes, 'notes'); const validationResult = ParameterValidator.combineValidationResults([ nameValidation, ownerValidation, targetsValidation, categoryValidation, notesValidation, ]); if (!validationResult.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, validationResult.errors); } const targetListData = { name: nameValidation.sanitizedValue, owner: ownerValidation.sanitizedValue, targets: targetsValidation.sanitizedValue, }; if (categoryValidation.sanitizedValue) { targetListData.category = categoryValidation.sanitizedValue; } if (notesValidation.sanitizedValue) { targetListData.notes = notesValidation.sanitizedValue; } const response = await withToolTimeout(async () => firew