Skip to main content
Glama

firewalla-mcp-server

rules.ts56.8 kB
/** * Firewall rule management tool handlers */ import { BaseToolHandler, type ToolArgs, type ToolResponse } from './base.js'; import type { FirewallaClient } from '../../firewalla/client.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'; /** * Rule status checking utility for preventing redundant operations */ interface RuleStatusInfo { exists: boolean; status: string; isPaused: boolean; isActive: boolean; resumeAt?: string; errorResponse?: ToolResponse; } /** * Check the current status of a rule before performing operations */ async function checkRuleStatus( ruleId: string, toolName: string, firewalla: FirewallaClient ): Promise<RuleStatusInfo> { 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', [] ) as any[]; 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' ) as string; const resumeTs = SafeAccess.getNestedValue(rule, 'resumeTs', undefined) as | number | undefined; // Determine if rule is paused or active const isPaused: boolean = status === 'paused' || status === 'disabled' || Boolean(resumeTs && resumeTs > Date.now() / 1000); const isActive: boolean = 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 { name = 'get_network_rules'; description = 'Retrieve firewall rules and conditions including target domains, actions, and status. Requires limit parameter. Data is cached for 10 minutes for performance.'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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 as boolean) ?? false; const limit = limitValidation.sanitizedValue! as number; const response = await withToolTimeout( async () => firewalla.getNetworkRules(query, limit), this.name ); // Apply additional optimization if summary mode requested let optimizedResponse: any = response; if (summaryOnly) { optimizedResponse = optimizeRuleResponse(response as any, { ...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 as any[]).slice(0, limit), (rule: any) => ({ 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) as | number | undefined, undefined ), updated_at: safeUnixToISOString( SafeAccess.getNestedValue(rule, 'updateTs', undefined) as | number | undefined, undefined ), resume_at: safeUnixToISOString( SafeAccess.getNestedValue(rule, 'resumeTs', undefined) as | number | 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: unknown) { 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 { name = 'pause_rule'; description = 'Temporarily disable a specific firewall rule. Requires rule_id parameter. Optional duration parameter (default 60 minutes).'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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 as string; const duration = durationValidation.sanitizedValue as number; // 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 as any, '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: unknown) { 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: string[] = []; const context: Record<string, any> = { 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 { name = 'resume_rule'; description = 'Resume a previously paused firewall rule. Requires rule_id parameter.'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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 as string; // 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 as any, '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: unknown) { 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 { name = 'get_target_lists'; description = 'Access security target lists (CloudFlare, CrowdSec) with domains and IPs. Requires limit parameter. Data cached for 1 hour for performance.'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { // 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! as number; const listType = args?.list_type as string | undefined; // 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: any) => SafeAccess.getNestedValue(l, 'category', undefined) ).filter(Boolean) ) ), target_lists: SafeAccess.safeArrayMap( listsResponse.results, (list: any) => ({ 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) as | number | 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 { name = 'get_network_rules_summary'; description = 'Get overview statistics and counts of network rules by category. Requires limit parameter. Data cached for 10 minutes for performance.'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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! as number; 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', [] ) as any[]; // Group rules by various categories for overview const rulesByAction = allRules.reduce( (acc: Record<string, number>, rule: any) => { const action = SafeAccess.getNestedValue( rule, 'action', 'unknown' ) as string; acc[action] = (acc[action] || 0) + 1; return acc; }, {} ); const rulesByDirection = allRules.reduce( (acc: Record<string, number>, rule: any) => { const direction = SafeAccess.getNestedValue( rule, 'direction', 'unknown' ) as string; acc[direction] = (acc[direction] || 0) + 1; return acc; }, {} ); const rulesByStatus = allRules.reduce( (acc: Record<string, number>, rule: any) => { const status = SafeAccess.getNestedValue( rule, 'status', 'active' ) as string; acc[status] = (acc[status] || 0) + 1; return acc; }, {} ); const rulesByTargetType = allRules.reduce( (acc: Record<string, number>, rule: any) => { const targetType = SafeAccess.getNestedValue( rule, 'target.type', 'unknown' ) as string; acc[targetType] = (acc[targetType] || 0) + 1; return acc; }, {} ); // Calculate hit statistics const rulesWithHits = allRules.filter((rule: any) => { const hitCount = SafeAccess.getNestedValue( rule, 'hit.count', 0 ) as number; return hitCount > 0; }); const totalHits = allRules.reduce( (sum: number, rule: any) => sum + (SafeAccess.getNestedValue(rule, 'hit.count', 0) as number), 0 ); const avgHitsPerRule = allRules.length > 0 ? Math.round((totalHits / allRules.length) * 100) / 100 : 0; // Find most recent rule activity let mostRecentRuleTs: number | undefined = undefined; let oldestRuleTs: number | undefined = undefined; if (allRules.length > 0) { const validTimestamps = allRules .map((rule: any) => { const ts = SafeAccess.getNestedValue(rule, 'ts', 0) as number; const updateTs = SafeAccess.getNestedValue( rule, 'updateTs', 0 ) as number; return Math.max(ts, updateTs); }) .filter((ts: number) => ts > 0); const creationTimestamps = allRules .map( (rule: any) => SafeAccess.getNestedValue(rule, 'ts', 0) as number ) .filter((ts: number) => 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: unknown) { 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 { name = 'get_most_active_rules'; description = 'Get rules with highest hit counts for traffic analysis. Requires limit parameter. Optional min_hits parameter.'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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! as number; 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! as number; // 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: any) => { const hitCount = SafeAccess.getNestedValue( rule, 'hit.count', 0 ) as number; return hitCount >= minHits; } ) .sort((a: any, b: any) => { const aHits = SafeAccess.getNestedValue(a, 'hit.count', 0) as number; const bHits = SafeAccess.getNestedValue(b, 'hit.count', 0) as number; 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: any) => { const targetValue = SafeAccess.getNestedValue( rule, 'target.value', '' ) as string; const notes = SafeAccess.getNestedValue(rule, 'notes', '') as string; 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) as | number | undefined, 'Never' ), created_at: safeUnixToISOString( SafeAccess.getNestedValue(rule, 'ts', undefined) as | number | 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) as number), 0 ), top_rule_hits: activeRules.length > 0 ? (SafeAccess.getNestedValue( activeRules[0], 'hit.count', 0 ) as number) : 0, analysis_timestamp: getCurrentTimestamp(), }, }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error: unknown) { 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 { name = 'get_recent_rules'; description = 'Get recently created or modified firewall rules. Requires limit parameter. Optional hours parameter (default 24 hours lookback).'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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! as number; const limit = limitValidation.sanitizedValue! as number; const includeModified = (args?.include_modified as boolean) ?? 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: any) => { const ts = SafeAccess.getNestedValue(rule, 'ts', 0) as number; const updateTs = SafeAccess.getNestedValue( rule, 'updateTs', 0 ) as number; const created = ts >= hoursAgoTs; const modified = includeModified && updateTs >= hoursAgoTs && updateTs > ts; return created || modified; } ) .sort((a: any, b: any) => { const aTs = SafeAccess.getNestedValue(a, 'ts', 0) as number; const aUpdateTs = SafeAccess.getNestedValue( a, 'updateTs', 0 ) as number; const bTs = SafeAccess.getNestedValue(b, 'ts', 0) as number; const bUpdateTs = SafeAccess.getNestedValue( b, 'updateTs', 0 ) as number; 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: any) => { const ts = SafeAccess.getNestedValue(rule, 'ts', 0) as number; const updateTs = SafeAccess.getNestedValue( rule, 'updateTs', 0 ) as number; const wasModified = updateTs > ts && updateTs >= hoursAgoTs; const targetValue = SafeAccess.getNestedValue( rule, 'target.value', '' ) as string; const notes = SafeAccess.getNestedValue(rule, 'notes', '') as string; 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: any) => { const ts = SafeAccess.getNestedValue(r, 'ts', 0) as number; const updateTs = SafeAccess.getNestedValue( r, 'updateTs', 0 ) as number; return ( ts >= hoursAgoTs && (updateTs <= ts || updateTs < hoursAgoTs) ); }).length, recently_modified: recentRules.filter((r: any) => { const ts = SafeAccess.getNestedValue(r, 'ts', 0) as number; const updateTs = SafeAccess.getNestedValue( r, 'updateTs', 0 ) as number; return updateTs > ts && updateTs >= hoursAgoTs; }).length, analysis_timestamp: getCurrentTimestamp(), }, }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error: unknown) { 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 { name = 'get_specific_target_list'; description = 'Retrieve a specific target list by ID from Firewalla'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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 as string; const response = await withToolTimeout( async () => firewalla.getSpecificTargetList(id), this.name ); return this.createUnifiedResponse(response); } catch (error: unknown) { 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 { name = 'create_target_list'; description = 'Create a new target list in Firewalla'; category = 'rule' as const; 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', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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: any = { 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 () => firewalla.createTargetList(targetListData), this.name ); return this.createUnifiedResponse(response); } catch (error: unknown) { 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 create target list: ${errorMessage}`, ErrorType.API_ERROR, { name: args?.name, owner: args?.owner } ); } } } /** * Handler for updating an existing target list */ export class UpdateTargetListHandler extends BaseToolHandler { name = 'update_target_list'; description = 'Update an existing target list in Firewalla'; category = 'rule' as const; constructor() { super({ enableGeoEnrichment: false, enableFieldNormalization: true, additionalMeta: { data_source: 'target_lists', entity_type: 'target_list_update', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { try { const idValidation = ParameterValidator.validateRequiredString( args?.id, 'id' ); const nameValidation = ParameterValidator.validateOptionalString( args?.name, 'name' ); const targetsValidation = ParameterValidator.validateArray( args?.targets, 'targets', { required: false } ); 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([ idValidation, nameValidation, targetsValidation, categoryValidation, notesValidation, ]); if (!validationResult.isValid) { return createErrorResponse( this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, validationResult.errors ); } const id = idValidation.sanitizedValue as string; const updateData: Record<string, unknown> = {}; if (nameValidation.sanitizedValue !== undefined) { updateData.name = nameValidation.sanitizedValue; } if (targetsValidation.sanitizedValue !== undefined) { updateData.targets = targetsValidation.sanitizedValue; } if (categoryValidation.sanitizedValue !== undefined) { updateData.category = categoryValidation.sanitizedValue; } if (notesValidation.sanitizedValue !== undefined) { updateData.notes = notesValidation.sanitizedValue; } const response = await withToolTimeout( async () => firewalla.updateTargetList(id, updateData), this.name ); return this.createUnifiedResponse(response); } catch (error: unknown) { 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 update target list: ${errorMessage}`, ErrorType.API_ERROR, { id: args?.id } ); } } } /** * Handler for deleting a target list */ export class DeleteTargetListHandler extends BaseToolHandler { name = 'delete_target_list'; description = 'Delete a target list from Firewalla'; category = 'rule' as const; constructor() { super({ enableGeoEnrichment: false, enableFieldNormalization: true, additionalMeta: { data_source: 'target_lists', entity_type: 'target_list_deletion', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); } async execute( args: ToolArgs, firewalla: FirewallaClient ): Promise<ToolResponse> { 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 as string; const response = await withToolTimeout( async () => firewalla.deleteTargetList(id), this.name ); return this.createUnifiedResponse(response); } catch (error: unknown) { 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 delete target list: ${errorMessage}`, ErrorType.API_ERROR, { id: args?.id } ); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/amittell/firewalla-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server