dissect.target 3.17.dev26__py3-none-any.whl → 3.17.dev28__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.
@@ -5,6 +5,7 @@ import re
5
5
  from collections import defaultdict
6
6
  from configparser import ConfigParser, MissingSectionHeaderError
7
7
  from io import StringIO
8
+ from itertools import chain
8
9
  from re import compile, sub
9
10
  from typing import Any, Callable, Iterable, Match, Optional
10
11
 
@@ -299,7 +300,8 @@ class Parser:
299
300
  return
300
301
 
301
302
  if section:
302
- config = config.get(section, {})
303
+ # account for values of sections which are None
304
+ config = config.get(section, {}) or {}
303
305
 
304
306
  for key, value in config.items():
305
307
  if key == option:
@@ -508,7 +510,7 @@ class LinuxNetworkManager:
508
510
 
509
511
 
510
512
  def parse_unix_dhcp_log_messages(target) -> list[str]:
511
- """Parse local syslog and cloud init log files for DHCP lease IPs.
513
+ """Parse local syslog, journal and cloud init-log files for DHCP lease IPs.
512
514
 
513
515
  Args:
514
516
  target: Target to discover and obtain network information from.
@@ -516,53 +518,68 @@ def parse_unix_dhcp_log_messages(target) -> list[str]:
516
518
  Returns:
517
519
  List of DHCP ip addresses.
