//! Wazuh Indexer alert tools
//!
//! This module contains tools for retrieving and analyzing Wazuh security alerts
//! from the Wazuh Indexer.
use rmcp::{
ErrorData as McpError,
model::{CallToolResult, Content},
tool,
};
use std::sync::Arc;
use wazuh_client::WazuhIndexerClient;
use super::ToolModule;
/// Parameters for getting alert summary
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GetAlertSummaryParams {
#[schemars(description = "Maximum number of alerts to retrieve (default: 300)")]
pub limit: Option<u32>,
}
/// Alert tools implementation
#[derive(Clone)]
pub struct AlertTools {
indexer_client: Arc<WazuhIndexerClient>,
}
impl AlertTools {
pub fn new(indexer_client: Arc<WazuhIndexerClient>) -> Self {
Self { indexer_client }
}
#[tool(
name = "get_wazuh_alert_summary",
description = "Retrieves a summary of Wazuh security alerts. Returns formatted alert information including ID, timestamp, and description."
)]
pub async fn get_wazuh_alert_summary(
&self,
params: GetAlertSummaryParams,
) -> Result<CallToolResult, McpError> {
let limit = params.limit.unwrap_or(300);
tracing::info!(limit = %limit, "Retrieving Wazuh alert summary");
match self.indexer_client.get_alerts(Some(limit)).await {
Ok(raw_alerts) => {
if raw_alerts.is_empty() {
tracing::info!("No Wazuh alerts found to process. Returning standard message.");
return Self::not_found_result("Wazuh alerts");
}
let num_alerts_to_process = raw_alerts.len();
let mcp_content_items: Vec<Content> = raw_alerts
.into_iter()
.map(|alert_value| {
let source = alert_value.get("_source").unwrap_or(&alert_value);
let id = source.get("id")
.and_then(|v| v.as_str())
.or_else(|| alert_value.get("_id").and_then(|v| v.as_str()))
.unwrap_or("Unknown ID");
let description = source.get("rule")
.and_then(|r| r.get("description"))
.and_then(|d| d.as_str())
.unwrap_or("No description available");
let timestamp = source.get("timestamp")
.and_then(|t| t.as_str())
.unwrap_or("Unknown time");
let agent_name = source.get("agent")
.and_then(|a| a.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("Unknown agent");
let rule_level = source.get("rule")
.and_then(|r| r.get("level"))
.and_then(|l| l.as_u64())
.unwrap_or(0);
// Extract source IP from data.srcip (common for SSH, network alerts)
let src_ip = source.get("data")
.and_then(|d| d.get("srcip"))
.and_then(|ip| ip.as_str())
.or_else(|| source.get("data")
.and_then(|d| d.get("src_ip"))
.and_then(|ip| ip.as_str()))
.unwrap_or("");
// Extract destination IP if available
let dst_ip = source.get("data")
.and_then(|d| d.get("dstip"))
.and_then(|ip| ip.as_str())
.or_else(|| source.get("data")
.and_then(|d| d.get("dst_ip"))
.and_then(|ip| ip.as_str()))
.unwrap_or("");
// Extract source user if available
let src_user = source.get("data")
.and_then(|d| d.get("srcuser"))
.and_then(|u| u.as_str())
.or_else(|| source.get("data")
.and_then(|d| d.get("dstuser"))
.and_then(|u| u.as_str()))
.unwrap_or("");
// Build formatted text with optional fields
let mut formatted_text = format!(
"Alert ID: {}\nTime: {}\nAgent: {}\nLevel: {}\nDescription: {}",
id, timestamp, agent_name, rule_level, description
);
if !src_ip.is_empty() {
formatted_text.push_str(&format!("\nSource IP: {}", src_ip));
}
if !dst_ip.is_empty() {
formatted_text.push_str(&format!("\nDestination IP: {}", dst_ip));
}
if !src_user.is_empty() {
formatted_text.push_str(&format!("\nUser: {}", src_user));
}
Content::text(formatted_text)
})
.collect();
tracing::info!("Successfully processed {} alerts into {} MCP content items", num_alerts_to_process, mcp_content_items.len());
Self::success_result(mcp_content_items)
}
Err(e) => {
let err_msg = Self::format_error("Indexer", "retrieving alerts", &e);
tracing::error!("{}", err_msg);
Self::error_result(err_msg)
}
}
}
}
impl ToolModule for AlertTools {}