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
258 lines • 9.07 kB
JavaScript
/**
* Time Range Filter Implementation
* Handles timestamp-based filtering for flows, alarms, and rules
*/
import { BaseFilter } from './base.js';
import { unixToISOString } from '../../utils/timestamp.js';
export class TimeRangeFilter extends BaseFilter {
constructor() {
super(...arguments);
this.name = 'time_range';
this.timeFields = [
'timestamp',
'ts',
'created_at',
'updated_at',
'last_updated',
'lastSeen',
];
/**
* Standardized time margin for timestamp matching (30 seconds)
*
* This margin is used consistently across both API-level filtering and
* post-processing to ensure identical behavior regardless of where
* the filtering occurs. The 30-second value provides a reasonable
* balance between accuracy and flexibility while accounting for:
*
* - Network latency between client and Firewalla API (typically <5s)
* - Clock synchronization differences between systems (typically <10s)
* - Timestamp precision variations (seconds vs milliseconds)
* - Small delays in data processing pipelines (<15s)
*
* Using a consistent value prevents subtle bugs where the same query
* might return different results for different entity types based on
* whether they support API-level time filtering or require post-processing.
*
* Note: Previously used inconsistent values (10s for API, 60s for post-processing)
* which caused discrepancies. Standardized to 30s for optimal balance.
*/
this.STANDARD_TIME_MARGIN = 30; // seconds
}
canHandle(node) {
if (node.type === 'field' ||
node.type === 'range' ||
node.type === 'comparison') {
return this.timeFields.includes(node.field);
}
return false;
}
apply(node, context) {
switch (node.type) {
case 'field': {
return this.handleFieldQuery(node, context);
}
case 'range': {
return this.handleRangeQuery(node, context);
}
case 'comparison': {
return this.handleComparisonQuery(node, context);
}
case 'logical':
case 'group':
case 'wildcard': {
// These node types are not handled by time filter
return { apiParams: {} };
}
default: {
return { apiParams: {} };
}
}
}
handleFieldQuery(node, context) {
const timestamp = this.parseTimestamp(node.value);
if (timestamp === null) {
return { apiParams: {} };
}
// For exact timestamp matches, use a configurable small range
const margin = context.timeMargin || this.STANDARD_TIME_MARGIN;
return {
apiParams: this.buildTimeParams(timestamp - margin, timestamp + margin, context),
cacheKeyComponent: this.createCacheKey(node),
};
}
handleRangeQuery(node, context) {
const minTime = node.min ? this.parseTimestamp(node.min) : null;
const maxTime = node.max ? this.parseTimestamp(node.max) : null;
return {
apiParams: this.buildTimeParams(minTime, maxTime, context),
cacheKeyComponent: this.createCacheKey(node),
};
}
handleComparisonQuery(node, context) {
const timestamp = this.parseTimestamp(node.value);
if (timestamp === null) {
return { apiParams: {} };
}
let minTime = null;
let maxTime = null;
switch (node.operator) {
case '>': {
minTime = timestamp + 1;
break;
}
case '>=': {
minTime = timestamp;
break;
}
case '<': {
maxTime = timestamp - 1;
break;
}
case '<=': {
maxTime = timestamp;
break;
}
}
return {
apiParams: this.buildTimeParams(minTime, maxTime, context),
cacheKeyComponent: this.createCacheKey(node),
};
}
buildTimeParams(minTime, maxTime, context) {
const params = {};
// Different entities use different parameter names
switch (context.entityType) {
case 'flows': {
if (minTime) {
params.start_time = unixToISOString(minTime);
}
if (maxTime) {
params.end_time = unixToISOString(maxTime);
}
break;
}
case 'alarms': {
if (minTime) {
params.since = minTime;
}
if (maxTime) {
params.until = maxTime;
}
break;
}
case 'rules': {
// Rules API might not support time filtering directly
// Will need post-processing
break;
}
case 'devices': {
// Device API doesn't typically support time filtering
break;
}
case 'target_lists': {
// Target lists might filter by last_updated
if (minTime) {
params.updated_since = minTime;
}
break;
}
}
return params;
}
/**
* Create post-processing filter for entities that don't support API-level time filtering
*/
createPostProcessingFilter(node) {
return (items) => {
return items.filter(item => {
const timestamp = this.extractTimestamp(item);
if (timestamp === null) {
return true;
} // Keep items without timestamps
return this.matchesTimeCondition(timestamp, node);
});
};
}
extractTimestamp(item) {
// Try different timestamp fields
for (const field of this.timeFields) {
const value = this.getNestedValue(item, field);
if (value) {
const parsed = this.parseTimestamp(value);
if (parsed !== null) {
return parsed;
}
}
}
return null;
}
matchesTimeCondition(timestamp, node) {
switch (node.type) {
case 'field': {
const targetTime = this.parseTimestamp(node.value);
return Boolean(targetTime &&
Math.abs(timestamp - targetTime) <= this.STANDARD_TIME_MARGIN);
}
case 'range': {
const rangeNode = node;
const min = rangeNode.min ? this.parseTimestamp(rangeNode.min) : null;
const max = rangeNode.max ? this.parseTimestamp(rangeNode.max) : null;
if (min && timestamp < min) {
return false;
}
if (max && timestamp > max) {
return false;
}
return true;
}
case 'comparison': {
const compNode = node;
const compTime = this.parseTimestamp(compNode.value);
if (!compTime) {
return false;
}
switch (compNode.operator) {
case '>': {
return timestamp > compTime;
}
case '>=': {
return timestamp >= compTime;
}
case '<': {
return timestamp < compTime;
}
case '<=': {
return timestamp <= compTime;
}
default: {
return false;
}
}
}
case 'group':
case 'logical':
case 'wildcard':
// These node types are not applicable for time-based filtering
return false;
default:
return false;
}
}
getOptimizations() {
return [
{
type: 'index',
priority: 10,
description: 'Time-based queries can use timestamp indexes',
condition: (context) => context.entityType === 'flows' || context.entityType === 'alarms',
},
{
type: 'pushdown',
priority: 8,
description: 'Time filters should be applied at API level when possible',
condition: (context) => context.entityType !== 'rules',
},
];
}
}
//# sourceMappingURL=time.js.map