main
py 598 lines 25.2 KB
Raw
1 import os
2
3 from fastapi import APIRouter
4 from fastapi import Depends
5 from fastapi import Header
6 from fastapi import HTTPException
7 from fastapi import Query
8 from fastapi import Security
9 from loguru import logger
10 from pydantic import ValidationError
11 from sqlalchemy.ext.asyncio import AsyncSession
12
13 from app.active_response.routes.graylog import verify_graylog_header
14 from app.active_response.schema.graylog import GraylogThresholdEventNotification
15 from app.auth.utils import AuthHandler
16 from app.db.db_session import get_db
17 from app.incidents.schema.alert_collection import AlertsPayload
18 from app.incidents.schema.incident_alert import AlertDetailsResponse
19 from app.incidents.schema.incident_alert import AlertTimelineResponse
20 from app.incidents.schema.incident_alert import AutoCreateAlertResponse
21 from app.incidents.schema.incident_alert import CreateAlertRequest
22 from app.incidents.schema.incident_alert import CreateAlertRequestRoute
23 from app.incidents.schema.incident_alert import CreateAlertResponse
24 from app.incidents.schema.incident_alert import CreatedAlertPayload
25 from app.incidents.schema.incident_alert import IndexNamesResponse
26 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlert
27 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlertResponse
28 from app.incidents.schema.velo_sigma import VeloSigmaExclusionCreate
29 from app.incidents.schema.velo_sigma import VeloSigmaExclusionListResponse
30 from app.incidents.schema.velo_sigma import VeloSigmaExclusionUpdate
31 from app.incidents.schema.velo_sigma import VeloSigmaExlcusionRouteResponse
32 from app.incidents.services.alert_collection import add_copilot_alert_id
33 from app.incidents.services.alert_collection import get_alerts_not_created_in_copilot
34 from app.incidents.services.alert_collection import get_graylog_event_indices
35 from app.incidents.services.alert_collection import get_original_alert_id
36 from app.incidents.services.alert_collection import get_original_alert_index_name
37 from app.incidents.services.incident_alert import add_alert_to_document
38 from app.incidents.services.incident_alert import create_alert
39 from app.incidents.services.incident_alert import create_alert_full
40 from app.incidents.services.incident_alert import get_single_alert_details
41 from app.incidents.services.incident_alert import retrieve_alert_timeline
42 from app.incidents.services.threshold_alert import resolve_threshold_asset
43 from app.incidents.services.threshold_alert import resolve_threshold_event
44 from app.incidents.services.threshold_alert import save_threshold_metadata
45 from app.incidents.services.velo_sigma import VeloSigmaExclusionService
46 from app.incidents.services.velo_sigma import create_velo_sigma_alert
47
48 incidents_alerts_router = APIRouter()
49
50
51 # Function to validate the Velociraptor header
52 async def verify_velociraptor_header(velociraptor: str = Header(None)):
53 """Verify that the request has the correct Velociraptor header."""
54 # Get the header value from environment variable or use "ab73de7a-6f61-4dde-87cd-3af5175a7281" as default
55 expected_header = os.getenv("VELOCIRAPTOR_API_HEADER_VALUE", "ab73de7a-6f61-4dde-87cd-3af5175a7281")
56
57 if velociraptor != expected_header:
58 logger.error("Invalid or missing Velociraptor header")
59 raise HTTPException(status_code=403, detail="Invalid or missing Velociraptor header")
60 return velociraptor
61
62
63 @incidents_alerts_router.get(
64 "/index/names",
65 response_model=IndexNamesResponse,
66 description="Get the Graylog event indices",
67 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
68 )
69 async def get_index_names_route() -> IndexNamesResponse:
70 """
71 Get the Graylog event indices. Get the Graylog event indices for the Graylog events.
72
73 Returns:
74 List[str]: The list of Graylog event indices.
75 """
76 return await get_graylog_event_indices()
77
78
79 @incidents_alerts_router.get(
80 "/alerts/not-created",
81 description="Get alerts not created in CoPilot",
82 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
83 )
84 async def get_alerts_not_created_route() -> AlertsPayload:
85 """
86 Get alerts not created in CoPilot. Get all the results from the list of indices, where `copilot_alert_id` does not exist.
87
88 Returns:
89 List[AlertPayloadItem]: The list of alerts that have not been created in CoPilot.
90 """
91 return await get_alerts_not_created_in_copilot()
92
93
94 @incidents_alerts_router.post(
95 "/alert/details",
96 description="Get the details of a single alert",
97 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))],
98 )
99 async def get_single_alert_details_route(
100 create_alert_request: CreateAlertRequestRoute,
101 ) -> AlertDetailsResponse:
102 """
103 Get the details of a single alert. Get the details of a single alert based on the alert id.
104 Takes the alert id and the index name as input.
105
106 Args:
107 create_alert_request (CreateAlertRequestRoute): The request object containing the details of the alert to be created.
108
109 Returns:
110 class AlertDetailsResponse(BaseModel): The response object containing the details of the alert.
111 """
112 return AlertDetailsResponse(
113 success=True,
114 message="Alert details retrieved",
115 alert_details=await get_single_alert_details(
116 CreateAlertRequest(index_name=create_alert_request.index_name, alert_id=create_alert_request.index_id),
117 ),
118 )
119
120
121 @incidents_alerts_router.post(
122 "/alert/timeline",
123 description="Get the timeline of an alert",
124 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))],
125 )
126 async def get_alert_timeline_route(
127 alert: CreateAlertRequestRoute,
128 session: AsyncSession = Depends(get_db),
129 ) -> AlertTimelineResponse:
130 """
131 Get the timeline of an alert. This route obtains the process_id from the alert details if it exists
132 and queries the Indexer for all events with the same process_id and hostname within a 24 hour period.
133
134 Args:
135 create_alert_request (CreateAlertRequestRoute): The request object containing the details of the alert to be created.
136
137
138 Returns:
139 class AlertTimelineResponse(BaseModel): The response object containing the details of the alert.
140 """
141 # await retrieve_alert_timeline(alert, session)
142 return AlertTimelineResponse(
143 success=True,
144 message="Alert timeline retrieved",
145 alert_timeline=await retrieve_alert_timeline(alert, session),
146 )
147
148
149 @incidents_alerts_router.post(
150 "/create/manual",
151 response_model=CreateAlertResponse,
152 description="Manually create an incident alert in CoPilot",
153 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
154 )
155 async def create_alert_manual_route(
156 create_alert_request: CreateAlertRequest,
157 session: AsyncSession = Depends(get_db),
158 ) -> CreateAlertResponse:
159 """
160 Create an incident alert in CoPilot. Manually create an incident alert within CoPilot.
161 Used via the Alerts, for manual incident alert creation.
162
163 Args:
164 create_alert_request (CreateAlertRequest): The request object containing the details of the alert to be created.
165 session (AsyncSession, optional): The database session. Defaults to Depends(get_session).
166
167 Returns:
168 CreateAlertResponse: The response object containing the result of the alert creation.
169 """
170 logger.info(f"Creating alert {create_alert_request.alert_id} in CoPilot")
171 return CreateAlertResponse(success=True, message="Alert created in CoPilot", alert_id=await create_alert(create_alert_request, session))
172
173
174 # @incidents_alerts_router.post(
175 # "/create/auto",
176 # response_model=CreateAlertResponse,
177 # description="Is invoked by the scheduler to create an incident alert in CoPilot",
178 # )
179 # async def create_alert_auto_route(
180 # session: AsyncSession = Depends(get_db),
181 # ) -> AutoCreateAlertResponse:
182 # """
183 # Create an incident alert in CoPilot. Automatically create an incident alert within CoPilot.
184 # This queries the `gl-events-*` indices for alerts that have not been created in CoPilot.
185 # It is important to note that Graylog must be configured for the alerts.
186
187 # Args:
188 # create_alert_request (CreateAlertRequest): The request object containing the details of the alert to be created.
189 # session (AsyncSession, optional): The database session. Defaults to Depends(get_session).
190
191 # Returns:
192 # CreateAlertResponse: The response object containing the result of the alert creation.
193 # """
194 # alerts = await get_alerts_not_created_in_copilot()
195 # logger.info(f"Alerts to create in CoPilot: {alerts}")
196 # if len(alerts.alerts) == 0:
197 # return AutoCreateAlertResponse(success=False, message="No alerts to create in CoPilot")
198
199 # created_alerts_count = 0
200
201 # for alert in alerts.alerts:
202 # try:
203 # logger.info(f"Creating alert {alert} in CoPilot")
204 # create_alert_request = CreateAlertRequest(
205 # index_name=await get_original_alert_index_name(origin_context=alert.source.origin_context),
206 # alert_id=await get_original_alert_id(alert.source.origin_context),
207 # )
208 # logger.info(f"Creating alert {create_alert_request.alert_id} in CoPilot")
209 # alert_id = await create_alert(create_alert_request, session)
210 # # ! ADD THE COPILOT ALERT ID TO GRAYLOG EVENT INDEX # !
211 # await add_copilot_alert_id(index_data=CreateAlertRequest(index_name=alert.index, alert_id=alert.id), alert_id=alert_id)
212 # created_alerts_count += 1
213 # except Exception as e:
214 # logger.error(f"Failed to create alert {alert} in CoPilot: {e}")
215
216
217 @incidents_alerts_router.post(
218 "/create/auto",
219 response_model=AutoCreateAlertResponse,
220 description="Is invoked by the scheduler to create an incident alert in CoPilot",
221 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
222 )
223 async def create_alert_auto_route(
224 session: AsyncSession = Depends(get_db),
225 ) -> AutoCreateAlertResponse:
226 """
227 Create incident alerts in CoPilot in batches. Automatically create incident alerts within CoPilot.
228 This queries the `gl-events-*` indices for alerts that have not been created in CoPilot.
229
230 Processing is done in batches to prevent memory issues with large numbers of alerts.
231 The scheduler will call this endpoint multiple times until all alerts are processed.
232
233 Args:
234 batch_size: Number of alerts to process per batch (default 100, max 500)
235 max_batches: Maximum number of batches to process in one scheduler run (default 10, max 50)
236 session (AsyncSession): The database session.
237
238 Returns:
239 AutoCreateAlertResponse: The response object containing the result of the alert creation.
240 """
241 batch_size = 100 # Number of alerts to process per batch
242 max_batches = 10 # Maximum number of batches to process in one scheduler run
243
244 total_created = 0
245 total_failed = 0
246 batches_processed = 0
247
248 logger.info(f"Starting auto alert creation with batch_size={batch_size}, max_batches={max_batches}")
249
250 for batch_num in range(max_batches):
251 # Fetch the next batch
252 alerts_payload, total_remaining = await get_alerts_not_created_in_copilot(batch_size=batch_size)
253
254 if len(alerts_payload.alerts) == 0:
255 logger.info(f"No more alerts to process after {batches_processed} batches")
256 break
257
258 logger.info(f"Processing batch {batch_num + 1}/{max_batches}: {len(alerts_payload.alerts)} alerts.)")
259 logger.info(f"Total remaining alerts after this batch: {total_remaining}")
260
261 # Process this batch
262 batch_created = 0
263 batch_failed = 0
264
265 for alert in alerts_payload.alerts:
266 try:
267 create_alert_request = CreateAlertRequest(
268 index_name=await get_original_alert_index_name(origin_context=alert.source.origin_context),
269 alert_id=await get_original_alert_id(alert.source.origin_context),
270 )
271
272 alert_id = await create_alert(create_alert_request, session)
273
274 # Add the CoPilot alert ID to Graylog event index
275 await add_copilot_alert_id(index_data=CreateAlertRequest(index_name=alert.index, alert_id=alert.id), alert_id=alert_id)
276
277 batch_created += 1
278 total_created += 1
279
280 except ValidationError as e:
281 # Pydantic validation failure — log per-field detail so a misshapen
282 # index document is fast to diagnose (which field, what was provided,
283 # which constraint failed).
284 logger.error(
285 f"Failed to create alert {alert.id} from index {alert.index}: "
286 f"Pydantic validation failed in {len(e.errors())} field(s)",
287 )
288 for err in e.errors():
289 loc = ".".join(str(x) for x in err.get("loc", ()))
290 inp = err.get("input", "<not captured>")
291 inp_repr = repr(inp)[:200] # truncate verbose inputs
292 logger.error(
293 f" field={loc} type={err.get('type')} msg={err.get('msg')} input={inp_repr}",
294 )
295 batch_failed += 1
296 total_failed += 1
297 except Exception as e:
298 # Any other error — preserve the full traceback so the source line
299 # of the failure is in the log, not just the message.
300 logger.opt(exception=True).error(
301 f"Failed to create alert {alert.id} from index {alert.index}: " f"{type(e).__name__}: {e}",
302 )
303 batch_failed += 1
304 total_failed += 1
305
306 batches_processed += 1
307 logger.info(f"Batch {batch_num + 1} complete: {batch_created} created, {batch_failed} failed")
308
309 # If we processed fewer alerts than the batch size, we're done
310 if len(alerts_payload.alerts) < batch_size:
311 logger.info("Processed final batch (fewer alerts than batch size)")
312 break
313
314 message = f"Processed {batches_processed} batches: {total_created} alerts created, {total_failed} failed"
315
316 if total_remaining > 0:
317 message += f". {total_remaining} alerts remaining for next run"
318
319 logger.info(message)
320
321 return AutoCreateAlertResponse(
322 success=True,
323 message=message,
324 alerts_created=total_created,
325 alerts_failed=total_failed,
326 batches_processed=batches_processed,
327 alerts_remaining=max(0, total_remaining - len(alerts_payload.alerts)) if batches_processed < max_batches else total_remaining,
328 )
329
330
331 @incidents_alerts_router.post(
332 "/create/threshold",
333 response_model=CreateAlertResponse,
334 description="Creates an incident alert in CoPilot for a Graylog configured threshold alert",
335 dependencies=[Depends(verify_graylog_header)],
336 )
337 async def invoke_alert_threshold_graylog_route(
338 request: GraylogThresholdEventNotification,
339 session: AsyncSession = Depends(get_db),
340 ) -> CreateAlertResponse:
341 """
342 This route accepts an HTTP Post from Graylog for any threshold alerts which needs a dedicated route
343 because there is no individual alert with an _id that we can use to grab from the
344 wazuh-indexer.
345 REQUIRED FILEDS:
346 1. CUSTOMER_CODE: str - the customer code
347 2. SOURCE: str - the source of the alert
348 3. ALERT_DESCRIPTION: str - the description of the alert
349 4. ASSET_NAME: str - the name of the asset
350
351 # ! IMPORTANT: DO NOT ADD THE "COPILOT_ALERT_ID": "NONE" AS A CUSTOM FIELD WHEN CREATING THE ALERT IN GRAYLOG # !
352 # ! THIS WILL BREAK THE AUTO-ALERT CREATION FUNCTIONALITY # !
353
354 # ! Make sure the Graylog Notification is just the standard HTTP Notification Type and not the Custom HTTP Notification Type !
355
356 Args:
357 request (InvokeActiveResponseRequest): The request object containing the command, custom, arguments, and alert.
358
359 Returns:
360 CreateAlertResponse: The response object containing the result of the alert creation.
361 """
362 logger.info("Invoking alert threshold Graylog...")
363 logger.info(f"Timestamp: {request.event.timestamp}")
364
365 # Resolve the underlying event from OpenSearch using the replay_info and group_by_fields
366 resolved_index_name, resolved_index_id = await resolve_threshold_event(
367 replay_query=request.event.replay_info.query,
368 timerange_start=request.event.replay_info.timerange_start,
369 timerange_end=request.event.replay_info.timerange_end,
370 group_by_fields=request.event.group_by_fields,
371 source=request.event.fields.SOURCE,
372 )
373 logger.info(f"Resolved threshold event: index={resolved_index_name}, id={resolved_index_id}")
374
375 # Resolve asset name from the actual event document in OpenSearch
376 asset_name = await resolve_threshold_asset(
377 index_name=resolved_index_name,
378 index_id=resolved_index_id,
379 source=request.event.fields.SOURCE,
380 session=session,
381 )
382 logger.info(f"Resolved threshold asset: {asset_name}")
383
384 alert_id = await create_alert_full(
385 alert_payload=CreatedAlertPayload(
386 alert_context_payload=request.event.fields.model_dump(),
387 asset_payload=asset_name,
388 timefield_payload=str(request.event.timestamp),
389 alert_title_payload=request.event.message,
390 source=request.event.fields.SOURCE,
391 index_name=resolved_index_name,
392 index_id=resolved_index_id,
393 ),
394 customer_code=request.event.fields.CUSTOMER_CODE,
395 session=session,
396 threshold_alert=True,
397 )
398
399 # Add the CoPilot alert_id to the resolved OpenSearch document
400 if resolved_index_name != "not_applicable" and resolved_index_id != "not_applicable":
401 await add_alert_to_document(
402 CreateAlertRequest(index_name=resolved_index_name, alert_id=resolved_index_id),
403 alert_id,
404 )
405
406 # Save threshold metadata for later timeline retrieval
407 try:
408 await save_threshold_metadata(
409 alert_id=alert_id,
410 event_definition_id=request.event.event_definition_id,
411 replay_query=request.event.replay_info.query,
412 timerange_start=request.event.replay_info.timerange_start,
413 timerange_end=request.event.replay_info.timerange_end,
414 group_by_fields=request.event.group_by_fields,
415 source_streams=request.event.source_streams,
416 source=request.event.fields.SOURCE,
417 resolved_index_name=resolved_index_name,
418 resolved_index_id=resolved_index_id,
419 session=session,
420 )
421 except Exception as e:
422 logger.error(f"Failed to save threshold metadata for alert {alert_id}: {e}")
423
424 return CreateAlertResponse(success=True, message="Alert threshold Graylog invoked successfully", alert_id=alert_id)
425
426
427 @incidents_alerts_router.post(
428 "/create/velo-sigma",
429 response_model=VelociraptorSigmaAlertResponse,
430 description="Creates an incident alert in CoPilot for a Velociraptor Sigma alert",
431 dependencies=[Depends(verify_velociraptor_header)],
432 )
433 async def process_sigma_alert(alert: VelociraptorSigmaAlert, session: AsyncSession = Depends(get_db)) -> VelociraptorSigmaAlertResponse:
434 """
435 This route receives a Velociraptor Sigma alert. You must have defined the Windows.Hayabusa.Monitoring
436 client Event defined which will search for the Sigma alert in the Velociraptor client.
437 When a Sigma alert is found, Velociraptor will us the `CoPilot.Events.Upload` to send a POST
438 request to this endpoint with the alert data.
439
440 An issue is that we want to fetch the wazuh event that is related to the Sigma alert so that we can
441 create the alert within CoPilot accordingly. To do this we extract the `computer` as the `agent_name`
442 and the `EventRecordID` as the `data_win_system_eventRecordID` and then query the Wazuh Indexer
443 to fetch this sepcific event with a timeframe of 1 hour.
444
445 Then we progress through the CoPilot Alert Creation process as normal.
446 """
447 logger.info(f"Processing Velociraptor Sigma alert: {alert}")
448 return await create_velo_sigma_alert(alert, session)
449
450
451 @incidents_alerts_router.post(
452 "/create/velo-sigma/exclusion",
453 response_model=VeloSigmaExlcusionRouteResponse,
454 summary="Create a new Velociraptor Sigma exclusion rule",
455 )
456 async def create_exclusion(
457 exclusion: VeloSigmaExclusionCreate,
458 current_user: str = Depends(AuthHandler().return_username_for_logging),
459 db: AsyncSession = Depends(get_db),
460 ):
461 """Create a new exclusion rule for Velociraptor Sigma alerts."""
462 # Set the created_by field to the current user
463 logger.info(f"Current user: {current_user}")
464
465 # Take only needed fields from exclusion, excluding created_by
466 exclusion_dict = exclusion.model_dump(exclude={"created_by"})
467 # Create a new exclusion with the current user
468 updated_exclusion = VeloSigmaExclusionCreate(**exclusion_dict, created_by=current_user)
469
470 # Log the exclusion data for debugging
471 logger.info(f"Exclusion data: {updated_exclusion.model_dump()}")
472
473 service = VeloSigmaExclusionService(db)
474 # return await service.create_exclusion(updated_exclusion)
475 return VeloSigmaExlcusionRouteResponse(
476 success=True,
477 message="Exclusion rule created successfully",
478 exclusion_response=await service.create_exclusion(updated_exclusion),
479 )
480
481
482 @incidents_alerts_router.get(
483 "/create/velo-sigma/exclusion/{exclusion_id}",
484 response_model=VeloSigmaExlcusionRouteResponse,
485 summary="Get an exclusion rule by ID",
486 )
487 async def get_exclusion(
488 exclusion_id: int,
489 db: AsyncSession = Depends(get_db),
490 current_user: str = Depends(AuthHandler().get_current_user),
491 ):
492 """Retrieve details of a specific exclusion rule."""
493 service = VeloSigmaExclusionService(db)
494 exclusion = await service.get_exclusion(exclusion_id)
495
496 if not exclusion:
497 raise HTTPException(status_code=404, detail="Exclusion rule not found")
498
499 return VeloSigmaExlcusionRouteResponse(
500 success=True,
501 message="Exclusion rule retrieved successfully",
502 exclusion_response=exclusion,
503 )
504
505
506 @incidents_alerts_router.get(
507 "/create/velo-sigma/exclusion",
508 response_model=VeloSigmaExclusionListResponse,
509 summary="List all exclusion rules",
510 )
511 async def list_exclusions(
512 skip: int = Query(0, description="Number of items to skip for pagination"),
513 limit: int = Query(100, description="Maximum number of items to return"),
514 enabled_only: bool = Query(False, description="Only return enabled exclusions"),
515 db: AsyncSession = Depends(get_db),
516 current_user: str = Depends(AuthHandler().get_current_user),
517 ):
518 """List all exclusion rules with pagination."""
519 service = VeloSigmaExclusionService(db)
520
521 # Get exclusions and total count
522 exclusions, total_count = await service.list_exclusions_with_count(skip=skip, limit=limit, enabled_only=enabled_only)
523
524 return VeloSigmaExclusionListResponse(
525 success=True,
526 message="Exclusion rules retrieved successfully",
527 exclusions=exclusions,
528 pagination={"total": total_count, "skip": skip, "limit": limit},
529 )
530
531
532 @incidents_alerts_router.patch(
533 "/create/velo-sigma/exclusion/{exclusion_id}",
534 response_model=VeloSigmaExlcusionRouteResponse,
535 summary="Update an exclusion rule",
536 )
537 async def update_exclusion(
538 exclusion_id: int,
539 exclusion: VeloSigmaExclusionUpdate,
540 db: AsyncSession = Depends(get_db),
541 current_user: str = Depends(AuthHandler().get_current_user),
542 ):
543 """Update an existing exclusion rule."""
544 service = VeloSigmaExclusionService(db)
545 updated = await service.update_exclusion(exclusion_id, exclusion.model_dump(exclude_unset=True))
546
547 if not updated:
548 raise HTTPException(status_code=404, detail="Exclusion rule not found")
549
550 # return updated
551 return VeloSigmaExlcusionRouteResponse(
552 success=True,
553 message="Exclusion rule updated successfully",
554 exclusion_response=updated,
555 )
556
557
558 @incidents_alerts_router.delete("/create/velo-sigma/exclusion/{exclusion_id}", summary="Delete an exclusion rule")
559 async def delete_exclusion(
560 exclusion_id: int,
561 db: AsyncSession = Depends(get_db),
562 current_user: str = Depends(AuthHandler().get_current_user),
563 ):
564 """Delete an exclusion rule."""
565 service = VeloSigmaExclusionService(db)
566 deleted = await service.delete_exclusion(exclusion_id)
567
568 if not deleted:
569 raise HTTPException(status_code=404, detail="Exclusion rule not found")
570
571 return {"message": "Exclusion rule deleted successfully", "success": True}
572
573
574 @incidents_alerts_router.post(
575 "/velo-sigma/exclusion/{exclusion_id}/toggle",
576 response_model=VeloSigmaExlcusionRouteResponse,
577 summary="Toggle an exclusion rule's enabled status",
578 )
579 async def toggle_exclusion(
580 exclusion_id: int,
581 db: AsyncSession = Depends(get_db),
582 current_user: str = Depends(AuthHandler().get_current_user),
583 ):
584 """Enable or disable an exclusion rule."""
585 service = VeloSigmaExclusionService(db)
586 exclusion = await service.get_exclusion(exclusion_id)
587
588 if not exclusion:
589 raise HTTPException(status_code=404, detail="Exclusion rule not found")
590
591 # Toggle the enabled status
592 updated = await service.update_exclusion(exclusion_id, {"enabled": not exclusion.enabled})
593 # return updated
594 return VeloSigmaExlcusionRouteResponse(
595 success=True,
596 message="Exclusion rule toggled successfully",
597 exclusion_response=updated,
598 )