dissect.target 3.20.dev31__py3-none-any.whl → 3.20.dev32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dissect/target/plugins/os/unix/log/journal.py +108 -91
- {dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.dist-info}/METADATA +1 -1
- {dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.dist-info}/RECORD +8 -8
- {dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.dist-info}/WHEEL +1 -1
- {dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.dist-info}/LICENSE +0 -0
- {dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.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,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
|
-
|
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
|
-
|
281
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
330
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
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
|
-
|
347
|
+
if self.fh.read(1)[0] != c_journal.ObjectType.OBJECT_DATA:
|
348
|
+
continue
|
337
349
|
|
338
|
-
|
339
|
-
|
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
|
-
|
342
|
-
|
343
|
-
else:
|
344
|
-
data_object = c_journal.DataObject(self.fh)
|
355
|
+
if not data_object.payload:
|
356
|
+
continue
|
345
357
|
|
346
|
-
|
358
|
+
data = data_object.payload
|
347
359
|
|
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)
|
360
|
+
if data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_XZ:
|
361
|
+
data = lzma.decompress(data)
|
357
362
|
|
358
|
-
|
359
|
-
|
363
|
+
elif data_object.flags & c_journal.ObjectFlag.OBJECT_COMPRESSED_LZ4:
|
364
|
+
data = lz4.decompress(data[8:])
|
360
365
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
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
|
-
|
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.
|
392
|
+
self.journal_files = []
|
380
393
|
|
381
|
-
for
|
382
|
-
self.
|
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
|
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
|
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
|
400
|
-
|
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
|
-
|
416
|
+
try:
|
417
|
+
fh = journal_file.open()
|
418
|
+
journal = JournalFile(fh, self.target)
|
403
419
|
|
404
|
-
|
405
|
-
self.target.log.warning("
|
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=
|
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
|
-
|
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.
|
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=
|
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.
|
374
|
-
dissect.target-3.20.
|
375
|
-
dissect.target-3.20.
|
376
|
-
dissect.target-3.20.
|
377
|
-
dissect.target-3.20.
|
378
|
-
dissect.target-3.20.
|
379
|
-
dissect.target-3.20.
|
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,,
|
File without changes
|
File without changes
|
{dissect.target-3.20.dev31.dist-info → dissect.target-3.20.dev32.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|