dissect.target 3.19.dev58__py3-none-any.whl → 3.20__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. dissect/target/container.py +1 -1
  2. dissect/target/exceptions.py +6 -5
  3. dissect/target/filesystem.py +2 -2
  4. dissect/target/filesystems/btrfs.py +14 -5
  5. dissect/target/filesystems/config.py +5 -1
  6. dissect/target/filesystems/extfs.py +5 -4
  7. dissect/target/filesystems/fat.py +22 -16
  8. dissect/target/filesystems/ffs.py +11 -4
  9. dissect/target/filesystems/jffs.py +12 -7
  10. dissect/target/filesystems/ntfs.py +22 -6
  11. dissect/target/filesystems/overlay.py +14 -4
  12. dissect/target/filesystems/smb.py +3 -3
  13. dissect/target/filesystems/squashfs.py +4 -4
  14. dissect/target/filesystems/vmfs.py +4 -4
  15. dissect/target/filesystems/xfs.py +15 -8
  16. dissect/target/helpers/compat/path_common.py +5 -5
  17. dissect/target/helpers/configutil.py +128 -32
  18. dissect/target/helpers/cyber.py +2 -0
  19. dissect/target/helpers/data/windowsZones.xml +19 -23
  20. dissect/target/helpers/docs.py +1 -1
  21. dissect/target/helpers/keychain.py +2 -0
  22. dissect/target/helpers/mount.py +2 -1
  23. dissect/target/helpers/record.py +29 -2
  24. dissect/target/helpers/record_modifier.py +5 -1
  25. dissect/target/helpers/regutil.py +56 -26
  26. dissect/target/loader.py +1 -1
  27. dissect/target/loaders/mqtt.py +104 -9
  28. dissect/target/loaders/proxmox.py +68 -0
  29. dissect/target/loaders/vma.py +1 -1
  30. dissect/target/loaders/xva.py +1 -1
  31. dissect/target/plugin.py +24 -21
  32. dissect/target/plugins/apps/av/mcafee.py +2 -0
  33. dissect/target/plugins/apps/av/sophos.py +2 -0
  34. dissect/target/plugins/apps/av/trendmicro.py +2 -0
  35. dissect/target/plugins/apps/browser/chromium.py +27 -6
  36. dissect/target/plugins/apps/container/docker.py +48 -32
  37. dissect/target/plugins/apps/editor/__init__.py +0 -0
  38. dissect/target/plugins/apps/editor/editor.py +23 -0
  39. dissect/target/plugins/apps/{texteditor → editor}/windowsnotepad.py +40 -31
  40. dissect/target/plugins/apps/other/__init__.py +0 -0
  41. dissect/target/plugins/apps/other/env.py +56 -0
  42. dissect/target/plugins/apps/shell/powershell.py +6 -2
  43. dissect/target/plugins/apps/shell/wget.py +91 -0
  44. dissect/target/plugins/apps/ssh/openssh.py +2 -0
  45. dissect/target/plugins/apps/ssh/opensshd.py +2 -0
  46. dissect/target/plugins/apps/virtualization/__init__.py +0 -0
  47. dissect/target/plugins/apps/virtualization/vmware_workstation.py +61 -0
  48. dissect/target/plugins/apps/vpn/wireguard.py +9 -9
  49. dissect/target/plugins/apps/webhosting/cpanel.py +2 -0
  50. dissect/target/plugins/apps/webserver/caddy.py +2 -0
  51. dissect/target/plugins/apps/webserver/nginx.py +2 -0
  52. dissect/target/plugins/child/esxi.py +3 -1
  53. dissect/target/plugins/child/parallels.py +68 -0
  54. dissect/target/plugins/child/proxmox.py +23 -0
  55. dissect/target/plugins/child/virtuozzo.py +12 -8
  56. dissect/target/plugins/child/vmware_workstation.py +23 -8
  57. dissect/target/plugins/filesystem/acquire_hash.py +2 -1
  58. dissect/target/plugins/filesystem/icat.py +15 -11
  59. dissect/target/plugins/filesystem/ntfs/mft.py +10 -6
  60. dissect/target/plugins/filesystem/ntfs/mft_timeline.py +3 -1
  61. dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -0
  62. dissect/target/plugins/filesystem/ntfs/utils.py +3 -1
  63. dissect/target/plugins/filesystem/unix/suid.py +4 -1
  64. dissect/target/plugins/filesystem/walkfs.py +2 -0
  65. dissect/target/plugins/general/example.py +2 -2
  66. dissect/target/plugins/general/loaders.py +18 -5
  67. dissect/target/plugins/general/network.py +20 -5
  68. dissect/target/plugins/general/osinfo.py +1 -0
  69. dissect/target/plugins/general/plugins.py +53 -10
  70. dissect/target/plugins/os/unix/_os.py +70 -44
  71. dissect/target/plugins/os/unix/applications.py +78 -0
  72. dissect/target/plugins/os/unix/bsd/citrix/history.py +2 -0
  73. dissect/target/plugins/os/unix/bsd/osx/_os.py +4 -21
  74. dissect/target/plugins/os/unix/bsd/osx/network.py +92 -0
  75. dissect/target/plugins/os/unix/bsd/osx/user.py +4 -0
  76. dissect/target/plugins/os/unix/cronjobs.py +8 -4
  77. dissect/target/plugins/os/unix/etc/etc.py +4 -0
  78. dissect/target/plugins/os/unix/generic.py +2 -0
  79. dissect/target/plugins/os/unix/history.py +27 -25
  80. dissect/target/plugins/os/unix/linux/_os.py +8 -10
  81. dissect/target/plugins/os/unix/linux/cmdline.py +2 -0
  82. dissect/target/plugins/os/unix/linux/debian/apt.py +4 -1
  83. dissect/target/plugins/os/unix/linux/debian/dpkg.py +3 -3
  84. dissect/target/plugins/os/unix/linux/debian/proxmox/__init__.py +0 -0
  85. dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py +141 -0
  86. dissect/target/plugins/os/unix/linux/debian/proxmox/vm.py +29 -0
  87. dissect/target/plugins/os/unix/linux/debian/snap.py +79 -0
  88. dissect/target/plugins/os/unix/linux/environ.py +2 -0
  89. dissect/target/plugins/os/unix/linux/fortios/_os.py +74 -63
  90. dissect/target/plugins/os/unix/linux/fortios/generic.py +2 -0
  91. dissect/target/plugins/os/unix/linux/fortios/locale.py +2 -0
  92. dissect/target/plugins/os/unix/linux/modules.py +2 -0
  93. dissect/target/plugins/os/unix/linux/netstat.py +2 -0
  94. dissect/target/{helpers → plugins/os/unix/linux}/network_managers.py +11 -9
  95. dissect/target/plugins/os/unix/linux/processes.py +2 -0
  96. dissect/target/plugins/os/unix/linux/redhat/yum.py +4 -1
  97. dissect/target/plugins/os/unix/linux/services.py +5 -3
  98. dissect/target/plugins/os/unix/linux/sockets.py +2 -0
  99. dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -1
  100. dissect/target/plugins/os/unix/locale.py +2 -0
  101. dissect/target/plugins/os/unix/locate/gnulocate.py +4 -2
  102. dissect/target/plugins/os/unix/locate/mlocate.py +2 -0
  103. dissect/target/plugins/os/unix/locate/plocate.py +3 -1
  104. dissect/target/plugins/os/unix/log/atop.py +2 -0
  105. dissect/target/plugins/os/unix/log/audit.py +3 -1
  106. dissect/target/plugins/os/unix/log/auth.py +351 -38
  107. dissect/target/plugins/os/unix/log/journal.py +123 -101
  108. dissect/target/plugins/os/unix/log/lastlog.py +5 -3
  109. dissect/target/plugins/os/unix/log/messages.py +51 -27
  110. dissect/target/plugins/os/unix/log/utmp.py +52 -71
  111. dissect/target/plugins/os/unix/packagemanager.py +5 -38
  112. dissect/target/plugins/os/unix/shadow.py +3 -1
  113. dissect/target/plugins/os/unix/trash.py +132 -0
  114. dissect/target/plugins/os/windows/_os.py +22 -41
  115. dissect/target/plugins/os/windows/activitiescache.py +9 -4
  116. dissect/target/plugins/os/windows/adpolicy.py +2 -1
  117. dissect/target/plugins/os/windows/amcache.py +16 -13
  118. dissect/target/plugins/os/windows/defender.py +4 -3
  119. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +3 -0
  120. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +3 -0
  121. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +3 -0
  122. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +3 -0
  123. dissect/target/plugins/os/windows/env.py +1 -2
  124. dissect/target/plugins/os/windows/exchange/exchange.py +6 -4
  125. dissect/target/plugins/os/windows/generic.py +68 -19
  126. dissect/target/plugins/os/windows/lnk.py +2 -0
  127. dissect/target/plugins/os/windows/locale.py +9 -3
  128. dissect/target/plugins/os/windows/log/etl.py +5 -4
  129. dissect/target/plugins/os/windows/log/evt.py +12 -8
  130. dissect/target/plugins/os/windows/log/evtx.py +9 -7
  131. dissect/target/plugins/os/windows/log/mssql.py +103 -0
  132. dissect/target/plugins/os/windows/log/pfro.py +2 -1
  133. dissect/target/plugins/os/windows/network.py +380 -0
  134. dissect/target/plugins/os/windows/notifications.py +6 -4
  135. dissect/target/plugins/os/windows/prefetch.py +7 -2
  136. dissect/target/plugins/os/windows/regf/7zip.py +9 -1
  137. dissect/target/plugins/os/windows/regf/applications.py +62 -0
  138. dissect/target/plugins/os/windows/regf/auditpol.py +2 -1
  139. dissect/target/plugins/os/windows/regf/bam.py +3 -1
  140. dissect/target/plugins/os/windows/regf/cit.py +14 -12
  141. dissect/target/plugins/os/windows/regf/clsid.py +6 -3
  142. dissect/target/plugins/os/windows/regf/firewall.py +2 -1
  143. dissect/target/plugins/os/windows/regf/mru.py +9 -8
  144. dissect/target/plugins/os/windows/regf/nethist.py +6 -3
  145. dissect/target/plugins/os/windows/regf/recentfilecache.py +3 -1
  146. dissect/target/plugins/os/windows/regf/regf.py +5 -1
  147. dissect/target/plugins/os/windows/regf/shellbags.py +351 -345
  148. dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
  149. dissect/target/plugins/os/windows/regf/usb.py +2 -1
  150. dissect/target/plugins/os/windows/regf/userassist.py +2 -1
  151. dissect/target/plugins/os/windows/registry.py +11 -0
  152. dissect/target/plugins/os/windows/services.py +3 -2
  153. dissect/target/plugins/os/windows/startupinfo.py +7 -2
  154. dissect/target/plugins/os/windows/syscache.py +5 -2
  155. dissect/target/plugins/os/windows/tasks.py +1 -1
  156. dissect/target/plugins/os/windows/thumbcache.py +11 -5
  157. dissect/target/plugins/os/windows/ual.py +12 -9
  158. dissect/target/plugins/os/windows/wer.py +21 -6
  159. dissect/target/plugins/os/windows/wua_history.py +0 -1
  160. dissect/target/target.py +13 -8
  161. dissect/target/tools/dump/utils.py +4 -0
  162. dissect/target/tools/fsutils.py +1 -1
  163. dissect/target/tools/info.py +1 -1
  164. dissect/target/tools/mount.py +15 -5
  165. dissect/target/tools/query.py +15 -9
  166. dissect/target/tools/shell.py +98 -9
  167. dissect/target/tools/utils.py +7 -7
  168. dissect/target/volume.py +4 -4
  169. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/METADATA +6 -2
  170. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/RECORD +176 -160
  171. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/WHEEL +1 -1
  172. dissect/target/helpers/targetd.py +0 -58
  173. dissect/target/loaders/targetd.py +0 -223
  174. dissect/target/plugins/apps/texteditor/texteditor.py +0 -13
  175. dissect/target/plugins/os/unix/etc.py +0 -9
  176. /dissect/target/plugins/apps/{texteditor → database}/__init__.py +0 -0
  177. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/COPYRIGHT +0 -0
  178. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/LICENSE +0 -0
  179. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/entry_points.txt +0 -0
  180. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/top_level.txt +0 -0
