mirror of
https://github.com/gbrigandi/mcp-server-wazuh.git
synced 2025-07-13 15:14:48 -06:00
Replaced bespoke indexer client with full fledges indexer and manager API crate.
This commit is contained in:
parent
f9efb70f19
commit
bfffdbfb52
@ -9,6 +9,7 @@ repository = "https://github.com/gbrigandi/mcp-server-wazuh"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
wazuh-client = "0.1.0"
|
||||||
rmcp = { version = "0.1.5", features = ["server", "transport-io"] }
|
rmcp = { version = "0.1.5", features = ["server", "transport-io"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||||
|
10
src/lib.rs
10
src/lib.rs
@ -1,4 +1,6 @@
|
|||||||
pub mod wazuh;
|
// Re-export the wazuh-client crate types for convenience
|
||||||
|
pub use wazuh_client::{
|
||||||
pub use wazuh::client::WazuhIndexerClient;
|
WazuhClientFactory, WazuhClients, WazuhIndexerClient, WazuhApiError,
|
||||||
pub use wazuh::error::WazuhApiError;
|
AgentsClient, RulesClient, ConfigurationClient, VulnerabilityClient,
|
||||||
|
ActiveResponseClient, ClusterClient, LogsClient, ConnectivityStatus
|
||||||
|
};
|
||||||
|
51
src/main.rs
51
src/main.rs
@ -49,12 +49,7 @@ use std::env;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
|
|
||||||
mod wazuh {
|
use wazuh_client::{WazuhClientFactory, WazuhIndexerClient};
|
||||||
pub mod client;
|
|
||||||
pub mod error;
|
|
||||||
}
|
|
||||||
|
|
||||||
use wazuh::client::WazuhIndexerClient;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "mcp-server-wazuh")]
|
#[command(name = "mcp-server-wazuh")]
|
||||||
@ -72,7 +67,9 @@ struct GetAlertSummaryParams {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct WazuhToolsServer {
|
struct WazuhToolsServer {
|
||||||
wazuh_client: Arc<WazuhIndexerClient>,
|
#[allow(dead_code)] // Kept for future expansion to other Wazuh clients
|
||||||
|
wazuh_factory: Arc<WazuhClientFactory>,
|
||||||
|
wazuh_indexer_client: Arc<WazuhIndexerClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(tool_box)]
|
#[tool(tool_box)]
|
||||||
@ -81,10 +78,21 @@ impl WazuhToolsServer {
|
|||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
|
||||||
let wazuh_host = env::var("WAZUH_HOST").unwrap_or_else(|_| "localhost".to_string());
|
let wazuh_host = env::var("WAZUH_HOST").unwrap_or_else(|_| "localhost".to_string());
|
||||||
let wazuh_port = env::var("WAZUH_PORT")
|
let wazuh_api_port = env::var("WAZUH_API_PORT")
|
||||||
|
.unwrap_or_else(|_| "55000".to_string())
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid WAZUH_API_PORT: {}", e))?;
|
||||||
|
let wazuh_indexer_port = env::var("WAZUH_INDEXER_PORT")
|
||||||
.unwrap_or_else(|_| "9200".to_string())
|
.unwrap_or_else(|_| "9200".to_string())
|
||||||
.parse::<u16>()
|
.parse::<u16>()
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid WAZUH_PORT: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Invalid WAZUH_INDEXER_PORT: {}", e))?;
|
||||||
|
|
||||||
|
// For backward compatibility, also check WAZUH_PORT
|
||||||
|
let wazuh_port = env::var("WAZUH_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parse::<u16>().ok())
|
||||||
|
.unwrap_or(wazuh_indexer_port);
|
||||||
|
|
||||||
let wazuh_user = env::var("WAZUH_USER").unwrap_or_else(|_| "admin".to_string());
|
let wazuh_user = env::var("WAZUH_USER").unwrap_or_else(|_| "admin".to_string());
|
||||||
let wazuh_pass = env::var("WAZUH_PASS").unwrap_or_else(|_| "admin".to_string());
|
let wazuh_pass = env::var("WAZUH_PASS").unwrap_or_else(|_| "admin".to_string());
|
||||||
let verify_ssl = env::var("VERIFY_SSL")
|
let verify_ssl = env::var("VERIFY_SSL")
|
||||||
@ -95,17 +103,26 @@ impl WazuhToolsServer {
|
|||||||
let protocol = env::var("WAZUH_TEST_PROTOCOL").unwrap_or_else(|_| "https".to_string());
|
let protocol = env::var("WAZUH_TEST_PROTOCOL").unwrap_or_else(|_| "https".to_string());
|
||||||
tracing::debug!(?protocol, "Using Wazuh protocol for client from WAZUH_TEST_PROTOCOL or default");
|
tracing::debug!(?protocol, "Using Wazuh protocol for client from WAZUH_TEST_PROTOCOL or default");
|
||||||
|
|
||||||
let wazuh_client = WazuhIndexerClient::new_with_protocol(
|
// Create the factory with both API and Indexer configurations
|
||||||
wazuh_host,
|
let wazuh_factory = WazuhClientFactory::new(
|
||||||
wazuh_port,
|
wazuh_host.clone(), // API host
|
||||||
wazuh_user,
|
wazuh_api_port, // API port
|
||||||
wazuh_pass,
|
wazuh_user.clone(), // API username
|
||||||
|
wazuh_pass.clone(), // API password
|
||||||
|
wazuh_host, // Indexer host (same as API host)
|
||||||
|
wazuh_port, // Indexer port (use WAZUH_PORT for backward compatibility)
|
||||||
|
wazuh_user, // Indexer username
|
||||||
|
wazuh_pass, // Indexer password
|
||||||
verify_ssl,
|
verify_ssl,
|
||||||
&protocol,
|
Some(protocol),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create the indexer client using the factory
|
||||||
|
let wazuh_indexer_client = wazuh_factory.create_indexer_client();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
wazuh_client: Arc::new(wazuh_client),
|
wazuh_factory: Arc::new(wazuh_factory),
|
||||||
|
wazuh_indexer_client: Arc::new(wazuh_indexer_client),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +138,7 @@ impl WazuhToolsServer {
|
|||||||
|
|
||||||
tracing::info!(limit = %limit, "Retrieving Wazuh alert summary");
|
tracing::info!(limit = %limit, "Retrieving Wazuh alert summary");
|
||||||
|
|
||||||
match self.wazuh_client.get_alerts().await {
|
match self.wazuh_indexer_client.get_alerts().await {
|
||||||
Ok(raw_alerts) => {
|
Ok(raw_alerts) => {
|
||||||
let alerts_to_process: Vec<_> = raw_alerts.into_iter().take(limit as usize).collect();
|
let alerts_to_process: Vec<_> = raw_alerts.into_iter().take(limit as usize).collect();
|
||||||
|
|
||||||
|
@ -1,140 +0,0 @@
|
|||||||
use reqwest::{header, Client, Method};
|
|
||||||
use serde_json::{json, Value};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tracing::{debug, error, info};
|
|
||||||
|
|
||||||
use super::error::WazuhApiError;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WazuhIndexerClient {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
base_url: String,
|
|
||||||
http_client: Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WazuhIndexerClient {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn new(
|
|
||||||
host: String,
|
|
||||||
indexer_port: u16,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
verify_ssl: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self::new_with_protocol(host, indexer_port, username, password, verify_ssl, "https")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_protocol(
|
|
||||||
host: String,
|
|
||||||
indexer_port: u16,
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
verify_ssl: bool,
|
|
||||||
protocol: &str,
|
|
||||||
) -> Self {
|
|
||||||
debug!(%host, indexer_port, %username, %verify_ssl, %protocol, "Creating new WazuhIndexerClient");
|
|
||||||
// Base URL now points to the Indexer
|
|
||||||
let base_url = format!("{}://{}:{}", protocol, host, indexer_port);
|
|
||||||
debug!(%base_url, "Wazuh Indexer base URL set");
|
|
||||||
|
|
||||||
let http_client = Client::builder()
|
|
||||||
.danger_accept_invalid_certs(!verify_ssl)
|
|
||||||
.timeout(Duration::from_secs(30))
|
|
||||||
.build()
|
|
||||||
.expect("Failed to create HTTP client");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
base_url,
|
|
||||||
http_client,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn make_indexer_request(
|
|
||||||
&self,
|
|
||||||
method: Method,
|
|
||||||
endpoint: &str,
|
|
||||||
body: Option<Value>,
|
|
||||||
) -> Result<Value, WazuhApiError> {
|
|
||||||
debug!(?method, %endpoint, ?body, "Making request to Wazuh Indexer");
|
|
||||||
let url = format!("{}{}", self.base_url, endpoint);
|
|
||||||
debug!(%url, "Constructed Indexer request URL");
|
|
||||||
|
|
||||||
let mut request_builder = self
|
|
||||||
.http_client
|
|
||||||
.request(method.clone(), &url)
|
|
||||||
.basic_auth(&self.username, Some(&self.password)); // Use Basic Auth
|
|
||||||
|
|
||||||
if let Some(json_body) = &body {
|
|
||||||
request_builder = request_builder
|
|
||||||
.header(header::CONTENT_TYPE, "application/json")
|
|
||||||
.json(json_body);
|
|
||||||
}
|
|
||||||
debug!("Request builder configured with Basic Auth");
|
|
||||||
|
|
||||||
let response = request_builder.send().await?;
|
|
||||||
let status = response.status();
|
|
||||||
debug!(%status, "Received response from Indexer endpoint");
|
|
||||||
|
|
||||||
if !status.is_success() {
|
|
||||||
let error_text = response
|
|
||||||
.text()
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| "Unknown error reading response body".to_string());
|
|
||||||
error!(%url, %status, %error_text, "Indexer API request failed");
|
|
||||||
// Provide more context in the error
|
|
||||||
return Err(WazuhApiError::ApiError(format!(
|
|
||||||
"Indexer request to {} failed with status {}: {}",
|
|
||||||
url, status, error_text
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!("Indexer API request successful");
|
|
||||||
response.json().await.map_err(|e| {
|
|
||||||
error!("Failed to parse JSON response from Indexer: {}", e);
|
|
||||||
WazuhApiError::RequestError(e) // Use appropriate error variant
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_alerts(&self) -> Result<Vec<Value>, WazuhApiError> {
|
|
||||||
let endpoint = "/wazuh-alerts*/_search";
|
|
||||||
let query_body = json!({
|
|
||||||
"size": 100,
|
|
||||||
"query": {
|
|
||||||
"match_all": {}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
debug!(%endpoint, ?query_body, "Preparing to get alerts from Wazuh Indexer");
|
|
||||||
info!("Retrieving up to 100 alerts from Wazuh Indexer");
|
|
||||||
|
|
||||||
let response = self
|
|
||||||
.make_indexer_request(Method::POST, endpoint, Some(query_body))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let hits = response
|
|
||||||
.get("hits")
|
|
||||||
.and_then(|h| h.get("hits"))
|
|
||||||
.and_then(|h_array| h_array.as_array())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
error!(
|
|
||||||
?response,
|
|
||||||
"Failed to find 'hits.hits' array in Indexer response"
|
|
||||||
);
|
|
||||||
WazuhApiError::ApiError("Indexer response missing 'hits.hits' array".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let alerts: Vec<Value> = hits
|
|
||||||
.iter()
|
|
||||||
.filter_map(|hit| hit.get("_source").cloned())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"Successfully retrieved {} alerts from Indexer",
|
|
||||||
alerts.len()
|
|
||||||
);
|
|
||||||
Ok(alerts)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum WazuhApiError {
|
|
||||||
#[error("Failed to create HTTP client: {0}")]
|
|
||||||
HttpClientCreationError(reqwest::Error),
|
|
||||||
|
|
||||||
#[error("HTTP request error: {0}")]
|
|
||||||
RequestError(#[from] reqwest::Error),
|
|
||||||
|
|
||||||
#[error("JWT token not found in Wazuh API response")]
|
|
||||||
JwtNotFound,
|
|
||||||
|
|
||||||
#[error("Wazuh API Authentication failed: {0}")]
|
|
||||||
AuthenticationError(String),
|
|
||||||
|
|
||||||
#[error("JSON parsing error: {0}")]
|
|
||||||
JsonError(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
#[error("Wazuh API error: {0}")]
|
|
||||||
ApiError(String),
|
|
||||||
|
|
||||||
#[error("Alert with ID '{0}' not found")]
|
|
||||||
AlertNotFound(String),
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
pub mod client;
|
|
||||||
pub mod error;
|
|
Loading…
Reference in New Issue
Block a user