Wazuh group update (#511)
* Add API endpoint and models for updating Wazuh group configuration * precommit fixes
taylor_socfortress committed
Sep 12, 2025 at 15:23 UTC
2940ef848e3976039d7d5d572e50efcb7762e233
3 files changed
+159
backend/app/connectors/wazuh_manager/routes/groups.py
+50
@@ -2,17 +2,27 @@ from typing import List
2
from typing import Optional
3
4
from fastapi import APIRouter
5
+from fastapi import Body
6
from fastapi import Path
7
from fastapi import Query
8
from fastapi import Security
9
10
from app.auth.routes.auth import AuthHandler
11
+from app.connectors.wazuh_manager.schema.groups import (
12
+ WazuhGroupConfigurationUpdateRequest,
13
+)
14
+from app.connectors.wazuh_manager.schema.groups import (
15
+ WazuhGroupConfigurationUpdateResponse,
16
+)
17
from app.connectors.wazuh_manager.schema.groups import WazuhGroupFileResponse
18
from app.connectors.wazuh_manager.schema.groups import WazuhGroupFilesResponse
19
from app.connectors.wazuh_manager.schema.groups import WazuhGroupsResponse
20
from app.connectors.wazuh_manager.services.groups import get_wazuh_group_file
21
from app.connectors.wazuh_manager.services.groups import get_wazuh_group_files
22
from app.connectors.wazuh_manager.services.groups import get_wazuh_groups
23
+from app.connectors.wazuh_manager.services.groups import (
24
+ update_wazuh_group_configuration,
25
+)
26
27
wazuh_manager_groups_router = APIRouter()
28
auth_handler = AuthHandler()
@@ -162,3 +172,43 @@ async def get_wazuh_group_file_endpoint(
172
# Use locals() to capture all parameters, excluding path parameters
173
params = {k: v for k, v in locals().items() if k not in ["group_id", "filename"]}
174
return await get_wazuh_group_file(group_id, filename, **params)
175
+
176
+
177
+@wazuh_manager_groups_router.put(
178
+ "/groups/{group_id}/configuration",
179
+ response_model=WazuhGroupConfigurationUpdateResponse,
180
+ description="Update group configuration",
181
+ dependencies=[Security(AuthHandler().get_current_user, scopes=["admin"])],
182
+)
183
+async def update_wazuh_group_configuration_endpoint(
184
+ group_id: str = Path(..., description="Group ID (name of the group)"),
185
+ request: WazuhGroupConfigurationUpdateRequest = Body(..., description="Configuration update request"),
186
+ pretty: Optional[bool] = Query(False, description="Show results in human-readable format"),
187
+ wait_for_complete: Optional[bool] = Query(False, description="Disable timeout response"),
188
+) -> WazuhGroupConfigurationUpdateResponse:
189
+ """
190
+ Update an specified group's configuration.
191
+
192
+ This API call expects a full valid XML file with the shared configuration tags/syntax.
193
+ The configuration will be applied to all agents belonging to the specified group.
194
+
195
+ Parameters:
196
+ - group_id: The ID (name) of the group to update (required)
197
+ - request: The configuration update request containing XML content (required)
198
+ - pretty: Format results for human readability
199
+ - wait_for_complete: Disable request timeout
200
+
201
+ Request Body:
202
+ - configuration: Full valid XML configuration content
203
+
204
+ Returns:
205
+ - WazuhGroupConfigurationUpdateResponse: Confirmation of successful update
206
+
207
+ Raises:
208
+ - 400: If the configuration content is invalid or malformed XML
209
+ - 404: If the specified group is not found
210
+ - 500: If there's an error updating the configuration
211
+ """
212
+ # Use locals() to capture all parameters, excluding path and body parameters
213
+ params = {k: v for k, v in locals().items() if k not in ["group_id", "request"]}
214
+ return await update_wazuh_group_configuration(group_id, request.configuration, **params)
backend/app/connectors/wazuh_manager/schema/groups.py
+31
@@ -57,3 +57,34 @@ class WazuhGroupFilesResponse(BaseModel):
57
group_id: str = Field(..., description="The group ID")
58
results: List[WazuhGroupFile] = Field(default=[], description="List of files in the group")
59
total_items: Optional[int] = Field(None, description="Total number of files")
60
+
61
+
62
+class WazuhGroupConfigurationUpdateRequest(BaseModel):
63
+ """Request model for updating group configuration."""
64
+
65
+ configuration: str = Field(..., description="Full valid XML configuration content")
66
+
67
+ class Config:
68
+ schema_extra = {
69
+ "example": {
70
+ "configuration": """<agent_config>
71
+ <labels>
72
+ <label key="customer">example</label>
73
+ </labels>
74
+ <client_buffer>
75
+ <disabled>no</disabled>
76
+ <queue_size>100000</queue_size>
77
+ <events_per_second>1000</events_per_second>
78
+ </client_buffer>
79
+</agent_config>""",
80
+ },
81
+ }
82
+
83
+
84
+class WazuhGroupConfigurationUpdateResponse(BaseModel):
85
+ """Response model for group configuration update."""
86
+
87
+ success: bool = Field(..., description="Whether the request was successful")
88
+ message: str = Field(..., description="Response message")
89
+ group_id: str = Field(..., description="The group ID that was updated")
90
+ total_items: Optional[int] = Field(None, description="Total affected items from API")
backend/app/connectors/wazuh_manager/services/groups.py
+78
@@ -1,10 +1,14 @@
1
from fastapi import HTTPException
2
from loguru import logger
3
4
+from app.connectors.wazuh_manager.schema.groups import (
5
+ WazuhGroupConfigurationUpdateResponse,
6
+)
7
from app.connectors.wazuh_manager.schema.groups import WazuhGroupFileResponse
8
from app.connectors.wazuh_manager.schema.groups import WazuhGroupFilesResponse
9
from app.connectors.wazuh_manager.schema.groups import WazuhGroupsResponse
10
from app.connectors.wazuh_manager.utils.universal import send_get_request
11
+from app.connectors.wazuh_manager.utils.universal import send_put_request
12
13
14
async def get_wazuh_groups(**params) -> WazuhGroupsResponse:
@@ -209,3 +213,77 @@ async def get_wazuh_group_files(group_id: str, **params) -> WazuhGroupFilesRespo
213
except Exception as e:
214
logger.error(f"Error fetching Wazuh group files for group '{group_id}': {e}")
215
raise HTTPException(status_code=500, detail=f"Error fetching group files: {str(e)}")
216
+
217
+
218
+async def update_wazuh_group_configuration(group_id: str, configuration: str, **params) -> WazuhGroupConfigurationUpdateResponse:
219
+ """
220
+ Update a Wazuh group's configuration.
221
+
222
+ Args:
223
+ group_id: The ID (name) of the group
224
+ configuration: The XML configuration content to update
225
+ **params: All query parameters passed directly to the API
226
+
227
+ Returns:
228
+ WazuhGroupConfigurationUpdateResponse: Structured response with update result
229
+
230
+ Raises:
231
+ HTTPException: If there's an error updating the group configuration
232
+ """
233
+ # Filter out None values
234
+ clean_params = {k: v for k, v in params.items() if v is not None}
235
+
236
+ logger.debug(f"Updating Wazuh group configuration for group '{group_id}' with params: {clean_params}")
237
+ logger.debug(f"Configuration content length: {len(configuration)} characters")
238
+
239
+ try:
240
+ # Validate that we have configuration content
241
+ if not configuration or not configuration.strip():
242
+ raise HTTPException(status_code=400, detail="Configuration content cannot be empty")
243
+
244
+ # Basic XML validation (check for XML tags)
245
+ if not configuration.strip().startswith("<"):
246
+ logger.warning("Configuration does not appear to be XML format")
247
+ raise HTTPException(status_code=400, detail="Configuration must be valid XML format")
248
+
249
+ # Send PUT request with XML data
250
+ response = await send_put_request(
251
+ endpoint=f"/groups/{group_id}/configuration",
252
+ data=configuration,
253
+ params=clean_params,
254
+ xml_data=True,
255
+ )
256
+
257
+ # Check if the API request was successful
258
+ if not response.get("success"):
259
+ error_detail = response.get("message", f"Failed to update group configuration for group '{group_id}'")
260
+ status_code = response.get("status_code", 500)
261
+ logger.error(f"Wazuh API error: {error_detail}")
262
+
263
+ # Handle specific errors
264
+ if "not found" in error_detail.lower():
265
+ raise HTTPException(status_code=404, detail=f"Group '{group_id}' not found")
266
+ elif status_code == 400:
267
+ raise HTTPException(status_code=400, detail=f"Invalid configuration data: {error_detail}")
268
+ else:
269
+ raise HTTPException(status_code=status_code, detail=error_detail)
270
+
271
+ # Extract data from response
272
+ wazuh_data = response.get("data", {}).get("data", {})
273
+ total_items = wazuh_data.get("total_affected_items", 1)
274
+
275
+ logger.info(f"Successfully updated configuration for group '{group_id}'")
276
+
277
+ return WazuhGroupConfigurationUpdateResponse(
278
+ success=True,
279
+ message=f"Successfully updated configuration for group '{group_id}'",
280
+ group_id=group_id,
281
+ total_items=total_items,
282
+ )
283
+
284
+ except HTTPException:
285
+ # Re-raise HTTP exceptions as-is
286
+ raise
287
+ except Exception as e:
288
+ logger.error(f"Error updating Wazuh group configuration for group '{group_id}': {e}")
289
+ raise HTTPException(status_code=500, detail=f"Error updating group configuration: {str(e)}")