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
JavaScript
/**
* 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