@@ -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,24 @@ 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
278
+
279
+
280
+ # Sometimes stringy None is inserted by external tools like Ansible
281
+ def int_or_none(value: str) -> int | None:
282
+ return int(value) if value and value != "None" else None
263
283
 
264
284
 
265
285
  class JournalFile:
@@ -273,136 +293,138 @@ class JournalFile:
273
293
  def __init__(self, fh: BinaryIO, target: Target):
274
294
  self.fh = fh
275
295
  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
296
 
280
- def entry_object_offsets(self) -> Iterator[int]:
281
- """Read object entry arrays."""
297
+ try:
298
+ self.header = c_journal.Header(self.fh)
299
+ except EOFError as e:
300
+ raise ValueError(f"Invalid systemd Journal file: {e}")
282
301
 
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
302
+ if self.header.signature != c_journal.HEADER_SIGNATURE:
303
+ raise ValueError(f"Journal file has invalid magic header: {self.header.signature!r}'")
306
304
 
307
305
  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
-
306
+ """Decode the given bytes to a key value pair."""
307
+ value = value.decode(errors="surrogateescape").strip().lstrip("_")
313
308
  key, value = value.split("=", 1)
314
309
  key = key.lower()
315
-
316
310
  return key, value
317
311
 
318
312
  def __iter__(self) -> Iterator[dict[str, int | str]]:
