Skip to content

Commit 24073a1

Browse files
committed
Fix get_wazuh_alerts to query Wazuh Indexer instead of non-existent Manager API endpoint
The Wazuh Manager API does not have a /alerts endpoint — alerts are stored in the Wazuh Indexer (OpenSearch) in the wazuh-alerts-* index. The previous implementation hit GET /alerts on port 55000 which returned 404. - Add get_alerts() to WazuhIndexerClient that queries wazuh-alerts-* index with OpenSearch query DSL, supporting filters for rule_id, level (min severity), agent_id, and timestamp range - Update WazuhClient.get_alerts() to route through the indexer client (same pattern as get_vulnerabilities) - Raise IndexerNotConfiguredError with clear setup instructions when indexer is not configured - Results sorted by timestamp descending, returned in standard Wazuh format compatible with existing compact mode - Skip empty/None filters to avoid sending blank query params
1 parent 47f1ea0 commit 24073a1

File tree

2 files changed

+124
-11
lines changed

2 files changed

+124
-11
lines changed

src/wazuh_mcp_server/api/wazuh_client.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,34 @@ async def _authenticate(self):
101101
raise ValueError(f"Wazuh API error: {e.response.status_code} - {e.response.text}")
102102

103103
async def get_alerts(self, **params) -> Dict[str, Any]:
104-
"""Get alerts from Wazuh."""
105-
return await self._request("GET", "/alerts", params=params)
104+
"""
105+
Get alerts from the Wazuh Indexer (wazuh-alerts-* index).
106+
107+
Alerts are stored in the Wazuh Indexer, not the Manager API.
108+
The Manager API does not have a /alerts endpoint.
109+
110+
Raises:
111+
IndexerNotConfiguredError: If Wazuh Indexer is not configured
112+
"""
113+
if not self._indexer_client:
114+
raise IndexerNotConfiguredError(
115+
"Wazuh Indexer not configured. "
116+
"Alerts are stored in the Wazuh Indexer and require WAZUH_INDEXER_HOST to be set.\n\n"
117+
"Please set the following environment variables:\n"
118+
" WAZUH_INDEXER_HOST=<indexer_hostname>\n"
119+
" WAZUH_INDEXER_USER=<indexer_username>\n"
120+
" WAZUH_INDEXER_PASS=<indexer_password>\n"
121+
" WAZUH_INDEXER_PORT=9200 (optional, default: 9200)"
122+
)
123+
124+
return await self._indexer_client.get_alerts(
125+
limit=params.get("limit", 100),
126+
rule_id=params.get("rule_id"),
127+
level=params.get("level"),
128+
agent_id=params.get("agent_id"),
129+
timestamp_start=params.get("timestamp_start"),
130+
timestamp_end=params.get("timestamp_end"),
131+
)
106132

107133
async def get_agents(self, **params) -> Dict[str, Any]:
108134
"""Get agents from Wazuh."""

src/wazuh_mcp_server/api/wazuh_indexer.py

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
"""
2-
Wazuh Indexer client for vulnerability queries (Wazuh 4.8.0+).
2+
Wazuh Indexer client for alert and vulnerability queries.
33
4-
Since Wazuh 4.8.0, vulnerability data is stored in the Wazuh Indexer
5-
(Elasticsearch/OpenSearch) instead of being available via the Wazuh Manager API.
6-
7-
The vulnerability API endpoint (/vulnerability/*) was deprecated in 4.7.0
8-
and removed in 4.8.0. This client queries the wazuh-states-vulnerabilities-*
9-
index directly.
4+
Wazuh stores alerts and vulnerability data in the Wazuh Indexer
5+
(Elasticsearch/OpenSearch), not the Manager API. This client queries:
6+
- wazuh-alerts-* — alert data (alerts have never had a Manager API endpoint)
7+
- wazuh-states-vulnerabilities-* — vulnerability data (removed from Manager API in 4.8.0)
108
"""
119

1210
import logging
@@ -17,7 +15,8 @@
1715

1816
logger = logging.getLogger(__name__)
1917

20-
# Vulnerability index pattern for Wazuh 4.8+
18+
# Index patterns for Wazuh 4.x
19+
ALERTS_INDEX = "wazuh-alerts-*"
2120
VULNERABILITY_INDEX = "wazuh-states-vulnerabilities-*"
2221

2322