518
520
  """
519
- ips = []
520
-
521
- # Search through parsed syslogs for DHCP leases.
522
- try:
523
- messages = target.messages()
524
- for record in messages:
525
- line = record.message
526
-
527
- # Ubuntu DHCP
528
- if ("DHCPv4" in line or "DHCPv6" in line) and " address " in line and " via " in line:
529
- ip = line.split(" address ")[1].split(" via ")[0].strip().split("/")[0]
530
- if ip not in ips:
531
- ips.append(ip)
532
-
533
- # Ubuntu DHCP NetworkManager
534
- elif "option ip_address" in line and ("dhcp4" in line or "dhcp6" in line) and "=> '" in line:
535
- ip = line.split("=> '")[1].replace("'", "").strip()
536
- if ip not in ips:
537
- ips.append(ip)
538
-
539
- # Debian and CentOS dhclient
540
- elif record.daemon == "dhclient" and "bound to" in line:
541
- ip = line.split("bound to")[1].split(" ")[1].strip()
542
- if ip not in ips:
543
- ips.append(ip)
544
-
545
- # CentOS DHCP and general NetworkManager
546
- elif " address " in line and ("dhcp4" in line or "dhcp6" in line):
547
- ip = line.split(" address ")[1].strip()
548
- if ip not in ips:
549
- ips.append(ip)
550
-
551
- except PluginError:
552
- target.log.debug("Can not search for DHCP leases in syslog files as they does not exist.")
553
-
554
- # A unix system might be provisioned using Ubuntu's cloud-init (https://cloud-init.io/).
555
- if (path := target.fs.path("/var/log/cloud-init.log")).exists():
556
- for line in path.open("rt"):
557
- # We are interested in the following log line:
558
- # YYYY-MM-DD HH:MM:SS,000 - dhcp.py[DEBUG]: Received dhcp lease on IFACE for IP/MASK
559
- if "Received dhcp lease on" in line:
560
- interface, ip, netmask = re.search(r"Received dhcp lease on (\w{0,}) for (\S+)\/(\S+)", line).groups()
561
- if ip not in ips:
562
- ips.append(ip)
563
-
564
- if not path and not messages:
565
- target.log.warning("Can not search for DHCP leases in syslog or cloud-init.log files as they does not exist.")
521
+ ips = set()
522
+ messages = set()
523
+
524
+ for log_func in ["messages", "journal"]:
525
+ try:
526
+ messages = chain(messages, getattr(target, log_func)())
527
+ except PluginError:
528
+ target.log.debug(f"Could not search for DHCP leases in {log_func} files.")
529
+
530
+ if not messages:
531
+ target.log.warning(f"Could not search for DHCP leases using {log_func}: No log entries found.")
532
+
533
+ for record in messages:
534
+ line = record.message
535
+
536
+ # Ubuntu cloud-init
537
+ if "Received dhcp lease on" in line:
538
+ interface, ip, netmask = re.search(r"Received dhcp lease on (\w{0,}) for (\S+)\/(\S+)", line).groups()
539
+ ips.add(ip)
540
+ continue
541
+
542
+ # Ubuntu DHCP
543
+ if ("DHCPv4" in line or "DHCPv6" in line) and " address " in line and " via " in line:
544
+ ip = line.split(" address ")[1].split(" via ")[0].strip().split("/")[0]
545
+ ips.add(ip)
546
+ continue
547
+
548
+ # Ubuntu DHCP NetworkManager
549
+ if "option ip_address" in line and ("dhcp4" in line or "dhcp6" in line) and "=> '" in line:
550
+ ip = line.split("=> '")[1].replace("'", "").strip()
551
+ ips.add(ip)
552
+ continue
553
+
554
+ # Debian and CentOS dhclient
555
+ if hasattr(record, "daemon") and record.daemon == "dhclient" and "bound to" in line:
556
+ ip = line.split("bound to")[1].split(" ")[1].strip()
557
+ ips.add(ip)
558
+ continue
559
+
560
+ # CentOS DHCP and general NetworkManager
561
+ if " address " in line and ("dhcp4" in line or "dhcp6" in line):
562
+ ip = line.split(" address ")[1].strip()
563
+ ips.add(ip)
564
+ continue
565
+
566
+ # Ubuntu/Debian DHCP networkd (Journal)
567
+ if (
568
+ hasattr(record, "code_func")
569
+ and record.code_func == "dhcp_lease_acquired"
570
+ and " address " in line
571
+ and " via " in line
572
+ ):
573
+ interface, ip, netmask, gateway = re.search(
574
+ r"^(\S+): DHCPv[4|6] address (\S+)\/(\S+) via (\S+)", line
575
+ ).groups()
576
+ ips.add(ip)
577
+ continue
578
+
579
+ # Journals and syslogs can be large and slow to iterate,
580
+ # so we stop if we have some results and have reached the journal plugin.
581
+ if len(ips) >= 2 and record._desc.name == "linux/log/journal":
582
+ break
566
583
 
567
584
  return ips
568
585
 
@@ -65,6 +65,9 @@ class MQTTStream(AlignedStream):
65
65
  class MQTTConnection:
66
66
  broker = None
67
67
  host = None
68
+ prev = -1
69
+ factor = 1
70
+ prefetch_factor_inc = 10
68
71
 
69
72
  def __init__(self, broker: Broker, host: str):
70
73
  self.broker = broker
@@ -95,20 +98,32 @@ class MQTTConnection:
95
98
 
96
99
  def read(self, disk_id: int, offset: int, length: int, optimization_strategy: int) -> bytes:
97
100
  message = None
98
- self.broker.seek(self.host, disk_id, offset, length, optimization_strategy)
99
101
 
102
+ message = self.broker.read(self.host, disk_id, offset, length)
103
+ if message:
104
+ return message.data
105
+
106
+ if self.prev == offset - (length * self.factor):
107
+ if self.factor < 500:
108
+ self.factor += self.prefetch_factor_inc
109
+ else:
110
+ self.factor = 1
111
+
112
+ self.prev = offset
113
+ flength = length * self.factor
114
+ self.broker.factor = self.factor
115
+ self.broker.seek(self.host, disk_id, offset, flength, optimization_strategy)
100
116
  attempts = 0
101
117
  while True:
102
- message = self.broker.read(self.host, disk_id, offset, length)
103
- # don't waste time with sleep if we have a response
104
- if message:
118
+ if message := self.broker.read(self.host, disk_id, offset, length):
119
+ # don't waste time with sleep if we have a response
105
120
  break
106
121
 
107
122
  attempts += 1
108
- time.sleep(0.01)
109
- if attempts > 100:
123
+ time.sleep(0.1)
124
+ if attempts > 300:
110
125
  # message might have not reached agent, resend...
111
- self.broker.seek(self.host, disk_id, offset, length, optimization_strategy)
126
+ self.broker.seek(self.host, disk_id, offset, flength, optimization_strategy)
112
127
  attempts = 0
113
128
 
114
129
  return message.data
@@ -127,6 +142,7 @@ class Broker:
127
142
  diskinfo = {}
128
143
  index = {}
129
144
  topo = {}
145
+ factor = 1
130
146
 
131
147
  def __init__(self, broker: Broker, port: str, key: str, crt: str, ca: str, case: str, **kwargs):
132
148
  self.broker_host = broker
@@ -137,10 +153,13 @@ class Broker:
137
153
  self.case = case
138
154
  self.command = kwargs.get("command", None)
139
155
 
156
+ def clear_cache(self) -> None:
157
+ self.index = {}
158
+
140
159
  @suppress
141
160
  def read(self, host: str, disk_id: int, seek_address: int, read_length: int) -> SeekMessage:
142
161
  key = f"{host}-{disk_id}-{seek_address}-{read_length}"
143
- return self.index.pop(key)
162
+ return self.index.get(key)
144
163
 
145
164
  @suppress
146
165
  def disk(self, host: str) -> DiskMessage:
@@ -165,14 +184,15 @@ class Broker:
165
184
  disk_id = tokens[3]
166
185
  seek_address = int(tokens[4], 16)
167
186
  read_length = int(tokens[5], 16)
168
- msg = SeekMessage(data=payload)
169
187
 
170
- key = f"{hostname}-{disk_id}-{seek_address}-{read_length}"
188
+ for i in range(self.factor):
189
+ sublength = int(read_length / self.factor)
190
+ start = i * sublength
191
+ key = f"{hostname}-{disk_id}-{seek_address+start}-{sublength}"
192
+ if key in self.index:
193
+ continue
171
194
 
172
- if key in self.index:
173
- return
174
-
175
- self.index[key] = msg
195
+ self.index[key] = SeekMessage(data=payload[start : start + sublength])
176
196
 
177
197
  def _on_id(self, hostname: str, payload: bytes) -> None:
178
198
  key = hostname
@@ -204,9 +224,14 @@ class Broker:
204
224
  elif response == "ID":
205
225
  self._on_id(hostname, msg.payload)
206
226
 
207
- def seek(self, host: str, disk_id: int, offset: int, length: int, optimization_strategy: int) -> None:
227
+ def seek(self, host: str, disk_id: int, offset: int, flength: int, optimization_strategy: int) -> None:
228
+ length = int(flength / self.factor)
229
+ key = f"{host}-{disk_id}-{offset}-{length}"
230
+ if key in self.index:
231
+ return
232
+
208
233
  self.mqtt_client.publish(
209
- f"{self.case}/{host}/SEEK/{disk_id}/{hex(offset)}/{hex(length)}", pack("<I", optimization_strategy)
234
+ f"{self.case}/{host}/SEEK/{disk_id}/{hex(offset)}/{hex(flength)}", pack("<I", optimization_strategy)
210
235
  )
211
236
 
212
237
  def info(self, host: str) -> None:
@@ -1,7 +1,8 @@
1
1
  import re
2
- from itertools import chain
2
+ from pathlib import Path
3
3
  from typing import Iterator
4
4
 
5
+ from dissect.target import Target
5
6
  from dissect.target.exceptions import UnsupportedPluginError
6
7
  from dissect.target.helpers.record import TargetRecordDescriptor
7
8
  from dissect.target.helpers.utils import year_rollover_helper
@@ -23,17 +24,28 @@ RE_TS = re.compile(r"(\w+\s{1,2}\d+\s\d{2}:\d{2}:\d{2})")
23
24
  RE_DAEMON = re.compile(r"^[^:]+:\d+:\d+[^\[\]:]+\s([^\[:]+)[\[|:]{1}")
24
25
  RE_PID = re.compile(r"\w\[(\d+)\]")
25
26
  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>.*)$")
26
28
 
27
29
 
28
30
  class MessagesPlugin(Plugin):
31
+ def __init__(self, target: Target):
32
+ super().__init__(target)
33
+ self.log_files = set(self._find_log_files())
34
+
35
+ def _find_log_files(self) -> Iterator[Path]:
36
+ log_dirs = ["/var/log/", "/var/log/installer/"]
37
+ file_globs = ["syslog*", "messages*", "cloud-init.log*"]
38
+ for log_dir in log_dirs:
39
+ for glob in file_globs:
40
+ yield from self.target.fs.path(log_dir).glob(glob)
41
+
29
42
  def check_compatible(self) -> None:
30
- var_log = self.target.fs.path("/var/log")
31
- if not any(var_log.glob("syslog*")) and not any(var_log.glob("messages*")):
32
- raise UnsupportedPluginError("No message files found")
43
+ if not self.log_files:
44
+ raise UnsupportedPluginError("No log files found")
33
45
 
34
46
  @export(record=MessagesRecord)
35
47
  def syslog(self) -> Iterator[MessagesRecord]:
36
- """Return contents of /var/log/messages* and /var/log/syslog*.
48
+ """Return contents of /var/log/messages*, /var/log/syslog* and cloud-init logs.
37
49
 
38
50
  See ``messages`` for more information.
39
51
  """
