main
py 451 lines 18.5 KB
Raw
1 import json
2 from datetime import datetime
3 from typing import Any
4 from typing import Dict
5 from typing import List
6 from typing import Optional
7 from typing import Union
8
9 from loguru import logger
10 from pydantic import BaseModel
11 from pydantic import ConfigDict
12 from pydantic import Field
13 from pydantic import field_validator
14
15
16 class SystemProvider(BaseModel):
17 Name: str
18 Guid: str
19
20
21 class EventID(BaseModel):
22 Value: int
23
24
25 class TimeCreated(BaseModel):
26 SystemTime: float
27
28
29 class Execution(BaseModel):
30 ProcessID: int
31 ThreadID: int
32
33
34 class Security(BaseModel):
35 UserID: str
36
37
38 class SystemData(BaseModel):
39 Provider: SystemProvider
40 EventID: EventID
41 Version: int
42 Level: int
43 Task: int
44 Opcode: int
45 Keywords: int
46 TimeCreated: TimeCreated
47 EventRecordID: int
48 Correlation: Dict[str, Any] = Field(default_factory=dict)
49 Execution: Execution
50 Channel: str
51 Computer: str
52 Security: Security
53
54
55 class SysmonEventData(BaseModel):
56 """Sysmon-specific event data structure"""
57
58 RuleName: str
59 UtcTime: str
60 SourceProcessGUID: str
61 SourceProcessId: int
62 SourceThreadId: int
63 SourceImage: str
64 TargetProcessGUID: str
65 TargetProcessId: int
66 TargetImage: str
67 GrantedAccess: int
68 CallTrace: str
69 SourceUser: str
70 TargetUser: str
71 model_config = ConfigDict(extra="allow")
72
73
74 # class DefenderEventData(BaseModel):
75 # """Windows Defender event data structure"""
76 # product_name: str = Field(alias="Product Name")
77 # product_version: str = Field(alias="Product Version")
78
79
80 # class Config:
81 # allow_population_by_field_name = True
82 class DefenderEventData(BaseModel):
83 """Windows Defender event data structure for various alert types"""
84
85 product_name: str = Field(alias="Product Name")
86 product_version: str = Field(alias="Product Version")
87
88 # Malware detection fields - all optional since some events don't have these
89 detection_id: Optional[str] = Field(None, alias="Detection ID")
90 detection_time: Optional[str] = Field(None, alias="Detection Time")
91 threat_id: Optional[str] = Field(None, alias="Threat ID")
92 threat_name: Optional[str] = Field(None, alias="Threat Name")
93 severity_id: Optional[str] = Field(None, alias="Severity ID")
94 severity_name: Optional[str] = Field(None, alias="Severity Name")
95 category_id: Optional[str] = Field(None, alias="Category ID")
96 category_name: Optional[str] = Field(None, alias="Category Name")
97 fw_link: Optional[str] = Field(None, alias="FWLink")
98 status_code: Optional[str] = Field(None, alias="Status Code")
99 status_description: Optional[str] = Field(None, alias="Status Description")
100 state: Optional[str] = Field(None, alias="State")
101 source_id: Optional[str] = Field(None, alias="Source ID")
102 source_name: Optional[str] = Field(None, alias="Source Name")
103 process_name: Optional[str] = Field(None, alias="Process Name")
104 detection_user: Optional[str] = Field(None, alias="Detection User")
105 path: Optional[str] = Field(None, alias="Path")
106 origin_id: Optional[str] = Field(None, alias="Origin ID")
107 origin_name: Optional[str] = Field(None, alias="Origin Name")
108 execution_id: Optional[str] = Field(None, alias="Execution ID")
109 execution_name: Optional[str] = Field(None, alias="Execution Name")
110 type_id: Optional[str] = Field(None, alias="Type ID")
111 type_name: Optional[str] = Field(None, alias="Type Name")
112 pre_execution_status: Optional[str] = Field(None, alias="Pre Execution Status")
113 action_id: Optional[str] = Field(None, alias="Action ID")
114 action_name: Optional[str] = Field(None, alias="Action Name")
115 error_code: Optional[str] = Field(None, alias="Error Code")
116 error_description: Optional[str] = Field(None, alias="Error Description")
117 post_clean_status: Optional[str] = Field(None, alias="Post Clean Status")
118 additional_actions_id: Optional[str] = Field(None, alias="Additional Actions ID")
119 additional_actions_string: Optional[str] = Field(None, alias="Additional Actions String")
120 remediation_user: Optional[str] = Field(None, alias="Remediation User")
121 security_intelligence_version: Optional[str] = Field(None, alias="Security intelligence Version")
122 engine_version: Optional[str] = Field(None, alias="Engine Version")
123 model_config = ConfigDict(populate_by_name=True, extra="allow")
124
125
126 class PowerShellEventData(BaseModel):
127 """PowerShell-specific event data structure"""
128
129 MessageNumber: int
130 MessageTotal: int
131 ScriptBlockText: str
132 ScriptBlockId: str
133 Path: str
134
135 # Optional fields that might be present in other PowerShell events
136 HostApplication: Optional[str] = None
137 HostName: Optional[str] = None
138 HostVersion: Optional[str] = None
139 EngineVersion: Optional[str] = None
140 RunspaceId: Optional[str] = None
141 PipelineId: Optional[int] = None
142 CommandName: Optional[str] = None
143 CommandType: Optional[str] = None
144 ConnectedUser: Optional[str] = None
145 model_config = ConfigDict(extra="allow")
146
147
148 # Generic event data model that accepts any fields
149 class GenericEventData(BaseModel):
150 """Generic event data structure that accepts any fields"""
151
152 model_config = ConfigDict(extra="allow")
153
154
155 class EventBase(BaseModel):
156 """Base event structure with common fields"""
157
158 System: SystemData
159 Message: str
160
161
162 class SysmonEvent(EventBase):
163 """Sysmon-specific event"""
164
165 EventData: SysmonEventData
166
167
168 class DefenderEvent(EventBase):
169 """Windows Defender-specific event"""
170
171 EventData: DefenderEventData
172
173
174 class PowerShellEvent(EventBase):
175 """PowerShell-specific event"""
176
177 EventData: PowerShellEventData
178
179
180 class GenericEvent(EventBase):
181 """Generic event that can hold any event data"""
182
183 EventData: GenericEventData
184
185
186 class VelociraptorSigmaAlert(BaseModel):
187 """
188 Represents a Sigma alert from Velociraptor with flexible event structure
189 """
190
191 computer: str
192 clientID: Optional[str] = None
193 channel: str
194 title: str
195 level: str
196 event: Union[str, Dict[str, Any], SysmonEvent, DefenderEvent, GenericEvent]
197 type: str = "sigma-alert"
198 source: str = "velociraptor"
199 index_pattern: str
200 sourceRef: str
201
202 @field_validator("event", mode="before")
203 @classmethod
204 def parse_event(cls, v):
205 """Parse the event if it's a string"""
206 if isinstance(v, str):
207 try:
208 return json.loads(v)
209 except json.JSONDecodeError as e:
210 raise ValueError(f"Invalid JSON in event field: {e}")
211 return v
212
213 def get_parsed_event(self) -> Union[SysmonEvent, DefenderEvent, PowerShellEvent, GenericEvent]:
214 """
215 Get the event object parsed into the appropriate type based on the channel
216
217 Detects the event type from:
218 1. The channel field in the alert (e.g. "Microsoft-Windows-Sysmon/Operational")
219 2. The System.Provider.Name in the event data
220 """
221 if isinstance(self.event, str):
222 # If still a string (though validator should have converted it)
223 event_data = json.loads(self.event)
224 elif isinstance(self.event, (SysmonEvent, DefenderEvent, PowerShellEvent, GenericEvent)):
225 # Already parsed into appropriate model
226 return self.event
227 else:
228 # Dictionary that needs to be converted
229 event_data = self.event
230
231 # First check the channel field in the alert
232 if self.channel:
233 channel_lower = self.channel.lower()
234
235 # Check for Sysmon in channel
236 if "sysmon" in channel_lower:
237 try:
238 return SysmonEvent(**event_data)
239 except Exception as e:
240 # Fall back to generic if structure doesn't match
241 logger.warning(f"Failed to parse Sysmon event: {e}")
242 return GenericEvent(**event_data)
243
244 # Check for Defender in channel
245 elif "defender" in channel_lower:
246 try:
247 return DefenderEvent(**event_data)
248 except Exception as e:
249 # Fall back to generic if structure doesn't match
250 logger.warning(f"Failed to parse Defender event: {e}")
251 return GenericEvent(**event_data)
252
253 # Check for PowerShell in channel
254 elif "powershell" in channel_lower:
255 try:
256 return PowerShellEvent(**event_data)
257 except Exception as e:
258 # Fall back to generic if structure doesn't match
259 logger.warning(f"Failed to parse PowerShell event: {e}")
260 return GenericEvent(**event_data)
261
262 # If channel doesn't give us enough info, check Provider.Name in the event
263 provider_name = ""
264 if isinstance(event_data, dict) and "System" in event_data:
265 system = event_data["System"]
266 if "Provider" in system and "Name" in system["Provider"]:
267 provider_name = system["Provider"]["Name"].lower()
268
269 # Check provider name
270 if "sysmon" in provider_name:
271 try:
272 return SysmonEvent(**event_data)
273 except Exception as e:
274 logger.warning(f"Failed to parse Sysmon event: {e}")
275 return GenericEvent(**event_data)
276 elif "defender" in provider_name:
277 try:
278 return DefenderEvent(**event_data)
279 except Exception as e:
280 logger.warning(f"Failed to parse Defender event: {e}")
281 return GenericEvent(**event_data)
282 elif "powershell" in provider_name:
283 try:
284 return PowerShellEvent(**event_data)
285 except Exception as e:
286 logger.warning(f"Failed to parse PowerShell event: {e}")
287 return GenericEvent(**event_data)
288
289 # If we have System.Channel, check that too
290 if "Channel" in system:
291 system_channel = system["Channel"].lower()
292 if "sysmon" in system_channel:
293 try:
294 return SysmonEvent(**event_data)
295 except Exception as e:
296 logger.warning(f"Failed to parse Sysmon event: {e}")
297 return GenericEvent(**event_data)
298 elif "defender" in system_channel:
299 try:
300 return DefenderEvent(**event_data)
301 except Exception as e:
302 logger.warning(f"Failed to parse Defender event: {e}")
303 return GenericEvent(**event_data)
304 elif "powershell" in system_channel:
305 try:
306 return PowerShellEvent(**event_data)
307 except Exception as e:
308 logger.warning(f"Failed to parse PowerShell event: {e}")
309 return GenericEvent(**event_data)
310
311 # Use generic model for other event types
312 return GenericEvent(**event_data)
313
314 model_config = ConfigDict(
315 json_schema_extra={
316 "example": {
317 "computer": "WIN-HFOU106TD7K",
318 "clientID": "C.475df76785008b04",
319 "channel": "Microsoft-Windows-Sysmon/Operational",
320 "title": "Proc Access (Sysmon Alert)",
321 "level": "high",
322 "event": (
323 '{"System":{"Provider":{"Name":"Microsoft-Windows-Sysmon","Guid":"5770385F-C22A-43E0-BF4C-06F5698FFBD9"},'
324 '"EventID":{"Value":10},"Version":3,"Level":4,"Task":10,"Opcode":0,"Keywords":9223372036854775808,'
325 '"TimeCreated":{"SystemTime":1744233485.0778975},"EventRecordID":564617,"Correlation":{},'
326 '"Execution":{"ProcessID":2320,"ThreadID":3540},"Channel":"Microsoft-Windows-Sysmon/Operational",'
327 '"Computer":"WIN-HFOU106TD7K","Security":{"UserID":"S-1-5-18"}},"EventData":{"RuleName":"technique_id=T1003,'
328 'technique_name=Credential Dumping","UtcTime":"2025-04-09 21:18:05.064",'
329 '"SourceProcessGUID":"691FF406-E40B-67F6-2901-000000003A00","SourceProcessId":4964,"SourceThreadId":4448,'
330 '"SourceImage":"C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe",'
331 '"TargetProcessGUID":"691FF406-DDC8-67F6-0C00-000000003A00","TargetProcessId":668,'
332 '"TargetImage":"C:\\\\Windows\\\\system32\\\\lsass.exe","GrantedAccess":2097151,'
333 '"CallTrace":"C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|'
334 "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
335 "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
336 "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
337 "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
338 "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
339 "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
340 "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
341 'C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)","SourceUser":"WIN-HFOU106TD7K\\\\Administrator",'
342 '"TargetUser":"NT AUTHORITY\\\\SYSTEM"},'
343 '"Message":"Process accessed:\\nRuleName: technique_id=T1003,technique_name=Credential Dumping!s!\\n'
344 "UtcTime: 2025-04-09 21:18:05.064!s!\\n"
345 "SourceProcessGUID: 691FF406-E40B-67F6-2901-000000003A00!s!\\n"
346 "SourceProcessId: 4964!s!\\n"
347 "SourceThreadId: 4448!s!\\n"
348 "SourceImage: C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe!s!\\n"
349 "TargetProcessGUID: 691FF406-DDC8-67F6-0C00-000000003A00!s!\\n"
350 "TargetProcessId: 668!s!\\n"
351 "TargetImage: C:\\\\Windows\\\\system32\\\\lsass.exe!s!\\n"
352 "GrantedAccess: 2097151!s!\\n"
353 "CallTrace: C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+9fc24|C:\\\\Windows\\\\System32\\\\wow64.dll+3cf4|"
354 "C:\\\\Windows\\\\System32\\\\wow64.dll+7783|C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1783|"
355 "C:\\\\Windows\\\\System32\\\\wow64cpu.dll+1199|C:\\\\Windows\\\\System32\\\\wow64.dll+cfda|"
356 "C:\\\\Windows\\\\System32\\\\wow64.dll+cea0|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+757db|"
357 "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+756c3|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7566e|"
358 "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+7070c(wow64)|C:\\\\Windows\\\\System32\\\\KERNELBASE.dll+10eca8(wow64)|"
359 "C:\\\\Users\\\\ADMINI~1\\\\AppData\\\\Local\\\\Temp\\\\2\\\\AttackSim\\\\procdump.exe+876e|"
360 "C:\\\\Windows\\\\System32\\\\KERNEL32.DLL+20419(wow64)|C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+6662d(wow64)|"
361 "C:\\\\Windows\\\\SYSTEM32\\\\ntdll.dll+665fd(wow64)!s!\\n"
362 "SourceUser: WIN-HFOU106TD7K\\\\Administrator!s!\\n"
363 'TargetUser: NT AUTHORITY\\\\SYSTEM!s!\\r\\n"}'
364 ),
365 "type": "sigma-alert",
366 "source": "velociraptor",
367 "index_pattern": "wazuh-*",
368 "sourceRef": "754600692",
369 },
370 },
371 )
372
373
374 class VelociraptorSigmaAlertResponse(BaseModel):
375 """
376 Response model for Velociraptor Sigma alert processing
377 """
378
379 success: bool
380 message: str
381 alert_id: Optional[str] = None
382
383
384 class VeloSigmaExclusionBase(BaseModel):
385 """Base class for Velociraptor Sigma exclusion rules."""
386
387 name: str = Field(..., description="Friendly name for this exclusion rule")
388 description: Optional[str] = Field(None, description="Description of why this exclusion exists")
389 channel: Optional[str] = Field(None, description="Windows event channel to match (exact match)")
390 title: Optional[str] = Field(None, description="Sigma rule title to match (exact match)")
391 field_matches: Optional[Dict] = Field(None, description="Field names and values to match in the event data")
392 customer_code: Optional[str] = Field(None, description="Customer code this exclusion applies to (null means all customers)")
393 enabled: bool = Field(True, description="Whether this exclusion is active")
394
395
396 class VeloSigmaExclusionCreate(VeloSigmaExclusionBase):
397 """Schema for creating a new exclusion rule."""
398
399 # Make created_by optional so it can be set by the server
400 created_by: Optional[str] = Field(None, description="User who created this exclusion rule")
401 model_config = ConfigDict(
402 json_schema_extra={
403 "example": {
404 "name": "Chainsaw Batch Script Exclusion",
405 "description": "Exclude alerts from chainsaw batch scripts in Windows Temp folder",
406 "channel": "Microsoft-Windows-Sysmon/Operational",
407 "title": "HackTool - Powerup Write Hijack DLL",
408 "field_matches": {"TargetFilename": "C:\\Windows\\Temp\\chainsaw_batch.bat"},
409 "customer_code": None, # Optional, NULL means apply to all customers
410 "enabled": True,
411 },
412 },
413 )
414
415
416 class VeloSigmaExclusionUpdate(BaseModel):
417 """Schema for updating an exclusion rule."""
418
419 name: Optional[str] = None
420 description: Optional[str] = None
421 channel: Optional[str] = None
422 title: Optional[str] = None
423 field_matches: Optional[Dict] = None
424 customer_code: Optional[str] = None
425 enabled: Optional[bool] = None
426
427
428 class VeloSigmaExclusionResponse(VeloSigmaExclusionBase):
429 """Response schema for exclusion rules."""
430
431 id: int
432 created_by: str
433 created_at: datetime
434 last_matched_at: Optional[datetime] = None
435 match_count: int
436 model_config = ConfigDict(from_attributes=True)
437
438
439 class VeloSigmaExlcusionRouteResponse(BaseModel):
440 """Response schema for exclusion rules."""
441
442 exclusion_response: VeloSigmaExclusionResponse
443 success: bool
444 message: str
445
446
447 class VeloSigmaExclusionListResponse(BaseModel):
448 success: bool
449 message: str
450 exclusions: List[VeloSigmaExclusionResponse]
451 pagination: dict = {"total": 0, "skip": 0, "limit": 0}