Velo sigma (#438)
* feat: add Velociraptor Sigma alert processing and schema definitions * feat: implement Velociraptor Sigma alert processing and service integration * feat: enhance Velociraptor Sigma alert processing with database session handling and index pattern support * feat: enhance Velociraptor Sigma alert processing with improved event handling and Wazuh integration * feat: refactor Velociraptor Sigma alert processing with improved event handling and schema organization * feat: add PowerShell event data model and processing logic to Velociraptor Sigma service * feat: add generic event processing for unrecognized alert channels in Velociraptor Sigma service
taylor_socfortress committed
Apr 11, 2025 at 15:50 UTC
aeee49634a159767670a3d9320f815cbf4ef9dad
3 files changed
+949
backend/app/incidents/routes/incident_alert.py
+22
@@ -17,6 +17,8 @@ from app.incidents.schema.incident_alert import CreateAlertRequestRoute
17
from app.incidents.schema.incident_alert import CreateAlertResponse
18
from app.incidents.schema.incident_alert import CreatedAlertPayload
19
from app.incidents.schema.incident_alert import IndexNamesResponse
20
+from app.incidents.schema.velo_sigma import VelociraptorSigmaAlert
21
+from app.incidents.schema.velo_sigma import VelociraptorSigmaAlertResponse
22
from app.incidents.services.alert_collection import add_copilot_alert_id
23
from app.incidents.services.alert_collection import get_alerts_not_created_in_copilot
24
from app.incidents.services.alert_collection import get_graylog_event_indices
@@ -26,6 +28,7 @@ from app.incidents.services.incident_alert import create_alert
28
from app.incidents.services.incident_alert import create_alert_full
29
from app.incidents.services.incident_alert import get_single_alert_details
30
from app.incidents.services.incident_alert import retrieve_alert_timeline
31
+from app.incidents.services.velo_sigma import create_velo_sigma_alert
32
33
incidents_alerts_router = APIRouter()
34
@@ -225,3 +228,22 @@ async def invoke_alert_threshold_graylog_route(
228
threshold_alert=True,
229
)
230
return CreateAlertResponse(success=True, message="Alert threshold Graylog invoked successfully", alert_id=alert_id)
231
+
232
+
233
+@incidents_alerts_router.post("/create/velo-sigma", response_model=VelociraptorSigmaAlertResponse)
234
+async def process_sigma_alert(alert: VelociraptorSigmaAlert, session: AsyncSession = Depends(get_db)) -> VelociraptorSigmaAlertResponse:
235
+ """
236
+ This route receives a Velociraptor Sigma alert. You must have defined the Windows.Hayabusa.Monitoring
237
+ client Event defined which will search for the Sigma alert in the Velociraptor client.
238
+ When a Sigma alert is found, Velociraptor will us the `CoPilot.Events.Upload` to send a POST
239
+ request to this endpoint with the alert data.
240
+
241
+ An issue is that we want to fetch the wazuh event that is related to the Sigma alert so that we can
242
+ create the alert within CoPilot accordingly. To do this we extract the `computer` as the `agent_name`
243
+ and the `EventRecordID` as the `data_win_system_eventRecordID` and then query the Wazuh Indexer
244
+ to fetch this sepcific event with a timeframe of 1 hour.
245
+
246
+ Then we progress through the CoPilot Alert Creation process as normal.
247
+ """
248
+ logger.info(f"Processing Velociraptor Sigma alert: {alert}")
249
+ return await create_velo_sigma_alert(alert, session)
backend/app/incidents/schema/velo_sigma.py
new
+384
@@ -0,0 +1,384 @@
1
+import json
2
+from typing import Any
3
+from typing import Dict
4
+from typing import Optional
5
+from typing import Union
6
+
7
+from loguru import logger
8
+from pydantic import BaseModel
9
+from pydantic import Field
10
+from pydantic import validator
11
+
12
+
13
+class SystemProvider(BaseModel):
14
+ Name: str
15
+ Guid: str
16
+
17
+
18
+class EventID(BaseModel):
19
+ Value: int
20
+
21
+
22
+class TimeCreated(BaseModel):
23
+ SystemTime: float
24
+
25
+
26
+class Execution(BaseModel):
27
+ ProcessID: int
28
+ ThreadID: int
29
+
30
+
31
+class Security(BaseModel):
32
+ UserID: str
33
+
34
+
35
+class SystemData(BaseModel):
36
+ Provider: SystemProvider
37
+ EventID: EventID
38
+ Version: int
39
+ Level: int
40
+ Task: int
41
+ Opcode: int
42
+ Keywords: int
43
+ TimeCreated: TimeCreated
44
+ EventRecordID: int
45
+ Correlation: Dict[str, Any] = Field(default_factory=dict)
46
+ Execution: Execution
47
+ Channel: str
48
+ Computer: str
49
+ Security: Security
50
+
51
+
52
+class SysmonEventData(BaseModel):
53
+ """Sysmon-specific event data structure"""
54
+
55
+ RuleName: str
56
+ UtcTime: str
57
+ SourceProcessGUID: str
58
+ SourceProcessId: int
59
+ SourceThreadId: int
60
+ SourceImage: str
61
+ TargetProcessGUID: str
62
+ TargetProcessId: int
63
+ TargetImage: str
64
+ GrantedAccess: int
65
+ CallTrace: str
66
+ SourceUser: str
67
+ TargetUser: str
68
+
69
+ class Config:
70
+ extra = "allow" # Allow additional fields not specified in the model
71
+
72
+
73
+# class DefenderEventData(BaseModel):
74
+# """Windows Defender event data structure"""
75
+# product_name: str = Field(alias="Product Name")
76
+# product_version: str = Field(alias="Product Version")
77
+
78
+
79
+# class Config:
80
+# allow_population_by_field_name = True
81
+class DefenderEventData(BaseModel):
82
+ """Windows Defender event data structure for various alert types"""
83
+
84
+ product_name: str = Field(alias="Product Name")
85
+ product_version: str = Field(alias="Product Version")
86
+
87
+ # Malware detection fields - all optional since some events don't have these
88
+ detection_id: Optional[str] = Field(None, alias="Detection ID")
89
+ detection_time: Optional[str] = Field(None, alias="Detection Time")
90
+ threat_id: Optional[str] = Field(None, alias="Threat ID")
91
+ threat_name: Optional[str] = Field(None, alias="Threat Name")
92
+ severity_id: Optional[str] = Field(None, alias="Severity ID")
93
+ severity_name: Optional[str] = Field(None, alias="Severity Name")
94
+ category_id: Optional[str] = Field(None, alias="Category ID")
95
+ category_name: Optional[str] = Field(None, alias="Category Name")
96
+ fw_link: Optional[str] = Field(None, alias="FWLink")
97
+ status_code: Optional[str] = Field(None, alias="Status Code")
98
+ status_description: Optional[str] = Field(None, alias="Status Description")
99
+ state: Optional[str] = Field(None, alias="State")
100
+ source_id: Optional[str] = Field(None, alias="Source ID")
101
+ source_name: Optional[str] = Field(None, alias="Source Name")
102
+ process_name: Optional[str] = Field(None, alias="Process Name")
103
+ detection_user: Optional[str] = Field(None, alias="Detection User")
104
+ path: Optional[str] = Field(None, alias="Path")
105
+ origin_id: Optional[str] = Field(None, alias="Origin ID")
106
+ origin_name: Optional[str] = Field(None, alias="Origin Name")
107
+ execution_id: Optional[str] = Field(None, alias="Execution ID")
108
+ execution_name: Optional[str] = Field(None, alias="Execution Name")
109
+ type_id: Optional[str] = Field(None, alias="Type ID")
110
+ type_name: Optional[str] = Field(None, alias="Type Name")
111
+ pre_execution_status: Optional[str] = Field(None, alias="Pre Execution Status")
112
+ action_id: Optional[str] = Field(None, alias="Action ID")
113
+ action_name: Optional[str] = Field(None, alias="Action Name")
114
+ error_code: Optional[str] = Field(None, alias="Error Code")
115
+ error_description: Optional[str] = Field(None, alias="Error Description")
116
+ post_clean_status: Optional[str] = Field(None, alias="Post Clean Status")
117
+ additional_actions_id: Optional[str] = Field(None, alias="Additional Actions ID")
118
+ additional_actions_string: Optional[str] = Field(None, alias="Additional Actions String")
119
+ remediation_user: Optional[str] = Field(None, alias="Remediation User")
120
+ security_intelligence_version: Optional[str] = Field(None, alias="Security intelligence Version")
121
+ engine_version: Optional[str] = Field(None, alias="Engine Version")
122
+
123
+ class Config:
124
+ allow_population_by_field_name = True
125
+ extra = "allow" # Allow additional fields not specified in the model
126
+
127
+
128
+class PowerShellEventData(BaseModel):
129
+ """PowerShell-specific event data structure"""
130
+
131
+ MessageNumber: int
132
+ MessageTotal: int
133
+ ScriptBlockText: str
134
+ ScriptBlockId: str
135
+ Path: str
136
+
137
+ # Optional fields that might be present in other PowerShell events
138
+ HostApplication: Optional[str] = None
139
+ HostName: Optional[str] = None
140
+ HostVersion: Optional[str] = None
141
+ EngineVersion: Optional[str] = None
142
+ RunspaceId: Optional[str] = None
143
+ PipelineId: Optional[int] = None
144
+ CommandName: Optional[str] = None
145
+ CommandType: Optional[str] = None
146
+ ConnectedUser: Optional[str] = None
147
+
148
+ class Config:
149
+ extra = "allow" # Allow additional fields not specified in the model
150
+
151
+
152
+# Generic event data model that accepts any fields
153
+class GenericEventData(BaseModel):
154
+ """Generic event data structure that accepts any fields"""
155
+
156
+ class Config:
157
+ extra = "allow"
158
+
159
+
160
+class EventBase(BaseModel):
161
+ """Base event structure with common fields"""
162
+
163
+ System: SystemData
164
+ Message: str
165
+
166
+
167
+class SysmonEvent(EventBase):
168
+ """Sysmon-specific event"""
169
+
170
+ EventData: SysmonEventData
171
+
172
+
173
+class DefenderEvent(EventBase):
174
+ """Windows Defender-specific event"""
175
+
176
+ EventData: DefenderEventData
177
+
178
+
179
+class PowerShellEvent(EventBase):
180
+ """PowerShell-specific event"""
181
+
182
+ EventData: PowerShellEventData
183
+
184
+
185
+class GenericEvent(EventBase):
186
+ """Generic event that can hold any event data"""
187
+
188
+ EventData: GenericEventData
189
+
190
+
191
+class VelociraptorSigmaAlert(BaseModel):
192
+ """
193
+ Represents a Sigma alert from Velociraptor with flexible event structure
194
+ """
195
+
196
+ computer: str
197
+ clientID: Optional[str] = None
198
+ channel: str
199
+ title: str
200
+ level: str
201
+ event: Union[str, Dict[str, Any], SysmonEvent, DefenderEvent, GenericEvent]
202
+ type: str = "sigma-alert"
203
+ source: str = "velociraptor"
204
+ index_pattern: str
205
+ sourceRef: str
206
+
207
+ @validator("event", pre=True)
208
+ def parse_event(cls, v):
209
+ """Parse the event if it's a string"""
210
+ if isinstance(v, str):
211
+ try:
212
+ return json.loads(v)
213
+ except json.JSONDecodeError as e:
214
+ raise ValueError(f"Invalid JSON in event field: {e}")
215
+ return v
216
+
217
+ def get_parsed_event(self) -> Union[SysmonEvent, DefenderEvent, PowerShellEvent, GenericEvent]:
218
+ """
219
+ Get the event object parsed into the appropriate type based on the channel
220
+
221
+ Detects the event type from:
222
+ 1. The channel field in the alert (e.g. "Microsoft-Windows-Sysmon/Operational")
223
+ 2. The System.Provider.Name in the event data
224
+ """
225
+ if isinstance(self.event, str):
226
+ # If still a string (though validator should have converted it)
227
+ event_data = json.loads(self.event)
228
+ elif isinstance(self.event, (SysmonEvent, DefenderEvent, PowerShellEvent, GenericEvent)):
229
+ # Already parsed into appropriate model
230
+ return self.event
231
+ else:
232
+ # Dictionary that needs to be converted
233
+ event_data = self.event
234
+
235
+ # First check the channel field in the alert
236
+ if self.channel:
237
+ channel_lower = self.channel.lower()
238
+
239
+ # Check for Sysmon in channel
240
+ if "sysmon" in channel_lower:
241
+ try:
242
+ return SysmonEvent(**event_data)
243
+ except Exception as e:
244
+ # Fall back to generic if structure doesn't match
245
+ logger.warning(f"Failed to parse Sysmon event: {e}")
246
+ return GenericEvent(**event_data)
247
+
248
+ # Check for Defender in channel
249
+ elif "defender" in channel_lower:
250
+ try:
251
+ return DefenderEvent(**event_data)
252
+ except Exception as e:
253
+ # Fall back to generic if structure doesn't match
254
+ logger.warning(f"Failed to parse Defender event: {e}")
255
+ return GenericEvent(**event_data)
256
+
257
+ # Check for PowerShell in channel
258
+ elif "powershell" in channel_lower:
259
+ try:
260
+ return PowerShellEvent(**event_data)
261
+ except Exception as e:
262
+ # Fall back to generic if structure doesn't match
263
+ logger.warning(f"Failed to parse PowerShell event: {e}")
264
+ return GenericEvent(**event_data)
265
+
266
+ # If channel doesn't give us enough info, check Provider.Name in the event
267
+ provider_name = ""
268
+ if isinstance(event_data, dict) and "System" in event_data:
269
+ system = event_data["System"]
270
+ if "Provider" in system and "Name" in system["Provider"]:
271
+ provider_name = system["Provider"]["Name"].lower()
272
+
273
+ # Check provider name
274
+ if "sysmon" in provider_name:
275
+ try:
276
+ return SysmonEvent(**event_data)
277
+ except Exception as e:
278
+ logger.warning(f"Failed to parse Sysmon event: {e}")
279
+ return GenericEvent(**event_data)
280
+ elif "defender" in provider_name:
281
+ try:
282
+ return DefenderEvent(**event_data)
283
+ except Exception as e:
284
+ logger.warning(f"Failed to parse Defender event: {e}")
285
+ return GenericEvent(**event_data)
286
+ elif "powershell" in provider_name:
287
+ try:
288
+ return PowerShellEvent(**event_data)
289
+ except Exception as e:
290
+ logger.warning(f"Failed to parse PowerShell event: {e}")
291
+ return GenericEvent(**event_data)
292
+
293
+ # If we have System.Channel, check that too
294
+ if "Channel" in system:
295
+ system_channel = system["Channel"].lower()
296
+ if "sysmon" in system_channel:
297
+ try:
298
+ return SysmonEvent(**event_data)
299
+ except Exception as e:
300
+ logger.warning(f"Failed to parse Sysmon event: {e}")
301
+ return GenericEvent(**event_data)
302
+ elif "defender" in system_channel:
303
+ try:
304
+ return DefenderEvent(**event_data)
305
+ except Exception as e:
306
+ logger.warning(f"Failed to parse Defender event: {e}")
307
+ return GenericEvent(**event_data)
308
+ elif "powershell" in system_channel:
309
+ try:
310
+ return PowerShellEvent(**event_data)
311
+ except Exception as e:
312
+ logger.warning(f"Failed to parse PowerShell event: {e}")
313
+ return GenericEvent(**event_data)
314
+
315
+ # Use generic model for other event types
316
+ return GenericEvent(**event_data)
317
+
318
+ class Config:
319
+ schema_extra = {
320
+ "example": {
321
+ "computer": "WIN-HFOU106TD7K",
322
+ "clientID": "C.475df76785008b04",
323
+ "channel": "Microsoft-Windows-Sysmon/Operational",
324
+ "title": "Proc Access (Sysmon Alert)",
325
+ "level": "high",
326
+ "event": (
327
+ '{"System":{"Provider":{"Name":"Microsoft-Windows-Sysmon","Guid":"5770385F-C22A-43E0-BF4C-06F5698FFBD9"},'
328
+ '"EventID":{"Value":10},"Version":3,"Level":4,"Task":10,"Opcode":0,"Keywords":9223372036854775808,'
329
+ '"TimeCreated":{"SystemTime":1744233485.0778975},"EventRecordID":564617,"Correlation":{},'
330
+ '"Execution":{"ProcessID":2320,"ThreadID":3540},"Channel":"Microsoft-Windows-Sysmon/Operational",'
331
+ '"Computer":"WIN-HFOU106TD7K","Security":{"UserID":"S-1-5-18"}},"EventData":{"RuleName":"technique_id=T1003,'
332
+ 'technique_name=Credential Dumping","UtcTime":"2025-04-09 21:18:05.064",'
333
+ '"SourceProcessGUID":"691FF406-E40B-67F6-2901-000000003A00","SourceProcessId":4964,"SourceThreadId":4448,'
334
+ '"SourceImage":"C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe",'
335
+ '"TargetProcessGUID":"691FF406-DDC8-67F6-0C00-000000003A00","TargetProcessId":668,'
336
+ '"TargetImage":"C:\\\\Windows\\\\system32\\\\lsass.exe","GrantedAccess":2097151,'
337
+ '"CallTrace":"C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|'
338
+ "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
339
+ "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
340
+ "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
341
+ "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
342
+ "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
343
+ "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
344
+ "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
345
+ 'C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)","SourceUser":"WIN-HFOU106TD7K\\\\Administrator",'
346
+ '"TargetUser":"NT AUTHORITY\\\\SYSTEM"},'
347
+ '"Message":"Process accessed:\\nRuleName: technique_id=T1003,technique_name=Credential Dumping!s!\\n'
348
+ "UtcTime: 2025-04-09 21:18:05.064!s!\\n"
349
+ "SourceProcessGUID: 691FF406-E40B-67F6-2901-000000003A00!s!\\n"
350
+ "SourceProcessId: 4964!s!\\n"
351
+ "SourceThreadId: 4448!s!\\n"
352
+ "SourceImage: C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe!s!\\n"
353
+ "TargetProcessGUID: 691FF406-DDC8-67F6-0C00-000000003A00!s!\\n"
354
+ "TargetProcessId: 668!s!\\n"
355
+ "TargetImage: C:\\\\Windows\\\\system32\\\\lsass.exe!s!\\n"
356
+ "GrantedAccess: 2097151!s!\\n"
357
+ "CallTrace: C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|"
358
+ "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
359
+ "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
360
+ "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
361
+ "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
362
+ "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
363
+ "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
364
+ "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
365
+ "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)!s!\\n"
366
+ "SourceUser: WIN-HFOU106TD7K\\\\Administrator!s!\\n"
367
+ 'TargetUser: NT AUTHORITY\\\\SYSTEM!s!\\r\\n"}'
368
+ ),
369
+ "type": "sigma-alert",
370
+ "source": "velociraptor",
371
+ "index_pattern": "wazuh-*",
372
+ "sourceRef": "754600692",
373
+ },
374
+ }
375
+
376
+
377
+class VelociraptorSigmaAlertResponse(BaseModel):
378
+ """
379
+ Response model for Velociraptor Sigma alert processing
380
+ """
381
+
382
+ success: bool
383
+ message: str
384
+ alert_id: Optional[str] = None
backend/app/incidents/services/velo_sigma.py
new
+543
@@ -0,0 +1,543 @@
1
+from datetime import datetime
2
+from datetime import timedelta
3
+from typing import Any
4
+from typing import Dict
5
+from typing import Optional
6
+from typing import Union
7
+
8
+from loguru import logger
9
+from sqlalchemy import select
10
+from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+from app.connectors.wazuh_indexer.utils.universal import (
13
+ create_wazuh_indexer_client_async,
14
+)
15
+from app.db.universal_models import Agents
16
+from app.incidents.schema.db_operations import AlertTagCreate
17
+from app.incidents.schema.db_operations import CommentCreate
18
+from app.incidents.schema.incident_alert import CreateAlertRequest
19
+from app.incidents.schema.incident_alert import CreatedAlertPayload
20
+from app.incidents.schema.velo_sigma import DefenderEvent
21
+from app.incidents.schema.velo_sigma import GenericEvent
22
+from app.incidents.schema.velo_sigma import PowerShellEvent
23
+from app.incidents.schema.velo_sigma import SysmonEvent
24
+from app.incidents.schema.velo_sigma import VelociraptorSigmaAlert
25
+from app.incidents.schema.velo_sigma import VelociraptorSigmaAlertResponse
26
+from app.incidents.services.db_operations import create_alert_tag
27
+from app.incidents.services.db_operations import create_comment
28
+from app.incidents.services.incident_alert import create_alert
29
+from app.incidents.services.incident_alert import create_alert_full
30
+
31
+
32
+class VelociraptorSigmaService:
33
+ """Service for handling Velociraptor Sigma alerts and their integration with Wazuh."""
34
+
35
+ def __init__(self, session: AsyncSession):
36
+ """Initialize with a database session."""
37
+ self.session = session
38
+
39
+ async def _create_fallback_alert(self, alert: VelociraptorSigmaAlert, result: Dict[str, Any]) -> None:
40
+ """
41
+ Create a fallback alert when no matching Wazuh event is found.
42
+ Uses create_alert_full to generate an alert directly from the Velociraptor data.
43
+ """
44
+ try:
45
+ # Extract timestamp from the event if available
46
+ timestamp = None
47
+ parsed_event = alert.get_parsed_event()
48
+ if hasattr(parsed_event, "System") and hasattr(parsed_event.System, "TimeCreated"):
49
+ timestamp = datetime.fromtimestamp(parsed_event.System.TimeCreated.SystemTime).isoformat()
50
+ else:
51
+ timestamp = datetime.utcnow().isoformat()
52
+
53
+ # Extract event information for context
54
+ event_context = {}
55
+ if hasattr(parsed_event, "EventData"):
56
+ # Try to convert EventData to dict for context
57
+ try:
58
+ event_context = parsed_event.EventData.dict()
59
+ except AttributeError:
60
+ # If not directly convertible, extract key attributes
61
+ event_context = {
62
+ "event_record_id": getattr(parsed_event.System, "EventRecordID", "Unknown"),
63
+ "channel": getattr(parsed_event.System, "Channel", "Unknown"),
64
+ "computer": getattr(parsed_event.System, "Computer", "Unknown"),
65
+ }
66
+
67
+ # Add alert metadata to context
68
+ event_context.update(
69
+ {
70
+ "alert_title": alert.title,
71
+ "alert_level": alert.level,
72
+ "alert_channel": alert.channel,
73
+ "alert_source": alert.source,
74
+ "computer": alert.computer,
75
+ "clientID": alert.clientID,
76
+ },
77
+ )
78
+
79
+ # Create a unique ID for this alert based on sourceRef and timestamp - Not using for now
80
+ # unique_id = f"{alert.sourceRef}_{int(datetime.utcnow().timestamp())}"
81
+
82
+ # Look up the customer code from Agents table using the clientID
83
+ customer_code = "not_found" # Default fallback
84
+ if alert.clientID:
85
+ # Query the Agents table to find matching agent by velociraptor_id
86
+ agent_query = select(Agents).where(Agents.velociraptor_id == alert.clientID)
87
+ agent_result = await self.session.execute(agent_query)
88
+ agent = agent_result.scalar_one_or_none()
89
+
90
+ if agent and agent.customer_code:
91
+ customer_code = agent.customer_code
92
+ logger.info(f"Found customer code '{customer_code}' for clientID {alert.clientID}")
93
+ else:
94
+ logger.warning(f"No agent found with velociraptor_id '{alert.clientID}', using default customer code")
95
+ else:
96
+ logger.warning("No clientID provided in the alert, using default customer code")
97
+
98
+ # Create the alert using create_alert_full
99
+ alert_id = await create_alert_full(
100
+ alert_payload=CreatedAlertPayload(
101
+ alert_context_payload=event_context,
102
+ asset_payload=alert.computer,
103
+ timefield_payload=timestamp,
104
+ alert_title_payload=alert.title,
105
+ source=alert.source,
106
+ index_id="not_applicable",
107
+ index_name="not_applicable",
108
+ ),
109
+ customer_code=customer_code, # Use the looked up customer code
110
+ session=self.session,
111
+ threshold_alert=True,
112
+ )
113
+ result["alert_id"] = alert_id
114
+
115
+ # Add a comment with more context
116
+ event_type = type(parsed_event).__name__
117
+ await create_comment(
118
+ comment=CommentCreate(
119
+ alert_id=result["alert_id"],
120
+ comment=(
121
+ f"Velociraptor Sigma Alert (No Wazuh match found)\n"
122
+ f"Title: {alert.title}\n"
123
+ f"Channel: {alert.channel}\n"
124
+ f"Level: {alert.level}\n"
125
+ f"Computer: {alert.computer}\n"
126
+ f"Event Type: {event_type}\n"
127
+ f"Event Record ID: {result.get('event_record_id', 'Unknown')}\n"
128
+ f"Customer Code: {customer_code}\n"
129
+ ),
130
+ user_name="admin",
131
+ created_at=datetime.utcnow(),
132
+ ),
133
+ db=self.session,
134
+ )
135
+
136
+ # Add tags
137
+ await create_alert_tag(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag=f"{alert.type}"), db=self.session)
138
+ await create_alert_tag(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="velociraptor-direct"), db=self.session)
139
+
140
+ # Add event-specific tags
141
+ if "Sysmon" in alert.channel:
142
+ await create_alert_tag(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:sysmon"), db=self.session)
143
+ elif "Defender" in alert.channel:
144
+ await create_alert_tag(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:defender"), db=self.session)
145
+ elif "PowerShell" in alert.channel:
146
+ await create_alert_tag(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:powershell"), db=self.session)
147
+ else:
148
+ # Generic event type
149
+ await create_alert_tag(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:generic"), db=self.session)
150
+
151
+ logger.info(f"Created fallback CoPilot alert with ID: {result['alert_id']} for customer {customer_code}")
152
+ result["success"] = True
153
+
154
+ except Exception as e:
155
+ logger.error(f"Failed to create fallback CoPilot alert: {str(e)}")
156
+ logger.exception(e)
157
+ result["alert_id"] = None
158
+ result["success"] = False
159
+ result["reason"] = f"Failed to create fallback alert: {str(e)}"
160
+
161
+ async def process_alert(self, alert: VelociraptorSigmaAlert) -> VelociraptorSigmaAlertResponse:
162
+ """
163
+ Process a Velociraptor Sigma alert and create a corresponding CoPilot alert.
164
+
165
+ Args:
166
+ alert: The Velociraptor Sigma alert to process
167
+
168
+ Returns:
169
+ Response indicating the success or failure of the processing
170
+ """
171
+ try:
172
+ # Parse event and determine event type
173
+ result = await self._process_event_by_type(alert)
174
+
175
+ # Create an alert in CoPilot if the processing was successful
176
+ if result.get("success"):
177
+ await self._create_copilot_alert(alert, result)
178
+ else:
179
+ # If no Wazuh alert was found, try to create a fallback alert
180
+ logger.info("No matching Wazuh alert found, creating fallback alert...")
181
+ await self._create_fallback_alert(alert, result)
182
+
183
+ # Build response
184
+ return self._build_response(alert, result)
185
+
186
+ except Exception as e:
187
+ logger.error(f"Error processing Velociraptor Sigma alert: {str(e)}")
188
+ logger.exception(e)
189
+ return VelociraptorSigmaAlertResponse(success=False, message=f"Error: {str(e)}", alert_id=getattr(alert, "sourceRef", None))
190
+
191
+ async def _process_event_by_type(self, alert: VelociraptorSigmaAlert) -> Dict[str, Any]:
192
+ """Process event according to its type or channel."""
193
+ parsed_event = alert.get_parsed_event()
194
+ logger.debug(f"Processing alert | Channel: {alert.channel} | Type: {type(parsed_event).__name__}")
195
+
196
+ # Use type checking and channel fallback
197
+ if isinstance(parsed_event, SysmonEvent):
198
+ return await self._process_sysmon_event(alert, parsed_event)
199
+ elif isinstance(parsed_event, DefenderEvent):
200
+ return await self._process_defender_event(alert, parsed_event)
201
+ elif "Sysmon" in alert.channel:
202
+ logger.warning(f"Expected SysmonEvent but got {type(parsed_event).__name__} for Sysmon channel")
203
+ return await self._process_sysmon_event(alert, parsed_event)
204
+ elif "Defender" in alert.channel:
205
+ logger.warning(f"Expected DefenderEvent but got {type(parsed_event).__name__} for Defender channel")
206
+ return await self._process_defender_event(alert, parsed_event)
207
+ elif "PowerShell" in alert.channel:
208
+ logger.warning(f"Expected PowerShellEvent but got {type(parsed_event).__name__} for PowerShell channel")
209
+ return await self._process_powershell_event(alert, parsed_event)
210
+ else:
211
+ # Use a generic processor for unknown event types
212
+ logger.info(f"Using generic processor for channel: {alert.channel}")
213
+ return await self._process_generic_event(alert, parsed_event)
214
+
215
+ async def _process_sysmon_event(self, alert: VelociraptorSigmaAlert, parsed_event: Union[SysmonEvent, GenericEvent]) -> Dict[str, Any]:
216
+ """Process a Sysmon event."""
217
+ logger.info(f"Processing Sysmon event from channel: {alert.channel}")
218
+
219
+ try:
220
+ # Extract key fields safely
221
+ event_record_id = str(parsed_event.System.EventRecordID)
222
+
223
+ # Safely access EventData fields with fallbacks
224
+ event_data = parsed_event.EventData
225
+ rule_name = getattr(event_data, "RuleName", "Unknown Rule")
226
+ source_image = getattr(event_data, "SourceImage", "Unknown Source")
227
+ target_image = getattr(event_data, "TargetImage", "Unknown Target")
228
+ source_process_id = getattr(event_data, "SourceProcessId", 0)
229
+ source_user = getattr(event_data, "SourceUser", "Unknown User")
230
+
231
+ # Fetch corresponding Wazuh alert
232
+ wazuh_event = await self._fetch_wazuh_alert(
233
+ agent_name=alert.computer,
234
+ event_record_id=event_record_id,
235
+ index_pattern=alert.index_pattern,
236
+ )
237
+
238
+ # Build result
239
+ result = {
240
+ "event_record_id": event_record_id,
241
+ "rule_name": rule_name,
242
+ "source_image": source_image,
243
+ "target_image": target_image,
244
+ "source_process_id": source_process_id,
245
+ "source_user": source_user,
246
+ "wazuh_data": wazuh_event,
247
+ "success": wazuh_event is not None,
248
+ }
249
+
250
+ logger.info(f"Sysmon event processed | EventRecordID: {event_record_id} | Success: {result['success']}")
251
+ return result
252
+
253
+ except AttributeError as e:
254
+ logger.error(f"Failed to process Sysmon event - missing attribute: {str(e)}")
255
+ return {"success": False, "reason": f"Failed to process Sysmon event: {str(e)}"}
256
+
257
+ async def _process_defender_event(
258
+ self,
259
+ alert: VelociraptorSigmaAlert,
260
+ parsed_event: Union[DefenderEvent, GenericEvent],
261
+ ) -> Dict[str, Any]:
262
+ """Process a Windows Defender event."""
263
+ logger.info(f"Processing Defender event from channel: {alert.channel}")
264
+
265
+ try:
266
+ # Extract event record ID
267
+ event_record_id = str(parsed_event.System.EventRecordID)
268
+
269
+ # Fetch corresponding Wazuh alert
270
+ wazuh_event = await self._fetch_wazuh_alert(
271
+ agent_name=alert.computer,
272
+ event_record_id=event_record_id,
273
+ index_pattern=alert.index_pattern,
274
+ )
275
+
276
+ # Build basic result
277
+ result = {"event_record_id": event_record_id, "wazuh_data": wazuh_event, "success": wazuh_event is not None}
278
+
279
+ # Safely extract additional fields
280
+ event_data = parsed_event.EventData
281
+
282
+ if hasattr(event_data, "product_name"):
283
+ result["product_name"] = event_data.product_name
284
+
285
+ if hasattr(event_data, "threat_name"):
286
+ result["threat_name"] = event_data.threat_name
287
+
288
+ if hasattr(event_data, "severity_name"):
289
+ result["severity"] = event_data.severity_name
290
+
291
+ logger.info(f"Defender event processed | EventRecordID: {event_record_id} | Success: {result['success']}")
292
+ return result
293
+
294
+ except AttributeError as e:
295
+ logger.error(f"Failed to process Defender event - missing attribute: {str(e)}")
296
+ return {"success": False, "reason": f"Failed to process Defender event: {str(e)}"}
297
+
298
+ async def _process_powershell_event(
299
+ self,
300
+ alert: VelociraptorSigmaAlert,
301
+ parsed_event: Union[PowerShellEvent, GenericEvent],
302
+ ) -> Dict[str, Any]:
303
+ """Process a PowerShell event."""
304
+ logger.info(f"Processing PowerShell event from channel: {alert.channel}")
305
+
306
+ try:
307
+ # Extract event record ID
308
+ event_record_id = str(parsed_event.System.EventRecordID)
309
+
310
+ # Fetch corresponding Wazuh alert
311
+ wazuh_event = await self._fetch_wazuh_alert(
312
+ agent_name=alert.computer,
313
+ event_record_id=event_record_id,
314
+ index_pattern=alert.index_pattern,
315
+ )
316
+
317
+ # Build basic result
318
+ result = {"event_record_id": event_record_id, "wazuh_data": wazuh_event, "success": wazuh_event is not None}
319
+
320
+ # Safely extract PowerShell specific fields
321
+ event_data = parsed_event.EventData
322
+
323
+ # Add ScriptBlock details if available
324
+ if hasattr(event_data, "ScriptBlockText"):
325
+ result["script_block_text"] = event_data.ScriptBlockText
326
+ # Store only first 100 chars as a preview to avoid overwhelming logs
327
+ preview = event_data.ScriptBlockText[:100] + "..." if len(event_data.ScriptBlockText) > 100 else event_data.ScriptBlockText
328
+ result["script_preview"] = preview
329
+
330
+ if hasattr(event_data, "ScriptBlockId"):
331
+ result["script_block_id"] = event_data.ScriptBlockId
332
+
333
+ if hasattr(event_data, "MessageNumber") and hasattr(event_data, "MessageTotal"):
334
+ result["message_part"] = f"{event_data.MessageNumber} of {event_data.MessageTotal}"
335
+
336
+ # Add host information if available
337
+ if hasattr(event_data, "HostApplication"):
338
+ result["host_application"] = event_data.HostApplication
339
+
340
+ if hasattr(event_data, "CommandName"):
341
+ result["command_name"] = event_data.CommandName
342
+
343
+ logger.info(f"PowerShell event processed | EventRecordID: {event_record_id} | Success: {result['success']}")
344
+ return result
345
+
346
+ except AttributeError as e:
347
+ logger.error(f"Failed to process PowerShell event - missing attribute: {str(e)}")
348
+ return {"success": False, "reason": f"Failed to process PowerShell event: {str(e)}"}
349
+
350
+ async def _process_generic_event(self, alert: VelociraptorSigmaAlert, parsed_event: GenericEvent) -> Dict[str, Any]:
351
+ """Process a generic event that doesn't match known types."""
352
+ logger.info(f"Processing generic event from channel: {alert.channel}")
353
+
354
+ try:
355
+ # Extract event record ID if available
356
+ event_record_id = str(getattr(parsed_event.System, "EventRecordID", "unknown"))
357
+
358
+ # Try to fetch corresponding Wazuh alert if we have an event record ID
359
+ wazuh_event = None
360
+ if event_record_id != "unknown":
361
+ wazuh_event = await self._fetch_wazuh_alert(
362
+ agent_name=alert.computer,
363
+ event_record_id=event_record_id,
364
+ index_pattern=alert.index_pattern,
365
+ )
366
+
367
+ # Build basic result
368
+ result = {
369
+ "event_record_id": event_record_id,
370
+ "wazuh_data": wazuh_event,
371
+ "success": wazuh_event is not None,
372
+ "channel": alert.channel,
373
+ }
374
+
375
+ # Extract some generic system info
376
+ if hasattr(parsed_event, "System"):
377
+ system = parsed_event.System
378
+ if hasattr(system, "Channel"):
379
+ result["system_channel"] = system.Channel
380
+ if hasattr(system, "Provider") and hasattr(system.Provider, "Name"):
381
+ result["provider_name"] = system.Provider.Name
382
+ if hasattr(system, "EventID") and hasattr(system.EventID, "Value"):
383
+ result["event_id"] = system.EventID.Value
384
+
385
+ # Try to extract some event data if available
386
+ if hasattr(parsed_event, "EventData"):
387
+ try:
388
+ # Add the first few items from EventData to the result
389
+ event_data = parsed_event.EventData
390
+ event_data_dict = {}
391
+
392
+ # Get all attributes that aren't methods or private
393
+ for attr_name in dir(event_data):
394
+ if not attr_name.startswith("_") and not callable(getattr(event_data, attr_name)):
395
+ try:
396
+ value = getattr(event_data, attr_name)
397
+ if not callable(value): # Skip methods
398
+ event_data_dict[attr_name] = str(value)
399
+ except Exception as attr_error:
400
+ logger.warning(f"Failed to access attribute '{attr_name}': {str(attr_error)}")
401
+ # Skip attributes that can't be accessed
402
+ pass
403
+
404
+ # Add to result, limited to prevent overwhelming logs
405
+ result["event_data"] = {k: v for i, (k, v) in enumerate(event_data_dict.items()) if i < 10}
406
+
407
+ except Exception as e:
408
+ logger.warning(f"Failed to extract event data details: {e}")
409
+
410
+ logger.info(f"Generic event processed | EventRecordID: {event_record_id} | Success: {result['success']}")
411
+ return result
412
+
413
+ except Exception as e:
414
+ logger.error(f"Failed to process generic event: {str(e)}")
415
+ logger.exception(e)
416
+ return {"success": False, "reason": f"Failed to process generic event: {str(e)}"}
417
+
418
+ async def _create_copilot_alert(self, alert: VelociraptorSigmaAlert, result: Dict[str, Any]) -> None:
419
+ """Create an alert in CoPilot with comments and tags."""
420
+ try:
421
+ # Create the alert
422
+ wazuh_data = result["wazuh_data"]
423
+ alert_response = await create_alert(
424
+ alert=CreateAlertRequest(index_name=wazuh_data["index_name"], alert_id=wazuh_data["alert_id"]),
425
+ session=self.session,
426
+ )
427
+ result["alert_id"] = alert_response
428
+
429
+ # Add a comment
430
+ await create_comment(
431
+ comment=CommentCreate(
432
+ alert_id=result["alert_id"],
433
+ comment=f"Velociraptor Sigma: {alert.title} | {alert.channel}",
434
+ user_name="admin",
435
+ created_at=datetime.utcnow(),
436
+ ),
437
+ db=self.session,
438
+ )
439
+
440
+ # Add a tag
441
+ await create_alert_tag(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag=f"{alert.type}"), db=self.session)
442
+
443
+ logger.info(f"Created CoPilot alert with ID: {result['alert_id']}")
444
+
445
+ except Exception as e:
446
+ logger.error(f"Failed to create CoPilot alert: {str(e)}")
447
+ logger.exception(e)
448
+ result["alert_id"] = None
449
+
450
+ def _build_response(self, alert: VelociraptorSigmaAlert, result: Dict[str, Any]) -> VelociraptorSigmaAlertResponse:
451
+ """Build the response based on processing results."""
452
+ success = result.get("success", False)
453
+
454
+ if not success:
455
+ message = f"Failed to process {alert.channel} alert: {result.get('reason', 'Unknown error')}"
456
+ logger.warning(f"{message} | EventRecordID: {result.get('event_record_id', 'Unknown')}")
457
+ else:
458
+ message = f"Successfully processed {alert.channel} alert"
459
+ logger.info(f"{message} | EventRecordID: {result.get('event_record_id')} | AlertID: {result.get('alert_id')}")
460
+
461
+ return VelociraptorSigmaAlertResponse(success=success, message=message, alert_id=result.get("alert_id"))
462
+
463
+ async def _fetch_wazuh_alert(self, agent_name: str, event_record_id: str, index_pattern: str) -> Optional[Dict[str, Any]]:
464
+ """Fetch alert data from Wazuh Indexer."""
465
+ try:
466
+ # Create client and prepare search parameters
467
+ client = await create_wazuh_indexer_client_async("Wazuh-Indexer")
468
+
469
+ # Use ISO format for timestamps to avoid format errors - default to current time -1 hour
470
+ # This is to ensure we are searching within the last hour
471
+ # ! Might need to revisit this if the time window is too small ! #
472
+ one_hour_ago = datetime.utcnow() - timedelta(hours=1)
473
+ timestamp = one_hour_ago.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
474
+
475
+ # Build query
476
+ query = self._build_wazuh_query(agent_name, event_record_id, timestamp)
477
+ logger.debug(f"Searching Wazuh Indexer | Query: {query}")
478
+
479
+ # Execute search
480
+ response = await self._execute_wazuh_search(client, query, index_pattern)
481
+
482
+ # Extract and return results
483
+ return self._extract_wazuh_results(response)
484
+
485
+ except Exception as e:
486
+ logger.error(f"Error fetching alert data: {str(e)}")
487
+ logger.exception(e)
488
+ return None
489
+
490
+ @staticmethod
491
+ def _build_wazuh_query(agent_name: str, event_record_id: str, timestamp: str) -> Dict[str, Any]:
492
+ """Build the OpenSearch query for finding the alert in Wazuh."""
493
+ return {
494
+ "bool": {
495
+ "must": [{"term": {"agent_name": agent_name}}, {"term": {"data_win_system_eventRecordID": event_record_id}}],
496
+ "filter": [{"range": {"timestamp": {"gte": timestamp}}}],
497
+ },
498
+ }
499
+
500
+ @staticmethod
501
+ async def _execute_wazuh_search(client, query: Dict[str, Any], index_pattern: str) -> Dict[str, Any]:
502
+ """Execute the search against the Wazuh Indexer."""
503
+ try:
504
+ response = await client.search(index=index_pattern, body={"query": query}, size=1, timeout="1m")
505
+ logger.debug(f"Search response received | Status: {'hits' in response}")
506
+ return response
507
+ except Exception as search_error:
508
+ logger.error(f"Search operation failed: {str(search_error)}")
509
+ logger.exception(search_error)
510
+ return {}
511
+
512
+ @staticmethod
513
+ def _extract_wazuh_results(response: Dict[str, Any]) -> Optional[Dict[str, Any]]:
514
+ """Extract the alert data from the Wazuh Indexer response."""
515
+ if not response or "hits" not in response or "hits" not in response["hits"]:
516
+ logger.warning("Invalid response structure from Wazuh Indexer")
517
+ return None
518
+
519
+ hits = response["hits"]["hits"]
520
+ if not hits:
521
+ logger.warning("No matching alerts found in Wazuh Indexer")
522
+ return None
523
+
524
+ # Get the first matching hit
525
+ hit = hits[0]
526
+
527
+ # Extract index, document ID and source data
528
+ index_name = hit.get("_index")
529
+ alert_id = hit.get("_id")
530
+ raw_alert = hit.get("_source", {})
531
+
532
+ # Enrich the raw alert with metadata needed for references
533
+ raw_alert["index_name"] = index_name
534
+ raw_alert["alert_id"] = alert_id
535
+
536
+ logger.debug(f"Successfully extracted raw alert data from index {index_name} with ID {alert_id}")
537
+ return raw_alert
538
+
539
+
540
+async def create_velo_sigma_alert(alert: VelociraptorSigmaAlert, session: AsyncSession) -> VelociraptorSigmaAlertResponse:
541
+ """Process a Velociraptor Sigma alert using the VelociraptorSigmaService."""
542
+ service = VelociraptorSigmaService(session)
543
+ return await service.process_alert(alert)