319
313
  "Iterate over the entry objects to read payloads."
320
314
 
321
- for offset in self.entry_object_offsets():
315
+ offset = self.header.entry_array_offset
316
+ while offset != 0:
322
317
  self.fh.seek(offset)
323
318
 
319
+ if self.fh.read(1)[0] != c_journal.ObjectType.OBJECT_ENTRY_ARRAY:
320
+ raise ValueError(f"Expected OBJECT_ENTRY_ARRAY at offset {offset}")
321
+
322
+ if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
323
+ entry_array_object = c_journal.EntryArrayObject_Compact(self.fh)
324
+ else:
325
+ entry_array_object = c_journal.EntryArrayObject(self.fh)
326
+
327
+ for entry_object_offset in entry_array_object.entry_object_offsets:
328
+ if entry_object_offset:
329
+ yield from self._parse_entry_object(offset=entry_object_offset)
330
+
331
+ offset = entry_array_object.next_entry_array_offset
332
+
333
+ def _parse_entry_object(self, offset: int) -> Iterator[dict]:
334
+ self.fh.seek(offset)
335
+
336
+ try:
324
337
  if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
325
338
  entry = c_journal.EntryObject_Compact(self.fh)
326
339
  else:
327
340
  entry = c_journal.EntryObject(self.fh)
