mirror of
https://github.com/gbrigandi/mcp-server-wazuh.git
synced 2025-12-21 20:52:17 -06:00
first commit
This commit is contained in:
60
tests/README.md
Normal file
60
tests/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Wazuh MCP Server Tests
|
||||
|
||||
This directory contains tests for the Wazuh MCP Server, including end-to-end tests that simulate a client interacting with the server.
|
||||
|
||||
## Test Files
|
||||
|
||||
- `e2e_client_test.rs`: End-to-end test for MCP client interacting with Wazuh MCP server
|
||||
- `integration_test.rs`: Integration test for Wazuh MCP Server with a mock Wazuh API
|
||||
- `mcp_client.rs`: Reusable MCP client implementation
|
||||
- `mcp_client_cli.rs`: Command-line tool for interacting with the MCP server
|
||||
|
||||
## Running the Tests
|
||||
|
||||
To run all tests:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
To run a specific test:
|
||||
|
||||
```bash
|
||||
cargo test --test e2e_client_test
|
||||
cargo test --test integration_test
|
||||
```
|
||||
|
||||
## Using the MCP Client CLI
|
||||
|
||||
The MCP Client CLI can be used to interact with the MCP server for testing purposes:
|
||||
|
||||
```bash
|
||||
# Build the CLI
|
||||
cargo build --bin mcp_client_cli
|
||||
|
||||
# Run the CLI
|
||||
MCP_SERVER_URL=http://localhost:8000 ./target/debug/mcp_client_cli get-data
|
||||
MCP_SERVER_URL=http://localhost:8000 ./target/debug/mcp_client_cli health
|
||||
MCP_SERVER_URL=http://localhost:8000 ./target/debug/mcp_client_cli query '{"severity": "high"}'
|
||||
```
|
||||
|
||||
## Test Environment Variables
|
||||
|
||||
The tests use the following environment variables:
|
||||
|
||||
- `MCP_SERVER_URL`: URL of the MCP server (default: http://localhost:8000)
|
||||
- `WAZUH_HOST`: Hostname of the Wazuh API server
|
||||
- `WAZUH_PORT`: Port of the Wazuh API server
|
||||
- `WAZUH_USER`: Username for Wazuh API authentication
|
||||
- `WAZUH_PASS`: Password for Wazuh API authentication
|
||||
- `VERIFY_SSL`: Whether to verify SSL certificates (default: false)
|
||||
- `RUST_LOG`: Log level for the tests (default: info)
|
||||
|
||||
## Mock Wazuh API Server
|
||||
|
||||
The tests use a mock Wazuh API server to simulate the Wazuh API. The mock server provides:
|
||||
|
||||
- Authentication endpoint: `/security/user/authenticate`
|
||||
- Alerts endpoint: `/wazuh-alerts-*_search`
|
||||
|
||||
The mock server returns predefined responses for these endpoints, allowing the tests to run without a real Wazuh API server.
|
||||
194
tests/e2e_client_test.rs
Normal file
194
tests/e2e_client_test.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use anyhow::Result;
|
||||
use httpmock::prelude::*;
|
||||
use reqwest::Client;
|
||||
use serde_json::{json, Value};
|
||||
use std::process::{Child, Command};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use uuid::Uuid;
|
||||
|
||||
struct MockWazuhServer {
|
||||
server: MockServer,
|
||||
}
|
||||
|
||||
impl MockWazuhServer {
|
||||
fn new() -> Self {
|
||||
let server = MockServer::start();
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(POST)
|
||||
.path("/security/user/authenticate")
|
||||
.header("Authorization", "Basic YWRtaW46YWRtaW4=");
|
||||
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"jwt": "mock.jwt.token"
|
||||
}));
|
||||
});
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path("/wazuh-alerts-*_search")
|
||||
.header("Authorization", "Bearer mock.jwt.token");
|
||||
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"hits": {
|
||||
"hits": [
|
||||
{
|
||||
"_source": {
|
||||
"id": "12345",
|
||||
"category": "intrusion_detection",
|
||||
"severity": "high",
|
||||
"description": "Possible intrusion attempt detected",
|
||||
"data": {
|
||||
"source_ip": "192.168.1.100",
|
||||
"destination_ip": "10.0.0.1",
|
||||
"port": 22
|
||||
},
|
||||
"notes": "Test alert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_source": {
|
||||
"id": "67890",
|
||||
"category": "malware",
|
||||
"severity": "critical",
|
||||
"description": "Malware detected on system",
|
||||
"data": {
|
||||
"file_path": "/tmp/malicious.exe",
|
||||
"hash": "abcdef123456",
|
||||
"signature": "EICAR-Test-File"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Self { server }
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
self.server.url("")
|
||||
}
|
||||
}
|
||||
|
||||
struct McpClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl McpClient {
|
||||
fn new(base_url: String) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self { client, base_url }
|
||||
}
|
||||
|
||||
async fn get_mcp_data(&self) -> Result<Vec<Value>> {
|
||||
let url = format!("{}/mcp", self.base_url);
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("MCP request failed with status: {}", response.status());
|
||||
}
|
||||
|
||||
let data = response.json::<Vec<Value>>().await?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn check_health(&self) -> Result<Value> {
|
||||
let url = format!("{}/health", self.base_url);
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("Health check failed with status: {}", response.status());
|
||||
}
|
||||
|
||||
let data = response.json::<Value>().await?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
fn start_mcp_server(wazuh_url: &str, port: u16) -> Child {
|
||||
let server_id = Uuid::new_v4().to_string();
|
||||
let wazuh_host_port: Vec<&str> = wazuh_url.trim_start_matches("http://").split(':').collect();
|
||||
let wazuh_host = wazuh_host_port[0];
|
||||
let wazuh_port = wazuh_host_port[1];
|
||||
|
||||
Command::new("cargo")
|
||||
.args(["run", "--"])
|
||||
.env("WAZUH_HOST", wazuh_host)
|
||||
.env("WAZUH_PORT", wazuh_port)
|
||||
.env("WAZUH_USER", "admin")
|
||||
.env("WAZUH_PASS", "admin")
|
||||
.env("VERIFY_SSL", "false")
|
||||
.env("MCP_SERVER_PORT", port.to_string())
|
||||
.env("RUST_LOG", "info")
|
||||
.env("SERVER_ID", server_id)
|
||||
.spawn()
|
||||
.expect("Failed to start MCP server")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_client_integration() -> Result<()> {
|
||||
let mock_wazuh = MockWazuhServer::new();
|
||||
let wazuh_url = mock_wazuh.url();
|
||||
|
||||
let mcp_port = 8765;
|
||||
let mut mcp_server = start_mcp_server(&wazuh_url, mcp_port);
|
||||
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let mcp_client = McpClient::new(format!("http://localhost:{}", mcp_port));
|
||||
|
||||
let health_data = mcp_client.check_health().await?;
|
||||
assert_eq!(health_data["status"], "ok");
|
||||
assert_eq!(health_data["service"], "wazuh-mcp-server");
|
||||
|
||||
let mcp_data = mcp_client.get_mcp_data().await?;
|
||||
|
||||
assert_eq!(mcp_data.len(), 2);
|
||||
|
||||
let first_message = &mcp_data[0];
|
||||
assert_eq!(first_message["protocol_version"], "1.0");
|
||||
assert_eq!(first_message["source"], "Wazuh");
|
||||
assert_eq!(first_message["event_type"], "alert");
|
||||
|
||||
let context = &first_message["context"];
|
||||
assert_eq!(context["id"], "12345");
|
||||
assert_eq!(context["category"], "intrusion_detection");
|
||||
assert_eq!(context["severity"], "high");
|
||||
assert_eq!(
|
||||
context["description"],
|
||||
"Possible intrusion attempt detected"
|
||||
);
|
||||
|
||||
let data = &context["data"];
|
||||
assert_eq!(data["source_ip"], "192.168.1.100");
|
||||
assert_eq!(data["destination_ip"], "10.0.0.1");
|
||||
assert_eq!(data["port"], 22);
|
||||
|
||||
let second_message = &mcp_data[1];
|
||||
let context = &second_message["context"];
|
||||
assert_eq!(context["id"], "67890");
|
||||
assert_eq!(context["category"], "malware");
|
||||
assert_eq!(context["severity"], "critical");
|
||||
assert_eq!(context["description"], "Malware detected on system");
|
||||
|
||||
let data = &context["data"];
|
||||
assert_eq!(data["file_path"], "/tmp/malicious.exe");
|
||||
assert_eq!(data["hash"], "abcdef123456");
|
||||
assert_eq!(data["signature"], "EICAR-Test-File");
|
||||
|
||||
mcp_server.kill().expect("Failed to kill MCP server");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
420
tests/integration_test.rs
Normal file
420
tests/integration_test.rs
Normal file
@@ -0,0 +1,420 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use httpmock::prelude::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde_json::json;
|
||||
use std::net::TcpListener;
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
mod mcp_client;
|
||||
use mcp_client::{McpClient, McpClientTrait, McpMessage};
|
||||
|
||||
static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
||||
|
||||
fn find_available_port() -> u16 {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port");
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.expect("Failed to get local address")
|
||||
.port();
|
||||
drop(listener);
|
||||
port
|
||||
}
|
||||
|
||||
struct MockWazuhServer {
|
||||
server: MockServer,
|
||||
}
|
||||
|
||||
impl MockWazuhServer {
|
||||
fn new() -> Self {
|
||||
let server = MockServer::start();
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(POST).path("/security/user/authenticate");
|
||||
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"jwt": "mock.jwt.token"
|
||||
}));
|
||||
});
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(GET).path("/wazuh-alerts-*_search");
|
||||
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"hits": {
|
||||
"hits": [
|
||||
{
|
||||
"_source": {
|
||||
"id": "12345",
|
||||
"category": "intrusion_detection",
|
||||
"severity": "high",
|
||||
"description": "Possible intrusion attempt detected",
|
||||
"data": {
|
||||
"source_ip": "192.168.1.100",
|
||||
"destination_ip": "10.0.0.1",
|
||||
"port": 22
|
||||
},
|
||||
"notes": "Test alert"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_source": {
|
||||
"id": "67890",
|
||||
"category": "malware",
|
||||
"severity": "critical",
|
||||
"description": "Malware detected on system",
|
||||
"data": {
|
||||
"file_path": "/tmp/malicious.exe",
|
||||
"hash": "abcdef123456",
|
||||
"signature": "EICAR-Test-File"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Self { server }
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
self.server.url("")
|
||||
}
|
||||
|
||||
fn host(&self) -> String {
|
||||
let url = self.url();
|
||||
let parts: Vec<&str> = url.trim_start_matches("http://").split(':').collect();
|
||||
parts[0].to_string()
|
||||
}
|
||||
|
||||
fn port(&self) -> u16 {
|
||||
let url = self.url();
|
||||
let parts: Vec<&str> = url.trim_start_matches("http://").split(':').collect();
|
||||
parts[1].parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_mock_wazuh_server() -> MockServer {
|
||||
let server = MockServer::start();
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(POST).path("/security/user/authenticate");
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({ "jwt": "mock.jwt.token" }));
|
||||
});
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new(r"/wazuh-alerts-.*_search").unwrap());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"hits": {
|
||||
"hits": [
|
||||
{
|
||||
"_source": {
|
||||
"id": "12345",
|
||||
"timestamp": "2024-01-01T10:00:00.000Z",
|
||||
"rule": {
|
||||
"level": 9,
|
||||
"description": "Possible intrusion attempt detected",
|
||||
"groups": ["intrusion_detection", "pci_dss"]
|
||||
},
|
||||
"agent": { "id": "001", "name": "test-agent" },
|
||||
"data": {
|
||||
"source_ip": "192.168.1.100",
|
||||
"destination_ip": "10.0.0.1",
|
||||
"port": 22
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_source": {
|
||||
"id": "67890",
|
||||
"timestamp": "2024-01-01T11:00:00.000Z",
|
||||
"rule": {
|
||||
"level": 12,
|
||||
"description": "Malware detected on system",
|
||||
"groups": ["malware"]
|
||||
},
|
||||
"agent": { "id": "002", "name": "another-agent" },
|
||||
"data": {
|
||||
"file_path": "/tmp/malicious.exe",
|
||||
"hash": "abcdef123456",
|
||||
"signature": "EICAR-Test-File"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
fn get_host_port(server: &MockServer) -> (String, u16) {
|
||||
let url = server.url("");
|
||||
let parts: Vec<&str> = url.trim_start_matches("http://").split(':').collect();
|
||||
let host = parts[0].to_string();
|
||||
let port = parts[1].parse().unwrap();
|
||||
(host, port)
|
||||
}
|
||||
|
||||
fn start_mcp_server(wazuh_host: &str, wazuh_port: u16, mcp_port: u16) -> Child {
|
||||
Command::new("cargo")
|
||||
.args(["run", "--"])
|
||||
.env("WAZUH_HOST", wazuh_host)
|
||||
.env("WAZUH_PORT", wazuh_port.to_string())
|
||||
.env("WAZUH_USER", "admin")
|
||||
.env("WAZUH_PASS", "admin")
|
||||
.env("VERIFY_SSL", "false")
|
||||
.env("MCP_SERVER_PORT", mcp_port.to_string())
|
||||
.env("RUST_LOG", "info")
|
||||
.spawn()
|
||||
.expect("Failed to start MCP server")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
async fn test_mcp_server_with_mock_wazuh() -> Result<()> {
|
||||
let _guard = TEST_MUTEX.lock().unwrap();
|
||||
|
||||
let mock_wazuh_server = setup_mock_wazuh_server();
|
||||
let (wazuh_host, wazuh_port) = get_host_port(&mock_wazuh_server);
|
||||
|
||||
let mcp_port = find_available_port();
|
||||
|
||||
let mut mcp_server = start_mcp_server(&wazuh_host, wazuh_port, mcp_port);
|
||||
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let mcp_client = McpClient::new(format!("http://localhost:{}", mcp_port));
|
||||
|
||||
let health_data = mcp_client.check_health().await?;
|
||||
assert_eq!(health_data["status"], "ok");
|
||||
assert_eq!(health_data["service"], "wazuh-mcp-server");
|
||||
|
||||
let mcp_data = mcp_client.get_mcp_data().await?;
|
||||
|
||||
assert_eq!(mcp_data.len(), 2);
|
||||
|
||||
let first_message: &McpMessage = &mcp_data[0];
|
||||
assert_eq!(first_message.protocol_version, "1.0");
|
||||
assert_eq!(first_message.source, "Wazuh");
|
||||
assert_eq!(first_message.event_type, "alert");
|
||||
|
||||
let context = &first_message.context;
|
||||
assert_eq!(context["id"], "12345");
|
||||
assert_eq!(context["category"], "intrusion_detection");
|
||||
assert_eq!(context["severity"], "high");
|
||||
assert_eq!(
|
||||
context["description"],
|
||||
"Possible intrusion attempt detected"
|
||||
);
|
||||
assert_eq!(context["agent"]["name"], "test-agent");
|
||||
|
||||
let data = &context["data"];
|
||||
assert_eq!(data["source_ip"], "192.168.1.100");
|
||||
assert_eq!(data["destination_ip"], "10.0.0.1");
|
||||
assert_eq!(data["port"], 22);
|
||||
|
||||
let second_message = &mcp_data[1];
|
||||
let context = &second_message.context;
|
||||
assert_eq!(context["id"], "67890");
|
||||
assert_eq!(context["category"], "malware");
|
||||
assert_eq!(context["severity"], "critical");
|
||||
assert_eq!(context["description"], "Malware detected on system");
|
||||
assert_eq!(context["agent"]["name"], "another-agent");
|
||||
|
||||
let data = &context["data"];
|
||||
assert_eq!(data["file_path"], "/tmp/malicious.exe");
|
||||
assert_eq!(data["hash"], "abcdef123456");
|
||||
assert_eq!(data["signature"], "EICAR-Test-File");
|
||||
|
||||
mcp_server.kill().expect("Failed to kill MCP server");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_server_wazuh_api_error() -> Result<()> {
|
||||
let _guard = TEST_MUTEX.lock().unwrap();
|
||||
|
||||
let mock_wazuh_server = setup_mock_wazuh_server();
|
||||
let (wazuh_host, wazuh_port) = get_host_port(&mock_wazuh_server);
|
||||
|
||||
mock_wazuh_server.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new(r"/wazuh-alerts-.*_search").unwrap());
|
||||
then.status(500)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({"error": "Wazuh internal error"}));
|
||||
});
|
||||
|
||||
let mcp_port = find_available_port();
|
||||
let mut mcp_server = start_mcp_server(&wazuh_host, wazuh_port, mcp_port);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let mcp_client = McpClient::new(format!("http://localhost:{}", mcp_port));
|
||||
|
||||
let result = mcp_client.get_mcp_data().await;
|
||||
assert!(result.is_err());
|
||||
let err_string = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
err_string.contains("500")
|
||||
|| err_string.contains("502")
|
||||
|| err_string.contains("API request failed")
|
||||
);
|
||||
|
||||
let health_result = mcp_client.check_health().await;
|
||||
assert!(health_result.is_ok());
|
||||
assert_eq!(health_result.unwrap()["status"], "ok");
|
||||
|
||||
mcp_server.kill().expect("Failed to kill MCP server");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_client_error_handling() -> Result<()> {
|
||||
let _guard = TEST_MUTEX.lock().unwrap();
|
||||
|
||||
let server = MockServer::start();
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(GET).path("/mcp");
|
||||
then.status(500)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"error": "Internal server error"
|
||||
}));
|
||||
});
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(GET).path("/health");
|
||||
then.status(503)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"error": "Service unavailable"
|
||||
}));
|
||||
});
|
||||
|
||||
let client = McpClient::new(server.url(""));
|
||||
|
||||
let result = client.get_mcp_data().await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.to_string().contains("500") || err.to_string().contains("MCP request failed"));
|
||||
|
||||
let result = client.check_health().await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.to_string().contains("503") || err.to_string().contains("Health check failed"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_server_missing_alert_data() -> Result<()> {
|
||||
let _guard = TEST_MUTEX.lock().unwrap();
|
||||
|
||||
let mock_wazuh_server = setup_mock_wazuh_server();
|
||||
let (wazuh_host, wazuh_port) = get_host_port(&mock_wazuh_server);
|
||||
|
||||
mock_wazuh_server.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new(r"/wazuh-alerts-.*_search").unwrap());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"hits": {
|
||||
"hits": [
|
||||
{
|
||||
"_source": {
|
||||
"id": "missing_all",
|
||||
"timestamp": "invalid-date-format"
|
||||
}
|
||||
},
|
||||
{
|
||||
"_source": {
|
||||
"id": "missing_rule_fields",
|
||||
"timestamp": "2024-05-05T11:00:00.000Z",
|
||||
"rule": { },
|
||||
"agent": { "id": "003", "name": "agent-minimal" },
|
||||
"data": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "no_source_nest",
|
||||
"timestamp": "2024-05-05T12:00:00.000Z",
|
||||
"rule": {
|
||||
"level": 2,
|
||||
"description": "Low severity event",
|
||||
"groups": ["low_sev"]
|
||||
},
|
||||
"agent": { "id": "004" },
|
||||
"data": { "info": "some data" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
let mcp_port = find_available_port();
|
||||
let mut mcp_server = start_mcp_server(&wazuh_host, wazuh_port, mcp_port);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let mcp_client = McpClient::new(format!("http://localhost:{}", mcp_port));
|
||||
|
||||
let mcp_data = mcp_client.get_mcp_data().await?;
|
||||
assert_eq!(mcp_data.len(), 3);
|
||||
|
||||
let msg1 = &mcp_data[0];
|
||||
assert_eq!(msg1.context["id"], "missing_all");
|
||||
assert_eq!(msg1.context["category"], "unknown_category");
|
||||
assert_eq!(msg1.context["severity"], "unknown_severity");
|
||||
assert_eq!(msg1.context["description"], "");
|
||||
assert!(
|
||||
msg1.context["agent"].is_object() && msg1.context["agent"].as_object().unwrap().is_empty()
|
||||
);
|
||||
assert!(
|
||||
msg1.context["data"].is_object() && msg1.context["data"].as_object().unwrap().is_empty()
|
||||
);
|
||||
let ts1 = DateTime::parse_from_rfc3339(&msg1.timestamp)
|
||||
.unwrap()
|
||||
.with_timezone(&Utc);
|
||||
assert!((Utc::now() - ts1).num_seconds() < 5);
|
||||
|
||||
let msg2 = &mcp_data[1];
|
||||
assert_eq!(msg2.context["id"], "missing_rule_fields");
|
||||
assert_eq!(msg2.context["category"], "unknown_category");
|
||||
assert_eq!(msg2.context["severity"], "unknown_severity");
|
||||
assert_eq!(msg2.context["description"], "");
|
||||
assert_eq!(msg2.context["agent"]["name"], "agent-minimal");
|
||||
assert!(
|
||||
msg2.context["data"].is_object() && msg2.context["data"].as_object().unwrap().is_empty()
|
||||
);
|
||||
assert_eq!(msg2.timestamp, "2024-05-05T11:00:00Z");
|
||||
|
||||
let msg3 = &mcp_data[2];
|
||||
assert_eq!(msg3.context["id"], "no_source_nest");
|
||||
assert_eq!(msg3.context["category"], "low_sev");
|
||||
assert_eq!(msg3.context["severity"], "low");
|
||||
assert_eq!(msg3.context["description"], "Low severity event");
|
||||
assert_eq!(msg3.context["agent"]["id"], "004");
|
||||
assert!(msg3.context["agent"].get("name").is_none());
|
||||
assert_eq!(msg3.context["data"]["info"], "some data");
|
||||
assert_eq!(msg3.timestamp, "2024-05-05T12:00:00Z");
|
||||
|
||||
mcp_server.kill().expect("Failed to kill MCP server");
|
||||
Ok(())
|
||||
}
|
||||
180
tests/mcp_client.rs
Normal file
180
tests/mcp_client.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct McpMessage {
|
||||
pub protocol_version: String,
|
||||
pub source: String,
|
||||
pub timestamp: String,
|
||||
pub event_type: String,
|
||||
pub context: Value,
|
||||
pub metadata: Value,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait McpClientTrait {
|
||||
async fn get_mcp_data(&self) -> Result<Vec<McpMessage>>;
|
||||
|
||||
async fn check_health(&self) -> Result<Value>;
|
||||
|
||||
async fn query_mcp_data(&self, filters: Value) -> Result<Vec<McpMessage>>;
|
||||
}
|
||||
|
||||
pub struct McpClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl McpClient {
|
||||
pub fn new(base_url: String) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self { client, base_url }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl McpClientTrait for McpClient {
|
||||
async fn get_mcp_data(&self) -> Result<Vec<McpMessage>> {
|
||||
let url = format!("{}/mcp", self.base_url);
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
let data = response.json::<Vec<McpMessage>>().await?;
|
||||
Ok(data)
|
||||
}
|
||||
status => {
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
anyhow::bail!("MCP request failed with status {}: {}", status, error_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_health(&self) -> Result<Value> {
|
||||
let url = format!("{}/health", self.base_url);
|
||||
let response = self.client.get(&url).send().await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
let data = response.json::<Value>().await?;
|
||||
Ok(data)
|
||||
}
|
||||
status => {
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
anyhow::bail!("Health check failed with status {}: {}", status, error_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_mcp_data(&self, filters: Value) -> Result<Vec<McpMessage>> {
|
||||
let url = format!("{}/mcp", self.base_url);
|
||||
let response = self.client.post(&url).json(&filters).send().await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => {
|
||||
let data = response.json::<Vec<McpMessage>>().await?;
|
||||
Ok(data)
|
||||
}
|
||||
status => {
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||
anyhow::bail!("MCP query failed with status {}: {}", status, error_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use httpmock::prelude::*;
|
||||
use serde_json::json;
|
||||
use tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_client_get_data() {
|
||||
let server = MockServer::start();
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(GET).path("/mcp");
|
||||
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!([
|
||||
{
|
||||
"protocol_version": "1.0",
|
||||
"source": "Wazuh",
|
||||
"timestamp": "2023-05-01T12:00:00Z",
|
||||
"event_type": "alert",
|
||||
"context": {
|
||||
"id": "12345",
|
||||
"category": "intrusion_detection",
|
||||
"severity": "high",
|
||||
"description": "Test alert",
|
||||
"data": {
|
||||
"source_ip": "192.168.1.100"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"integration": "Wazuh-MCP",
|
||||
"notes": "Test note"
|
||||
}
|
||||
}
|
||||
]));
|
||||
});
|
||||
|
||||
let client = McpClient::new(server.url(""));
|
||||
|
||||
let result = client.get_mcp_data().await.unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].protocol_version, "1.0");
|
||||
assert_eq!(result[0].source, "Wazuh");
|
||||
assert_eq!(result[0].event_type, "alert");
|
||||
|
||||
let context = &result[0].context;
|
||||
assert_eq!(context["id"], "12345");
|
||||
assert_eq!(context["category"], "intrusion_detection");
|
||||
assert_eq!(context["severity"], "high");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_client_health_check() {
|
||||
let server = MockServer::start();
|
||||
|
||||
server.mock(|when, then| {
|
||||
when.method(GET).path("/health");
|
||||
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!({
|
||||
"status": "ok",
|
||||
"service": "wazuh-mcp-server",
|
||||
"timestamp": "2023-05-01T12:00:00Z"
|
||||
}));
|
||||
});
|
||||
|
||||
let client = McpClient::new(server.url(""));
|
||||
|
||||
let result = client.check_health().await.unwrap();
|
||||
|
||||
assert_eq!(result["status"], "ok");
|
||||
assert_eq!(result["service"], "wazuh-mcp-server");
|
||||
}
|
||||
}
|
||||
97
tests/mcp_client_cli.rs
Normal file
97
tests/mcp_client_cli.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
mod mcp_client;
|
||||
use mcp_client::{McpClient, McpClientTrait};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} <command> [options]", args[0]);
|
||||
eprintln!("Commands:");
|
||||
eprintln!(" get-data - Get MCP data from the server");
|
||||
eprintln!(" health - Check server health");
|
||||
eprintln!(" query - Query MCP data with filters");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let command = &args[1];
|
||||
let mcp_url =
|
||||
env::var("MCP_SERVER_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
|
||||
|
||||
println!("Connecting to MCP server at: {}", mcp_url);
|
||||
let client = McpClient::new(mcp_url);
|
||||
|
||||
match command.as_str() {
|
||||
"get-data" => {
|
||||
println!("Fetching MCP data...");
|
||||
let data = client.get_mcp_data().await?;
|
||||
|
||||
println!("Received {} MCP messages:", data.len());
|
||||
for (i, message) in data.iter().enumerate() {
|
||||
println!("\nMessage {}:", i + 1);
|
||||
println!(" Source: {}", message.source);
|
||||
println!(" Event Type: {}", message.event_type);
|
||||
println!(" Timestamp: {}", message.timestamp);
|
||||
|
||||
let context = &message.context;
|
||||
println!(" Context:");
|
||||
println!(" ID: {}", context["id"]);
|
||||
println!(" Category: {}", context["category"]);
|
||||
println!(" Severity: {}", context["severity"]);
|
||||
println!(" Description: {}", context["description"]);
|
||||
|
||||
if let Some(data) = context.get("data").and_then(|d| d.as_object()) {
|
||||
println!(" Data:");
|
||||
for (key, value) in data {
|
||||
println!(" {}: {}", key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"health" => {
|
||||
println!("Checking server health...");
|
||||
let health = client.check_health().await?;
|
||||
|
||||
println!("Health status: {}", health["status"]);
|
||||
println!("Service: {}", health["service"]);
|
||||
println!("Timestamp: {}", health["timestamp"]);
|
||||
}
|
||||
"query" => {
|
||||
if args.len() < 3 {
|
||||
eprintln!("Error: Missing query parameters");
|
||||
eprintln!("Usage: {} query <json_filter>", args[0]);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let filter_str = &args[2];
|
||||
let filters: Value = serde_json::from_str(filter_str)?;
|
||||
|
||||
println!("Querying MCP data with filters: {}", filters);
|
||||
let data = client.query_mcp_data(filters).await?;
|
||||
|
||||
println!("Received {} MCP messages:", data.len());
|
||||
for (i, message) in data.iter().enumerate() {
|
||||
println!("\nMessage {}:", i + 1);
|
||||
println!(" Source: {}", message.source);
|
||||
println!(" Event Type: {}", message.event_type);
|
||||
|
||||
let context = &message.context;
|
||||
println!(" Context:");
|
||||
println!(" ID: {}", context["id"]);
|
||||
println!(" Category: {}", context["category"]);
|
||||
println!(" Severity: {}", context["severity"]);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Error: Unknown command '{}'", command);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
15
tests/run_tests.sh
Executable file
15
tests/run_tests.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Running all tests..."
|
||||
cargo test
|
||||
|
||||
echo "Building MCP client CLI..."
|
||||
cargo build --bin mcp_client_cli
|
||||
|
||||
if nc -z localhost 8000 2>/dev/null; then
|
||||
echo "Testing MCP client CLI against running server..."
|
||||
./target/debug/mcp_client_cli health
|
||||
./target/debug/mcp_client_cli get-data
|
||||
else
|
||||
echo "MCP server is not running. Start it with 'cargo run' to test the CLI."
|
||||
fi
|
||||
Reference in New Issue
Block a user