alertStatistics
Analyze and aggregate security alert statistics by time range, field, and index pattern using OpenSearch MCP Server for actionable insights.
Instructions
Get statistics about security alerts
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| field | No | Field to aggregate by | rule.level |
| index | No | Index pattern | wazuh-alerts-* |
| timeRange | No | Time range (e.g., 1h, 24h, 7d) | 24h |
Implementation Reference
- index.js:589-641 (handler)The main handler function that executes the tool: queries OpenSearch for term aggregations on the specified field over the given time range, computes statistics and percentages, and formats the results.
execute: async (args, { log }) => { log.info("Getting alert statistics", { timeRange: args.timeRange, field: args.field }); return safeOpenSearchQuery(async () => { const timeRangeMs = parseTimeRange(args.timeRange); const now = new Date(); const from = new Date(now.getTime() - timeRangeMs); const response = await client.search({ index: args.index, body: { size: 0, query: { range: { timestamp: { gte: from.toISOString(), lte: now.toISOString(), }, }, }, aggs: { stats: { terms: { field: args.field, size: 20, }, }, }, timeout: "25s" }, }); const buckets = response.body.aggregations?.stats?.buckets || []; const total = buckets.reduce((sum, bucket) => sum + bucket.doc_count, 0); log.info(`Found statistics for ${total} alerts`, { count: total }); if (total === 0) { return "No alerts found in the specified time range."; } let resultText = `## Alert Statistics for the past ${args.timeRange}\n\n`; resultText += `Total alerts: ${total}\n\n`; resultText += `### Breakdown by ${args.field}\n\n`; buckets.forEach(bucket => { const percentage = ((bucket.doc_count / total) * 100).toFixed(2); resultText += `- **${bucket.key}**: ${bucket.doc_count} (${percentage}%)\n`; }); return resultText; }, `Failed to get alert statistics. The field "${args.field}" may not be aggregatable or the connection timed out.`); }, - index.js:584-588 (schema)Zod schema defining the input parameters for the alertStatistics tool.
parameters: z.object({ timeRange: z.string().default("24h").describe("Time range (e.g., 1h, 24h, 7d)"), field: z.string().default("rule.level").describe("Field to aggregate by"), index: z.string().default("wazuh-alerts-*").describe("Index pattern"), }), - index.js:581-642 (registration)The server.addTool call that registers the alertStatistics tool with FastMCP.
server.addTool({ name: "alertStatistics", description: "Get statistics about security alerts", parameters: z.object({ timeRange: z.string().default("24h").describe("Time range (e.g., 1h, 24h, 7d)"), field: z.string().default("rule.level").describe("Field to aggregate by"), index: z.string().default("wazuh-alerts-*").describe("Index pattern"), }), execute: async (args, { log }) => { log.info("Getting alert statistics", { timeRange: args.timeRange, field: args.field }); return safeOpenSearchQuery(async () => { const timeRangeMs = parseTimeRange(args.timeRange); const now = new Date(); const from = new Date(now.getTime() - timeRangeMs); const response = await client.search({ index: args.index, body: { size: 0, query: { range: { timestamp: { gte: from.toISOString(), lte: now.toISOString(), }, }, }, aggs: { stats: { terms: { field: args.field, size: 20, }, }, }, timeout: "25s" }, }); const buckets = response.body.aggregations?.stats?.buckets || []; const total = buckets.reduce((sum, bucket) => sum + bucket.doc_count, 0); log.info(`Found statistics for ${total} alerts`, { count: total }); if (total === 0) { return "No alerts found in the specified time range."; } let resultText = `## Alert Statistics for the past ${args.timeRange}\n\n`; resultText += `Total alerts: ${total}\n\n`; resultText += `### Breakdown by ${args.field}\n\n`; buckets.forEach(bucket => { const percentage = ((bucket.doc_count / total) * 100).toFixed(2); resultText += `- **${bucket.key}**: ${bucket.doc_count} (${percentage}%)\n`; }); return resultText; }, `Failed to get alert statistics. The field "${args.field}" may not be aggregatable or the connection timed out.`); }, }); - index.js:62-86 (helper)Helper function used by the tool to safely execute OpenSearch queries with error handling.
async function safeOpenSearchQuery(operation, fallbackMessage) { try { debugLog('Executing OpenSearch query'); const result = await operation(); debugLog('OpenSearch query completed successfully'); return result; } catch (error) { console.error(`OpenSearch error: ${error.message}`, error); debugLog('OpenSearch query failed:', error); // Check for common OpenSearch errors if (error.message.includes('timeout')) { throw new UserError(`OpenSearch request timed out. The query may be too complex or the cluster is under heavy load.`); } else if (error.message.includes('connect')) { throw new UserError(`Cannot connect to OpenSearch. Please check your connection settings in .env file.`); } else if (error.message.includes('no such index')) { throw new UserError(`The specified index doesn't exist in OpenSearch.`); } else if (error.message.includes('unauthorized')) { throw new UserError(`Authentication failed with OpenSearch. Please check your credentials in .env file.`); } // For any other errors throw new UserError(fallbackMessage || `OpenSearch operation failed: ${error.message}`); } } - index.js:771-791 (helper)Helper function used to parse time range strings (e.g., '24h') into milliseconds.
function parseTimeRange(timeRange) { const unit = timeRange.slice(-1); const value = parseInt(timeRange.slice(0, -1)); debugLog('Parsing time range:', timeRange, 'to milliseconds'); switch (unit) { case 'h': return value * 60 * 60 * 1000; // hours to ms case 'd': return value * 24 * 60 * 60 * 1000; // days to ms case 'w': return value * 7 * 24 * 60 * 60 * 1000; // weeks to ms case 'm': return value * 30 * 24 * 60 * 60 * 1000; // months to ms (approximate) default: const error = `Invalid time range format: ${timeRange}`; debugLog('Error:', error); throw new Error(error); } }