328
341
 
329
- event = {}
330
- event["ts"] = ts.from_unix_us(entry.realtime)
342
+ except EOFError as e:
343
+ self.target.log.warning("Unable to read Journal EntryObject at offset %s in: %s", offset, self.fh)
344
+ self.target.log.debug("", exc_info=e)
345
+ return
346
+
347
+ event = {"ts": ts.from_unix_us(entry.realtime)}
348
+ for item in entry.items:
349
+ try:
350
+ self.fh.seek(item.object_offset)
331
351
 
332
- for item in entry.items:
333
- try:
334
- self.fh.seek(item.object_offset)
352
+ if self.fh.read(1)[0] != c_journal.ObjectType.OBJECT_DATA:
353
+ continue
335
354
 
336
- object = c_journal.ObjectHeader(self.fh)
355
+ if self.header.incompatible_flags & c_journal.IncompatibleFlag.HEADER_INCOMPATIBLE_COMPACT:
356
+ data_object = c_journal.DataObject_Compact(self.fh)
357
+ else:
358
+ data_object = c_journal.DataObject(self.fh)
337
359
 
338
- if object.type == c_journal.ObjectType.OBJECT_DATA:
339
- self.fh.seek(item.object_offset)
360
+ if not data_object.payload:
361
+ continue
340
362
 
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)
363
+ data = data_object.payload
345
364
 
346
- data = data_object.payload
365
+ if data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_XZ:
366
+ data = lzma.decompress(data)
347
367
 
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)
368
+ elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_LZ4:
369
+ data = lz4.decompress(data[8:])
357
370
 
358
- key, value = self.decode_value(data)
359
- event[key] = value
371
+ elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_ZSTD:
372
+ data = zstandard.decompress(data)
360
373
 
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
374
+ key, value = self.decode_value(data)
375
+ event[key] = value
368
376
 
369
- yield event
377
+ except Exception as e:
378
+ self.target.log.warning(
379
+ "Journal DataObject could not be parsed at offset %s in %s",
380
+ item.object_offset,
381
+ getattr(self.fh, "name", None),
382
+ )
383
+ self.target.log.debug("", exc_info=e)
384
+ continue
385
+
386
+ yield event
370
387
 
371
388
 
372
389
  class JournalPlugin(Plugin):
