dissect.target 3.20.dev30__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.
@@ -0,0 +1,91 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
5
+ from dissect.target.helpers.fsutil import TargetPath
6
+ from dissect.target.helpers.record import create_extended_descriptor
7
+ from dissect.target.plugin import Plugin, export
8
+ from dissect.target.plugins.general.users import UserDetails
9
+ from dissect.target.target import Target
10
+
11
+ WgetHstsRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
12
+ "apps/shell/wget/hsts",
13
+ [
14
+ ("datetime", "ts_created"),
15
+ ("uri", "host"),
16
+ ("boolean", "explicit_port"),
17
+ ("boolean", "include_subdomains"),
18
+ ("datetime", "max_age"),
19
+ ("path", "source"),
20
+ ],
21
+ )
22
+
23
+
24
+ class WgetPlugin(Plugin):
25
+ """Wget shell plugin."""
26
+
27
+ __namespace__ = "wget"
28
+
29
+ def __init__(self, target: Target):
30
+ super().__init__(target)
31
+ self.artifacts = list(self._find_artifacts())
32
+
33
+ def _find_artifacts(self) -> Iterator[tuple[UserDetails, TargetPath]]:
34
+ for user_details in self.target.user_details.all_with_home():
35
+ if (hsts_file := user_details.home_path.joinpath(".wget-hsts")).exists():
36
+ yield hsts_file, user_details
37
+
38
+ def check_compatible(self) -> None:
39
+ if not self.artifacts:
40
+ raise UnsupportedPluginError("No .wget-hsts files found on target")
41
+
42
+ @export(record=WgetHstsRecord)
43
+ def hsts(self) -> Iterator[WgetHstsRecord]:
44
+ """Yield domain entries found in wget HSTS files.
45
+
46
+ When using the ``wget`` command-line utility, a file named ``.wget-hsts`` is created in the user's home
47
+ directory by default. The ``.wget-hsts`` file records HTTP Strict Transport Security (HSTS) information for the
48
+ websites visited by the user via ``wget``.
49
+
50
+ Resources:
51
+ - https://www.gnu.org/software/wget
52
+ - https://gitlab.com/gnuwget/wget/-/blob/master/src/hsts.c
53
+ - https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
54
+
55
+ Yields ``WgetHstsRecord``s with the following fields:
56
+
57
+ .. code-block:: text
58
+
59
+ ts_created (datetime): When the host was first added to the HSTS file
60
+ host (uri): The host that was accessed over TLS by wget
61
+ explicit_port (boolean): If the TCP port for TLS should be checked
62
+ include_subdomains (boolean): If subdomains are included in the HSTS check
63
+ max_age (datetime): Time to live of the entry in the HSTS file
64
+ source (path): Location of the .wget-hsts file
65
+ """
66
+ for hsts_file, user_details in self.artifacts:
67
+ if not hsts_file.is_file():
68
+ continue
69
+
70
+ for line in hsts_file.open("rt").readlines():
71
+ if not (line := line.strip()) or line.startswith("#"):
72
+ continue
73
+
74
+ try:
75
+ host, port, subdomain_count, created, max_age = line.split("\t")
76
+
77
+ except ValueError as e:
78
+ self.target.log.warning("Unexpected wget hsts line in file: %s", hsts_file)
79
+ self.target.log.debug("", exc_info=e)
80
+ continue
81
+
82
+ yield WgetHstsRecord(
83
+ ts_created=int(created),
84
+ host=host,
85
+ explicit_port=int(port),
86
+ include_subdomains=int(subdomain_count),
87
+ max_age=int(created) + int(max_age),
88
+ source=hsts_file,
89
+ _user=user_details.user,
90
+ _target=self.target,
91
+ )
@@ -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.dev30
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
@@ -134,6 +134,7 @@ dissect/target/plugins/apps/remoteaccess/remoteaccess.py,sha256=DWXkRDVUpFr1icK2
134
134
  dissect/target/plugins/apps/remoteaccess/teamviewer.py,sha256=tOg07gEqEmjfvoZmk1qxhKKXQyPS0jklh-IBCy5m8Mo,4987
135
135
  dissect/target/plugins/apps/shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
136
  dissect/target/plugins/apps/shell/powershell.py,sha256=biPSMRWxPI6kRqP0-75yMtrw0Ti2Bzfl_xI3xbmmF48,2641
137
+ dissect/target/plugins/apps/shell/wget.py,sha256=LyEy4RNl1eAWdsF2TW3xdLyHLjEu9NuWQy_HW6rmLzk,3717
137
138
  dissect/target/plugins/apps/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
139
  dissect/target/plugins/apps/ssh/openssh.py,sha256=oaJeKmTvVMo4aePo4Ep7t0ludJPNuuokGEW07w4gAvQ,7216
139
140
  dissect/target/plugins/apps/ssh/opensshd.py,sha256=DaXKdgGF3GYHHA4buEvphcm6FF4C8YFjgD96Dv6rRnM,5510
@@ -261,7 +262,7 @@ dissect/target/plugins/os/unix/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQe
261
262
  dissect/target/plugins/os/unix/log/atop.py,sha256=ljvGipVG16qTECnV1kIORykcGH9tTlpDmcMo5CXSPns,16332
262
263
  dissect/target/plugins/os/unix/log/audit.py,sha256=OjorWTmCFvCI5RJq6m6WNW0Lhb-poB2VAggKOGZUHK4,3722
263
264
  dissect/target/plugins/os/unix/log/auth.py,sha256=l7gCuRdvv9gL0U1N0yrR9hVsMnr4t_k4t-n-f6PrOxg,2388
264
- 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
265
266
  dissect/target/plugins/os/unix/log/lastlog.py,sha256=Wq89wRSFZSBsoKVCxjDofnC4yw9XJ4iOF0XJe9EucCo,2448
266
267
  dissect/target/plugins/os/unix/log/messages.py,sha256=O10Uw3PGTanfGpphUWYqOwOIR7XiiM-clfboVCoiP0U,4501
267
268
  dissect/target/plugins/os/unix/log/utmp.py,sha256=1nPHIaBUHt_9z6PDrvyqg4huKLihUaWLrMmgMsbaeIo,7755
@@ -369,10 +370,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
369
370
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
370
371
  dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
371
372
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
372
- dissect.target-3.20.dev30.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
373
- dissect.target-3.20.dev30.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
374
- dissect.target-3.20.dev30.dist-info/METADATA,sha256=MmnIwD9iA43ivjxZv0F1VRMWpjOTNIv0NNnRqyq85C4,12897
375
- dissect.target-3.20.dev30.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
376
- dissect.target-3.20.dev30.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
377
- dissect.target-3.20.dev30.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
378
- dissect.target-3.20.dev30.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