(g)ULP!
Loading...
Searching...
No Matches
win_evtx.py
Go to the documentation of this file.
1import os
2
3import muty.dict
4import muty.file
5import muty.jsend
6import muty.log
7import muty.string
8import muty.time
9import muty.xml
10from evtx import PyEvtxParser
11from lxml import etree
12from sigma.pipelines.elasticsearch.windows import ecs_windows
13
14from gulp.api.collab.base import GulpRequestStatus
15from gulp.api.collab.stats import GulpStats, TmpIngestStats
16from gulp.api.elastic.structs import GulpDocument, GulpIngestionFilter
17from gulp.api.mapping.models import FieldMappingEntry, GulpMapping
18from gulp.defs import GulpLogLevel, GulpPluginType
19from gulp.plugin import PluginBase
20from gulp.plugin_internal import GulpPluginParams
21
22
24 """
25 windows evtx log file processor.
26 """
27
28 def _normalize_loglevel(self, l: int | str) -> GulpLogLevel:
29 ll = int(l)
30 if ll == 0:
31 return GulpLogLevel.ALWAYS
32 if ll == 1:
33 return GulpLogLevel.CRITICAL
34 if ll == 2:
35 return GulpLogLevel.ERROR
36 if ll == 3:
37 return GulpLogLevel.WARNING
38 if ll == 4:
39 return GulpLogLevel.INFO
40 if ll == 5:
41 return GulpLogLevel.VERBOSE
42 if ll > 5:
43 # maps directly to GulpLogLevel.CUSTOM_n
44 try:
45 return GulpLogLevel(ll)
46 except:
47 return GulpLogLevel.UNEXPECTED
48
49 # wtf ?!
50 return GulpLogLevel.VERBOSE
51
52 def type(self) -> GulpPluginType:
53 return GulpPluginType.INGESTION
54
55 def desc(self) -> str:
56 return "Windows EVTX log file processor."
57
58 def name(self) -> str:
59 return "win_evtx"
60
61 def version(self) -> str:
62 return "1.0"
63
64 def _map_evt_code(self, ev_code: str) -> dict:
65 """
66 better map an event code to fields
67
68 Args:
69 ev_code (str): The event code to be converted.
70 Returns:
71 dict: A dictionary with 'event.category' and 'event.type'
72 """
73 codes = {
74 "100": {"event.category": ["package"], "event.type": ["start"]},
75 "106": {"event.category": ["package"], "event.type": ["install"]},
76 "140": {"event.category": ["package"], "event.type": ["change"]},
77 "141": {"event.category": ["package"], "event.type": ["delete"]},
78 "1006": {"event.category": ["host"], "event.type": ["change"]},
79 "4624": { # eventid
80 "event.category": ["authentication"],
81 "event.type": ["start"],
82 },
83 "4672": {
84 "event.category": ["authentication"],
85 },
86 "4648": {
87 "event.category": ["authentication"],
88 },
89 "4798": {"event.category": ["iam"]},
90 "4799": {"event.category": ["iam"]},
91 "5379": {"event.category": ["iam"], "event.type": ["access"]},
92 "5857": {"event.category": ["process"], "event.type": ["access"]},
93 "5858": {"event.category": ["process"], "event.type": ["error"]},
94 "5859": {"event.category": ["process"], "event.type": ["change"]},
95 "5860": {"event.category": ["process"], "event.type": ["change"]},
96 "5861": {"event.category": ["process"], "event.type": ["change"]},
97 "7036": {
98 "event.category": ["package"],
99 "event.type": ["change"],
100 },
101 "7040": {
102 "event.category": ["package"],
103 "event.type": ["change"],
104 },
105 "7045": {
106 "event.category": ["package"],
107 "event.type": ["install"],
108 },
109 "13002": {"event.type": ["change"]},
110 }
111 if ev_code in codes:
112 return codes[ev_code]
113
114 return None
115
117 self,
118 operation_id: int,
119 client_id: int,
120 context: str,
121 source: str,
122 fs: TmpIngestStats,
123 record: any,
124 record_idx: int,
125 custom_mapping: GulpMapping = None,
126 index_type_mapping: dict = None,
127 plugin: str = None,
128 plugin_params: GulpPluginParams = None,
129 **kwargs,
130 ) -> list[GulpDocument]:
131 # process record
132 # self.logger().debug(record)
133 evt_str: str = record["data"].encode()
134
135 data_elem = None
136 data_elem = etree.fromstring(evt_str)
137
138 # check if we should ignore mapping
139 ignore_mapping = False
140 if plugin_params is not None and plugin_params.ignore_mapping_ingest:
141 ignore_mapping = True
142
143 cat_tree = data_elem[0]
144
145 # extra mapping from event code
146 fme: list[FieldMappingEntry] = []
147 entry = FieldMappingEntry(result={})
148
149 evt_code = str(muty.xml.child_node_text(cat_tree, "EventID"))
150 try:
151 evt_channel = str(muty.xml.child_node_text(cat_tree, "Channel"))
152 except:
153 evt_channel = str(muty.xml.strip_namespace(cat_tree.tag))
154
155 entry.result["winlog.channel"] = evt_channel
156 try:
157 evt_provider = str(muty.xml.child_attrib(cat_tree, "Provider", "Name"))
158 entry.result["event.provider"] = evt_provider
159 except:
160 evt_provider = None
161
162 d = self._map_evt_code(evt_code)
163 if d is not None:
164 entry.result.update(d)
165
166 evt_id = muty.xml.child_node_text(cat_tree, "EventRecordID")
167 original_log_level: str = muty.xml.child_node_text(cat_tree, "Level")
168 gulp_log_level = self._normalize_loglevel(
169 muty.xml.child_node_text(cat_tree, "Level")
170 )
171
172 # evtx timestamp is always UTC string, i.e.: '2021-11-19 16:53:03.836551 UTC'
173 time_str = record["timestamp"]
174 time_nanosec = muty.time.string_to_epoch_nsec(time_str)
175 time_msec = muty.time.nanos_to_millis(time_nanosec)
176 # Plugin.logger().debug('%s - %d' % (time_str, time_msec))
177
178 # raw event as text
179 raw_text = str(record["data"])
180
181 if ignore_mapping:
182 # persist these into the event
183 entry.result["EventID"] = evt_code
184 entry.result["Level"] = original_log_level
185 entry.result["Provider"] = evt_provider
186 entry.result["EventRecordID"] = evt_id
187 entry.result["Channel"] = evt_channel
188
189 # append this entry
190 fme.append(entry)
191
192 e_tree: etree.ElementTree = etree.ElementTree(data_elem)
193
194 # self.logger().debug("e_tree: %s" % (e_tree))
195
196 for e in e_tree.iter():
197 e.tag = muty.xml.strip_namespace(e.tag)
198
199 # these are already processed
200 if e.tag in ["EventID", "EventRecordID", "Level", "Provider"]:
201 continue
202
203 # self.logger().debug(
204 # "found e_tag=%s, value=%s" % (e.tag, e.text)
205 # )
206
207 # Check XML tag name and set extra mapping
208 entries = self._map_source_key(
209 plugin_params,
210 custom_mapping,
211 e.tag,
212 e.text,
213 index_type_mapping=index_type_mapping,
214 **kwargs,
215 )
216 for entry in entries:
217 fme.append(entry)
218
219 # add attributes as more extra data
220 for attr_k, attr_v in e.attrib.items():
221 kk = "gulp.winevtx.xml.attr.%s" % (attr_k)
222 inner_entries = self._map_source_key(
223 plugin_params,
224 custom_mapping,
225 kk,
226 attr_v,
227 index_type_mapping=index_type_mapping,
228 **kwargs,
229 )
230 for entry in inner_entries:
231 fme.append(entry)
232
233 # Check attributes and set extra mapping
234 for attr_k, attr_v in e.attrib.items():
235 # Plugin.logger().info("name: %s value: %s" % (attr_k, attr_v))
236 # self.logger().debug("processing attr_v=%s, value=%s" % (attr_v, e.text))
237 entries = self._map_source_key(
238 plugin_params,
239 custom_mapping,
240 attr_v,
241 e.text,
242 index_type_mapping=index_type_mapping,
243 **kwargs,
244 )
245 for entry in entries:
246 fme.append(entry)
247
248 if e.text is None:
249 # no more processing
250 continue
251
252 value = e.text.strip()
253 if attr_v is not None:
254 # prefer attr_v
255 value = attr_v.strip()
256
257 entries = self._map_source_key(
258 plugin_params,
259 custom_mapping,
260 attr_k,
261 value,
262 index_type_mapping=index_type_mapping,
263 **kwargs,
264 )
265 for entry in entries:
266 fme.append(entry)
267
268
269 # finally create documents
270 docs = self._build_gulpdocuments(
271 fme,
272 idx=record_idx,
273 operation_id=operation_id,
274 context=context,
275 plugin=self.namename(),
276 client_id=client_id,
277 raw_event=raw_text,
278 original_id=evt_id,
279 src_file=os.path.basename(source),
280 timestamp=time_msec,
281 timestamp_nsec=time_nanosec,
282 event_code=evt_code,
283 gulp_log_level=gulp_log_level,
284 original_log_level=original_log_level
285 )
286 return docs
287
288 async def ingest(
289 self,
290 index: str,
291 req_id: str,
292 client_id: int,
293 operation_id: int,
294 context: str,
295 source: str | list[dict],
296 ws_id: str,
297 plugin_params: GulpPluginParams = None,
298 flt: GulpIngestionFilter = None,
299 **kwargs,
300 ) -> GulpRequestStatus:
301
302 await super().ingest(
303 index=index,
304 req_id=req_id,
305 client_id=client_id,
306 operation_id=operation_id,
307 context=context,
308 source=source,
309 ws_id=ws_id,
310 plugin_params=plugin_params,
311 flt=flt,
312 **kwargs,
313 )
314
315 parser = None
316
317 fs = TmpIngestStats(source)
318
319 # initialize mapping
320 try:
321 index_type_mapping, custom_mapping = await self.ingest_plugin_initialize(
322 index,
323 source=source,
324 pipeline=ecs_windows(),
325 mapping_file="windows.json",
326 plugin_params=plugin_params,
327 )
328 # Plugin.logger().debug("win_mappings: %s" % (win_mapping))
329 except Exception as ex:
330 fs=self._parser_failed(fs, source, ex)
331 return await self._finish_ingestion(index, source, req_id, client_id, ws_id, fs=fs, flt=flt)
332
333 # init parser
334 try:
335 parser = PyEvtxParser(source)
336 except Exception as ex:
337 # cannot parse this file at all
338 fs=self._parser_failed(fs, source, ex)
339 return await self._finish_ingestion(index, source, req_id, client_id, ws_id, fs=fs, flt=flt)
340
341 ev_idx = 0
342
343 try:
344 for rr in parser.records():
345 # process (ingest + update stats)
346 try:
347 fs, must_break = await self._process_record(
348 index,
349 rr,
350 ev_idx,
352 ws_id,
353 req_id,
354 operation_id,
355 client_id,
356 context,
357 source,
358 fs,
359 custom_mapping=custom_mapping,
360 index_type_mapping=index_type_mapping,
361 plugin_params=plugin_params,
362 flt=flt,
363 **kwargs,
364 )
365
366 ev_idx += 1
367 if must_break:
368 break
369
370 except Exception as ex:
371 fs=self._record_failed(fs, rr, source, ex)
372
373 except Exception as ex:
374 fs=self._parser_failed(fs, source, ex)
375
376 # done
377 return await self._finish_ingestion(index, source, req_id, client_id, ws_id, fs=fs, flt=flt)
GulpRequestStatus _finish_ingestion(self, str index, str|dict source, str req_id, int client_id, str ws_id, TmpIngestStats fs, GulpIngestionFilter flt=None)
Definition plugin.py:972
TmpIngestStats _record_failed(self, TmpIngestStats fs, any entry, str|dict source, Exception|str ex)
Definition plugin.py:947
TmpIngestStats _parser_failed(self, TmpIngestStats fs, str|dict source, Exception|str ex)
Definition plugin.py:934
list[GulpDocument] _build_gulpdocuments(self, list[FieldMappingEntry] fme, int idx, int operation_id, str context, str plugin, int client_id, str raw_event, str original_id, str src_file, int timestamp=None, int timestamp_nsec=None, str event_code=None, list[str] cat=None, int duration_nsec=0, GulpLogLevel gulp_log_level=None, str original_log_level=None, bool remove_raw_event=False, **kwargs)
Definition plugin.py:575
list[GulpDocument] record_to_gulp_document(self, int operation_id, int client_id, str context, str source, TmpIngestStats fs, any record, int record_idx, GulpMapping custom_mapping=None, dict index_type_mapping=None, str plugin=None, GulpPluginParams plugin_params=None, **kwargs)
Definition plugin.py:469
list[FieldMappingEntry] _map_source_key(self, GulpPluginParams plugin_params, GulpMapping custom_mapping, str source_key, Any v, dict index_type_mapping=None, bool ignore_custom_mapping=False, **kwargs)
Definition plugin.py:692
tuple[dict, GulpMapping] ingest_plugin_initialize(self, str index, str|dict source, bool skip_mapping=False, ProcessingPipeline pipeline=None, str mapping_file=None, str mapping_id=None, GulpPluginParams plugin_params=None)
Definition plugin.py:325
tuple[TmpIngestStats, bool] _process_record(self, str index, any record, int record_idx, Callable my_record_to_gulp_document_fun, str ws_id, str req_id, int operation_id, int client_id, str context, str source, TmpIngestStats fs, GulpMapping custom_mapping=None, dict index_type_mapping=None, str plugin=None, GulpPluginParams plugin_params=None, GulpIngestionFilter flt=None, **kwargs)
Definition plugin.py:259
list[GulpDocument] record_to_gulp_document(self, int operation_id, int client_id, str context, str source, TmpIngestStats fs, any record, int record_idx, GulpMapping custom_mapping=None, dict index_type_mapping=None, str plugin=None, GulpPluginParams plugin_params=None, **kwargs)
Definition win_evtx.py:130
GulpPluginType type(self)
Definition win_evtx.py:52
dict _map_evt_code(self, str ev_code)
Definition win_evtx.py:64
GulpLogLevel _normalize_loglevel(self, int|str l)
Definition win_evtx.py:28
GulpRequestStatus ingest(self, str index, str req_id, int client_id, int operation_id, str context, str|list[dict] source, str ws_id, GulpPluginParams plugin_params=None, GulpIngestionFilter flt=None, **kwargs)
Definition win_evtx.py:300