@cryptotaxi247 / CoPilot / commits / 2940ef84

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)}")