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
1,934 lines • 90.7 kB
JavaScript
/**
* Search Tools Implementation for Firewalla MCP Server
* Provides advanced search capabilities across all entity types
*/
import { queryParser } from '../search/parser.js';
import { filterFactory } from '../search/filters/index.js';
import { translateBooleanQuery } from '../utils/simple-boolean-translator.js';
import { ParameterValidator, SafeAccess } from '../validation/error-handler.js';
import { EnhancedQueryValidator } from '../validation/enhanced-query-validator.js';
import { validateEnhancedCrossReference, suggestEntityType, getSupportedCorrelationCombinations, getFieldValue, FIELD_MAPPINGS, } from '../validation/field-mapper.js';
import { FieldValidator } from '../validation/field-validator.js';
import { ErrorFormatter } from '../validation/error-formatter.js';
import { validateCountryCodes, enrichObjectWithGeo, } from '../utils/geographic.js';
import { correlateResults } from '../validation/enhanced-correlation.js';
/**
* Default search configuration that can be overridden via environment variables or configuration
*/
const DEFAULT_SEARCH_CONFIG = {
riskThresholds: {
lowMax: parseInt(process.env.RISK_THRESHOLD_LOW_MAX || '3', 10),
mediumMax: parseInt(process.env.RISK_THRESHOLD_MEDIUM_MAX || '6', 10),
highMax: parseInt(process.env.RISK_THRESHOLD_HIGH_MAX || '8', 10),
highRiskCountryMin: parseInt(process.env.RISK_THRESHOLD_COUNTRY_MIN || '6', 10),
highRiskFlowMin: parseInt(process.env.RISK_THRESHOLD_FLOW_MIN || '7', 10),
suspiciousAsnMin: parseInt(process.env.RISK_THRESHOLD_ASN_MIN || '7', 10),
},
performance: {
correlationTimeoutMs: parseInt(process.env.CORRELATION_TIMEOUT_MS || '30000', 10),
maxCorrelationResults: parseInt(process.env.MAX_CORRELATION_RESULTS || '1000', 10),
cacheExpirationMs: parseInt(process.env.CACHE_EXPIRATION_MS || '300000', 10), // 5 minutes
},
};
/**
* Current search configuration (can be updated at runtime)
*/
let currentSearchConfig = DEFAULT_SEARCH_CONFIG;
/**
* Update search configuration at runtime
*/
export function updateSearchConfig(newConfig) {
currentSearchConfig = {
...currentSearchConfig,
...newConfig,
riskThresholds: {
...currentSearchConfig.riskThresholds,
...(newConfig.riskThresholds || {}),
},
performance: {
...currentSearchConfig.performance,
...(newConfig.performance || {}),
},
};
}
/**
* Get current search configuration
*/
export function getSearchConfig() {
return currentSearchConfig;
}
/**
* Get risk thresholds (backward compatibility)
*/
function getRiskThresholds() {
return currentSearchConfig.riskThresholds;
}
/**
* Search Engine for executing complex queries
*/
export class SearchEngine {
constructor(firewalla) {
this.firewalla = firewalla;
this.strategies = new Map();
this.initializeStrategies();
}
/**
* Validate basic search parameters
*/
validateBasicSearchParams(params, _methodName) {
if (!params ||
typeof params !== 'object' ||
!params.query ||
typeof params.query !== 'string' ||
!params.query.trim()) {
throw new Error('Parameters object with query property is required and query must be a non-empty string');
}
}
/**
* Validate correlation field count
*/
validateCorrelationFieldCount(fields, maxFields = 5) {
const fieldArray = Array.isArray(fields)
? fields
: fields.split(',').map(f => f.trim());
if (fieldArray.length > maxFields) {
const excessFields = fieldArray.slice(maxFields);
throw new Error(`Maximum ${maxFields} correlation fields allowed, but ${fieldArray.length} provided. Please remove these fields: ${excessFields.join(', ')}`);
}
}
/**
* Validate geographic filters and return a sanitized (cloned) version.
* This avoids mutating the caller-supplied object which could lead to
* subtle side-effects elsewhere in the codebase.
*/
validateGeographicFilters(geographic_filters) {
if (geographic_filters !== undefined &&
(geographic_filters === null || typeof geographic_filters !== 'object')) {
throw new Error('geographic_filters must be an object if provided');
}
if (!geographic_filters) {
// undefined or null – nothing to validate, return as-is
return geographic_filters;
}
// Work on a shallow clone so the original reference is untouched
const sanitized = { ...geographic_filters };
/* ---------- Country codes ---------- */
if (sanitized.countries?.length) {
const countryValidation = validateCountryCodes(sanitized.countries);
if (countryValidation.invalid.length > 0) {
throw new Error(`Country code validation failed: Invalid country codes: ${countryValidation.invalid.join(', ')}`);
}
sanitized.countries = countryValidation.valid;
}
/* ---------- Array fields ---------- */
const arrayFields = [
'continents',
'regions',
'cities',
'asns',
'hosting_providers',
];
for (const field of arrayFields) {
if (sanitized[field] !== undefined) {
if (!Array.isArray(sanitized[field])) {
throw new Error(`${field} must be an array if provided`);
}
const invalidValues = sanitized[field].filter((value) => typeof value !== 'string' || value.trim() === '');
if (invalidValues.length > 0) {
throw new Error(`${field} array contains invalid values: must be non-empty strings`);
}
}
}
/* ---------- Boolean fields ---------- */
const booleanFields = ['exclude_vpn', 'exclude_cloud'];
for (const field of booleanFields) {
if (sanitized[field] !== undefined &&
typeof sanitized[field] !== 'boolean') {
throw new Error(`${field} must be a boolean if provided`);
}
}
/* ---------- Numeric fields ---------- */
if (sanitized.min_risk_score !== undefined) {
if (typeof sanitized.min_risk_score !== 'number' ||
sanitized.min_risk_score < 0 ||
sanitized.min_risk_score > 10) {
throw new Error('min_risk_score must be a number between 0 and 10 if provided');
}
}
return sanitized;
}
/**
* Helper function to extract and apply query filters from a query string
* Extracts common patterns like field:value and applies them to filter results
*/
applyQueryFilters(results, query, fieldExtractors) {
if (!query || typeof query !== 'string') {
return results;
}
let filteredResults = results;
// Apply each field extractor to filter results
for (const [fieldPattern, filterFunction] of Object.entries(fieldExtractors)) {
const match = query.match(new RegExp(`${fieldPattern}:([^\\s]+)`, 'i'));
if (match) {
const extractedValue = match[1];
filteredResults = filterFunction(filteredResults, extractedValue);
}
}
return filteredResults;
}
/**
* Initialize search strategies for different entity types
*/
initializeStrategies() {
this.strategies.set('flows', {
entityType: 'flows',
executeApiCall: async (client, params, apiParams, searchOptions) => {
// Use getFlowData instead of searchFlows since it handles parameters better
let queryString = params.query;
// Add time range to query if provided
if (searchOptions.time_range?.start && searchOptions.time_range?.end) {
const startDate = new Date(searchOptions.time_range.start);
const endDate = new Date(searchOptions.time_range.end);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new Error('Invalid time range format');
}
if (startDate >= endDate) {
throw new Error('Start time must be before end time');
}
const startTs = Math.floor(startDate.getTime() / 1000);
const endTs = Math.floor(endDate.getTime() / 1000);
queryString = `ts:${startTs}-${endTs} AND (${params.query})`;
}
// Use getFlowData which works reliably
return client.getFlowData(queryString, params.group_by, 'ts:desc', apiParams.limit);
},
});
this.strategies.set('alarms', {
entityType: 'alarms',
executeApiCall: async (client, params, apiParams, _searchOptions) => {
// Pass the full query including any severity filters
return client.getActiveAlarms(apiParams.queryString || params.query || undefined, params.group_by, params.sort_by || 'timestamp:desc', params.limit, params.cursor);
},
processResults: (results, params) => {
let filteredResults = results;
// Client-side filtering to ensure results match query criteria
if (params.query && typeof params.query === 'string') {
// Extract severity filter from query (e.g., "severity:medium")
const severityMatch = params.query.match(/severity:(high|medium|low|critical)/i);
if (severityMatch) {
const expectedSeverity = severityMatch[1].toLowerCase();
// Map severity names to their string values for filtering
const severityMapping = {
low: ['low'],
medium: ['medium'],
high: ['high'],
critical: ['critical'],
};
const validSeverities = severityMapping[expectedSeverity] || [
expectedSeverity,
];
filteredResults = results.filter(alarm => alarm.severity &&
validSeverities.includes(alarm.severity.toLowerCase()));
}
// Extract type filter from query (e.g., "type:1" or "type:>=4")
const typeMatch = params.query.match(/type:([><=]*\d+)/i);
if (typeMatch) {
const typeExpression = typeMatch[1];
if (typeExpression.startsWith('>=')) {
const minType = parseInt(typeExpression.substring(2));
filteredResults = filteredResults.filter(alarm => alarm.type && parseInt(String(alarm.type)) >= minType);
}
else if (typeExpression.startsWith('<=')) {
const maxType = parseInt(typeExpression.substring(2));
filteredResults = filteredResults.filter(alarm => alarm.type && parseInt(String(alarm.type)) <= maxType);
}
else if (typeExpression.startsWith('>')) {
const minType = parseInt(typeExpression.substring(1));
filteredResults = filteredResults.filter(alarm => alarm.type && parseInt(String(alarm.type)) > minType);
}
else if (typeExpression.startsWith('<')) {
const maxType = parseInt(typeExpression.substring(1));
filteredResults = filteredResults.filter(alarm => alarm.type && parseInt(String(alarm.type)) < maxType);
}
else {
const exactType = parseInt(typeExpression);
filteredResults = filteredResults.filter(alarm => alarm.type && parseInt(String(alarm.type)) === exactType);
}
}
// Extract status filter from query (e.g., "status:1" or "resolved:true")
const statusMatch = params.query.match(/status:(\d+)/i);
if (statusMatch) {
const expectedStatus = parseInt(statusMatch[1]);
filteredResults = filteredResults.filter(alarm => alarm.status &&
parseInt(String(alarm.status)) === expectedStatus);
}
const resolvedMatch = params.query.match(/resolved:(true|false)/i);
if (resolvedMatch) {
const isResolved = resolvedMatch[1].toLowerCase() === 'true';
// Assuming resolved means status === 2 (based on common patterns)
filteredResults = filteredResults.filter(alarm => {
const status = parseInt(String(alarm.status));
return isResolved ? status === 2 : status !== 2;
});
}
// Extract source_ip filter from query (e.g., "source_ip:192.168.1.1")
const sourceIpMatch = params.query.match(/source_ip:([^\s]+)/i);
if (sourceIpMatch) {
const expectedIp = sourceIpMatch[1];
// Support wildcard matching for IP addresses
if (expectedIp.includes('*')) {
const pattern = expectedIp.replace(/\*/g, '.*');
const regex = new RegExp(pattern, 'i');
filteredResults = filteredResults.filter(alarm => {
const sourceIp = alarm.remote?.ip || alarm.device?.ip || '';
return regex.test(sourceIp);
});
}
else {
filteredResults = filteredResults.filter(alarm => {
const sourceIp = alarm.remote?.ip || alarm.device?.ip || '';
return sourceIp.includes(expectedIp);
});
}
}
}
if (params.limit) {
return filteredResults.slice(0, params.limit);
}
return filteredResults;
},
});
this.strategies.set('rules', {
entityType: 'rules',
executeApiCall: async (client, params, _apiParams, _searchOptions) => {
// Use reasonable limit for search operations to prevent memory issues
// Fetch 2x the requested limit (capped at 2000) to account for post-processing filters
const searchLimit = params.limit
? Math.min(params.limit * 2, 2000)
: 2000;
return client.getNetworkRules(params.query, searchLimit);
},
processResults: (results, params) => {
// Use the helper function to apply query filters
const filteredResults = this.applyQueryFilters(results, params.query || '', {
action: (items, value) => {
const expectedAction = value.toLowerCase();
return items.filter(rule => rule.action?.toLowerCase() === expectedAction);
},
target_value: (items, value) => {
const expectedTarget = value.toLowerCase();
// Support wildcard matching
if (expectedTarget.includes('*')) {
const pattern = expectedTarget.replace(/\*/g, '.*');
const regex = new RegExp(pattern, 'i');
return items.filter(rule => regex.test(rule.target?.value?.toLowerCase() || ''));
}
return items.filter(rule => rule.target?.value?.toLowerCase().includes(expectedTarget));
},
status: (items, value) => {
const expectedStatus = value.toLowerCase();
return items.filter(rule => rule.status?.toLowerCase() === expectedStatus);
},
});
if (params.limit) {
return filteredResults.slice(0, params.limit);
}
return filteredResults;
},
});
this.strategies.set('devices', {
entityType: 'devices',
executeApiCall: async (client, params, _apiParams, searchOptions) => {
const searchQuery = {
query: params.query,
limit: params.limit,
cursor: params?.cursor,
sort_by: params?.sort_by,
group_by: params?.group_by,
aggregate: params?.aggregate,
};
// Add time range to searchOptions if provided
if (params.time_range?.start && params.time_range?.end) {
searchOptions.time_range = {
start: params.time_range.start,
end: params.time_range.end,
};
}
return client.searchDevices(searchQuery, searchOptions);
},
processResults: (results, params) => {
if (params.offset && !params.cursor) {
// Legacy offset support - only if cursor not provided
let processedResults = results.slice(params.offset);
if (params.limit) {
processedResults = processedResults.slice(0, params.limit);
}
return processedResults;
}
return results;
},
});
this.strategies.set('target_lists', {
entityType: 'target_lists',
executeApiCall: async (client, _params, _apiParams, _searchOptions) => {
return client.getTargetLists();
},
processResults: (results, params) => {
if (params.limit) {
return results.slice(0, params.limit);
}
return results;
},
});
}
/**
* Shared helper for validating limit parameter across all search methods
* @param limit - The limit value to validate
* @param entityType - The entity type for context-specific limits
* @param options - Additional validation options
*/
validateLimitParameter(limit, entityType, options = {}) {
const { required = true, defaultLimit = 1000, maxLimit = entityType === 'flows'
? 1000
: entityType === 'cross_reference'
? 5000
: 1000, } = options;
const actualLimit = limit ?? defaultLimit;
const validation = ParameterValidator.validateNumber(actualLimit, 'limit', {
required,
min: 1,
max: maxLimit,
integer: true,
});
return {
isValid: validation.isValid,
errors: validation.errors,
validatedLimit: actualLimit,
};
}
/**
* Standardized parameter validation for all search operations
*/
validateSearchParams(params, entityType, config) {
const errors = [];
// Validate params is not null/undefined
if (!params || typeof params !== 'object') {
errors.push('Parameters object is required');
}
if (errors.length > 0) {
throw new Error(`Parameter validation failed: ${errors.join(', ')}`);
}
// Validate query parameter
if (config.requireQuery !== false) {
const queryValidation = ParameterValidator.validateRequiredString(params?.query, 'query');
if (!queryValidation.isValid) {
errors.push(...queryValidation.errors);
}
else if (!config.allowEmptyQuery &&
!queryValidation.sanitizedValue?.trim()) {
errors.push('query cannot be empty');
}
}
// Validate limit parameter with consistent boundary checking
if (config.requireLimit !== false) {
const limitValidation = this.validateLimitParameter(params?.limit, entityType, {
required: true,
maxLimit: config.maxLimit || (entityType === 'flows' ? 1000 : 500),
});
if (!limitValidation.isValid) {
errors.push(...limitValidation.errors);
}
}
// Validate sort_by parameter if provided
if (params.sort_by !== undefined) {
const sortValidation = ParameterValidator.validateOptionalString(params.sort_by, 'sort_by');
if (!sortValidation.isValid) {
errors.push(...sortValidation.errors);
}
}
// Validate group_by parameter if provided
if (params.group_by !== undefined) {
const groupValidation = ParameterValidator.validateOptionalString(params.group_by, 'group_by');
if (!groupValidation.isValid) {
errors.push(...groupValidation.errors);
}
}
// Validate cursor parameter if cursor is supported
if (config.supportsCursor && params.cursor !== undefined) {
const cursorValidation = ParameterValidator.validateOptionalString(params.cursor, 'cursor');
if (!cursorValidation.isValid) {
errors.push(...cursorValidation.errors);
}
}
// Validate time_range parameter if time range is supported
if (config.supportsTimeRange && params.time_range !== undefined) {
if (!params.time_range || typeof params.time_range !== 'object') {
errors.push('time_range must be an object with start and end properties');
}
else {
const { start, end } = params.time_range;
if (start !== undefined) {
const startDate = new Date(start);
if (isNaN(startDate.getTime())) {
errors.push('time_range.start must be a valid ISO 8601 date string');
}
}
if (end !== undefined) {
const endDate = new Date(end);
if (isNaN(endDate.getTime())) {
errors.push('time_range.end must be a valid ISO 8601 date string');
}
}
if (start && end) {
const startDate = new Date(start);
const endDate = new Date(end);
if (!isNaN(startDate.getTime()) &&
!isNaN(endDate.getTime()) &&
startDate >= endDate) {
errors.push('time_range.start must be before time_range.end');
}
}
}
}
if (errors.length > 0) {
throw new Error(`Parameter validation failed: ${errors.join(', ')}`);
}
}
/**
* Generic search execution method that handles common patterns
*/
async executeSearch(params, entityType, validationConfig = {}) {
const startTime = Date.now();
try {
// Get strategy for entity type
const strategy = this.strategies.get(entityType);
if (!strategy) {
throw new Error(`No search strategy found for entity type: ${entityType}`);
}
// Use standardized parameter validation
this.validateSearchParams(params, entityType, validationConfig);
// Enhanced query validation with detailed error messages and comprehensive checks
const enhancedValidation = EnhancedQueryValidator.validateQuery(params.query, entityType);
if (!enhancedValidation.isValid) {
// Try instance method for detailed position tracking if static method fails
const enhancedValidator = new EnhancedQueryValidator();
const detailedValidation = enhancedValidator.validateQuery(params.query, entityType);
// Use detailed errors if available, otherwise use standard errors
if (detailedValidation.detailedErrors?.length) {
const errorReport = ErrorFormatter.formatMultipleErrors(detailedValidation.detailedErrors);
const formattedText = ErrorFormatter.formatReportAsText(errorReport);
throw new Error(`Enhanced query validation failed:\n${formattedText}`);
}
else {
// Fallback to standard enhanced validation format
const errorDetails = [
...enhancedValidation.errors,
...(enhancedValidation.fieldIssues?.map(issue => `Field '${issue.field}': ${issue.issue}${issue.suggestion ? ` - ${issue.suggestion}` : ''}`) || []),
];
let errorMessage = `Query validation failed: ${errorDetails.join(', ')}`;
if (enhancedValidation.suggestions?.length) {
errorMessage += ` | Suggestions: ${enhancedValidation.suggestions.join(', ')}`;
}
if (enhancedValidation.correctedQuery) {
errorMessage += ` | Try: "${enhancedValidation.correctedQuery}"`;
}
throw new Error(errorMessage);
}
}
// Use corrected query if available
const finalQuery = enhancedValidation.correctedQuery ||
enhancedValidation.sanitizedValue ||
params.query;
// Validate entityType before parsing
const validEntityTypes = [
'flows',
'alarms',
'rules',
'devices',
'target_lists',
];
if (!validEntityTypes.includes(entityType)) {
throw new Error(`Invalid entity type: ${entityType}`);
}
const validation = queryParser.parse(finalQuery, entityType);
if (!validation.isValid || !validation.ast) {
// Provide enhanced error messages for syntax issues not caught by enhanced validator
let enhancedError = `Invalid query syntax: ${validation.errors.join(', ')}`;
if (validation.suggestions && validation.suggestions.length > 0) {
enhancedError += `\n\nSuggestions:\n${validation.suggestions.map(s => `• ${s}`).join('\n')}`;
}
throw new Error(enhancedError);
}
// Set up filter context (entityType already validated above)
const context = {
entityType: entityType,
apiParams: {},
postProcessing: [],
metadata: {
filtersApplied: [],
optimizations: [],
},
};
const filterResult = this.applyFiltersRecursively(validation.ast, context);
// Prepare API parameters
const apiParams = {
...filterResult.apiParams,
limit: params.limit,
start_time: params.time_range?.start,
end_time: params.time_range?.end,
queryString: filterResult.queryString,
};
// Prepare search options
const searchOptions = {};
if (entityType === 'devices') {
searchOptions.include_resolved = true;
}
// Time range handling is done within individual strategies to avoid duplication
// Execute API call using strategy
const response = await strategy.executeApiCall(this.firewalla, params, apiParams, searchOptions);
// Process results
let results = response.results || [];
// Apply post-processing filters
if (filterResult.postProcessing && results.length > 0) {
results = filterResult.postProcessing(results);
}
// Apply strategy-specific result processing
if (strategy.processResults) {
results = strategy.processResults(results, params);
}
// Apply sorting
if (params.sort_by) {
results = this.sortResults(results, params.sort_by, params.sort_order);
}
// Apply pagination (for non-cursor based)
if (params.offset && entityType !== 'devices') {
results = results.slice(params.offset);
}
// Generate aggregations
const aggregations = params.aggregate
? this.generateAggregations(results, params.group_by)
: undefined;
// Build result object
const result = {
results,
count: response.count || results.length,
limit: params.limit || 100,
offset: params.offset || 0,
query: finalQuery,
execution_time_ms: Date.now() - startTime,
aggregations,
};
// Add cursor for devices with proper typing
if (entityType === 'devices' && response.next_cursor) {
const resultWithCursor = result;
resultWithCursor.next_cursor = response.next_cursor;
}
return result;
}
catch (error) {
throw new Error(`${entityType} search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Execute a search query for flows using simplified implementation with direct API calls.
* Performs basic validation and uses getFlowData API directly for improved reliability.
* Supports time range filtering and proper limit enforcement.
*/
async searchFlows(params) {
const startTime = Date.now();
try {
// Validate basic search parameters
this.validateBasicSearchParams(params, 'searchFlows');
// Basic security check for dangerous patterns
const dangerousPatterns = [
/DROP\s+TABLE/i,
/<script/i,
/javascript:/i,
/data:text\/html/i,
];
if (dangerousPatterns.some(pattern => pattern.test(params.query))) {
throw new Error('Query validation failed: Query contains potentially dangerous content');
}
if (!params.limit ||
typeof params.limit !== 'number' ||
params.limit < 1 ||
params.limit > 1000) {
throw new Error('limit parameter is required and must be between 1 and 1000');
}
// Apply boolean field translation before building query string
const translatedQuery = translateBooleanQuery(params.query, 'flows');
// Build query string with time range if provided
let queryString = translatedQuery;
if (params.time_range?.start && params.time_range?.end) {
const startDate = new Date(params.time_range.start);
const endDate = new Date(params.time_range.end);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new Error('Parameter validation failed: time_range must contain valid ISO 8601 dates');
}
if (startDate >= endDate) {
throw new Error('time_range.start must be before time_range.end');
}
const startTs = Math.floor(startDate.getTime() / 1000);
const endTs = Math.floor(endDate.getTime() / 1000);
queryString = `ts:${startTs}-${endTs} AND (${translatedQuery})`;
}
// Add geographic filters if provided
if (params.geographic_filters) {
// Validate and obtain a sanitized copy
const sanitizedGeoFilters = this.validateGeographicFilters(params.geographic_filters);
const geographicQuery = this.buildGeographicQuery(sanitizedGeoFilters);
if (geographicQuery) {
queryString = queryString
? `${queryString} AND ${geographicQuery}`
: geographicQuery;
}
}
// Call API directly without complex validation/parsing
const response = await this.firewalla.getFlowData(queryString, params.group_by, params.sort_by || 'ts:desc', params.limit, params.cursor);
// Apply client-side offset if needed (for backward compatibility)
let results = response.results || [];
// Enrich results with geographic data (filtering out null entries)
results = results
.filter(flow => flow !== null && flow !== undefined)
.map(flow => enrichObjectWithGeo(flow));
if (params.offset && !params.cursor) {
results = results.slice(params.offset);
}
// Apply client-side limit enforcement to ensure exact limit compliance
if (results.length > params.limit) {
results = results.slice(0, params.limit);
}
// Add geographic analysis if requested
let geographicAnalysis;
if (params.include_analytics || params.geographic_filters) {
geographicAnalysis = this.analyzeGeographicData(results);
}
const result = {
results,
count: results.length,
limit: params.limit,
offset: params.offset || 0,
query: queryString,
execution_time_ms: Date.now() - startTime,
next_cursor: response.next_cursor,
};
// Add boolean translation debug info if translation was applied
if (translatedQuery !== params.query) {
result.boolean_translation = {
original_query: params.query,
translated_query: translatedQuery,
translation_applied: true,
};
}
// Add optional fields
if (geographicAnalysis) {
result.geographic_analysis = geographicAnalysis;
}
if (params.geographic_filters) {
result.geographic_filters_applied = true;
}
return result;
}
catch (error) {
throw new Error(`search_flows failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Execute a search query for alarms using simplified implementation with direct API calls.
* Performs basic validation and uses getActiveAlarms API directly for improved reliability.
* Supports query filtering and proper limit enforcement.
*/
async searchAlarms(params) {
const startTime = Date.now();
try {
// Validate basic search parameters
this.validateBasicSearchParams(params, 'searchFlows');
// Basic security check for dangerous patterns
const dangerousPatterns = [
/DROP\s+TABLE/i,
/<script/i,
/javascript:/i,
/data:text\/html/i,
];
if (dangerousPatterns.some(pattern => pattern.test(params.query))) {
throw new Error('Enhanced query validation failed: Query contains potentially dangerous content');
}
if (!params.limit ||
typeof params.limit !== 'number' ||
params.limit < 1 ||
params.limit > 5000) {
throw new Error('limit parameter is required and must be between 1 and 5000');
}
// Apply boolean field translation before building query string
const translatedQuery = translateBooleanQuery(params.query, 'alarms');
// Build query string with optional severity filter
let alarmQuery = translatedQuery;
if (params.severity) {
const sev = params.severity;
alarmQuery = alarmQuery
? `${alarmQuery} AND severity:${sev}`
: `severity:${sev}`;
}
// Call API directly without complex validation/parsing
const response = await this.firewalla.getActiveAlarms(alarmQuery, params.group_by, params.sort_by || 'timestamp:desc', params.limit, params.cursor);
// Apply client-side offset if needed (for backward compatibility)
let results = response.results || [];
// Enrich results with geographic data (filtering out null entries)
results = results
.filter(flow => flow !== null && flow !== undefined)
.map(flow => enrichObjectWithGeo(flow));
if (params.offset && !params.cursor) {
results = results.slice(params.offset);
}
// Apply client-side limit enforcement to ensure exact limit compliance
if (results.length > params.limit) {
results = results.slice(0, params.limit);
}
const result = {
results,
count: results.length,
limit: params.limit,
offset: params.offset || 0,
query: params.query,
execution_time_ms: Date.now() - startTime,
next_cursor: response.next_cursor,
};
// Add boolean translation debug info if translation was applied
if (translatedQuery !== params.query) {
result.boolean_translation = {
original_query: params.query,
translated_query: translatedQuery,
translation_applied: true,
};
}
return result;
}
catch (error) {
throw new Error(`search_alarms failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Execute a search query for rules
*/
async searchRules(params) {
return this.executeSearch(params, 'rules', {
requireQuery: true,
requireLimit: true,
maxLimit: 3000,
});
}
/**
* Execute a search query for devices using cursor-based pagination
*/
async searchDevices(params) {
return this.executeSearch(params, 'devices', {
requireQuery: true,
requireLimit: true,
supportsCursor: true,
supportsTimeRange: true,
maxLimit: 2000,
});
}
/**
* Execute a search query for target lists
*/
async searchTargetLists(params) {
return this.executeSearch(params, 'target_lists', {
requireQuery: true,
requireLimit: true,
maxLimit: 1000,
});
}
/**
* Execute search for a specific entity type and query
*/
async executeSearchByType(entityType, query, limit = 1000) {
switch (entityType) {
case 'flows':
return this.searchFlows({ query, limit });
case 'alarms':
return this.searchAlarms({ query, limit });
case 'rules':
return this.searchRules({ query, limit });
case 'devices':
return this.searchDevices({ query, limit });
case 'target_lists':
return this.searchTargetLists({ query, limit });
default:
return this.searchFlows({ query, limit });
}
}
/**
* Enhanced cross-reference search with multi-field correlation capabilities
*/
async enhancedCrossReferenceSearch(params) {
const startTime = Date.now();
try {
// Validate limit parameter
const limitValidation = this.validateLimitParameter(params.limit, 'cross_reference', {
defaultLimit: 1000,
maxLimit: 5000,
});
if (!limitValidation.isValid) {
throw new Error(`Parameter validation failed: ${limitValidation.errors.join(', ')}`);
}
const limit = limitValidation.validatedLimit;
// Validate correlation field count before other validations
if (params.correlation_params.correlationFields) {
try {
this.validateCorrelationFieldCount(params.correlation_params.correlationFields);
}
catch (error) {
throw new Error(`Enhanced cross-reference validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Validate enhanced cross-reference parameters with detailed error messages
const validation = validateEnhancedCrossReference(params.primary_query, params.secondary_queries, params.correlation_params);
if (!validation.isValid) {
// Create enhanced correlation error messages
const enhancedErrors = validation.errors.map(error => {
if (error.includes('correlation field')) {
const fieldMatch = error.match(/field '([^']+)'/);
if (fieldMatch) {
const field = fieldMatch[1];
const entityTypes = [
'flows',
'alarms',
'rules',
'devices',
];
const fieldValidation = FieldValidator.validateFieldAcrossTypes(field, entityTypes);
if (!fieldValidation.isValid &&
fieldValidation.suggestions.length > 0) {
return `${error}. ${fieldValidation.suggestions[0]}`;
}
}
}
if (error.includes('correlation type')) {
return `${error}. Valid correlation types are: "AND" (all fields must match), "OR" (any field can match)`;
}
return error;
});
throw new Error(`Enhanced cross-reference validation failed:\n${enhancedErrors.map(e => `• ${e}`).join('\n')}`);
}
// Determine entity types (use provided types or fall back to pattern matching)
const primaryType = params.entity_types?.primary ||
suggestEntityType(params.primary_query) ||
'flows';
const secondaryTypes = params.secondary_queries.map((q, index) => {
return (params.entity_types?.secondary?.[index] ||
suggestEntityType(q) ||
'alarms');
});
// Apply boolean field translation to queries
const translatedPrimaryQuery = translateBooleanQuery(params.primary_query, primaryType);
const translatedSecondaryQueries = params.secondary_queries.map((query, index) => translateBooleanQuery(query, secondaryTypes[index]));
// Execute primary query
const primaryResult = await this.executeSearchByType(primaryType, translatedPrimaryQuery, limit);
// Execute secondary queries and perform enhanced correlation
const correlatedResults = {
primary: {
query: params.primary_query,
results: primaryResult.results,
count: primaryResult.count,
entity_type: primaryType,
},
correlations: [],
correlation_params: params.correlation_params,
};
for (let index = 0; index < params.secondary_queries.length; index++) {
const secondaryQuery = translatedSecondaryQueries[index];
const secondaryType = secondaryTypes[index];
// Execute secondary query
const secondaryResult = await this.executeSearchByType(secondaryType, secondaryQuery, limit);
// Perform in-memory correlation using correlateResults function
// Use the first correlation field from the enhanced parameters
const correlationField = params.correlation_params.correlationFields?.[0] || 'source_ip';
const correlated = correlateResults(primaryResult.results, secondaryResult.results, correlationField);
// Create correlation result structure to match expected format
const correlationResult = {
correlatedResults: correlated.map(c => c.secondary),
scoredResults: correlated.map(c => ({
entity: c.secondary,
score: c.correlationScore,
matchedFields: [correlationField],
})),
correlationStats: {
correlationRate: (correlated.length / Math.max(primaryResult.results.length, 1)) *
100,
},
enhancedStats: {
totalCorrelated: correlated.length,
},
};
// Map correlation results to include scoring information
const mappedResults = correlationResult.scoredResults
? correlationResult.scoredResults.map((scoredItem) => ({
correlation_strength: scoredItem.score || 0,
matched_fields: scoredItem.matchedFields || [],
data: scoredItem.entity,
}))
: correlationResult.correlatedResults.map((item) => ({
correlation_strength: 0,
matched_fields: [],
data: item,
}));
correlatedResults.correlations.push({
query: secondaryQuery,
results: mappedResults,
count: correlationResult.correlatedResults.length,
correlation_stats: correlationResult.correlationStats,
enhanced_stats: correlationResult.enhancedStats,
entity_type: secondaryType,
});
}
// Calculate overall correlation summary
const totalCorrelated = correlatedResults.correlations.reduce((sum, c) => sum + c.count, 0);
const avgCorrelationRate = correlatedResults.correlations.length > 0
? Math.round(correlatedResults.correlations.reduce((sum, c) => sum + c.correlation_stats.correlationRate, 0) / correlatedResults.correlations.length)
: 0;
return {
...correlatedResults,
execution_time_ms: Date.now() - startTime,
correlation_summary: {
primary_count: primaryResult.count,
total_correlated_count: totalCorrelated,
average_correlation_rate: avgCorrelationRate,
correlation_fields: params.correlation_params.correlationFields,
correlation_type: params.correlation_params.correlationType,
temporal_window_applied: !!params.correlation_params.temporalWindow,
},
};
}
catch (error) {
throw new Error(`Enhanced cross-reference search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Cross-reference search across multiple entity types with improved field mapping
*/
async crossReferenceSearch(params) {
const startTime = Date.now();
try {
// Validate limit parameter
const limitValidation = this.validateLimitParameter(params.limit, 'cross_reference', {
defaultLimit: 1000,
maxLimit: 5000,
});
if (!limitValidation.isValid) {
throw new Error(`Parameter validation failed: ${limitValidation.errors.join(', ')}`);
}
const limit = limitValidation.validatedLimit;
// Validate correlation field count for regular cross-reference (single field only)
if (typeof params.correlation_field === 'string' &&
params.correlation_field.includes(',')) {
const fields = params.correlation_field.split(',').map(f => f.trim());
if (fields.length > 5) {
const excessFields = fields.slice(5);
throw new Error(`Cross-reference validation failed: Maximum 5 correlation fields allowed, but ${fields.length} provided. Please remove these fields: ${excessFields.join(', ')}`);
}
}
// Determine entity types (use provided type or fall back to pattern matching)
const primaryType = params.primary_entity_type ||
suggestEntityType(params.primary_query) ||
'flows';
const secondaryTypes = params.secondary_queries.map(q => {
return suggestEntityType(q) || 'alarms';
});
// Apply boolean field translation to queries
const translatedPrimaryQuery = translateBooleanQuery(params.primary_query, primaryType);
const translatedSecondaryQueries = params.secondary_queries.map((query, index) => translateBooleanQuery(query, secondaryTypes[index]));
// Fetch primary data (no API-level correlation parameters)
let primaryResult;
switch (primaryType) {
case 'flows':
primaryResult = await this.searchFlows({
query: translatedPrimaryQuery,
limit,
});
break;
case 'alarms':
primaryResult = await this.searchAlarms({
query: translatedPrimaryQuery,
limit,
});
break;
case 'rules':
primaryResult = await this.searchRules({
query: translatedPrimaryQuery,
limit,
});
break;
case 'devices':
primaryResult = await this.searchDevices({
query: translatedPrimaryQuery,
limit,
});
break;
case 'target_lists':
primaryResult = await this.searchTargetLists({
query: translatedPrimaryQuery,
limit,
});
break;
default:
primaryResult = await this.searchFlows({
query: translatedPrimaryQuery,
limit,
});
}
// Extract correlation values for client-side filtering
const correlationValues = new Set();
primaryResult.results.forEach((item) => {
const value = this.extractFieldValue(item, params.correlation_field, primaryType);
if (value) {
correlationValues.add(String(value));
}
});
// Fetch secondary data separately and perform client-side correlation
const correlatedResults = {
primary: {
query: params.primary_query,
results: primaryResult.results,
count: primaryResult.count,
entity_type: primaryType,
},
correlations: [],
};
for (let index = 0; index < params.secondary_queries.length; index++) {
const secondaryQuery = translatedSecondaryQueries[index];
const secondaryType = secondaryTypes[index];
// Fetch secondary data (no API-level correlation parameters)
let secondaryResult;
switch (secondaryType) {
case 'flows':
secondaryResult = await this.searchFlows({
query: secondaryQuery,
limit,
});
break;
case 'alarms':
secondaryResult = await this.searchAlarms({
query: secondaryQuery,
limit,
});
break;
case 'rules':
secondaryResult = await this.searchRules({
query: secondaryQuery,
limit,
});
break;
case 'devices':
secondaryResult = await this.searchDevices({
query: secondaryQuery,
limit,
});
break;
case 'target_lists':
secondaryResult = await this.searchTargetLists({
query: secondaryQuery,
limit,
});
break;
default:
secondaryResult = await this.searchAlarms({
query: secondaryQuery,
limit,
});
}
// Perform in-memory correlation using correlateResults function
const correlated = correlateResults(primaryResult.results, secondaryResult.results, params.correlation_field);
const correlatedItems = correlated.map(c => c.secondary);
correlatedResults.correlations.push({
query: secondaryQuery,
results: correlatedItems,
count: correlatedItems.length,
correlation_field: params.correlation_field,
entity_type: secondaryType,
});
}
return {
...correlatedResults,
execution_time_ms: Date.now() - startTime,
correlation_summary: {
primary_count: primaryResult.count,
unique_correlation_values: correlationValues.size,
correlated_count: correlatedResults.correlations.reduce((sum, c) => sum + c.count, 0),
correlation_rate: primaryResult.count > 0
? Math.round((correlatedResults.correlations.reduce((sum, c) => sum + c.count, 0) /
primaryResult.count) *
100)
: 0,
},
};
}
catch (error) {
throw new Error(`Cross-reference search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Extract field value from item for correlation
*/
extractFieldValue(item, field, _entityType) {
// Handle common correlation fields across entity types
switch (field) {
case 'source_ip':
return item.source?.ip || item.device?.ip || item.source_ip || item.ip;
case 'destination_ip':
return (item.destination?.ip ||
item.remote?.ip ||
item.destination_ip ||
item.target_ip);
case 'device_ip':
return item.device?.ip || item.source?.ip || item.device_ip || item.ip;
case 'protocol':
return item.protocol;
case 'country':
return item.remote?.country || item.country;
case 'device_id':
return item.device?.id || item.device_id;
default:
// Try direct field access
return item[field];
}
}
/**
* Recursively apply filters to a query AST
*/
applyFiltersRecursively(node, context) {
switch (node.type) {
case 'logical': {
// For logical nodes, apply filters to operands and combine
const combinedResult = { apiParams: {} };
if (node.left) {
const leftResult = this.applyFiltersRecursively(node.left, context);
Object.assign(combinedResult.apiParams, leftResult.apiParams);
if (leftResult.postProcessing) {
combinedResult.postProcessing = leftResult.postProcessing;
}
}
if (node.right) {
const rightResult = this.applyFiltersRecursively(node.right, context);
Object.assign(combinedResult.apiParams, rightResult.apiParams);
// Combine post-processing for logical operations
if (rightResult.postProcessing) {
const existingPostProcessing = combinedResult.postProcessing;
if (existingPostProcessing && node.operator === 'AND') {
combinedResult.postProcessing = (items) => rightResult.postProcessing(existingPostProcessing(items));
}
else if (node.operator === 'OR') {
// OR logic is more complex, simplified for now
combinedResult.postProcessing = rightResult.postProcessing;
}
else {
combinedResult.postProcessing = rightResult.postProcessing;
}
}
}
if (node.operand) {
// NOT operation
const operandResult = this.applyFiltersRecursively(node.operand, context);
if (operandResult.postProcessing) {
combinedResult.postProcessing = (items) => {
const filtered = operandResult.postProcessing(items);
return items.filter(item => !filtered.includes(item));
};
}
}
return combinedResult;
}
case 'group':
return this.applyFiltersRecursively(node.query, context);
default:
// Apply filters for leaf nodes
return filterFactory.applyFilters(node, context);
}
}
/**
* Sort results by a field
*/
sortResults(results, sortBy, sortOrder = 'desc') {
// Pre-compute values for efficient sorting
const itemsWithValues = results.map(item => ({
item,
value: this.getNestedValue(item, sortBy),
}));
itemsWithValues.sort((a, b) => {
const valueA = a.value;
const valueB = b.value;
// Handle null/undefined values - sort nulls to the end
if ((valueA === null || valueA === undefined) &&
(valueB === null || valueB === undefined)) {
return 0;
}
if (valueA === null || valueA === undefined) {
return 1;
} // null values go to end
if (valueB === null || valueB === undefined) {
return -1;
} // null values go to end
if (valueA === valueB) {
return 0;
}
// Handle string vs number comparisons
const comparison = valueA < valueB ? -1 : 1;
return sortOrder === 'asc' ? comparison : -comparison;
});
return itemsWithValues.map(({ item }) => item);
}
/**
* Generate aggregations for results
*/
generateAggregations(results, groupBy) {
if (!groupBy) {
return {
total: { count: results.length },
};
}
const groups = {};
for (const item of results) {
const groupValue = String(this.getNestedValue(item, groupBy) || 'unknown');
if (!groups[groupValue]) {
groups[groupValue] = [];
}
groups[groupValue].push(item);
}
const aggregations = {};
for (const [groupValue, groupItems] of Object.entries(groups)) {
aggregations[groupValue] = {
count: groupItems.length,
percentage: Math.round((groupItems.length / results.length) * 100),
};
}
return aggregations;
}
/**
* Get nested value from object using dot notation (enhanced with field mapping)
*/
getNestedValue(obj, path) {
return SafeAccess.getNestedValue(obj, path);
}
/**
* Get suggested correlation field combinations for given queries with enhanced validation
*/
async getCorrelationSuggestions(params) {
const startTime = Date.now();
try {
// Validate input parameters
const primaryValidation = ParameterValidator.validateRequiredString(params.primary_query, 'primary_query');
if (!primaryValidation.isValid) {
throw new Error(`Primary query validation failed: ${primaryValidation.errors.join(', ')}`);
}
if (!Array.isArray(params.secondary_queries)) {
throw new Error('secondary_queries must be an array');
}
if (params.secondary_queries.length === 0) {
throw new Error('secondary_queries array cannot be empty - at least one secondary query is required');
}
// Validate each secondary query
for (let i = 0; i < params.secondary_queries.length; i++) {
const secondaryValidation = ParameterValidator.validateRequiredString(params.secondary_queries[i], `secondary_queries[${i}]`);
if (!secondaryValidation.isValid) {
throw new Error(`Secondary query ${i} validation failed: ${secondaryValidation.errors.join(', ')}`);
}
}
// Parse and validate queries to determine entity types more accurately
const primaryType = this.determineEntityTypeFromQuery(params.primary_query) || 'flows';
const secondaryTypes = params.secondary_queries.map(q => {
const type = this.determineEntityTypeFromQuery(q);
if (!type) {
// Could not determine entity type for secondary query, defaulting to 'alarms'
return 'alarms';
}
return type;
});
const allTypes = [primaryType, ...secondaryTypes];
// Validate entity type combinations
const uniqueTypes = [...new Set(allTypes)];
if (uniqueTypes.length === 1) {
// All queries appear to target the same entity type. Cross-reference may not be meaningful.
}
// Get validated field combinations using enhanced validation
// Use a more comprehensive list of common correlation fields
const fieldValidationResult = EnhancedQueryValidator.validateCorrelationFields([
'source_ip',
'destination_ip',
'device_ip',
'protocol',
'timestamp',
'country',
'asn',
'port',
'device_id',
'gid',
'type',
'severity',
'status',
'action',
'target_value',
'mac',
'application',
'user_agent',
'domain',
'subnet',
'city',
'region',
'continent',
'organization',
'isp',
'hosting_provider',
'is_cloud_provider',
'is_vpn',
'is_proxy',
], primaryType, secondaryTypes);
// Get supported correlation combinations with better filtering
const combinations = getSupportedCorrelationCombinations(uniqueTypes);
const validCombinations = combinations.filter(combo => combo.every(field => fieldValidationResult.compatibleFields?.includes(field)));
// Enhanced categorization with confidence scoring
const suggestions = {
primary_entity_type: primaryType,
secondary_entity_types: secondaryTypes,
// Compatibility analysis
compatibility_analysis: {
compatible_fields_count: fieldValidationResult.compatibleFields?.length || 0,
incompatible_fields: fieldValidationResult.errors || [],
compatibility_suggestions: fieldValidationResult.suggestions || [],
},
// Field combinations by complexity
single_field: validCombinations.filter(combo => combo.length === 1),
dual_field: validCombinations.filter(combo => combo.length === 2),
multi_field: validCombinations.filter(combo => combo.length >= 3),
// Context-aware recommendations based on entity types
recommended_by_context: this.generateContextualRecommendations(primaryType, secondaryTypes),
// High-confidence recommendations
high_confidence: [
// Network identity correlations (highest confidence)
['source_ip', 'device_ip'],
['device_ip', 'destination_ip'],
// Protocol and network behavior
['protocol', 'source_ip'],
['protocol', 'device_ip'],
// Temporal correlations
['timestamp', 'device_ip'],
// Geographic correlations
['country', 'source_ip'],
['asn', 'source_ip'],
].filter(combo => validCombinations.some(c => c.length === combo.length && combo.every(f => c.includes(f)))),
// Medium-confidence recommendations
medium_confidence: [
// Application-level correlations
['application', 'device_ip'],
['user_agent', 'source_ip'],
// Security-focused correlations
['threat_level', 'source_ip'],
['geographic_risk_score', 'country'],
// Behavioral correlations
['session_duration', 'device_ip'],
['frequency_score', 'source_ip'],
].filter(combo => validCombinations.some(c => c.length === combo.length && combo.every(f => c.includes(f)))),
// All compatible fields for manual selection
all_compatible_fields: fieldValidationResult.compatibleFields || [],
// Field usage statistics for the discovered entity types
field_usage_stats: this.generateFieldUsageStats(uniqueTypes),
// Validation warnings and errors
validation_issues: {
errors: fieldValidationResult.errors || [],
warnings: fieldValidationResult.suggestions || [],
},
execution_time_ms: Date.now() - startTime,
};
return suggestions;
}
catch (error) {
throw new Error(`Correlation suggestions failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Determine entity type from query content with improved heuristics
*/
determineEntityTypeFromQuery(query) {
const lowerQuery = query.toLowerCase();
// Flow-specific indicators
if (lowerQuery.includes('bytes') ||
lowerQuery.includes('download') ||
lowerQuery.includes('upload') ||
lowerQuery.includes('protocol:') ||
lowerQuery.includes('direction:') ||
lowerQuery.includes('blocked:')) {
return 'flows';
}
// Alarm-specific indicators
if (lowerQuery.includes('severity:') ||
lowerQuery.includes('type:') ||
lowerQuery.includes('resolved:') ||
lowerQuery.includes('alarm') ||
lowerQuery.includes('alert')) {
return 'alarms';
}
// Rule-specific indicators
if (lowerQuery.includes('action:') ||
lowerQuery.includes('target:') ||
lowerQuery.includes('rule') ||
lowerQuery.includes('policy') ||
lowerQuery.includes('active:')) {
return 'rules';
}
// Device-specific indicators
if (lowerQuery.includes('online:') ||
lowerQuery.includes('mac:') ||
lowerQuery.includes('device') ||
lowerQuery.includes('mac_vendor:') ||
lowerQuery.includes('last_seen:')) {
return 'devices';
}
// Target list indicators
if (lowerQuery.includes('category:') ||
lowerQuery.includes('owner:') ||
lowerQuery.includes('target_count:')) {
return 'target_lists';
}
return null;
}
/**
* Generate contextual recommendations based on entity type combinations
*/
generateContextualRecommendations(primaryType, secondaryTypes) {
const recommendations = {};
if (primaryType === 'flows') {
if (secondaryTypes.includes('alarms')) {
recommendations.flows_to_alarms = {
description: 'Correlate network flows with security alarms',
recommended_fields: [
'source_ip',
'device_ip',
'protocol',
'timestamp',
],
use_cases: [
'Incident investigation',
'Threat hunting',
'Security analysis',
],
};
}
if (secondaryTypes.includes('devices')) {
recommendations.flows_to_devices = {
description: 'Correlate network flows with device information',
recommended_fields: ['device_ip', 'mac', 'device_id'],
use_cases: ['Device behavior analysis', 'Network usage tracking'],
};
}
}
if (primaryType === 'alarms' && secondaryTypes.includes('flows')) {
recommendations.alarms_to_flows = {
description: 'Correlate security alarms with network activity',
recommended_fields: ['source_ip', 'timestamp', 'protocol'],
use_cases: ['Attack pattern analysis', 'Impact assessment'],
};
}
return recommendations;
}
/**
* Generate field usage statistics for entity types
*/
generateFieldUsageStats(entityTypes) {
const stats = {};
for (const entityType of entityTypes) {
const mappings = FIELD_MAPPINGS[entityType];
const fieldCount = Object.keys(mappings).length;
stats[entityType] = {
total_fields: fieldCount,
common_fields: Object.keys(mappings).filter(field => ['source_ip', 'device_ip', 'timestamp', 'protocol'].includes(field)).length,
geographic_fields: Object.keys(mappings).filter(field => ['country', 'region', 'city', 'asn'].includes(field)).length,
network_fields: Object.keys(mappings).filter(field => ['source_ip', 'destination_ip', 'protocol', 'port'].includes(field)).length,
};
}
return stats;
}
/**
* Build geographic query string from filters using FirewallaClient
*/
buildGeographicQuery(filters) {
// Use FirewallaClient's buildGeoQuery method for proper API syntax
return this.firewalla.buildGeoQuery(filters);
}
/**
* Advanced geographic search for alarms with location-based threat analysis.
* Builds proper query strings for the Firewalla API.
*/
async searchAlarmsByGeography(params) {
const startTime = Date.now();
try {
// Validate limit parameter
const limitValidation = ParameterValidator.validateNumber(params.limit, 'limit', {
required: true,
min: 1,
max: 5000,
integer: true,
});
if (!limitValidation.isValid) {
throw new Error(`Parameter validation failed: ${limitValidation.errors.join(', ')}`);
}
// Validate geographic filters if provided
let sanitizedGeoFilters = params.geographic_filters;
if (params.geographic_filters) {
sanitizedGeoFilters = this.validateGeographicFilters(params.geographic_filters);
}
// Fetch unfiltered results first (client-side geographic filtering)
const unfilteredResults = await this.searchAlarms({
query: params.query || '*',
limit: 5000, // Fetch larger set for client-side filtering (max allowed by searchAlarms)
sort_by: params.sort_by,
group_by: params.group_by,
});
// Apply client-side geographic filtering if filters provided
let filteredResults = unfilteredResults.results;
if (sanitizedGeoFilters &&
typeof sanitizedGeoFilters === 'object' &&
sanitizedGeoFilters !== null) {
const { filterByGeography } = await import('../utils/geographic.js');
filteredResults = filterByGeography(unfilteredResults.results, sanitizedGeoFilters);
}
// Apply limit to filtered results
if (filteredResults.length > params.limit) {
filteredResults = filteredResults.slice(0, params.limit);
}
const result = {
...unfilteredResults,
results: filteredResults,
count: filteredResults.length,
};
// Add geographic threat analysis if requested
let threatAnalysis;
if (params.geographic_filters?.threat_analysis) {
threatAnalysis = this.analyzeGeographicThreats(result.results);
}
return {
...result,
geographic_filters_applied: !!params.geographic_filters,
geographic_threat_analysis: threatAnalysis,
execution_time_ms: Date.now() - startTime,
};
}
catch (error) {
throw new Error(`Geographic alarms search failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Comprehensive geographic statistics and analytics
*/
async getGeographicStatistics(params) {
const startTime = Date.now();
try {
const limitValidation = this.validateLimitParameter(params.limit, 'statistics', {
defaultLimit: 1000,
maxLimit: 1000,
});
if (!limitValidation.isValid) {
throw new Error(`Parameter validation failed: ${limitValidation.errors.join(', ')}`);
}
const limit = limitValidation.validatedLimit;
// Build base query for geographic analysis
let baseQuery = '*';
// Add time range if specified using Firewalla API format
if (params.time_range) {
const startTs = Math.floor(new Date(params.time_range.start).getTime() / 1000);
const endTs = Math.floor(new Date(params.time_range.end).getTime() / 1000);
// Use Firewalla's time range format: ts:start-end
baseQuery = `ts:${startTs}-${endTs}`;
}
// Fetch unfiltered results first (client-side geographic processing)
let searchResult;
if (params.entity_type === 'flows') {
searchResult = await this.searchFlows({
query: baseQuery,
limit: 1000, // Max allowed by searchFlows
});
}
else {
searchResult = await this.searchAlarms({
query: baseQuery,
limit: 5000, // Max allowed by searchAlarms
});
}
// Process results with client-side geographic grouping
const processedResults = searchResult.results;
// Group results by specified field for statistics
const groupByField = params.group_by || 'country';
const groupedData = this.groupResultsByGeographicField(processedResults, groupByField);
// Apply limit to grouped results
const limitedGroups = Object.entries(groupedData)
.sort(([, a], [, b]) => b.length - a.length)
.slice(0, limit);
const limitedGroupedData = Object.fromEntries(limitedGroups);
// Generate geographic statistics from grouped data
const statistics = this.generateGeographicStatistics({ results: processedResults, count: processedResults.length }, params.analysis_type || 'summary', groupByField, limitedGroupedData);
return {
entity_type: params.entity_type,
time_range: params.time_range,
analysis_type: params.analysis_type || 'summary',
group_by: groupByField,
statistics,
total_records: processedResults.length,
execution_time_ms: Date.now() - startTime,
};
}
catch (error) {
throw new Error(`Geographic statistics failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Analyze geographic data for patterns and insights
*/
analyzeGeographicData(results) {
const analysis = {
total_flows: results.length,
unique_countries: new Set(),
unique_continents: new Set(),
unique_asns: new Set(),
cloud_provider_flows: 0,
vpn_flows: 0,
high_risk_flows: 0,
top_countries: {},
top_asns: {},
geographic_data_available: false,
warnings: [],
};
let hasGeographicData = false;
results.forEach(flow => {
// Extract geographic data using field mapping
const country = getFieldValue(flow, 'country', 'flows');
const continent = getFieldValue(flow, 'continent', 'flows');
const asn = getFieldValue(flow, 'asn', 'flows');
const isCloud = getFieldValue(flow, 'is_cloud_provider', 'flows');
const isVpn = getFieldValue(flow, 'is_vpn', 'flows');
const riskScore = getFieldValue(flow, 'geographic_risk_score', 'flows');
// Check if any geographic data is available
if (country || continent || asn || isCloud || isVpn || riskScore) {
hasGeographicData = true;
}
if (country && typeof country === 'string') {
analysis.unique_countries.add(country);
analysis.top_countries[country] =
(analysis.top_countries[country] || 0) + 1;
}
if (continent) {
analysis.unique_continents.add(continent);
}
if (asn && typeof asn === 'string') {
analysis.unique_asns.add(asn);
analysis.top_asns[asn] = (analysis.top_asns[asn] || 0) + 1;
}
if (isCloud) {
analysis.cloud_provider_flows++;
}
if (isVpn) {
analysis.vpn_flows++;
}
if (riskScore &&
Number(riskScore) >= getRiskThresholds().highRiskFlowMin) {
analysis.high_risk_flows++;
}
});
// Add warnings if no geographic data found
analysis.geographic_data_available = hasGeographicData;
if (!hasGeographicData && results.length > 0) {
analysis.warnings.push('No geographic data found in flow results. Geographic enrichment may be disabled or unavailable.');
analysis.warnings.push('Consider enabling geographic enrichment in Firewalla settings or check API configuration.');
}
// Convert sets to counts
return {
...analysis,
unique_countries: analysis.unique_countries.size,
unique_continents: analysis.unique_continents.size,
unique_asns: analysis.unique_asns.size,
top_countries: Object.entries(analysis.top_countries)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.reduce((acc, [country, count]) => {
acc[country] = count;
return acc;
}, {}),
top_asns: Object.entries(analysis.top_asns)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.reduce((acc, [asn, count]) => {
acc[asn] = count;
return acc;
}, {}),
};
}
/**
* Analyze geographic threats for security insights
*/
analyzeGeographicThreats(results) {
const threats = {
total_alarms: results.length,
high_risk_countries: {},
threat_by_continent: {},
suspicious_asns: {},
cloud_threats: 0,
vpn_threats: 0,
proxy_threats: 0,
risk_distribution: {
low: 0, // 0-3
medium: 0, // 4-6
high: 0, // 7-8
critical: 0, // 9-10
},
};
results.forEach(alarm => {
const country = getFieldValue(alarm, 'country', 'alarms');
const continent = getFieldValue(alarm, 'continent', 'alarms');
const asn = getFieldValue(alarm, 'asn', 'alarms');
const isCloud = getFieldValue(alarm, 'is_cloud_provider', 'alarms');
const isVpn = getFieldValue(alarm, 'is_vpn', 'alarms');
const isProxy = getFieldValue(alarm, 'is_proxy', 'alarms');
const riskScore = getFieldValue(alarm, 'geographic_risk_score', 'alarms') || 0;
if (country &&
typeof country === 'string' &&
Number(riskScore) >= getRiskThresholds().highRiskCountryMin) {
threats.high_risk_countries[country] =
(threats.high_risk_countries[country] || 0) + 1;
}
if (continent && typeof continent === 'string') {
threats.threat_by_continent[continent] =
(threats.threat_by_continent[continent] || 0) + 1;
}
if (asn &&
typeof asn === 'string' &&
Number(riskScore) >= getRiskThresholds().suspiciousAsnMin) {
threats.suspicious_asns[asn] = (threats.suspicious_asns[asn] || 0) + 1;
}
if (isCloud) {
threats.cloud_threats++;
}
if (isVpn) {
threats.vpn_threats++;
}
if (isProxy) {
threats.proxy_threats++;
}
// Categorize risk
if (Number(riskScore) <= getRiskThresholds().lowMax) {
threats.risk_distribution.low++;
}
else if (Number(riskScore) <= getRiskThresholds().mediumMax) {
threats.risk_distribution.medium++;
}
else if (Number(riskScore) <= getRiskThresholds().highMax) {
threats.risk_distribution.high++;
}
else {
threats.risk_distribution.critical++;
}
});
return threats;
}
/**
* Group results by geographic field for client-side processing
*/
groupResultsByGeographicField(results, groupByField) {
const grouped = {};
results.forEach(result => {
let groupValue = 'Unknown';
// Extract geographic field value from result
if (result.remote) {
switch (groupByField) {
case 'country':
groupValue =
result.remote.country || result.remote.countryName || 'Unknown';
break;
case 'continent':
groupValue = result.remote.continent || 'Unknown';
break;
case 'region':
groupValue =
result.remote.region || result.remote.regionName || 'Unknown';
break;
case 'asn':
groupValue =
result.remote.asn ||
result.remote.autonomousSystemNumber ||
'Unknown';
break;
case 'provider':
groupValue =
result.remote.isp ||
result.remote.provider ||
result.remote.organization ||
'Unknown';
break;
}
}
if (!grouped[groupValue]) {
grouped[groupValue] = [];
}
grouped[groupValue].push(result);
});
return grouped;
}
/**
* Generate detailed geographic statistics
*/
generateGeographicStatistics(searchResult, analysisType, groupBy, groupedData) {
// Use grouped data if provided, otherwise fall back to searchResult aggregations
const distribution = groupedData
? Object.fromEntries(Object.entries(groupedData).map(([key, values]) => [
key,
{ count: values.length },
]))
: searchResult.aggregations || {};
const stats = {
summary: {
total_records: searchResult.count || 0,
grouped_by: groupBy,
analysis_type: analysisType,
},
distribution,
insights: [],
};
// Add insights based on analysis
if (distribution && Object.keys(distribution).length > 0) {
const entries = Object.entries(distribution);
const totalRecords = searchResult.count || 0;
// Find top entries
const sortedEntries = entries.sort(([, a], [, b]) => (b.count || 0) - (a.count || 0));
if (sortedEntries.length > 0) {
const topEntry = sortedEntries[0];
const percentage = Math.round((topEntry[1].count / totalRecords) * 100);
stats.insights.push(`Top ${groupBy}: ${topEntry[0]} (${percentage}% of total)`);
}
if (sortedEntries.length > 1) {
const secondEntry = sortedEntries[1];
const percentage = Math.round((secondEntry[1].count / totalRecords) * 100);
stats.insights.push(`Second most active ${groupBy}: ${secondEntry[0]} (${percentage}% of total)`);
}
// Geographic diversity insight
stats.insights.push(`Geographic diversity: ${entries.length} unique ${groupBy}s`);
}
return stats;
}
}
/**
* Creates and returns a set of advanced search functions for querying Firewalla MCP server entities.
*
* The returned object provides methods for searching flows, alarms, rules, devices, target lists, and performing cross-reference searches, all using the provided Firewalla client instance.
*/
export function createSearchTools(firewalla) {
const searchEngine = new SearchEngine(firewalla);
return {
search_flows: searchEngine.searchFlows.bind(searchEngine),
search_alarms: searchEngine.searchAlarms.bind(searchEngine),
search_rules: searchEngine.searchRules.bind(searchEngine),
search_devices: searchEngine.searchDevices.bind(searchEngine),
search_target_lists: searchEngine.searchTargetLists.bind(searchEngine),
search_cross_reference: searchEngine.crossReferenceSearch.bind(searchEngine),
search_enhanced_cross_reference: searchEngine.enhancedCrossReferenceSearch.bind(searchEngine),
get_correlation_suggestions: searchEngine.getCorrelationSuggestions.bind(searchEngine),
search_alarms_by_geography: searchEngine.searchAlarmsByGeography.bind(searchEngine),
get_geographic_statistics: searchEngine.getGeographicStatistics.bind(searchEngine),
};
}
//# sourceMappingURL=search.js.map