390
+ """Systemd Journal plugin."""
391
+
373
392
  JOURNAL_PATHS = ["/var/log/journal"] # TODO: /run/systemd/journal
374
393
  JOURNAL_GLOB = "*/*.journal*" # The extensions .journal and .journal~
375
- JOURNAL_SIGNATURE = "LPKSHHRH"
376
394
 
377
395
  def __init__(self, target: Target):
378
396
  super().__init__(target)
379
- self.journal_paths = []
397
+ self.journal_files = []
380
398
 
381
- for _path in self.JOURNAL_PATHS:
382
- self.journal_paths.extend(self.target.fs.path(_path).glob(self.JOURNAL_GLOB))
399
+ for journal_path in self.JOURNAL_PATHS:
400
+ self.journal_files.extend(self.target.fs.path(journal_path).glob(self.JOURNAL_GLOB))
383
401
 
384
402
  def check_compatible(self) -> None:
385
- if not len(self.journal_paths):
403
+ if not self.journal_files:
386
404
  raise UnsupportedPluginError("No journald files found")
387
405
 
388
406
  @export(record=JournalRecord)
389
407
  def journal(self) -> Iterator[JournalRecord]:
390
- """Return the content of Systemd Journal log files.
408
+ """Return the contents of Systemd Journal log files.
391
409
 
392
410
  References:
393
411
  - https://wiki.archlinux.org/title/Systemd/Journal
394
412
  - https://github.com/systemd/systemd/blob/9203abf79f1d05fdef9b039e7addf9fc5a27752d/man/systemd.journal-fields.xml
395
413
  """ # noqa: E501
396
-
397
414
  path_function = self.target.fs.path
398
415
 
399
- for _path in self.journal_paths:
400
- fh = _path.open()
416
+ for journal_file in self.journal_files:
417
+ if not journal_file.is_file():
418
+ self.target.log.warning("Unable to parse journal file as it is not a file: %s", journal_file)
419
+ continue
401
420
 
402
- journal = JournalFile(fh, self.target)
421
+ try:
422
+ fh = journal_file.open()
423
+ journal = JournalFile(fh, self.target)
403
424
 
404
- if not journal.signature == self.JOURNAL_SIGNATURE:
405
- self.target.log.warning("The Journal log file %s has an invalid magic header", _path)
425
+ except Exception as e:
426
+ self.target.log.warning("Unable to parse journal file structure: %s: %s", journal_file, str(e))
427
+ self.target.log.debug("", exc_info=e)
406
428
  continue
407
429
 
408
430
  for entry in journal:
@@ -410,30 +432,30 @@ class JournalPlugin(Plugin):
410
432
  ts=entry.get("ts"),
411
433
  message=entry.get("message"),
412
434
  message_id=entry.get("message_id"),
413
- priority=get_optional(entry.get("priority"), int),
435
+ priority=int_or_none(entry.get("priority")),
414
436
  code_file=get_optional(entry.get("code_file"), path_function),
415
- code_line=get_optional(entry.get("code_line"), int),
437
+ code_line=int_or_none(entry.get("code_line")),
416
438
  code_func=entry.get("code_func"),
417
- errno=get_optional(entry.get("errno"), int),
439
+ errno=int_or_none(entry.get("errno")),
418
440
  invocation_id=entry.get("invocation_id"),
419
441
  user_invocation_id=entry.get("user_invocation_id"),
420
- syslog_facility=get_optional(entry.get("syslog_facility"), int),
442
+ syslog_facility=entry.get("syslog_facility"),
421
443
  syslog_identifier=entry.get("syslog_identifier"),
422
- syslog_pid=get_optional(entry.get("syslog_pid"), int),
444
+ syslog_pid=int_or_none(entry.get("syslog_pid")),
423
445
  syslog_raw=entry.get("syslog_raw"),
424
446
  documentation=entry.get("documentation"),
425
- tid=get_optional(entry.get("tid"), int),
447
+ tid=int_or_none(entry.get("tid")),
426
448
  unit=entry.get("unit"),
427
449
  user_unit=entry.get("user_unit"),
428
- pid=get_optional(entry.get("pid"), int),
429
- uid=get_optional(entry.get("uid"), int),
430
- gid=get_optional(entry.get("gid"), int),
450
+ pid=int_or_none(entry.get("pid")),
451
+ uid=int_or_none(entry.get("uid")),
452
+ gid=int_or_none(entry.get("gid")),
431
453
  comm=entry.get("comm"),
432
454
  exe=get_optional(entry.get("exe"), path_function),
433
455
  cmdline=entry.get("cmdline"),
434
456
  cap_effective=entry.get("cap_effective"),
435
- audit_session=get_optional(entry.get("audit_session"), int),
436
- audit_loginuid=get_optional(entry.get("audit_loginuid"), int),
457
+ audit_session=int_or_none(entry.get("audit_session")),
458
+ audit_loginuid=int_or_none(entry.get("audit_loginuid")),
437
459
  systemd_cgroup=get_optional(entry.get("systemd_cgroup"), path_function),
438
460
  systemd_slice=entry.get("systemd_slice"),
439
461
  systemd_unit=entry.get("systemd_unit"),
@@ -456,6 +478,6 @@ class JournalPlugin(Plugin):
456
478
  udev_devnode=get_optional(entry.get("udev_devnode"), path_function),
457
479
  udev_devlink=get_optional(entry.get("udev_devlink"), path_function),
458
480
  journal_hostname=entry.get("hostname"),
459
- filepath=_path,
481
+ source=journal_file,
460
482
  _target=self.target,
461
483
  )
@@ -1,4 +1,4 @@
1
- from typing import BinaryIO
1
+ from typing import BinaryIO, Iterator
2
2
 
3
3
  from dissect.cstruct import cstruct
4
4
  from dissect.util import ts
@@ -52,13 +52,15 @@ class LastLogFile:
52
52
 
53
53
 
54
54
  class LastLogPlugin(Plugin):
55
+ """Unix lastlog plugin."""
56
+
55
57
  def check_compatible(self) -> None:
56
58
  lastlog = self.target.fs.path("/var/log/lastlog")
57
59
  if not lastlog.exists():
58
60
  raise UnsupportedPluginError("No lastlog file found")
59
61
 
60
- @export(record=[LastLogRecord])
61
- def lastlog(self):
62
+ @export(record=LastLogRecord)
63
+ def lastlog(self) -> Iterator[LastLogRecord]:
62
64
  """Return last logins information from /var/log/lastlog.
