dissect.target 3.20.dev31__py3-none-any.whl → 3.20.dev32__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import lzma
4
- from typing import BinaryIO, Callable, Iterator
5
+ from typing import Any, BinaryIO, Callable, Iterator
5
6
 
6
7
  import zstandard
7
8
  from dissect.cstruct import cstruct
@@ -13,6 +14,8 @@ from dissect.target.exceptions import UnsupportedPluginError
13
14
  from dissect.target.helpers.record import TargetRecordDescriptor
14
15
  from dissect.target.plugin import Plugin, export
15
16
 
17
+ log = logging.getLogger(__name__)
18
+
16
19
  # The events have undocumented fields that are not part of the record
17
20
  JournalRecord = TargetRecordDescriptor(
18
21
  "linux/log/journal",
@@ -28,7 +31,7 @@ JournalRecord = TargetRecordDescriptor(
28
31
  ("varint", "errno"),
29
32
  ("string", "invocation_id"),
30
33
  ("string", "user_invocation_id"),
31
- ("varint", "syslog_facility"),
34
+ ("string", "syslog_facility"),
32
35
  ("string", "syslog_identifier"),
33
36
  ("varint", "syslog_pid"),
34
37
  ("string", "syslog_raw"),
@@ -70,11 +73,13 @@ JournalRecord = TargetRecordDescriptor(
70
73
  ("path", "udev_devlink"),
71
74
  # Other fields
72
75
  ("string", "journal_hostname"),
73
- ("path", "filepath"),
76
+ ("path", "source"),
74
77
  ],
75
78
  )
76
79
 
77
80
  journal_def = """
81
+ #define HEADER_SIGNATURE b"LPKSHHRH"
82
+
78
83
  typedef uint8 uint8_t;
79
84
  typedef uint32 le32_t;
80
85
  typedef uint64 le64_t;
@@ -100,7 +105,7 @@ flag IncompatibleFlag : le32_t {
100
105
  };
101
106
 
102
107
  struct Header {
103
- uint8_t signature[8];
108
+ char signature[8];
104
109
  le32_t compatible_flags;
105
110
  IncompatibleFlag incompatible_flags;
106
111
  State state;
@@ -165,7 +170,7 @@ struct ObjectHeader {
165
170
 
166
171
  // The first four members are copied from ObjectHeader, so that the size can be used as the length of payload
167
172
  struct DataObject {
168
- ObjectType type;
173
+ // ObjectType type;
169
174
  ObjectFlag flags;
170
175
  uint8_t reserved[6];
171
176
  le64_t size;
@@ -181,7 +186,7 @@ struct DataObject {
181
186
  // If the HEADER_INCOMPATIBLE_COMPACT flag is set, two extra fields are stored to allow immediate access
182
187
  // to the tail entry array in the DATA object's entry array chain.
183
188
  struct DataObject_Compact {
184
- ObjectType type;
189
+ // ObjectType type;
185
190
  ObjectFlag flags;
186
191
  uint8_t reserved[6];
187
192
  le64_t size;
@@ -236,7 +241,7 @@ struct EntryObject_Compact {
236
241
 
237
242
  // The first four members are copied from from ObjectHeader, so that the size can be used as the length of entry_object_offsets
238
243
  struct EntryArrayObject {
239
- ObjectType type;
244
+ // ObjectType type;
240
245
  uint8_t flags;
241
246
  uint8_t reserved[6];
242
247
  le64_t size;
@@ -245,7 +250,7 @@ struct EntryArrayObject {
245
250
  };
246
251
 
247
252
  struct EntryArrayObject_Compact {
248
- ObjectType type;
253
+ // ObjectType type;
249
254
  uint8_t flags;
250
255
  uint8_t reserved[6];
251
256
  le64_t size;
@@ -257,9 +262,19 @@ struct EntryArrayObject_Compact {
257
262
  c_journal = cstruct().load(journal_def)
258
263
 
259
264
 
260
- def get_optional(value: str, to_type: Callable):
265
+ def get_optional(value: str, to_type: Callable) -> Any | None:
261
266
  """Return the value if True, otherwise return None."""
262
- return to_type(value) if value else None
267
+
268
+ if not value:
269
+ return None
270
+
271
+ try:
272
+ return to_type(value)
273
+
274
+ except ValueError as e:
275
+ log.error("Unable to cast '%s' to %s", value, to_type)
276
+ log.debug("", exc_info=e)
277
+ return None
263
278
 
264
279
 
265
280
  class JournalFile:
@@ -273,136 +288,138 @@ class JournalFile:
273
288
  def __init__(self, fh: BinaryIO, target: Target):
274
289
  self.fh = fh
275
290
  self.target = target
276
- self.header = c_journal.Header(self.fh)
277
- self.signature = "".join(chr(c) for c in self.header.signature)
278
- self.entry_array_offset = self.header.entry_array_offset
279
291
 
280
- def entry_object_offsets(self) -> Iterator[int]:
281
- """Read object entry arrays."""
292
+ try:
293
+ self.header = c_journal.Header(self.fh)
294
+ except EOFError as e:
295
+ raise ValueError(f"Invalid systemd Journal file: {e}")
282
296
 
283
- offset = self.entry_array_offset
284
-
285
- # Entry Array with next_entry_array_offset set to 0 is the last in the list
286
- while offset != 0:
287
- self.fh.seek(offset)
288
-
289
- object = c_journal.ObjectHeader(self.fh)
290
-
291
- if object.type == c_journal.ObjectType.OBJECT_ENTRY_ARRAY:
292
- # After the object is checked, read again but with EntryArrayObject instead of ObjectHeader
293
- self.fh.seek(offset)
294
-
295
- if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
296
- entry_array_object = c_journal.EntryArrayObject_Compact(self.fh)
297
- else:
298
- entry_array_object = c_journal.EntryArrayObject(self.fh)
299
-
300
- for entry_object_offset in entry_array_object.entry_object_offsets:
301
- # Check if the offset is not zero and points to nothing
302
- if entry_object_offset:
303
- yield entry_object_offset
304
-
305
- offset = entry_array_object.next_entry_array_offset
297
+ if self.header.signature != c_journal.HEADER_SIGNATURE:
298
+ raise ValueError(f"Journal file has invalid magic header: {self.header.signature!r}'")
306
299
 
307
300
  def decode_value(self, value: bytes) -> tuple[str, str]:
308
- value = value.decode(encoding="utf-8", errors="surrogateescape").strip()
309
-
310
- # Strip leading underscores part of the field name
311
- value = value.lstrip("_")
312
-
301
+ """Decode the given bytes to a key value pair."""
302
+ value = value.decode(errors="surrogateescape").strip().lstrip("_")
313
303
  key, value = value.split("=", 1)
314
304
  key = key.lower()
315
-
316
305
  return key, value
317
306
 
318
307
  def __iter__(self) -> Iterator[dict[str, int | str]]:
319
308
  "Iterate over the entry objects to read payloads."
320
309
 
321
- for offset in self.entry_object_offsets():
310
+ offset = self.header.entry_array_offset
311
+ while offset != 0:
322
312
  self.fh.seek(offset)
323
313
 
314
+ if self.fh.read(1)[0] != c_journal.ObjectType.OBJECT_ENTRY_ARRAY:
315
+ raise ValueError(f"Expected OBJECT_ENTRY_ARRAY at offset {offset}")
316
+
317
+ if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
318
+ entry_array_object = c_journal.EntryArrayObject_Compact(self.fh)
319
+ else:
320
+ entry_array_object = c_journal.EntryArrayObject(self.fh)
321
+
322
+ for entry_object_offset in entry_array_object.entry_object_offsets:
323
+ if entry_object_offset:
324
+ yield from self._parse_entry_object(offset=entry_object_offset)
325
+
326
+ offset = entry_array_object.next_entry_array_offset
327
+
328
+ def _parse_entry_object(self, offset: int) -> Iterator[dict]:
329
+ self.fh.seek(offset)
330
+
331
+ try:
324
332
  if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
325
333
  entry = c_journal.EntryObject_Compact(self.fh)
326
334
  else:
327
335
  entry = c_journal.EntryObject(self.fh)
328
336
 
329
- event = {}
330
- event["ts"] = ts.from_unix_us(entry.realtime)
337
+ except EOFError as e:
338
+ self.target.log.warning("Unable to read Journal EntryObject at offset %s in: %s", offset, self.fh)
339
+ self.target.log.debug("", exc_info=e)
340
+ return
331
341
 
332
- for item in entry.items:
333
- try:
334
- self.fh.seek(item.object_offset)
342
+ event = {"ts": ts.from_unix_us(entry.realtime)}
343
+ for item in entry.items:
344
+ try:
345
+ self.fh.seek(item.object_offset)
335
346
 
336
- object = c_journal.ObjectHeader(self.fh)
347
+ if self.fh.read(1)[0] != c_journal.ObjectType.OBJECT_DATA:
348
+ continue
337
349
 
338
- if object.type == c_journal.ObjectType.OBJECT_DATA:
339
- self.fh.seek(item.object_offset)
350
+ if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
351
+ data_object = c_journal.DataObject_Compact(self.fh)
352
+ else:
353
+ data_object = c_journal.DataObject(self.fh)
340
354
 
341
- if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
342
- data_object = c_journal.DataObject_Compact(self.fh)
343
- else:
344
- data_object = c_journal.DataObject(self.fh)
355
+ if not data_object.payload:
356
+ continue
345
357
 
346
- data = data_object.payload
358
+ data = data_object.payload
347
359
 
348
- if not data:
349
- # If the payload is empty
350
- continue
351
- elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_XZ:
352
- data = lzma.decompress(data)
353
- elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_LZ4:
354
- data = lz4.decompress(data[8:])
355
- elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_ZSTD:
356
- data = zstandard.decompress(data)
360
+ if data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_XZ:
361
+ data = lzma.decompress(data)
357
362
 
358
- key, value = self.decode_value(data)
359
- event[key] = value
363
+ elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_LZ4:
364
+ data = lz4.decompress(data[8:])
360
365
 
361
- except Exception as e:
362
- self.target.log.warning(
363
- "The data object in Journal file %s could not be parsed",
364
- getattr(self.fh, "name", None),
365
- exc_info=e,
366
- )
367
- continue
366
+ elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_ZSTD:
367
+ data = zstandard.decompress(data)
368
+
369
+ key, value = self.decode_value(data)
370
+ event[key] = value
371
+
372
+ except Exception as e:
373
+ self.target.log.warning(
374
+ "Journal DataObject could not be parsed at offset %s in %s",
375
+ item.object_offset,
376
+ getattr(self.fh, "name", None),
377
+ )
378
+ self.target.log.debug("", exc_info=e)
379
+ continue
368
380
 
369
- yield event
381
+ yield event
370
382
 
371
383
 
372
384
  class JournalPlugin(Plugin):
385
+ """Systemd Journal plugin."""
386
+
373
387
  JOURNAL_PATHS = ["/var/log/journal"] # TODO: /run/systemd/journal
374
388
  JOURNAL_GLOB = "*/*.journal*" # The extensions .journal and .journal~
375
- JOURNAL_SIGNATURE = "LPKSHHRH"
376
389
 
377
390
  def __init__(self, target: Target):
378
391
  super().__init__(target)
379
- self.journal_paths = []
392
+ self.journal_files = []
380
393
 
381
- for _path in self.JOURNAL_PATHS:
382
- self.journal_paths.extend(self.target.fs.path(_path).glob(self.JOURNAL_GLOB))
394
+ for journal_path in self.JOURNAL_PATHS:
395
+ self.journal_files.extend(self.target.fs.path(journal_path).glob(self.JOURNAL_GLOB))
383
396
 
384
397
  def check_compatible(self) -> None:
385
- if not len(self.journal_paths):
398
+ if not self.journal_files:
386
399
  raise UnsupportedPluginError("No journald files found")
387
400
 
388
401
  @export(record=JournalRecord)
389
402
  def journal(self) -> Iterator[JournalRecord]:
390
- """Return the content of Systemd Journal log files.
403
+ """Return the contents of Systemd Journal log files.
391
404
 
392
405
  References:
393
406
  - https://wiki.archlinux.org/title/Systemd/Journal
394
407
  - https://github.com/systemd/systemd/blob/9203abf79f1d05fdef9b039e7addf9fc5a27752d/man/systemd.journal-fields.xml
395
408
  """ # noqa: E501
396
-
397
409
  path_function = self.target.fs.path
398
410
 
399
- for _path in self.journal_paths:
400
- fh = _path.open()
411
+ for journal_file in self.journal_files:
412
+ if not journal_file.is_file():
413
+ self.target.log.warning("Unable to parse journal file as it is not a file: %s", journal_file)
414
+ continue
401
415
 
402
- journal = JournalFile(fh, self.target)
416
+ try:
417
+ fh = journal_file.open()
418
+ journal = JournalFile(fh, self.target)
403
419
 
404
- if not journal.signature == self.JOURNAL_SIGNATURE:
405
- self.target.log.warning("The Journal log file %s has an invalid magic header", _path)
420
+ except Exception as e:
421
+ self.target.log.warning("Unable to parse journal file structure: %s: %s", journal_file, str(e))
422
+ self.target.log.debug("", exc_info=e)
406
423
  continue
407
424
 
408
425
  for entry in journal:
@@ -417,7 +434,7 @@ class JournalPlugin(Plugin):
417
434
  errno=get_optional(entry.get("errno"), int),
418
435
  invocation_id=entry.get("invocation_id"),
419
436
  user_invocation_id=entry.get("user_invocation_id"),
420
- syslog_facility=get_optional(entry.get("syslog_facility"), int),
437
+ syslog_facility=entry.get("syslog_facility"),
421
438
  syslog_identifier=entry.get("syslog_identifier"),
422
439
  syslog_pid=get_optional(entry.get("syslog_pid"), int),
423
440
  syslog_raw=entry.get("syslog_raw"),
@@ -456,6 +473,6 @@ class JournalPlugin(Plugin):
456
473
  udev_devnode=get_optional(entry.get("udev_devnode"), path_function),
457
474
  udev_devlink=get_optional(entry.get("udev_devlink"), path_function),
458
475
  journal_hostname=entry.get("hostname"),
459
- filepath=_path,
476
+ source=journal_file,
460
477
  _target=self.target,
461
478
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.20.dev31
3
+ Version: 3.20.dev32
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -262,7 +262,7 @@ dissect/target/plugins/os/unix/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQe
262
262
  dissect/target/plugins/os/unix/log/atop.py,sha256=ljvGipVG16qTECnV1kIORykcGH9tTlpDmcMo5CXSPns,16332
263
263
  dissect/target/plugins/os/unix/log/audit.py,sha256=OjorWTmCFvCI5RJq6m6WNW0Lhb-poB2VAggKOGZUHK4,3722
264
264
  dissect/target/plugins/os/unix/log/auth.py,sha256=l7gCuRdvv9gL0U1N0yrR9hVsMnr4t_k4t-n-f6PrOxg,2388
265
- dissect/target/plugins/os/unix/log/journal.py,sha256=auVRfrW4NRU7HguoDLTz4l_IwNdPZLPAqD7jhrOTzH8,17404
265
+ dissect/target/plugins/os/unix/log/journal.py,sha256=xe8p8MM_95uYjFNzNSP5IsoIthJtxwFEDicYR42RYAI,17681
266
266
  dissect/target/plugins/os/unix/log/lastlog.py,sha256=Wq89wRSFZSBsoKVCxjDofnC4yw9XJ4iOF0XJe9EucCo,2448
267
267
  dissect/target/plugins/os/unix/log/messages.py,sha256=O10Uw3PGTanfGpphUWYqOwOIR7XiiM-clfboVCoiP0U,4501
268
268
  dissect/target/plugins/os/unix/log/utmp.py,sha256=1nPHIaBUHt_9z6PDrvyqg4huKLihUaWLrMmgMsbaeIo,7755
@@ -370,10 +370,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
370
370
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
371
371
  dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
372
372
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
373
- dissect.target-3.20.dev31.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
374
- dissect.target-3.20.dev31.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
375
- dissect.target-3.20.dev31.dist-info/METADATA,sha256=dxajIRMu3_ON9FATcZv5T2A4VGo63CentqV8LrQyGXg,12897
376
- dissect.target-3.20.dev31.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
377
- dissect.target-3.20.dev31.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
378
- dissect.target-3.20.dev31.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
379
- dissect.target-3.20.dev31.dist-info/RECORD,,
373
+ dissect.target-3.20.dev32.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
374
+ dissect.target-3.20.dev32.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
375
+ dissect.target-3.20.dev32.dist-info/METADATA,sha256=SuQ-1t2xvit888n0TOA-4zCj7Bc1BdYAi-hC1BqvJhs,12897
376
+ dissect.target-3.20.dev32.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
377
+ dissect.target-3.20.dev32.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
378
+ dissect.target-3.20.dev32.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
379
+ dissect.target-3.20.dev32.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5