dissect.target 3.19.dev57__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.
- dissect/target/container.py +1 -1
- dissect/target/exceptions.py +6 -5
- dissect/target/filesystem.py +2 -2
- dissect/target/filesystems/btrfs.py +14 -5
- dissect/target/filesystems/config.py +5 -1
- dissect/target/filesystems/extfs.py +5 -4
- dissect/target/filesystems/fat.py +22 -16
- dissect/target/filesystems/ffs.py +11 -4
- dissect/target/filesystems/jffs.py +12 -7
- dissect/target/filesystems/ntfs.py +22 -6
- dissect/target/filesystems/overlay.py +14 -4
- dissect/target/filesystems/smb.py +3 -3
- dissect/target/filesystems/squashfs.py +4 -4
- dissect/target/filesystems/vmfs.py +4 -4
- dissect/target/filesystems/xfs.py +15 -8
- dissect/target/helpers/compat/path_common.py +5 -5
- dissect/target/helpers/configutil.py +128 -32
- dissect/target/helpers/cyber.py +2 -0
- dissect/target/helpers/data/windowsZones.xml +19 -23
- dissect/target/helpers/docs.py +1 -1
- dissect/target/helpers/keychain.py +2 -0
- dissect/target/helpers/mount.py +2 -1
- dissect/target/helpers/record.py +29 -2
- dissect/target/helpers/record_modifier.py +5 -1
- dissect/target/helpers/regutil.py +56 -26
- dissect/target/loader.py +1 -1
- dissect/target/loaders/mqtt.py +104 -9
- dissect/target/loaders/proxmox.py +68 -0
- dissect/target/loaders/vma.py +1 -1
- dissect/target/loaders/xva.py +1 -1
- dissect/target/plugin.py +24 -21
- dissect/target/plugins/apps/av/mcafee.py +2 -0
- dissect/target/plugins/apps/av/sophos.py +2 -0
- dissect/target/plugins/apps/av/trendmicro.py +2 -0
- dissect/target/plugins/apps/browser/chromium.py +27 -6
- dissect/target/plugins/apps/container/docker.py +48 -32
- dissect/target/plugins/apps/editor/__init__.py +0 -0
- dissect/target/plugins/apps/editor/editor.py +23 -0
- dissect/target/plugins/apps/{texteditor → editor}/windowsnotepad.py +40 -31
- dissect/target/plugins/apps/other/__init__.py +0 -0
- dissect/target/plugins/apps/other/env.py +56 -0
- dissect/target/plugins/apps/shell/powershell.py +6 -2
- dissect/target/plugins/apps/shell/wget.py +91 -0
- dissect/target/plugins/apps/ssh/openssh.py +2 -0
- dissect/target/plugins/apps/ssh/opensshd.py +2 -0
- dissect/target/plugins/apps/virtualization/__init__.py +0 -0
- dissect/target/plugins/apps/virtualization/vmware_workstation.py +61 -0
- dissect/target/plugins/apps/vpn/wireguard.py +9 -9
- dissect/target/plugins/apps/webhosting/cpanel.py +2 -0
- dissect/target/plugins/apps/webserver/caddy.py +2 -0
- dissect/target/plugins/apps/webserver/nginx.py +2 -0
- dissect/target/plugins/child/esxi.py +3 -1
- dissect/target/plugins/child/parallels.py +68 -0
- dissect/target/plugins/child/proxmox.py +23 -0
- dissect/target/plugins/child/virtuozzo.py +12 -8
- dissect/target/plugins/child/vmware_workstation.py +23 -8
- dissect/target/plugins/filesystem/acquire_hash.py +2 -1
- dissect/target/plugins/filesystem/icat.py +15 -11
- dissect/target/plugins/filesystem/ntfs/mft.py +10 -6
- dissect/target/plugins/filesystem/ntfs/mft_timeline.py +3 -1
- dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -0
- dissect/target/plugins/filesystem/ntfs/utils.py +3 -1
- dissect/target/plugins/filesystem/unix/suid.py +4 -1
- dissect/target/plugins/filesystem/walkfs.py +2 -0
- dissect/target/plugins/general/example.py +2 -2
- dissect/target/plugins/general/loaders.py +18 -5
- dissect/target/plugins/general/network.py +20 -5
- dissect/target/plugins/general/osinfo.py +1 -0
- dissect/target/plugins/general/plugins.py +53 -10
- dissect/target/plugins/os/unix/_os.py +70 -44
- dissect/target/plugins/os/unix/applications.py +78 -0
- dissect/target/plugins/os/unix/bsd/citrix/history.py +2 -0
- dissect/target/plugins/os/unix/bsd/osx/_os.py +4 -21
- dissect/target/plugins/os/unix/bsd/osx/network.py +92 -0
- dissect/target/plugins/os/unix/bsd/osx/user.py +4 -0
- dissect/target/plugins/os/unix/cronjobs.py +8 -4
- dissect/target/plugins/os/unix/etc/etc.py +4 -0
- dissect/target/plugins/os/unix/generic.py +2 -0
- dissect/target/plugins/os/unix/history.py +27 -25
- dissect/target/plugins/os/unix/linux/_os.py +8 -10
- dissect/target/plugins/os/unix/linux/cmdline.py +2 -0
- dissect/target/plugins/os/unix/linux/debian/apt.py +4 -1
- dissect/target/plugins/os/unix/linux/debian/dpkg.py +3 -3
- dissect/target/plugins/os/unix/linux/debian/proxmox/__init__.py +0 -0
- dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py +141 -0
- dissect/target/plugins/os/unix/linux/debian/proxmox/vm.py +29 -0
- dissect/target/plugins/os/unix/linux/debian/snap.py +79 -0
- dissect/target/plugins/os/unix/linux/environ.py +2 -0
- dissect/target/plugins/os/unix/linux/fortios/_os.py +74 -63
- dissect/target/plugins/os/unix/linux/fortios/generic.py +2 -0
- dissect/target/plugins/os/unix/linux/fortios/locale.py +2 -0
- dissect/target/plugins/os/unix/linux/modules.py +2 -0
- dissect/target/plugins/os/unix/linux/netstat.py +2 -0
- dissect/target/{helpers → plugins/os/unix/linux}/network_managers.py +11 -9
- dissect/target/plugins/os/unix/linux/processes.py +2 -0
- dissect/target/plugins/os/unix/linux/redhat/yum.py +4 -1
- dissect/target/plugins/os/unix/linux/services.py +5 -3
- dissect/target/plugins/os/unix/linux/sockets.py +2 -0
- dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -1
- dissect/target/plugins/os/unix/locale.py +2 -0
- dissect/target/plugins/os/unix/locate/gnulocate.py +4 -2
- dissect/target/plugins/os/unix/locate/mlocate.py +2 -0
- dissect/target/plugins/os/unix/locate/plocate.py +3 -1
- dissect/target/plugins/os/unix/log/atop.py +2 -0
- dissect/target/plugins/os/unix/log/audit.py +3 -1
- dissect/target/plugins/os/unix/log/auth.py +351 -38
- dissect/target/plugins/os/unix/log/journal.py +123 -101
- dissect/target/plugins/os/unix/log/lastlog.py +5 -3
- dissect/target/plugins/os/unix/log/messages.py +51 -27
- dissect/target/plugins/os/unix/log/utmp.py +52 -71
- dissect/target/plugins/os/unix/packagemanager.py +5 -38
- dissect/target/plugins/os/unix/shadow.py +3 -1
- dissect/target/plugins/os/unix/trash.py +132 -0
- dissect/target/plugins/os/windows/_os.py +22 -41
- dissect/target/plugins/os/windows/activitiescache.py +9 -4
- dissect/target/plugins/os/windows/adpolicy.py +2 -1
- dissect/target/plugins/os/windows/amcache.py +16 -13
- dissect/target/plugins/os/windows/defender.py +4 -3
- dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +3 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +3 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +3 -0
- dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +3 -0
- dissect/target/plugins/os/windows/env.py +1 -2
- dissect/target/plugins/os/windows/exchange/exchange.py +6 -4
- dissect/target/plugins/os/windows/generic.py +68 -19
- dissect/target/plugins/os/windows/lnk.py +2 -0
- dissect/target/plugins/os/windows/locale.py +9 -3
- dissect/target/plugins/os/windows/log/etl.py +5 -4
- dissect/target/plugins/os/windows/log/evt.py +12 -8
- dissect/target/plugins/os/windows/log/evtx.py +9 -7
- dissect/target/plugins/os/windows/log/mssql.py +103 -0
- dissect/target/plugins/os/windows/log/pfro.py +2 -1
- dissect/target/plugins/os/windows/network.py +380 -0
- dissect/target/plugins/os/windows/notifications.py +6 -4
- dissect/target/plugins/os/windows/prefetch.py +7 -2
- dissect/target/plugins/os/windows/regf/7zip.py +9 -1
- dissect/target/plugins/os/windows/regf/applications.py +62 -0
- dissect/target/plugins/os/windows/regf/auditpol.py +2 -1
- dissect/target/plugins/os/windows/regf/bam.py +3 -1
- dissect/target/plugins/os/windows/regf/cit.py +14 -12
- dissect/target/plugins/os/windows/regf/clsid.py +6 -3
- dissect/target/plugins/os/windows/regf/firewall.py +2 -1
- dissect/target/plugins/os/windows/regf/mru.py +9 -8
- dissect/target/plugins/os/windows/regf/nethist.py +6 -3
- dissect/target/plugins/os/windows/regf/recentfilecache.py +3 -1
- dissect/target/plugins/os/windows/regf/regf.py +5 -1
- dissect/target/plugins/os/windows/regf/shellbags.py +351 -345
- dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
- dissect/target/plugins/os/windows/regf/usb.py +2 -1
- dissect/target/plugins/os/windows/regf/userassist.py +2 -1
- dissect/target/plugins/os/windows/registry.py +11 -0
- dissect/target/plugins/os/windows/services.py +3 -2
- dissect/target/plugins/os/windows/startupinfo.py +7 -2
- dissect/target/plugins/os/windows/syscache.py +5 -2
- dissect/target/plugins/os/windows/tasks.py +1 -1
- dissect/target/plugins/os/windows/thumbcache.py +11 -5
- dissect/target/plugins/os/windows/ual.py +12 -9
- dissect/target/plugins/os/windows/wer.py +21 -6
- dissect/target/plugins/os/windows/wua_history.py +0 -1
- dissect/target/target.py +13 -8
- dissect/target/tools/dump/utils.py +4 -0
- dissect/target/tools/fsutils.py +1 -1
- dissect/target/tools/info.py +1 -1
- dissect/target/tools/mount.py +15 -5
- dissect/target/tools/query.py +15 -9
- dissect/target/tools/shell.py +98 -9
- dissect/target/tools/utils.py +7 -7
- dissect/target/volume.py +4 -4
- {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/METADATA +6 -2
- {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/RECORD +176 -160
- {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/WHEEL +1 -1
- dissect/target/helpers/targetd.py +0 -58
- dissect/target/loaders/targetd.py +0 -223
- dissect/target/plugins/apps/texteditor/texteditor.py +0 -13
- dissect/target/plugins/os/unix/etc.py +0 -9
- /dissect/target/plugins/apps/{texteditor → database}/__init__.py +0 -0
- {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/LICENSE +0 -0
- {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.19.dev57.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
|
-
("
|
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", "
|
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
|
-
|
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
|
-
|
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
|
-
|
281
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
330
|
-
|
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
|
-
|
333
|
-
|
334
|
-
self.fh.seek(item.object_offset)
|
352
|
+
if self.fh.read(1)[0] != c_journal.ObjectType.OBJECT_DATA:
|
353
|
+
continue
|
335
354
|
|
336
|
-
|
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
|
-
|
339
|
-
|
360
|
+
if not data_object.payload:
|
361
|
+
continue
|
340
362
|
|
341
|
-
|
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
|
-
|
365
|
+
if data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_XZ:
|
366
|
+
data = lzma.decompress(data)
|
347
367
|
|
348
|
-
|
349
|
-
|
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
|
-
|
359
|
-
|
371
|
+
elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_ZSTD:
|
372
|
+
data = zstandard.decompress(data)
|
360
373
|
|
361
|
-
|
362
|
-
|
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
|
-
|
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.
|
397
|
+
self.journal_files = []
|
380
398
|
|
381
|
-
for
|
382
|
-
self.
|
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
|
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
|
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
|
400
|
-
|
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
|
-
|
421
|
+
try:
|
422
|
+
fh = journal_file.open()
|
423
|
+
journal = JournalFile(fh, self.target)
|
403
424
|
|
404
|
-
|
405
|
-
self.target.log.warning("
|
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=
|
435
|
+
priority=int_or_none(entry.get("priority")),
|
414
436
|
code_file=get_optional(entry.get("code_file"), path_function),
|
415
|
-
code_line=
|
437
|
+
code_line=int_or_none(entry.get("code_line")),
|
416
438
|
code_func=entry.get("code_func"),
|
417
|
-
errno=
|
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=
|
442
|
+
syslog_facility=entry.get("syslog_facility"),
|
421
443
|
syslog_identifier=entry.get("syslog_identifier"),
|
422
|
-
syslog_pid=
|
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=
|
447
|
+
tid=int_or_none(entry.get("tid")),
|
426
448
|
unit=entry.get("unit"),
|
427
449
|
user_unit=entry.get("user_unit"),
|
428
|
-
pid=
|
429
|
-
uid=
|
430
|
-
gid=
|
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=
|
436
|
-
audit_loginuid=
|
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
|
-
|
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=
|
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(
|
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
|
-
@
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
+
)
|