@@ -41,7 +53,7 @@ class MessagesPlugin(Plugin):
41
53
 
42
54
  @export(record=MessagesRecord)
43
55
  def messages(self) -> Iterator[MessagesRecord]:
44
- """Return contents of /var/log/messages* and /var/log/syslog*.
56
+ """Return contents of /var/log/messages*, /var/log/syslog* and cloud-init logs.
45
57
 
46
58
  Note: due to year rollover detection, the contents of the files are returned in reverse.
47
59
 
@@ -52,12 +64,16 @@ class MessagesPlugin(Plugin):
52
64
  References:
53
65
  - https://geek-university.com/linux/var-log-messages-file/
54
66
  - https://www.geeksforgeeks.org/file-timestamps-mtime-ctime-and-atime-in-linux/
67
+ - https://cloudinit.readthedocs.io/en/latest/development/logging.html#logging-command-output
55
68
  """
56
69
 
57
70
  tzinfo = self.target.datetime.tzinfo
58
71
 
59
- var_log = self.target.fs.path("/var/log")
60
- for log_file in chain(var_log.glob("syslog*"), var_log.glob("messages*")):
72
+ for log_file in self.log_files:
73
+ if "cloud-init" in log_file.name:
74
+ yield from self._parse_cloud_init_log(log_file)
75
+ continue
76
+
61
77
  for ts, line in year_rollover_helper(log_file, RE_TS, DEFAULT_TS_LOG_FORMAT, tzinfo):
62
78
  daemon = dict(enumerate(RE_DAEMON.findall(line))).get(0)
63
79
  pid = dict(enumerate(RE_PID.findall(line))).get(0)
@@ -71,3 +87,32 @@ class MessagesPlugin(Plugin):
71
87
  source=log_file,
72
88
  _target=self.target,
73
89
  )
90
+
91
+ def _parse_cloud_init_log(self, log_file: Path) -> Iterator[MessagesRecord]:
92
+ """Parse a cloud-init.log file.
93
+
94
+ Lines are structured in the following format:
95
+ ``YYYY-MM-DD HH:MM:SS,000 - dhcp.py[DEBUG]: Received dhcp lease on IFACE for IP/MASK``
96
+
97
+ NOTE: ``cloud-init-output.log`` files are not supported as they do not contain structured logs.
98
+
99
+ Args:
100
+ ``log_file``: path to cloud-init.log file.
101
+
102
+ Returns: ``MessagesRecord``
103
+ """
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")
118
+ self.target.log.debug("No match for line '%s'", line)
@@ -173,8 +173,7 @@ def main():
173
173
  collected_plugins = {}
174
174
 
175
175
  if targets:
176
- for target in targets:
177
- plugin_target = Target.open(target)
176
+ for plugin_target in Target.open_all(targets, args.children):
178
177
  if isinstance(plugin_target._loader, ProxyLoader):
179
178
  parser.error("can't list compatible plugins for remote targets.")
180
179
  funcs, _ = find_plugin_functions(plugin_target, args.list, compatibility=True, show_hidden=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev26
3
+ Version: 3.17.dev28
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
@@ -57,7 +57,7 @@ dissect/target/helpers/loaderutil.py,sha256=kiyMWra_gVxfNSGwLlgxLcuuqAYuCMDc5NiC
57
57
  dissect/target/helpers/localeutil.py,sha256=Y4Fh4jDSGfm5356xSLMriUCN8SZP_FAHg_iodkAxNq4,1504
58
58
  dissect/target/helpers/mount.py,sha256=JxhUYyEbDnHfzPpfuWy4nV9OwCJPoDSGdHHNiyvd_l0,3949
59
59
  dissect/target/helpers/mui.py,sha256=i-7XoHbu4WO2fYapK9yGAMW04rFlgRispknc1KQIS5Q,22258
60
- dissect/target/helpers/network_managers.py,sha256=OgrYhbBqM6K5OfUnCdTLG0RBrR-DcpR1CPezbNddK7k,24667
60
+ dissect/target/helpers/network_managers.py,sha256=uRh_P8ICbKke2N7eFJ6AS2-I5DmIRiaQUlxR7oqxPaU,24975
61
61
  dissect/target/helpers/polypath.py,sha256=h8p7m_OCNiQljGwoZh5Aflr9H2ot6CZr6WKq1OSw58o,2175
62
62
  dissect/target/helpers/protobuf.py,sha256=NwKrZD4q9v7J8GnZX9gbzMUMV5pR78eAV17jgWOz_EY,1730
63
63
  dissect/target/helpers/record.py,sha256=lWl7k2Mp9Axllm0tXzPGJx2zj2zONsyY_p5g424T0Lc,4826
@@ -85,7 +85,7 @@ dissect/target/loaders/itunes.py,sha256=69aMTQiiGYpmD_EYSmf9mO1re8C3jAZIEStmwlMx
85
85
  dissect/target/loaders/kape.py,sha256=t5TfrGLqPeIpUUpXzIl6aHsqXMEGDqJ5YwDCs07DiBA,1237
86
86
  dissect/target/loaders/local.py,sha256=Ul-LCd_fY7SyWOVR6nH-NqbkuNpxoZVmffwrkvQElU8,16453
87
87
  dissect/target/loaders/log.py,sha256=cCkDIRS4aPlX3U-n_jUKaI2FPSV3BDpfqKceaU7rBbo,1507
88
- dissect/target/loaders/mqtt.py,sha256=b0VrQ75_tmc4POkcfnUwKJoj1qmcjm1OKsVBQ9MjgqI,9552
88
+ dissect/target/loaders/mqtt.py,sha256=D8AmdOz2atD92z8bhjVFi3tC1H7pYmP4UrOCtMgfwMY,10396
89
89
  dissect/target/loaders/multiraw.py,sha256=4a3ZST0NwjnfPDxHkcEfAcX2ddUlT_C-rcrMHNg1wp4,1046
90
90
  dissect/target/loaders/ova.py,sha256=6h4O-7i87J394C6KgLsPkdXRAKNwtPubzLNS3vBGs7U,744
91
91
  dissect/target/loaders/ovf.py,sha256=ELMq6J2y6cPKbp7pjWAqMMnFYefWxXNqzIiAQdvGGXQ,1061
@@ -247,7 +247,7 @@ dissect/target/plugins/os/unix/log/audit.py,sha256=OjorWTmCFvCI5RJq6m6WNW0Lhb-po
247
247
  dissect/target/plugins/os/unix/log/auth.py,sha256=l7gCuRdvv9gL0U1N0yrR9hVsMnr4t_k4t-n-f6PrOxg,2388
248
248
  dissect/target/plugins/os/unix/log/journal.py,sha256=eiNNVLmKWFj4dTQX8PNRNgKpVwzQWEHEsKyYfGUAPXQ,17376
249
249
  dissect/target/plugins/os/unix/log/lastlog.py,sha256=eL_dbB1sPoy0tyavIjT457ZLVfXcCr17GiwDrMEEh8A,2458
250
- dissect/target/plugins/os/unix/log/messages.py,sha256=W3CeI0tchdRql9SKLFDxk9AKwUvqIrnpCujcERvDt90,2846
250
+ dissect/target/plugins/os/unix/log/messages.py,sha256=CXA-SkMPLaCgnTQg9nzII-7tO8Il_ENQmuYvDxo33rI,4698
251
251
  dissect/target/plugins/os/unix/log/utmp.py,sha256=21tvzG977LqzRShV6uAoU-83WDcLUrI_Tv__2ZVi9rw,7756
252
252
  dissect/target/plugins/os/windows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
253
253
  dissect/target/plugins/os/windows/_os.py,sha256=EA9B9Rgb1C9NMvlX3gXhTRFXYaI6zrrKRg0OYq4v1ts,12589
@@ -320,7 +320,7 @@ dissect/target/tools/fs.py,sha256=cizCrW8rqdpT1irA8g6mslkaXX7CynWVQ7fvRUrcxNU,37
320
320
  dissect/target/tools/info.py,sha256=3smHr8I71yj3kCjsQ5nXkOHI9T_N8UwvkVa1CNOxB-s,5461
321
321
  dissect/target/tools/logging.py,sha256=5ZnumtMWLyslxfrUGZ4ntRyf3obOOhmn8SBjKfdLcEg,4174
322
322
  dissect/target/tools/mount.py,sha256=L_0tSmiBdW4aSaF0vXjB0bAkTC0kmT2N1hrbW6s5Jow,3254
323
- dissect/target/tools/query.py,sha256=6zz9SXS6YnHj7eguORS8Je7N4iM0i1PZDIQ-gyJ1nPY,15593
323
+ dissect/target/tools/query.py,sha256=ONHu2FVomLccikb84qBrlhNmEfRoHYFQMcahk_y2c9A,15580
324
324
  dissect/target/tools/reg.py,sha256=FDsiBBDxjWVUBTRj8xn82vZe-J_d9piM-TKS3PHZCcM,3193
325
325
  dissect/target/tools/shell.py,sha256=4v6Z06YJDjKv6e6SRvWNjQ2n_KHo_CjL4P0w1_gY_ro,44827
326
326
  dissect/target/tools/utils.py,sha256=sQizexY3ui5vmWw4KOBLg5ecK3TPFjD-uxDqRn56ZTY,11304
@@ -336,10 +336,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
336
336
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
337
337
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
338
338
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
339
- dissect.target-3.17.dev26.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
- dissect.target-3.17.dev26.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
- dissect.target-3.17.dev26.dist-info/METADATA,sha256=fyuJSNpaOXUDx5rJFQYpsaxFKwa7VqFttG1XIvZxXco,11300
342
- dissect.target-3.17.dev26.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
- dissect.target-3.17.dev26.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
- dissect.target-3.17.dev26.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
- dissect.target-3.17.dev26.dist-info/RECORD,,
339
+ dissect.target-3.17.dev28.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
+ dissect.target-3.17.dev28.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
+ dissect.target-3.17.dev28.dist-info/METADATA,sha256=BUIYxmU-67ACgU91l-B3EP-B6QpBkjk-OBY7EOMDfBU,11300
342
+ dissect.target-3.17.dev28.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
+ dissect.target-3.17.dev28.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
+ dissect.target-3.17.dev28.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
+ dissect.target-3.17.dev28.dist-info/RECORD,,