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