63
65
 
64
66
  The lastlog file contains the most recent logins of all users on a Unix based operating system.
@@ -1,12 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
4
+ from datetime import datetime, timezone, tzinfo
2
5
  from pathlib import Path
3
6
  from typing import Iterator
4
7
 
5
8
  from dissect.target import Target
6
9
  from dissect.target.exceptions import UnsupportedPluginError
10
+ from dissect.target.helpers.fsutil import open_decompress
7
11
  from dissect.target.helpers.record import TargetRecordDescriptor
8
12
  from dissect.target.helpers.utils import year_rollover_helper
9
- from dissect.target.plugin import Plugin, export
13
+ from dissect.target.plugin import Plugin, alias, export
10
14
 
11
15
  MessagesRecord = TargetRecordDescriptor(
12
16
  "linux/log/messages",
@@ -24,10 +28,14 @@ RE_TS = re.compile(r"(\w+\s{1,2}\d+\s\d{2}:\d{2}:\d{2})")
24
28
  RE_DAEMON = re.compile(r"^[^:]+:\d+:\d+[^\[\]:]+\s([^\[:]+)[\[|:]{1}")
25
29
  RE_PID = re.compile(r"\w\[(\d+)\]")
26
30
  RE_MSG = re.compile(r"[^:]+:\d+:\d+[^:]+:\s(.*)$")
27
- RE_CLOUD_INIT_LINE = re.compile(r"(?P<ts>.*) - (?P<daemon>.*)\[(?P<log_level>\w+)\]\: (?P<message>.*)$")
31
+ RE_CLOUD_INIT_LINE = re.compile(
32
+ r"^(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (?P<daemon>.*)\[(?P<log_level>\w+)\]\: (?P<message>.*)$"
33
+ )
28
34
 
29
35
 
30
36
  class MessagesPlugin(Plugin):
37
+ """Unix messages log plugin."""
38
+
31
39
  def __init__(self, target: Target):
32
40
  super().__init__(target)
33
41
  self.log_files = set(self._find_log_files())
@@ -43,19 +51,12 @@ class MessagesPlugin(Plugin):
43
51
  if not self.log_files:
44
52
  raise UnsupportedPluginError("No log files found")
45
53
 
46
- @export(record=MessagesRecord)
47
- def syslog(self) -> Iterator[MessagesRecord]:
48
- """Return contents of /var/log/messages*, /var/log/syslog* and cloud-init logs.
49
-
50
- See ``messages`` for more information.
51
- """
52
- return self.messages()
53
-
54
+ @alias("syslog")
54
55
  @export(record=MessagesRecord)
55
56
  def messages(self) -> Iterator[MessagesRecord]:
56
57
  """Return contents of /var/log/messages*, /var/log/syslog* and cloud-init logs.
57
58
 
58
- Note: due to year rollover detection, the contents of the files are returned in reverse.
59
+ Due to year rollover detection, the contents of the files are returned in reverse.
59
60
 
60
61
  The messages log file holds information about a variety of events such as the system error messages, system
61
62
  startups and shutdowns, change in the network configuration, etc. Aims to store valuable, non-debug and
@@ -71,7 +72,7 @@ class MessagesPlugin(Plugin):
71
72
 
72
73
  for log_file in self.log_files:
73
74
  if "cloud-init" in log_file.name:
74
- yield from self._parse_cloud_init_log(log_file)
75
+ yield from self._parse_cloud_init_log(log_file, tzinfo)
75
76
  continue
76
77
 
77
78
  for ts, line in year_rollover_helper(log_file, RE_TS, DEFAULT_TS_LOG_FORMAT, tzinfo):
@@ -88,7 +89,7 @@ class MessagesPlugin(Plugin):
88
89
  _target=self.target,
89
90
  )
90
91
 
91
- def _parse_cloud_init_log(self, log_file: Path) -> Iterator[MessagesRecord]:
92
+ def _parse_cloud_init_log(self, log_file: Path, tzinfo: tzinfo | None = timezone.utc) -> Iterator[MessagesRecord]:
92
93
  """Parse a cloud-init.log file.
93
94
 
94
95
  Lines are structured in the following format:
@@ -101,18 +102,41 @@ class MessagesPlugin(Plugin):
101
102
 
102
103
  Returns: ``MessagesRecord``
103
104
  """
104
- for line in log_file.open("rt").readlines():
105
- if line := line.strip():
106
- if match := RE_CLOUD_INIT_LINE.match(line):
107
- match = match.groupdict()
108
- yield MessagesRecord(
109
- ts=match["ts"].split(",")[0],
110
- daemon=match["daemon"],
111
- pid=None,
112
- message=match["message"],
113
- source=log_file,
114
- _target=self.target,
115
- )
116
- else:
117
- self.target.log.warning("Could not match cloud-init log line")
105
+
106
+ ts_fmt = "%Y-%m-%d %H:%M:%S,%f"
107
+
108
+ with open_decompress(log_file, "rt") as fh:
109
+ for line in fh:
110
+ if not (line := line.strip()):
111
+ continue
112
+
113
+ if not (match := RE_CLOUD_INIT_LINE.match(line)):
114
+ self.target.log.warning("Could not match cloud-init log line in file: %s", log_file)
118
115
  self.target.log.debug("No match for line '%s'", line)
116
+ continue
117
+
118
+ values = match.groupdict()
119
+
120
+ # Actual format is ``YYYY-MM-DD HH:MM:SS,000`` (asctime with milliseconds) but python has no strptime
121
+ # operator for 3 digit milliseconds, so we convert and pad to six digit microseconds.
122
+ # https://github.com/canonical/cloud-init/blob/main/cloudinit/log/loggers.py#DEFAULT_LOG_FORMAT
123
+ # https://docs.python.org/3/library/logging.html#asctime
124
+ raw_ts, _, milliseconds = values["ts"].rpartition(",")
125
+ raw_ts += "," + str((int(milliseconds) * 1000)).zfill(6)
126
+
127
+ try:
128
+ ts = datetime.strptime(raw_ts, ts_fmt).replace(tzinfo=tzinfo)
129
+
130
+ except ValueError as e:
131
+ self.target.log.warning("Timestamp '%s' does not match format '%s'", raw_ts, ts_fmt)
132
+ self.target.log.debug("", exc_info=e)
133
+ ts = datetime(1970, 1, 1, 0, 0, 0, 0)
134
+
135
+ yield MessagesRecord(
136
+ ts=ts,
137
+ daemon=values["daemon"],
138
+ pid=None,
139
+ message=values["message"],
140
+ source=log_file,
141
+ _target=self.target,
142
+ )