@@ -120,6 +119,94 @@ async def _search(self, index: str, query: Dict[str, Any], size: int = 100) -> D
120119
except httpx.TimeoutException:
121120
raise ConnectionError(f"Timeout connecting to Wazuh Indexer at {self.host}:{self.port}")
122121

122+
async def get_alerts(
123+
self,
124+
limit: int = 100,
125+
rule_id: Optional[str] = None,
126+
level: Optional[str] = None,
127+
agent_id: Optional[str] = None,
128+
timestamp_start: Optional[str] = None,
129+
timestamp_end: Optional[str] = None,
130+
) -> Dict[str, Any]:
131+
"""
132+
Get alerts from the Wazuh Indexer (wazuh-alerts-* index).
133+
134+
Args:
135+
limit: Maximum number of results
136+
rule_id: Filter by rule ID
137+
level: Minimum rule level (severity)
138+
agent_id: Filter by agent ID
139+
timestamp_start: Start of time range (ISO 8601)
140+
timestamp_end: End of time range (ISO 8601)
141+
142+
Returns:
143+
Alert data in standard Wazuh format
144+
"""
145+
await self._ensure_initialized()
146+
147+
# Build bool query with must clauses for each non-empty filter
148+
must_clauses: list = []
149+
150+
if rule_id:
151+
must_clauses.append({"match": {"rule.id": rule_id}})
152+
153+
if agent_id:
154+
must_clauses.append({"match": {"agent.id": agent_id}})
155+
156+
if level:
157+
# level is a minimum severity threshold (e.g. "10" means level >= 10)
158+
try:
159+
min_level = int(level.rstrip("+"))
160+
must_clauses.append({"range": {"rule.level": {"gte": min_level}}})
161+
except (ValueError, AttributeError):
162+
pass
163+
164+
if timestamp_start or timestamp_end:
165+
time_range: Dict[str, str] = {}
166+
if timestamp_start:
167+
time_range["gte"] = timestamp_start
168+
if timestamp_end:
169+
time_range["lte"] = timestamp_end
170+
must_clauses.append({"range": {"timestamp": time_range}})
171+
172+
if must_clauses:
173+
query = {"bool": {"must": must_clauses}}
174+
else:
175+
query = {"match_all": {}}
176+
177+
# Build the full search body with sort by timestamp desc
178+
url = f"{self.base_url}/{ALERTS_INDEX}/_search"
179+
body = {
180+
"query": query,
181+
"size": limit,
182+
"sort": [{"timestamp": {"order": "desc"}}],
183+
}
184+
185+
try:
186+
response = await self.client.post(url, json=body, headers={"Content-Type": "application/json"})
187+
response.raise_for_status()
188+
result = response.json()
189+
except httpx.HTTPStatusError as e:
190+
logger.error(f"Alerts query failed: {e.response.status_code} - {e.response.text}")
191+
raise ValueError(f"Alerts query failed: {e.response.status_code}")
192+
except httpx.ConnectError:
193+
raise ConnectionError(f"Cannot connect to Wazuh Indexer at {self.host}:{self.port}")
194+
except httpx.TimeoutException:
195+
raise ConnectionError(f"Timeout connecting to Wazuh Indexer at {self.host}:{self.port}")
196+
197+
# Transform to standard Wazuh format
198+
hits = result.get("hits", {})
199+
alerts = [hit.get("_source", {}) for hit in hits.get("hits", [])]
200+
201+
return {
202+
"data": {
203+
"affected_items": alerts,
204+
"total_affected_items": hits.get("total", {}).get("value", len(alerts)),
205+
"total_failed_items": 0,
206+
"failed_items": [],
207+
}
208+
}
209+
123210
async def get_vulnerabilities(
124211
self,
125212
agent_id: Optional[str] = None,
@@ -291,7 +378,7 @@ class IndexerNotConfiguredError(Exception):
291378
def __init__(self, message: str = None):
292379
default_message = (
293380
"Wazuh Indexer not configured. "
294-
"Vulnerability tools require the Wazuh Indexer for Wazuh 4.8.0+.\n\n"
381+
"Alert and vulnerability tools require the Wazuh Indexer.\n\n"
295382
"Please set the following environment variables:\n"
296383
" WAZUH_INDEXER_HOST=<indexer_hostname>\n"
297384
" WAZUH_INDEXER_USER=<indexer_username>\n"

0 commit comments

Comments
 (0)