dissect.target 3.15.dev34__py3-none-any.whl → 3.15.dev38__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import gzip
4
+ import hashlib
4
5
  from base64 import b64decode
5
6
  from datetime import datetime
7
+ from io import BytesIO
6
8
  from tarfile import ReadError
7
- from typing import Iterator, Optional, TextIO, Union
9
+ from typing import BinaryIO, Iterator, Optional, TextIO, Union
8
10
 
9
- from Crypto.Cipher import AES
10
11
  from dissect.util import cpio
11
12
  from dissect.util.compression import xz
12
13
 
@@ -18,6 +19,13 @@ from dissect.target.plugin import OperatingSystem, export
18
19
  from dissect.target.plugins.os.unix.linux._os import LinuxPlugin
19
20
  from dissect.target.target import Target
20
21
 
22
+ try:
23
+ from Crypto.Cipher import AES, ChaCha20
24
+
25
+ HAS_PYCRYPTODOME = True
26
+ except ImportError:
27
+ HAS_PYCRYPTODOME = False
28
+
21
29
  FortiOSUserRecord = TargetRecordDescriptor(
22
30
  "fortios/user",
23
31
  [
@@ -39,7 +47,7 @@ class FortiOSPlugin(LinuxPlugin):
39
47
 
40
48
  def _load_config(self) -> dict:
41
49
  CONFIG_FILES = {
42
- "/data/system.conf": None,
50
+ "/data/system.conf": "global-config", # FortiManager
43
51
  "/data/config/daemon.conf.gz": "daemon", # FortiOS 4.x
44
52
  "/data/config/sys_global.conf.gz": "global-config", # Seen in FortiOS 5.x - 7.x
45
53
  "/data/config/sys_vd_root.conf.gz": "root-config", # FortiOS 4.x
@@ -55,7 +63,7 @@ class FortiOSPlugin(LinuxPlugin):
55
63
  else:
56
64
  fh = conf_path.open("rt")
57
65
 
58
- if not self._version and section in [None, "global-config", "root-config"]:
66
+ if not self._version and section in ["global-config", "root-config"]:
59
67
  self._version = fh.readline().split("=", 1)[1]
60
68
 
61
69
  parsed = FortiOSConfig.from_fh(fh)
@@ -72,20 +80,31 @@ class FortiOSPlugin(LinuxPlugin):
72
80
 
73
81
  @classmethod
74
82
  def create(cls, target: Target, sysvol: Filesystem) -> FortiOSPlugin:
83
+ target.log.warning("Attempting to load rootfs.gz, this can take a while.")
75
84
  rootfs = sysvol.path("/rootfs.gz")
85
+ vfs = None
76
86
 
77
87
  try:
78
- target.log.warning("Attempting to load compressed rootfs.gz, this can take a while.")
79
- rfs_fh = open_decompress(rootfs)
80
- if rfs_fh.read(4) == b"07" * 2:
88
+ if open_decompress(rootfs).read(4) == b"0707":
81
89
  vfs = TarFilesystem(rootfs.open(), tarinfo=cpio.CpioInfo)
82
90
  else:
83
91
  vfs = TarFilesystem(rootfs.open())
92
+ except ReadError:
93
+ # The rootfs.gz file could be encrypted.
94
+ try:
95
+ rfs_fh = decrypt_rootfs(rootfs.open(), get_kernel_hash(sysvol))
96
+ vfs = TarFilesystem(rfs_fh, tarinfo=cpio.CpioInfo)
97
+ except RuntimeError:
98
+ target.log.warning("Could not decrypt rootfs.gz. Missing `pycryptodome` dependency.")
99
+ except ValueError as e:
100
+ target.log.warning("Could not decrypt rootfs.gz. Unsupported kernel version.")
101
+ target.log.debug("", exc_info=e)
102
+ except ReadError as e:
103
+ target.log.warning("Could not mount rootfs.gz. It could be corrupt.")
104
+ target.log.debug("", exc_info=e)
105
+
106
+ if vfs:
84
107
  target.fs.mount("/", vfs)
85
- except ReadError as e:
86
- # Since FortiOS version ~7.4.1 the rootfs.gz file is encrypted.
87
- target.log.warning("Could not mount FortiOS `/rootfs.gz`. It could be encrypted or corrupt.")
88
- target.log.debug("", exc_info=e)
89
108
 
90
109
  target.fs.mount("/data", sysvol)
91
110
 
@@ -93,13 +112,21 @@ class FortiOSPlugin(LinuxPlugin):
93
112
  if (datafs_tar := sysvol.path("/datafs.tar.gz")).exists():
94
113
  target.fs.add_layer().mount("/data", TarFilesystem(datafs_tar.open("rb")))
95
114
 
96
- # Additional FortiGate tars with corrupt XZ streams
97
- for path in ("bin.tar.xz", "usr.tar.xz", "migadmin.tar.xz", "node-scripts.tar.xz"):
98
- if (tar := target.fs.path(path)).exists():
115
+ # Additional FortiGate or FortiManager tars with corrupt XZ streams
116
+ target.log.warning("Attempting to load XZ files, this can take a while.")
117
+ for path in (
118
+ "bin.tar.xz",
119
+ "usr.tar.xz",
120
+ "migadmin.tar.xz",
121
+ "node-scripts.tar.xz",
122
+ "docker.tar.xz",
123
+ "syntax.tar.xz",
124
+ ):
125
+ if (tar := target.fs.path(path)).exists() or (tar := sysvol.path(path)).exists():
99
126
  fh = xz.repair_checksum(tar.open("rb"))
100
127
  target.fs.add_layer().mount("/", TarFilesystem(fh))
101
128
 
102
- # FortiAnalyzer
129
+ # FortiAnalyzer and FortiManager
103
130
  if (rootfs_ext_tar := sysvol.path("rootfs-ext.tar.xz")).exists():
104
131
  target.fs.add_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb")))
105
132
 
@@ -117,9 +144,18 @@ class FortiOSPlugin(LinuxPlugin):
117
144
  target.fs.mount("/boot", fs)
118
145
 
119
146
  # data2 partition
120
- if fs.__type__ == "ext" and fs.path("/new_alert_msg").exists() and fs.path("/template").exists():
147
+ if fs.__type__ == "ext" and (
148
+ (fs.path("/new_alert_msg").exists() and fs.path("/template").exists()) # FortiGate
149
+ or (fs.path("/swapfile").exists() and fs.path("/old_fmversion").exists()) # FortiManager
150
+ ):
121
151
  target.fs.mount("/data2", fs)
122
152
 
153
+ # Symlink unix-like paths
154
+ unix_paths = [("/data/passwd", "/etc/passwd")]
155
+ for src, dst in unix_paths:
156
+ if target.fs.path(src).exists() and not target.fs.path(dst).exists():
157
+ target.fs.symlink(src, dst)
158
+
123
159
  return cls(target)
124
160
 
125
161
  @export(property=True)
@@ -158,8 +194,11 @@ class FortiOSPlugin(LinuxPlugin):
158
194
  def dns(self) -> list[str]:
159
195
  """Return configured WAN DNS servers."""
160
196
  entries = []
161
- for _, entry in self._config["global-config"]["system"]["dns"].items():
162
- entries.append(entry[0])
197
+ try:
198
+ for entry in self._config["global-config"]["system"]["dns"].values():
199
+ entries.append(entry[0])
200
+ except KeyError:
201
+ pass
163
202
  return entries
164
203
 
165
204
  @export(property=True)
@@ -176,7 +215,7 @@ class FortiOSPlugin(LinuxPlugin):
176
215
  # Possible unix-like users
177
216
  yield from super().users()
178
217
 
179
- # Administrative users
218
+ # FortiGate administrative users
180
219
  try:
181
220
  for username, entry in self._config["global-config"]["system"]["admin"].items():
182
221
  yield FortiOSUserRecord(
@@ -190,13 +229,27 @@ class FortiOSPlugin(LinuxPlugin):
190
229
  self.target.log.warning("Exception while parsing FortiOS admin users")
191
230
  self.target.log.debug("", exc_info=e)
192
231
 
232
+ # FortiManager administrative users
233
+ try:
234
+ for username, entry in self._config["global-config"]["system"]["admin"]["user"].items():
235
+ yield FortiOSUserRecord(
236
+ name=username,
237
+ password=":".join(entry.get("password", [])),
238
+ groups=[entry["profileid"][0]],
239
+ home="/root",
240
+ _target=self.target,
241
+ )
242
+ except KeyError as e:
243
+ self.target.log.warning("Exception while parsing FortiManager admin users")
244
+ self.target.log.debug("", exc_info=e)
245
+
193
246
  # Local users
194
247
  try:
195
248
  local_groups = local_groups_to_users(self._config["root-config"]["user"]["group"])
196
249
  for username, entry in self._config["root-config"]["user"].get("local", {}).items():
197
250
  try:
198
251
  password = decrypt_password(entry["passwd"][-1])
199
- except ValueError:
252
+ except (ValueError, RuntimeError):
200
253
  password = ":".join(entry.get("passwd", []))
201
254
 
202
255
  yield FortiOSUserRecord(
@@ -215,7 +268,7 @@ class FortiOSPlugin(LinuxPlugin):
215
268
  for _, entry in self._config["root-config"]["user"]["group"].get("guestgroup", {}).get("guest", {}).items():
216
269
  try:
217
270
  password = decrypt_password(entry.get("password")[-1])
218
- except ValueError:
271
+ except (ValueError, RuntimeError):
219
272
  password = ":".join(entry.get("password"))
220
273
 
221
274
  yield FortiOSUserRecord(
@@ -236,7 +289,10 @@ class FortiOSPlugin(LinuxPlugin):
236
289
  @export(property=True)
237
290
  def architecture(self) -> Optional[str]:
238
291
  """Return architecture FortiOS runs on."""
239
- return self._get_architecture(path="/lib/libav.so")
292
+ paths = ["/lib/libav.so", "/bin/ctr"]
293
+ for path in paths:
294
+ if self.target.fs.path(path).exists():
295
+ return self._get_architecture(path=path)
240
296
 
241
297
 
242
298
  class ConfigNode(dict):
@@ -344,7 +400,7 @@ def parse_version(input: str) -> str:
344
400
  }
345
401
 
346
402
  try:
347
- version_str = input.split(":", 1)[0]
403
+ version_str = input.split(":", 1)[0].strip()
348
404
  type, version, _, build_num, build_date = version_str.rsplit("-", 4)
349
405
 
350
406
  build_num = build_num.replace("build", "build ", 1)
@@ -368,15 +424,111 @@ def local_groups_to_users(config_groups: dict) -> dict:
368
424
  return user_groups
369
425
 
370
426
 
371
- def decrypt_password(ciphertext: str) -> str:
372
- """Decrypt FortiOS version 6 and 7 encrypted secrets."""
427
+ def decrypt_password(input: str) -> str:
428
+ """Decrypt FortiOS encrypted secrets.
429
+
430
+ Works for FortiGate 5.x, 6.x and 7.x (CVE-2019-6693).
431
+
432
+ NOTE:
433
+ - FortiManager uses a 16-byte IV and is not supported (CVE-2020-9289).
434
+ - FortiGate 4.x uses DES and a static 8-byte key and is not supported.
435
+
436
+ Returns decoded plaintext or original input ciphertext when decryption failed.
437
+
438
+ Resources:
439
+ - https://www.fortiguard.com/psirt/FG-IR-19-007
440
+ """
441
+
442
+ if not HAS_PYCRYPTODOME:
443
+ raise RuntimeError("PyCryptodome module not available")
373
444
 
374
- if ciphertext[:3] in ["SH2", "AK1"]:
445
+ if input[:3] in ["SH2", "AK1"]:
375
446
  raise ValueError("Password is a hash (SHA-256 or SHA-1) and cannot be decrypted.")
376
447
 
377
- ciphertext = b64decode(ciphertext)
448
+ ciphertext = b64decode(input)
378
449
  iv = ciphertext[:4] + b"\x00" * 12
379
450
  key = b"Mary had a littl"
380
451
  cipher = AES.new(key, iv=iv, mode=AES.MODE_CBC)
381
452
  plaintext = cipher.decrypt(ciphertext[4:])
382
- return plaintext.split(b"\x00", 1)[0].decode()
453
+
454
+ try:
455
+ return plaintext.split(b"\x00", 1)[0].decode()
456
+ except UnicodeDecodeError:
457
+ return "ENC:" + input
458
+
459
+
460
+ def decrypt_rootfs(fh: BinaryIO, kernel_hash: str) -> BinaryIO:
461
+ """Attempt to decrypt an encrypted ``rootfs.gz`` file.
462
+
463
+ FortiOS releases as of 7.4.1 / 2023-08-31, have ChaCha20 encrypted ``rootfs.gz`` files.
464
+ This function attempts to decrypt a ``rootfs.gz`` file using a static key and IV
465
+ which can be found in the kernel.
466
+
467
+ Currently supported versions (each release has a new key):
468
+ - FortiGate VM 7.0.13
469
+ - FortiGate VM 7.4.1
470
+ - FortiGate VM 7.4.2
471
+
472
+ Resources:
473
+ - https://docs.fortinet.com/document/fortimanager/7.4.2/release-notes/519207/special-notices
474
+ - Reversing kernel (fgt_verifier_iv, fgt_verifier_decrypt, fgt_verifier_initrd)
475
+ """
476
+
477
+ if not HAS_PYCRYPTODOME:
478
+ raise RuntimeError("PyCryptodome module not available")
479
+
480
+ # SHA256 hashes of kernel files
481
+ KERNEL_KEY_MAP = {
482
+ # FortiGate VM 7.0.13
483
+ "25cb2c8a419cde1f42d38fc6cbc95cf8b53db41096d0648015674d8220eba6bf": (
484
+ bytes.fromhex("c87e13e1f7d21c1aca81dc13329c3a948d6e420d3a859f3958bd098747873d08"),
485
+ bytes.fromhex("87486a24637e9a66f09ec182eee25594"),
486
+ ),
487
+ # FortiGate VM 7.4.1
488
+ "a008b47327293e48502a121ee8709f243ad5da4e63d6f663c253db27bd01ea28": _kdf_7_4_x(
489
+ "366486c0f2c6322ec23e4f33a98caa1b19d41c74bb4f25f6e8e2087b0655b30f"
490
+ ),
491
+ # FortiGate VM 7.4.2
492
+ "c392cf83ab484e0b2419b2711b02cdc88a73db35634c10340037243394a586eb": _kdf_7_4_x(
493
+ "480767be539de28ee773497fa731dd6368adc9946df61da8e1253fa402ba0302"
494
+ ),
495
+ }
496
+
497
+ if not (key_data := KERNEL_KEY_MAP.get(kernel_hash)):
498
+ raise ValueError("Failed to decrypt: Unknown kernel hash.")
499
+
500
+ key, iv = key_data
501
+ # First 8 bytes = counter, last 8 bytes = nonce
502
+ # PyCryptodome interally divides this seek by 64 to get a (position, offset) tuple
503
+ # We're interested in updating the position in the ChaCha20 internal state, so to make
504
+ # PyCryptodome "OpenSSL-compatible" we have to multiply the counter by 64
505
+ cipher = ChaCha20.new(key=key, nonce=iv[8:])
506
+ cipher.seek(int.from_bytes(iv[:8], "little") * 64)
507
+ result = cipher.decrypt(fh.read())
508
+
509
+ if result[0:2] != b"\x1f\x8b":
510
+ raise ValueError("Failed to decrypt: No gzip magic header found.")
511
+
512
+ return BytesIO(result)
513
+
514
+
515
+ def _kdf_7_4_x(key_data: Union[str, bytes]) -> tuple[bytes, bytes]:
516
+ """Derive 32 byte key and 16 byte IV from 32 byte seed.
517
+
518
+ As the IV needs to be 16 bytes, we return the first 16 bytes of the sha256 hash.
519
+ """
520
+
521
+ if isinstance(key_data, str):
522
+ key_data = bytes.fromhex(key_data)
523
+
524
+ key = hashlib.sha256(key_data[4:32] + key_data[:4]).digest()
525
+ iv = hashlib.sha256(key_data[5:32] + key_data[:5]).digest()[:16]
526
+ return key, iv
527
+
528
+
529
+ def get_kernel_hash(sysvol: Filesystem) -> Optional[str]:
530
+ """Return the SHA256 hash of the (compressed) kernel."""
531
+ kernel_files = ["flatkc", "vmlinuz", "vmlinux"]
532
+ for k in kernel_files:
533
+ if sysvol.path(k).exists():
534
+ return sysvol.sha256(k)
@@ -16,13 +16,15 @@ class GenericPlugin(Plugin):
16
16
  @export(property=True)
17
17
  def install_date(self) -> Optional[datetime]:
18
18
  """Return the likely install date of FortiOS."""
19
- if (init_log := self.target.fs.path("/data/etc/cloudinit.log")).exists():
20
- return ts.from_unix(init_log.stat().st_mtime)
19
+ files = ["/data/etc/cloudinit.log", "/data/.vm_provisioned", "/data/etc/ssh/ssh_host_dsa_key"]
20
+ for file in files:
21
+ if (fp := self.target.fs.path(file)).exists():
22
+ return ts.from_unix(fp.stat().st_mtime)
21
23
 
22
24
  @export(property=True)
23
25
  def activity(self) -> Optional[datetime]:
24
26
  """Return last seen activity based on filesystem timestamps."""
25
- log_dirs = ["/var/log/log/root", "/var/log/root"]
27
+ log_dirs = ["/var/log/log/root", "/var/log/root", "/data"]
26
28
  for log_dir in log_dirs:
27
29
  if (var_log := self.target.fs.path(log_dir)).exists():
28
30
  return calculate_last_activity(var_log)
@@ -1,3 +1,5 @@
1
+ from typing import Optional
2
+
1
3
  from dissect.target.exceptions import UnsupportedPluginError
2
4
  from dissect.target.plugin import Plugin, export
3
5
 
@@ -8,13 +10,16 @@ class LocalePlugin(Plugin):
8
10
  raise UnsupportedPluginError("FortiOS specific plugin loaded on non-FortiOS target")
9
11
 
10
12
  @export(property=True)
11
- def timezone(self) -> str:
13
+ def timezone(self) -> Optional[str]:
12
14
  """Return configured UI/system timezone."""
13
- timezone_num = self.target._os._config["global-config"]["system"]["global"]["timezone"][0]
14
- return translate_timezone(timezone_num)
15
+ try:
16
+ timezone_num = self.target._os._config["global-config"]["system"]["global"]["timezone"][0]
17
+ return translate_timezone(timezone_num)
18
+ except KeyError:
19
+ pass
15
20
 
16
21
  @export(property=True)
17
- def language(self) -> str:
22
+ def language(self) -> Optional[str]:
18
23
  """Return configured UI language."""
19
24
  LANG_MAP = {
20
25
  "english": "en_US",
@@ -26,8 +31,11 @@ class LocalePlugin(Plugin):
26
31
  "simch": "zh_CN",
27
32
  "korean": "ko_KR",
28
33
  }
29
- lang_str = self.target._os._config["global-config"]["system"]["global"].get("language", ["english"])[0]
30
- return LANG_MAP.get(lang_str, lang_str)
34
+ try:
35
+ lang_str = self.target._os._config["global-config"]["system"]["global"].get("language", ["english"])[0]
36
+ return LANG_MAP.get(lang_str, lang_str)
37
+ except KeyError:
38
+ pass
31
39
 
32
40
 
33
41
  def translate_timezone(timezone_num: str) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.15.dev34
3
+ Version: 3.15.dev38
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
@@ -218,9 +218,9 @@ dissect/target/plugins/os/unix/linux/debian/dpkg.py,sha256=DPBLQiHAF7ZS8IorRsGAi
218
218
  dissect/target/plugins/os/unix/linux/debian/vyos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
219
  dissect/target/plugins/os/unix/linux/debian/vyos/_os.py,sha256=q8qG2FLJhUbpjfwlNCmWAhFdTWMzSWUh7s7H8m4x7Fw,1741
220
220
  dissect/target/plugins/os/unix/linux/fortios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
221
- dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=U6LKdFxsRBYvx5I-tJLZTixBFhfQ8OZBt1J1S2eSY4M,13388
222
- dissect/target/plugins/os/unix/linux/fortios/generic.py,sha256=jWs1KEBV68h8TWhBw9gjjGoVbvb8NcMqx82lFqqqnPQ,1116
223
- dissect/target/plugins/os/unix/linux/fortios/locale.py,sha256=hd71u6-fdY8_GaWggjauFOIUrPuZWzpEuWcyC365ffo,5034
221
+ dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=mYwmGAeY1GQdPdFbGxwNhlRuMD2hTuL1nlEAaXhao4o,19091
222
+ dissect/target/plugins/os/unix/linux/fortios/generic.py,sha256=tT4-lE0Z_DeDIN3zHrQbE8JB3cRJop1_TiEst-Au0bs,1230
223
+ dissect/target/plugins/os/unix/linux/fortios/locale.py,sha256=VDdk60sqe2JTfftssO05C667-_BpI3kcqKOTVzO3ueU,5209
224
224
  dissect/target/plugins/os/unix/linux/redhat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
225
225
  dissect/target/plugins/os/unix/linux/redhat/_os.py,sha256=l_SygO1WMBTvaLvAvhe08yPHLBpUZ9wizW28a9_JhJE,578
226
226
  dissect/target/plugins/os/unix/linux/redhat/yum.py,sha256=kEvB-C2CNoqxSbgGRZiuo6CMPBo_hMWy2KQIE4SNkdQ,2134
@@ -321,10 +321,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
321
321
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
322
322
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
323
323
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
324
- dissect.target-3.15.dev34.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
325
- dissect.target-3.15.dev34.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
326
- dissect.target-3.15.dev34.dist-info/METADATA,sha256=Hs8t65rvQ451oa-ZQTM3oWM5-BL_zTRjRqOXi_LbMpc,11113
327
- dissect.target-3.15.dev34.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
328
- dissect.target-3.15.dev34.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
329
- dissect.target-3.15.dev34.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
330
- dissect.target-3.15.dev34.dist-info/RECORD,,
324
+ dissect.target-3.15.dev38.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
325
+ dissect.target-3.15.dev38.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
326
+ dissect.target-3.15.dev38.dist-info/METADATA,sha256=hXL7MrjO-icCXVn6ZBQgh0xFBZsV4sePxr5u-eeICUo,11113
327
+ dissect.target-3.15.dev38.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
328
+ dissect.target-3.15.dev38.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
329
+ dissect.target-3.15.dev38.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
330
+ dissect.target-3.15.dev38.dist-info/RECORD,,