@cryptotaxi247 / CoPilot / commits / aeee4963

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)