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

930 lines (891 loc) 68.5 kB
/** * Advanced search tool handlers */ import { BaseToolHandler } from './base.js'; import { SafeAccess, QuerySanitizer, ParameterValidator, createErrorResponse, ErrorType, } from '../../validation/error-handler.js'; import { getLimitValidationConfig } from '../../config/limits.js'; import { validateFirewallaQuerySyntax, getExampleQueries, } from '../../utils/query-validator.js'; import { withToolTimeout, TimeoutError, createTimeoutErrorResponse, } from '../../utils/timeout-manager.js'; import { withRetryAndTimeout, isRetryableError, } from '../../utils/retry-manager.js'; import { createSearchTools } from '../search.js'; import { unixToISOStringOrNow } from '../../utils/timestamp.js'; import { SEARCH_FIELDS } from '../../search/types.js'; // ResponseStandardizer import removed - using direct response creation import { validateCountryCodes } from '../../utils/geographic.js'; function validateCommonSearchParameters(args, toolName, entityType) { // Validate optional limit parameter with default const limitValidation = ParameterValidator.validateNumber(args.limit, 'limit', { required: false, defaultValue: 200, ...getLimitValidationConfig(toolName), }); if (!limitValidation.isValid) { return { isValid: false, response: createErrorResponse(toolName, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, limitValidation.errors), }; } // Validate required query parameter const queryValidation = ParameterValidator.validateRequiredString(args.query, 'query'); if (!queryValidation.isValid) { return { isValid: false, response: createErrorResponse(toolName, 'Query parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, queryValidation.errors), }; } // Validate query syntax const querySyntaxValidation = validateFirewallaQuerySyntax(args.query); if (!querySyntaxValidation.isValid) { const examples = getExampleQueries(entityType); return { isValid: false, response: createErrorResponse(toolName, 'Invalid query syntax', ErrorType.VALIDATION_ERROR, { query: args.query, syntax_errors: querySyntaxValidation.errors, examples: examples.slice(0, 3), hint: 'Use field:value syntax with logical operators (AND, OR, NOT)', }, querySyntaxValidation.errors), }; } // Validate field names in the query const fieldValidation = QuerySanitizer.validateQueryFields(args.query, entityType); if (!fieldValidation.isValid) { return { isValid: false, response: createErrorResponse(toolName, 'Query contains invalid field names', ErrorType.VALIDATION_ERROR, { query: args.query, documentation: entityType === 'alarms' ? 'See /docs/error-handling-guide.md for troubleshooting' : 'See /docs/query-syntax-guide.md for valid field names', }, fieldValidation.errors), }; } // Validate cursor format if provided if (args.cursor !== undefined) { const cursorValidation = ParameterValidator.validateCursor(args.cursor, 'cursor'); if (!cursorValidation.isValid) { return { isValid: false, response: createErrorResponse(toolName, 'Invalid cursor format', ErrorType.VALIDATION_ERROR, undefined, cursorValidation.errors), }; } } // Validate group_by parameter if provided if (args.group_by !== undefined) { const groupByValidation = ParameterValidator.validateEnum(args.group_by, 'group_by', SEARCH_FIELDS[entityType], false); if (!groupByValidation.isValid) { return { isValid: false, response: createErrorResponse(toolName, 'Invalid group_by field', ErrorType.VALIDATION_ERROR, { group_by: args.group_by, valid_fields: SEARCH_FIELDS[entityType], documentation: 'See /docs/query-syntax-guide.md for valid fields', }, groupByValidation.errors), }; } } return { isValid: true, limit: args.limit, query: args.query, cursor: args.cursor, groupBy: args.group_by, }; } export class SearchFlowsHandler extends BaseToolHandler { constructor() { // Enable full standardization: geographic enrichment and field normalization for network flows super({ enableGeoEnrichment: true, // Network flows have IP addresses that require geographic enrichment enableFieldNormalization: true, // Ensure consistent snake_case field naming across all responses additionalMeta: { data_source: 'flows', entity_type: 'network_flows', supports_geographic_enrichment: true, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'search_flows'; this.description = `Advanced network flow searching with powerful query syntax and enhanced reliability. Data cached for 15 seconds, use force_refresh=true for real-time network analysis. Search through network traffic flows using complex queries with logical operators, wildcards, and field-specific filters. Features automatic boolean query translation for improved compatibility. REQUIRED PARAMETERS: - query: Search query string using flow field syntax OPTIONAL PARAMETERS: - limit: Maximum number of results to return (default: 200, max: 500) - force_refresh: Bypass cache for real-time data (default: false) - cursor: Pagination cursor from previous response - time_range: Time window for search (start/end timestamps) - sort_by: Field to sort results by - group_by: Field to group results by for aggregation - aggregate: Enable aggregation statistics QUERY EXAMPLES (with automatic boolean translation): - Boolean fields (both syntaxes supported): "blocked:true" OR "blocked=true", "allowed:false" OR "allowed=false" (automatically converted to backend format) - Basic field queries: "protocol:tcp", "source_ip:192.168.1.100", "destination_port:443" - Logical operators: "protocol:tcp AND blocked:false", "blocked=true OR allowed=false" - Wildcards: "source_ip:192.168.*", "destination_domain:*.facebook.com" - Ranges: "bytes:[1000 TO 50000]", "timestamp:>=2024-01-01" - Complex queries: "(protocol:tcp OR protocol:udp) AND source_ip:192.168.* NOT blocked=true" CACHE CONTROL: - Default: 15-second cache for optimal performance - Real-time: Use force_refresh=true for live network monitoring - Cache info included in responses for timing awareness PERFORMANCE TIPS: - Use specific time ranges for better performance: {"time_range": {"start": "2024-01-01T00:00:00Z", "end": "2024-01-02T00:00:00Z"}} - Limit results with reasonable values (100-1000) for faster responses - Use cursor for pagination with large datasets - Group by fields like "source_ip" or "protocol" for aggregated insights See the Query Syntax Guide for complete documentation: /docs/query-syntax-guide.md`; this.category = 'search'; } async execute(args, firewalla) { const searchArgs = args; const startTime = Date.now(); try { // Validate common search parameters const validation = validateCommonSearchParameters(searchArgs, this.name, 'flows'); if (!validation.isValid) { return validation.response; } // Validate force_refresh parameter if provided const forceRefreshValidation = ParameterValidator.validateBoolean(searchArgs.force_refresh, 'force_refresh', false); if (!forceRefreshValidation.isValid) { return createErrorResponse(this.name, 'Force refresh parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, forceRefreshValidation.errors); } const finalQuery = searchArgs.query; // ------------------------------------------------------------ // Validate geographic_filters if provided // ------------------------------------------------------------ if (searchArgs.geographic_filters !== undefined) { // Validate it's an object if (typeof searchArgs.geographic_filters !== 'object' || searchArgs.geographic_filters === null) { return createErrorResponse(this.name, 'Invalid geographic_filters parameter', ErrorType.VALIDATION_ERROR, { provided_value: searchArgs.geographic_filters, expected: 'object with optional fields: countries, continents, regions, cities, etc.', }); } // Validate country codes if provided if (searchArgs.geographic_filters.countries && searchArgs.geographic_filters.countries.length > 0) { const countryValidation = validateCountryCodes(searchArgs.geographic_filters.countries); if (!countryValidation.valid) { return createErrorResponse(this.name, `Country code validation failed: Invalid country codes: ${countryValidation.invalid.join(', ')}`, ErrorType.VALIDATION_ERROR, { invalid_codes: countryValidation.invalid, valid_codes: countryValidation.valid, documentation: 'Country codes must be ISO 3166-1 alpha-2 format (e.g., US, CN, GB)', }); } } } // ------------------------------------------------------------ // Validate include_analytics parameter if provided // ------------------------------------------------------------ const includeAnalyticsValidation = ParameterValidator.validateBoolean(searchArgs.include_analytics, 'include_analytics', false); if (!includeAnalyticsValidation.isValid) { return createErrorResponse(this.name, 'Include analytics parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, includeAnalyticsValidation.errors); } const searchTools = createSearchTools(firewalla); const searchParams = { query: finalQuery, limit: searchArgs.limit, offset: searchArgs.offset, cursor: searchArgs.cursor, sort_by: searchArgs.sort_by, sort_order: searchArgs.sort_order, group_by: searchArgs.group_by, aggregate: searchArgs.aggregate, time_range: searchArgs.time_range, force_refresh: forceRefreshValidation.sanitizedValue, geographic_filters: searchArgs.geographic_filters, include_analytics: includeAnalyticsValidation.sanitizedValue, }; // Use retry logic for search operations as they can be prone to timeouts const result = await withRetryAndTimeout(async () => searchTools.search_flows(searchParams), this.name, { maxAttempts: 2, // Conservative retry for search operations initialDelayMs: 2000, // Wait 2 seconds before retry shouldRetry: (error, attempt) => { // Retry on timeouts and network errors, but not on validation errors if (error instanceof TimeoutError) { return true; } return isRetryableError(error) && attempt === 1; // Only retry once for search }, }); const executionTime = Date.now() - startTime; // Process flow data with enhanced standardization let processedFlows = SafeAccess.safeArrayMap(result.results, (flow) => ({ timestamp: unixToISOStringOrNow(flow.ts), source_ip: SafeAccess.getNestedValue(flow, 'source.ip', 'unknown'), source_country: SafeAccess.getNestedValue(flow, 'source.geo.country', 'unknown'), source_city: SafeAccess.getNestedValue(flow, 'source.geo.city', 'unknown'), source_continent: SafeAccess.getNestedValue(flow, 'source.geo.continent', 'unknown'), destination_ip: SafeAccess.getNestedValue(flow, 'destination.ip', 'unknown'), destination_country: SafeAccess.getNestedValue(flow, 'destination.geo.country', 'unknown'), destination_city: SafeAccess.getNestedValue(flow, 'destination.geo.city', 'unknown'), destination_continent: SafeAccess.getNestedValue(flow, 'destination.geo.continent', 'unknown'), protocol: SafeAccess.getNestedValue(flow, 'protocol', 'unknown'), // bytes field is calculated as total traffic: download + upload bytes: SafeAccess.getNestedValue(flow, 'download', 0) + SafeAccess.getNestedValue(flow, 'upload', 0), blocked: SafeAccess.getNestedValue(flow, 'block', false), direction: SafeAccess.getNestedValue(flow, 'direction', 'unknown'), device: SafeAccess.getNestedValue(flow, 'device', {}), })); // Apply geographic enrichment pipeline for IP addresses processedFlows = await this.enrichGeoIfNeeded(processedFlows, [ 'source_ip', 'destination_ip', ]); // Create metadata for standardized response const metadata = { query: SafeAccess.getNestedValue(result, 'query', searchArgs.query || ''), entityType: 'flows', executionTime: SafeAccess.getNestedValue(result, 'execution_time_ms', executionTime), cached: false, cursor: result.next_cursor, hasMore: !!result.next_cursor, limit: searchArgs.limit, aggregations: SafeAccess.getNestedValue(result, 'aggregations', null), }; // Create unified response with standardized metadata const unifiedResponseData = { flows: processedFlows, metadata, query_info: { original_query: searchArgs.query, final_query: finalQuery, applied_filters: { geographic: !!searchArgs.geographic_filters, time_range: !!searchArgs.time_range, analytics: !!searchArgs.include_analytics, }, }, }; // Return unified response return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout from timeout-manager ); } // Handle retry failure errors with enhanced context if (error instanceof Error && error.name === 'RetryFailureError') { const { retryContext } = error; const { userGuidance } = error; return createErrorResponse(this.name, `Search flows operation failed after ${retryContext?.attempts || 'multiple'} attempts: ${error.message}`, ErrorType.SEARCH_ERROR, { retry_attempts: retryContext?.attempts, total_duration_ms: retryContext?.totalDurationMs, final_error: retryContext?.originalError instanceof Error ? retryContext.originalError.message : 'Unknown error', }, userGuidance || [ 'Multiple retry attempts failed', 'Try reducing the scope of your search query', 'Check network connectivity and try again later', ]); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return createErrorResponse(this.name, `Failed to search flows: ${errorMessage}`, ErrorType.SEARCH_ERROR); } } } export class SearchAlarmsHandler extends BaseToolHandler { constructor() { // Enable full standardization: geographic enrichment and field normalization for security alarms super({ enableGeoEnrichment: true, // Security alarms often contain IP addresses that require geographic enrichment enableFieldNormalization: true, // Ensure consistent snake_case field naming across all responses additionalMeta: { data_source: 'alarms', entity_type: 'security_alarms', supports_geographic_enrichment: true, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'search_alarms'; this.description = `Security alarm searching with powerful filtering and enhanced reliability. Data cached for 15 seconds, use force_refresh=true for real-time security data. Search through security alerts and alarms using flexible query syntax to identify threats and suspicious activities. Features automatic boolean query translation, enhanced schema harmonization with device information, and improved alarm ID resolution for seamless integration with get_specific_alarm and delete_alarm. REQUIRED PARAMETERS: - query: Search query string using alarm field syntax OPTIONAL PARAMETERS: - limit: Maximum number of results to return (default: 200, max: 500) - force_refresh: Bypass cache for real-time data (default: false) - cursor: Pagination cursor from previous response - sort_by: Field to sort results by - aggregate: Enable aggregation statistics QUERY EXAMPLES (with automatic boolean translation): - Boolean status (both syntaxes supported): "resolved:true" OR "resolved=true", "acknowledged:false" OR "acknowledged=false" (automatically converted to backend format) - IP-based searches: "source_ip:192.168.1.100", "destination_ip:10.0.*" - Type filtering: "type:8", "type:9", "type:10" (use numeric alarm types) - Time-based: "timestamp:>=2024-01-01", "last_24_hours:true" - Complex combinations: "type:8 AND source_ip:192.168.* NOT resolved:true" CACHE CONTROL: - Default: 15-second cache for optimal performance - Real-time: Use force_refresh=true for incident response - Cache info included in responses for timing awareness COMMON USE CASES: - Active security alerts: "type:1 AND resolved:false" - Geographic threats: "country:China AND type:2" - Video/Gaming/Porn activity: "type:8 OR type:9 OR type:10" - VPN issues: "type:13" (VPN Connection Error) ERROR RECOVERY: - If no results, try broader time ranges or different type filters - Check field names against the API documentation - Use wildcards (*) for partial matches when exact queries fail See the Error Handling Guide for troubleshooting: /docs/error-handling-guide.md`; this.category = 'search'; } async execute(args, firewalla) { const searchArgs = args; const startTime = Date.now(); try { // Validate common search parameters const validation = validateCommonSearchParameters(searchArgs, this.name, 'alarms'); if (!validation.isValid) { return validation.response; } // Validate force_refresh parameter if provided const forceRefreshValidation = ParameterValidator.validateBoolean(searchArgs.force_refresh, 'force_refresh', false); if (!forceRefreshValidation.isValid) { return createErrorResponse(this.name, 'Force refresh parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, forceRefreshValidation.errors); } const searchTools = createSearchTools(firewalla); const searchParams = { query: searchArgs.query, limit: searchArgs.limit, offset: searchArgs.offset, cursor: searchArgs.cursor, sort_by: searchArgs.sort_by, sort_order: searchArgs.sort_order, group_by: searchArgs.group_by, aggregate: searchArgs.aggregate, time_range: searchArgs.time_range, force_refresh: forceRefreshValidation.sanitizedValue, }; const result = await withToolTimeout(async () => searchTools.search_alarms(searchParams), this.name); const executionTime = Date.now() - startTime; // Process alarm data with enhanced standardization and schema harmonization let processedAlarms = SafeAccess.safeArrayMap(result.results, (alarm) => { // Try to extract device information from various possible locations const deviceInfo = { id: SafeAccess.getNestedValue(alarm, 'device.id', SafeAccess.getNestedValue(alarm, 'deviceId', SafeAccess.getNestedValue(alarm, 'mac', 'unknown'))), name: SafeAccess.getNestedValue(alarm, 'device.name', SafeAccess.getNestedValue(alarm, 'deviceName', 'unknown')), ip: SafeAccess.getNestedValue(alarm, 'device.ip', SafeAccess.getNestedValue(alarm, 'deviceIp', SafeAccess.getNestedValue(alarm, 'ip', 'unknown'))), mac: SafeAccess.getNestedValue(alarm, 'device.mac', SafeAccess.getNestedValue(alarm, 'mac', 'unknown')), }; const rawAid = SafeAccess.getNestedValue(alarm, 'aid', null); // Use the actual alarm ID directly, properly handling 0 as a valid ID const finalAid = rawAid !== null && rawAid !== undefined ? String(rawAid) : 'unknown'; return { aid: finalAid, timestamp: unixToISOStringOrNow(alarm.ts), type: SafeAccess.getNestedValue(alarm, 'type', 'unknown'), message: SafeAccess.getNestedValue(alarm, 'message', 'No message'), direction: SafeAccess.getNestedValue(alarm, 'direction', 'unknown'), protocol: SafeAccess.getNestedValue(alarm, 'protocol', 'unknown'), status: SafeAccess.getNestedValue(alarm, 'status', 'unknown'), // Enhanced device information (only include if meaningful data found) device: deviceInfo.id !== 'unknown' || deviceInfo.name !== 'unknown' ? deviceInfo : undefined, // Extract IP addresses for potential geographic enrichment source_ip: SafeAccess.getNestedValue(alarm, 'remote.ip', SafeAccess.getNestedValue(alarm, 'source_ip', SafeAccess.getNestedValue(alarm, 'src', 'unknown'))), destination_ip: SafeAccess.getNestedValue(alarm, 'destination.ip', SafeAccess.getNestedValue(alarm, 'destination_ip', SafeAccess.getNestedValue(alarm, 'dst', 'unknown'))), }; }); // Apply geographic enrichment pipeline for IP addresses in alarms processedAlarms = await this.enrichGeoIfNeeded(processedAlarms, [ 'source_ip', 'destination_ip', ]); // Create metadata for standardized response const metadata = { query: SafeAccess.getNestedValue(result, 'query', searchArgs.query || ''), entityType: 'alarms', executionTime: SafeAccess.getNestedValue(result, 'execution_time_ms', executionTime), cached: false, cursor: result.next_cursor, hasMore: !!result.next_cursor, limit: searchArgs.limit, aggregations: SafeAccess.getNestedValue(result, 'aggregations', null), }; // Add schema harmonization warning for search vs active alarms const schemaNote = { warning: 'Search endpoint returns limited fields compared to get_active_alarms', recommendation: 'Use get_active_alarms for complete device and alarm information', differences: [ 'Device objects may not be fully populated in search results', "Some severity and status fields may show 'unknown' values", 'Geographic enrichment is applied but original data may be limited', ], }; // Create unified response with standardized metadata const unifiedResponseData = { alarms: processedAlarms, metadata, schema_harmonization: schemaNote, query_info: { original_query: searchArgs.query, applied_filters: { time_range: !!searchArgs.time_range, force_refresh: !!searchArgs.force_refresh, }, }, }; // Return unified response return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } 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 search alarms: ${errorMessage}`, ErrorType.SEARCH_ERROR); } } } export class SearchRulesHandler extends BaseToolHandler { constructor() { // Enable field normalization for firewall rules (no geographic enrichment needed) super({ enableGeoEnrichment: false, // Firewall rules don't typically contain IP addresses enableFieldNormalization: true, // Ensure consistent snake_case field naming across all responses additionalMeta: { data_source: 'rules', entity_type: 'firewall_rules', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'search_rules'; this.description = `Firewall rule searching with comprehensive filtering for actions, targets, and status. Search through firewall rules to manage policies, troubleshoot blocking issues, and analyze rule effectiveness. QUERY EXAMPLES: - Action filtering: "action:block", "action:allow", "action:timelimit" - Target searches: "target_value:*.facebook.com", "target_type:domain", "target_value:192.168.*" - Status queries: "enabled:true", "paused:false", "active:true" - Direction: "direction:inbound", "direction:outbound", "direction:bidirection" - Combined filters: "action:block AND target_value:*.social.* AND enabled:true" RULE MANAGEMENT EXAMPLES: - Social media blocks: "action:block AND (target_value:*.facebook.com OR target_value:*.twitter.com)" - Gaming restrictions: "action:timelimit AND target_category:gaming" - Security rules: "action:block AND target_type:malware_domain" - Active blocking rules: "action:block AND enabled:true AND paused:false" TROUBLESHOOTING: - Find conflicting rules: "target_value:example.com" (then check different actions) - Identify inactive rules: "enabled:false OR paused:true" - Review recent changes: "modified:>=yesterday" PERFORMANCE NOTES: - Rules are cached for 10 minutes for optimal performance - Use specific target_value searches for fastest results - Group by action or target_type for rule analysis For rule management operations, see pause_rule and resume_rule tools.`; this.category = 'search'; } async execute(args, firewalla) { const searchArgs = args; const startTime = Date.now(); try { // Validate common search parameters const validation = validateCommonSearchParameters(searchArgs, this.name, 'rules'); if (!validation.isValid) { return validation.response; } const searchTools = createSearchTools(firewalla); const searchParams = { query: searchArgs.query, limit: searchArgs.limit, offset: searchArgs.offset, cursor: searchArgs.cursor, sort_by: searchArgs.sort_by, sort_order: searchArgs.sort_order, group_by: searchArgs.group_by, aggregate: searchArgs.aggregate, }; const result = await withToolTimeout(async () => searchTools.search_rules(searchParams), this.name); const executionTime = Date.now() - startTime; // Process rule data const processedRules = SafeAccess.safeArrayMap(result.results, (rule) => ({ id: SafeAccess.getNestedValue(rule, 'id', 'unknown'), action: SafeAccess.getNestedValue(rule, 'action', 'unknown'), target_type: SafeAccess.getNestedValue(rule, 'target.type', 'unknown'), target_value: SafeAccess.getNestedValue(rule, 'target.value', 'unknown'), direction: SafeAccess.getNestedValue(rule, 'direction', 'unknown'), status: SafeAccess.getNestedValue(rule, 'status', 'unknown'), hit_count: SafeAccess.getNestedValue(rule, 'hit.count', 0), })); // Create metadata for standardized response const metadata = { query: SafeAccess.getNestedValue(result, 'query', searchArgs.query || ''), entityType: 'rules', executionTime: SafeAccess.getNestedValue(result, 'execution_time_ms', executionTime), cached: false, cursor: result.next_cursor, hasMore: !!result.next_cursor, limit: searchArgs.limit, aggregations: SafeAccess.getNestedValue(result, 'aggregations', null), }; // Create unified response with standardized metadata const unifiedResponseData = { rules: processedRules, metadata, query_info: { original_query: searchArgs.query, applied_filters: { grouping: !!searchArgs.group_by, sorting: !!searchArgs.sort_by, aggregation: !!searchArgs.aggregate, }, }, }; // Return unified response return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } 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 search rules: ${errorMessage}`, ErrorType.SEARCH_ERROR); } } } export class SearchDevicesHandler extends BaseToolHandler { constructor() { super(...arguments); this.name = 'search_devices'; this.description = `Network device searching with comprehensive filtering for status, usage patterns, and network properties. Data cached for 5 minutes, use force_refresh=true for real-time device status. Search through network devices to monitor connectivity, identify issues, and analyze usage patterns. REQUIRED PARAMETERS: - query: Search query string using device field syntax OPTIONAL PARAMETERS: - limit: Maximum number of results to return (default: 200, max: 500) - force_refresh: Bypass cache for real-time status (default: false) - cursor: Pagination cursor from previous response - time_range: Time window for search (start/end timestamps) - sort_by: Field to sort results by - group_by: Field to group results by for aggregation - aggregate: Enable aggregation statistics QUERY EXAMPLES: - Status filtering: "online:true", "online:false", "last_seen:>=yesterday" - Device identification: "mac_vendor:Apple", "name:*iPhone*", "ip:192.168.1.*" - Network properties: "network_id:main", "dhcp:true", "static_ip:true" - Usage patterns: "bandwidth_usage:>1000000", "active_connections:>10" - Device types: "device_type:smartphone", "os_type:iOS", "manufacturer:Samsung" CACHE CONTROL: - Default: 5-minute cache for optimal performance - Real-time: Use force_refresh=true for device troubleshooting - Cache info included in responses for timing awareness NETWORK MONITORING: - Offline devices: "online:false AND last_seen:>=24h" (recently offline) - Heavy bandwidth users: "bandwidth_usage:>5000000 AND online:true" - Unknown devices: "name:unknown OR mac_vendor:unknown" - Mobile devices: "device_type:smartphone OR device_type:tablet" - IoT devices: "device_category:IoT OR manufacturer:smart_*" TROUBLESHOOTING: - Connection issues: "online:false AND dhcp_errors:>0" - Security concerns: "new_device:true AND trust_level:low" - Performance problems: "packet_loss:>5 OR latency:>100" PAGINATION: - Use cursor-based pagination for large device lists - Supports up to 10,000 devices per query - Include offline devices with include_offline:true FIELD CONSISTENCY: - Device names normalized to remove unknown/null inconsistencies - IP addresses validated and standardized - Timestamps converted to ISO format for consistency See the Data Normalization Guide for field details.`; this.category = 'search'; } async execute(args, firewalla) { const searchArgs = args; try { // Validate common search parameters const validation = validateCommonSearchParameters(searchArgs, this.name, 'devices'); if (!validation.isValid) { return validation.response; } // Validate that both cursor and offset are not provided simultaneously if (searchArgs.cursor !== undefined && searchArgs.offset !== undefined) { return createErrorResponse(this.name, 'Cannot provide both cursor and offset parameters simultaneously', ErrorType.VALIDATION_ERROR, { provided_cursor: searchArgs.cursor, provided_offset: searchArgs.offset, documentation: 'Use either cursor-based pagination (cursor) or offset-based pagination (offset), but not both', }, ['cursor and offset parameters are mutually exclusive']); } // Validate force_refresh parameter if provided const forceRefreshValidation = ParameterValidator.validateBoolean(searchArgs.force_refresh, 'force_refresh', false); if (!forceRefreshValidation.isValid) { return createErrorResponse(this.name, 'Force refresh parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, forceRefreshValidation.errors); } const searchTools = createSearchTools(firewalla); const searchParams = { query: searchArgs.query, limit: searchArgs.limit, offset: searchArgs.offset, cursor: searchArgs.cursor, sort_by: searchArgs.sort_by, sort_order: searchArgs.sort_order, group_by: searchArgs.group_by, aggregate: searchArgs.aggregate, time_range: searchArgs.time_range, force_refresh: forceRefreshValidation.sanitizedValue, }; const result = await withToolTimeout(async () => searchTools.search_devices(searchParams), this.name); // Process and enrich device data with geographic information const deviceData = await this.enrichGeoIfNeeded(SafeAccess.safeArrayMap(result.results, (device) => ({ id: SafeAccess.getNestedValue(device, 'id', 'unknown'), name: SafeAccess.getNestedValue(device, 'name', 'Unknown Device'), ip: SafeAccess.getNestedValue(device, 'ip', 'unknown'), online: SafeAccess.getNestedValue(device, 'online', false), macVendor: SafeAccess.getNestedValue(device, 'macVendor', 'unknown'), lastSeen: SafeAccess.getNestedValue(device, 'lastSeen', 0), })), ['ip'] // Enrich the device IP addresses ); const unifiedResponseData = { devices: deviceData, count: deviceData.length, query_executed: SafeAccess.getNestedValue(result, 'query', ''), execution_time_ms: SafeAccess.getNestedValue(result, 'execution_time_ms', 0), aggregations: SafeAccess.getNestedValue(result, 'aggregations', null), query_info: { original_query: searchArgs.query, applied_filters: { time_range: !!searchArgs.time_range, force_refresh: !!searchArgs.force_refresh, cursor_pagination: !!searchArgs.cursor, offset_pagination: !!searchArgs.offset, }, }, }; // Return unified response return this.createUnifiedResponse(unifiedResponseData); } 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 search devices: ${errorMessage}`, ErrorType.SEARCH_ERROR); } } } export class SearchTargetListsHandler extends BaseToolHandler { constructor() { // Enable field normalization for target lists (no geographic enrichment needed) super({ enableGeoEnrichment: false, // Target lists don't typically contain IP addresses enableFieldNormalization: true, // Ensure consistent snake_case field naming across all responses additionalMeta: { data_source: 'target_lists', entity_type: 'target_lists', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'search_target_lists'; this.description = `Target list searching with comprehensive filtering for categories, ownership, and content analysis. Search through Firewalla target lists including domains, IPs, and security categories for policy management and analysis. QUERY EXAMPLES: - Category filtering: "category:ad", "category:social_media", "category:malware" - Ownership: "owner:global", "owner:custom", "owner:user_defined" - Content type: "type:domain", "type:ip", "type:url_pattern" - Size filtering: "target_count:>100", "active_targets:>50" - Status queries: "enabled:true", "updated:>=2024-01-01" TARGET LIST MANAGEMENT: - Ad blocking lists: "category:ad AND enabled:true" - Security lists: "category:malware OR category:phishing OR category:threat" - Social media controls: "category:social_media AND owner:custom" - Custom domain lists: "owner:user_defined AND type:domain" - Large lists analysis: "target_count:>1000 AND category:security" CONTENT ANALYSIS: - Popular categories: group_by:"category" for category distribution - List effectiveness: "hit_count:>0 AND enabled:true" - Maintenance needed: "updated:<=30d AND enabled:true" - Unused lists: "hit_count:0 AND enabled:true" PERFORMANCE CONSIDERATIONS: - Target lists cached for 10 minutes for optimal performance - Use specific category filters for faster searches - Large lists (>10,000 targets) may have slower response times - Aggregate queries provide faster overview statistics FIELD NORMALIZATION: - Categories standardized to lowercase with consistent naming - Target counts validated as non-negative numbers - Timestamps normalized to ISO format - Unknown values replaced with "unknown" for consistency See the Target List Management guide for configuration details.`; this.category = 'search'; } async execute(args, firewalla) { const searchArgs = args; try { // Validate common search parameters const validation = validateCommonSearchParameters(searchArgs, this.name, 'target_lists'); if (!validation.isValid) { return validation.response; } const searchTools = createSearchTools(firewalla); const searchParams = { query: searchArgs.query, limit: searchArgs.limit, offset: searchArgs.offset, cursor: searchArgs.cursor, sort_by: searchArgs.sort_by, sort_order: searchArgs.sort_order, group_by: searchArgs.group_by, aggregate: searchArgs.aggregate, }; const result = await withToolTimeout(async () => searchTools.search_target_lists(searchParams), this.name); // Create unified response with standardized target list data const unifiedResponseData = { target_lists: SafeAccess.safeArrayMap(result.results, (list) => ({ id: SafeAccess.getNestedValue(list, 'id', 'unknown'), name: SafeAccess.getNestedValue(list, 'name', 'Unknown List'), category: SafeAccess.getNestedValue(list, 'category', 'unknown'), owner: SafeAccess.getNestedValue(list, 'owner', 'unknown'), entry_count: SafeAccess.safeArrayAccess(list.targets, arr => arr.length, 0), })), count: SafeAccess.safeArrayAccess(result.results, arr => arr.length, 0), query_executed: SafeAccess.getNestedValue(result, 'query', ''), execution_time_ms: SafeAccess.getNestedValue(result, 'execution_time_ms', 0), aggregations: SafeAccess.getNestedValue(result, 'aggregations', null), query_info: { original_query: searchArgs.query, applied_filters: { grouping: !!searchArgs.group_by, sorting: !!searchArgs.sort_by, aggregation: !!searchArgs.aggregate, }, }, }; // Return unified response return this.createUnifiedResponse(unifiedResponseData); } 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 search target lists: ${errorMessage}`, ErrorType.SEARCH_ERROR); } } } export class SearchCrossReferenceHandler extends BaseToolHandler { constructor() { // Enable field normalization for cross-reference searches (no geographic enrichment needed) super({ enableGeoEnrichment: false, // Cross-reference tools work on metadata, not IP addresses directly enableFieldNormalization: true, // Ensure consistent snake_case field naming across all responses additionalMeta: { data_source: 'cross_reference', entity_type: 'correlation_data', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'search_cross_reference'; this.description = 'Multi-entity searches with correlation across different data types'; this.category = 'search'; } async execute(args, firewalla) { const searchArgs = args; try { const searchTools = createSearchTools(firewalla); const result = await withToolTimeout(async () => searchTools.search_cross_reference({ primary_query: searchArgs.primary_query, secondary_queries: searchArgs.secondary_queries, correlation_field: searchArgs.correlation_field, limit: searchArgs.limit, }), this.name); // Create unified response with standardized correlation data const unifiedResponseData = { primary_query: SafeAccess.getNestedValue(result, 'primary.query', ''), primary_results: SafeAccess.getNestedValue(result, 'primary.count', 0), correlations: SafeAccess.safeArrayMap(result.correlations, (corr) => ({ query: SafeAccess.getNestedValue(corr, 'query', ''), matches: SafeAccess.getNestedValue(corr, 'count', 0), correlation_field: SafeAccess.getNestedValue(corr, 'correlation_field', ''), })), correlation_summary: SafeAccess.getNestedValue(result, 'correlation_summary', {}), execution_time_ms: SafeAccess.getNestedValue(result, 'execution_time_ms', 0), query_info: { primary_query: searchArgs.primary_query, secondary_queries: searchArgs.secondary_queries, correlation_field: searchArgs.correlation_field, }, }; // Return unified response return this.createUnifiedResponse(unifiedResponseData); } catch (error) { if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return this.createErrorResponse(`Failed to search cross reference: ${errorMessage}`); } } } export class SearchEnhancedCrossReferenceHandler extends BaseToolHandler { constructor() { // Enable field normalization for enhanced cross-reference searches (no geographic enrichment needed) super({ enableGeoEnrichment: false, // Enhanced cross-reference tools work on metadata and correlations enableFieldNormalization: true, // Ensure consistent snake_case field naming across all responses additionalMeta: { data_source: 'enhanced_cross_reference', entity_type: 'enhanced_correlation_data', supports_geographic_enrichment: false, supports_field_normalization: true, standardization_version: '2.0.0', }, }); this.name = 'search_enhanced_cross_reference'; this.description = 'Advanced multi-field correlation with temporal windows and network scoping'; this.category = 'search'; } async execute(args, firewalla) { const searchArgs = args; try { const searchTools = createSearchTools(firewalla); const result = await withToolTimeout(async () => searchTools.search_enhanced_cross_reference({ primary_query: searchArgs.primary_query, secondary_queries: searchArgs.secondary_queries, correlation_params: searchArgs.correlation_params, limit: searchArgs.limit, }), this.name); // Simplified correlation response structure for better user experience const simplifiedResponse = { // Basic query information query_info: { primary_query: SafeAccess.getNestedValue(result, 'primary.query', ''), secondary_queries: SafeAccess.safeArrayMap(SafeAccess.getNestedValue(result, 'correlations', []), (corr) => SafeAccess.getNestedValue(corr, 'query', '')), correlation_method: SafeAccess.getNestedValue(result, 'correlation_summary.correlation_type', 'AND'), correlation_fields: SafeAccess.getNestedValue(result, 'correlation_summary.correlation_fields', []).join(', '), }, // Summary statistics in simple format summary: { primary_results_count: SafeAccess.getNestedValue(result, 'primary.count', 0), total_correlated_items: SafeAccess.getNestedValue(result, 'correlation_summary.total_correlated_count', 0), correlations_found: SafeAccess.safeArrayAccess(SafeAccess.getNestedValue(result, 'correlations', []), arr => arr.length, 0), execution_time_ms: SafeAccess.getNestedValue(result, 'execution_time_ms', 0), temporal_filtering_used: SafeAccess.getNestedValue(result, 'correlation_summary.temporal_window_applied', false), }, // Simplified correlation results - focus on actionable information correlations: SafeAccess.safeArrayMap(SafeAccess.getNestedValue(result, 'correlations', []), (correlation) => { const correlationResults = SafeAccess.getNestedValue(correlation, 'results', []); const topResults = correlationResults.slice(0, 5); // Show top 5 matches only