dissect.target 3.20.dev45__py3-none-any.whl → 3.20.dev47__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/auth.py +350 -39
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/METADATA +1 -1
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/RECORD +8 -8
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/LICENSE +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/WHEEL +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/top_level.txt +0 -0
@@ -1,62 +1,373 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import itertools
|
4
|
+
import logging
|
1
5
|
import re
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from datetime import datetime
|
8
|
+
from functools import lru_cache
|
2
9
|
from itertools import chain
|
3
|
-
from
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Any, Iterator
|
4
12
|
|
13
|
+
from dissect.target import Target
|
5
14
|
from dissect.target.exceptions import UnsupportedPluginError
|
6
|
-
from dissect.target.helpers.
|
15
|
+
from dissect.target.helpers.fsutil import open_decompress
|
16
|
+
from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor
|
7
17
|
from dissect.target.helpers.utils import year_rollover_helper
|
8
|
-
from dissect.target.plugin import Plugin, export
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
18
|
+
from dissect.target.plugin import Plugin, alias, export
|
19
|
+
|
20
|
+
log = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
RE_TS = re.compile(r"^[A-Za-z]{3}\s*\d{1,2}\s\d{1,2}:\d{2}:\d{2}")
|
23
|
+
RE_TS_ISO = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}")
|
24
|
+
RE_LINE = re.compile(
|
25
|
+
r"""
|
26
|
+
\d{2}:\d{2}\s # First match on the similar ending of the different timestamps
|
27
|
+
(?P<hostname>\S+)\s # The hostname
|
28
|
+
(?P<service>\S+?)(\[(?P<pid>\d+)\])?: # The service with optionally the PID between brackets
|
29
|
+
\s*(?P<message>.+?)\s*$ # The log message stripped from spaces left and right
|
30
|
+
""",
|
31
|
+
re.VERBOSE,
|
32
|
+
)
|
33
|
+
|
34
|
+
# Generic regular expressions
|
35
|
+
RE_IPV4_ADDRESS = re.compile(
|
36
|
+
r"""
|
37
|
+
((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} # First three octets
|
38
|
+
(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) # Last octet
|
39
|
+
""",
|
40
|
+
re.VERBOSE,
|
17
41
|
)
|
42
|
+
RE_USER = re.compile(r"for ([^\s]+)")
|
43
|
+
|
44
|
+
|
45
|
+
class BaseService(ABC):
|
46
|
+
@classmethod
|
47
|
+
@abstractmethod
|
48
|
+
def parse(cls, message: str) -> dict[str, any]:
|
49
|
+
pass
|
50
|
+
|
51
|
+
|
52
|
+
class SudoService(BaseService):
|
53
|
+
"""Parsing of sudo service messages in the auth log."""
|
54
|
+
|
55
|
+
RE_SUDO_COMMAND = re.compile(
|
56
|
+
r"""
|
57
|
+
TTY=(?P<tty>\w+\/\w+)\s;\s # The TTY -> TTY=pts/0 ;
|
58
|
+
PWD=(?P<pwd>[\/\w]+)\s;\s # The current working directory -> PWD="/home/user" ;
|
59
|
+
USER=(?P<effective_user>\w+)\s;\s # The effective user -> USER=root ;
|
60
|
+
COMMAND=(?P<command>.+)$ # The command -> COMMAND=/usr/bin/whoami
|
61
|
+
""",
|
62
|
+
re.VERBOSE,
|
63
|
+
)
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
def parse(cls, message: str) -> dict[str, str]:
|
67
|
+
"""Parse auth log message from sudo."""
|
68
|
+
if not (match := cls.RE_SUDO_COMMAND.search(message)):
|
69
|
+
return {}
|
70
|
+
|
71
|
+
additional_fields = {}
|
72
|
+
for key, value in match.groupdict().items():
|
73
|
+
additional_fields[key] = value
|
74
|
+
|
75
|
+
return additional_fields
|
76
|
+
|
77
|
+
|
78
|
+
class SshdService(BaseService):
|
79
|
+
"""Class for parsing sshd messages in the auth log."""
|
80
|
+
|
81
|
+
RE_SSHD_PORTREGEX = re.compile(r"port\s(\d+)")
|
82
|
+
RE_USER = re.compile(r"for\s([^\s]+)")
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def parse(cls, message: str) -> dict[str, str | int]:
|
86
|
+
"""Parse message from sshd"""
|
87
|
+
additional_fields = {}
|
88
|
+
if ip_address := RE_IPV4_ADDRESS.search(message):
|
89
|
+
field_name = "host_ip" if "listening" in message else "remote_ip"
|
90
|
+
additional_fields[field_name] = ip_address.group(0)
|
91
|
+
if port := cls.RE_SSHD_PORTREGEX.search(message):
|
92
|
+
additional_fields["port"] = int(port.group(1))
|
93
|
+
if user := cls.RE_USER.search(message):
|
94
|
+
additional_fields["user"] = user.group(1)
|
95
|
+
# Accepted publickey for test_user from 8.8.8.8 IP port 12345 ssh2: RSA SHA256:123456789asdfghjklertzuio
|
96
|
+
if "Accepted publickey" in message:
|
97
|
+
ssh_protocol, encryption_algo, key_info = message.split()[-3:]
|
98
|
+
hash_algo, key_hash = key_info.split(":")
|
99
|
+
additional_fields["ssh_protocol"] = ssh_protocol.strip(":")
|
100
|
+
additional_fields["encryption_algorithm"] = encryption_algo
|
101
|
+
additional_fields["hash_algorithm"] = hash_algo
|
102
|
+
additional_fields["key_hash"] = key_hash
|
103
|
+
if (failed := "Failed" in message) or "Accepted" in message:
|
104
|
+
action_type = "failed" if failed else "accepted"
|
105
|
+
additional_fields["action"] = f"{action_type} authentication"
|
106
|
+
additional_fields["authentication_type"] = "password" if "password" in message else "publickey"
|
107
|
+
|
108
|
+
return additional_fields
|
109
|
+
|
110
|
+
|
111
|
+
class SystemdLogindService(BaseService):
|
112
|
+
"""Class for parsing systemd-logind messages in the auth log."""
|
113
|
+
|
114
|
+
RE_SYSTEMD_LOGIND_WATCHING = re.compile(
|
115
|
+
r"""
|
116
|
+
(?P<action>Watching\ssystem\sbuttons)\s # Action is "Watching system buttons"
|
117
|
+
on\s(?P<device>[^\s]+)\s # The device the button is related to -> /dev/input/event0
|
118
|
+
\((?P<device_name>.*?)\) # The device (button) name -> (Power button)
|
119
|
+
""",
|
120
|
+
re.VERBOSE,
|
121
|
+
)
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
def parse(cls, message: str):
|
125
|
+
"""Parse auth log message from systemd-logind."""
|
126
|
+
additional_fields = {}
|
127
|
+
# Example: Nov 14 07:14:09 ubuntu-1 systemd-logind[4]: Removed session 4.
|
128
|
+
if "Removed" in message:
|
129
|
+
additional_fields["action"] = "removed session"
|
130
|
+
additional_fields["session"] = message.split()[-1].strip(".")
|
131
|
+
elif "Watching" in message and (match := cls.RE_SYSTEMD_LOGIND_WATCHING.search(message)):
|
132
|
+
for key, value in match.groupdict().items():
|
133
|
+
additional_fields[key] = value
|
134
|
+
# Example: New session 4 of user sampleuser.
|
135
|
+
elif "New session" in message:
|
136
|
+
parts = message.removeprefix("New session ").split()
|
137
|
+
additional_fields["action"] = "new session"
|
138
|
+
additional_fields["session"] = parts[0]
|
139
|
+
additional_fields["user"] = parts[-1].strip(".")
|
140
|
+
# Example: Session 4 logged out. Waiting for processes to exit.
|
141
|
+
elif "logged out" in message:
|
142
|
+
session = message.removeprefix("Session ").split(maxsplit=1)[0]
|
143
|
+
additional_fields["action"] = "logged out session"
|
144
|
+
additional_fields["session"] = session
|
145
|
+
# Example: New seat seat0.
|
146
|
+
elif "New seat" in message:
|
147
|
+
seat = message.split()[-1].strip(".")
|
148
|
+
additional_fields["action"] = "new seat"
|
149
|
+
additional_fields["seat"] = seat
|
150
|
+
|
151
|
+
return additional_fields
|
152
|
+
|
153
|
+
|
154
|
+
class SuService(BaseService):
|
155
|
+
"""Class for parsing su messages in the auth log."""
|
156
|
+
|
157
|
+
RE_SU_BY = re.compile(r"by\s([^\s]+)")
|
158
|
+
RE_SU_ON = re.compile(r"on\s([^\s]+)")
|
159
|
+
RE_SU_COMMAND = re.compile(r"'(.*?)'")
|
160
|
+
|
161
|
+
@classmethod
|
162
|
+
def parse(cls, message: str) -> dict[str, str]:
|
163
|
+
additional_fields = {}
|
164
|
+
if user := RE_USER.search(message):
|
165
|
+
additional_fields["user"] = user.group(1)
|
166
|
+
if by := cls.RE_SU_BY.search(message):
|
167
|
+
additional_fields["by"] = by.group(1)
|
168
|
+
if on := cls.RE_SU_ON.search(message):
|
169
|
+
additional_fields["device"] = on.group(1)
|
170
|
+
if command := cls.RE_SU_COMMAND.search(message):
|
171
|
+
additional_fields["command"] = command.group(1)
|
172
|
+
if (failed := "failed" in message) or "Successful" in message:
|
173
|
+
additional_fields["su_result"] = "failed" if failed else "success"
|
174
|
+
|
175
|
+
return additional_fields
|
176
|
+
|
177
|
+
|
178
|
+
class PkexecService(BaseService):
|
179
|
+
"""Class for parsing pkexec messages in the auth log."""
|
180
|
+
|
181
|
+
RE_PKEXEC_COMMAND = re.compile(
|
182
|
+
r"""
|
183
|
+
(?P<user>\S+?):\sExecuting\scommand\s # Starts with actual user -> user:
|
184
|
+
\[USER=(?P<effective_user>[^\]]+)\]\s # The impersonated user -> [USER=root]
|
185
|
+
\[TTY=(?P<tty>[^\]]+)\]\s # The tty -> [TTY=unknown]
|
186
|
+
\[CWD=(?P<cwd>[^\]]+)\]\s # Current working directory -> [CWD=/home/user]
|
187
|
+
\[COMMAND=(?P<command>[^\]]+)\] # Command -> [COMMAND=/usr/lib/example]
|
188
|
+
""",
|
189
|
+
re.VERBOSE,
|
190
|
+
)
|
191
|
+
|
192
|
+
@classmethod
|
193
|
+
def parse(cls, message: str) -> dict[str, str]:
|
194
|
+
"""Parse auth log message from pkexec"""
|
195
|
+
additional_fields = {}
|
196
|
+
if exec_cmd := cls.RE_PKEXEC_COMMAND.search(message):
|
197
|
+
additional_fields["action"] = "executing command"
|
198
|
+
for key, value in exec_cmd.groupdict().items():
|
199
|
+
if value and value.isdigit():
|
200
|
+
value = int(value)
|
201
|
+
additional_fields[key] = value
|
18
202
|
|
19
|
-
|
20
|
-
|
21
|
-
|
203
|
+
return additional_fields
|
204
|
+
|
205
|
+
|
206
|
+
class PamUnixService(BaseService):
|
207
|
+
RE_PAM_UNIX = re.compile(
|
208
|
+
r"""
|
209
|
+
pam_unix\([^\s]+:session\):\s(?P<action>session\s\w+)\s # Session action, usually opened or closed
|
210
|
+
for\suser\s(?P<user>[^\s\(]+)(?:\(uid=(?P<user_uid>\d+)\))? # User may contain uid like: root(uid=0)
|
211
|
+
(?:\sby\s\(uid=(?P<by_uid>\d+)\))?$ # Opened action also contains by
|
212
|
+
""",
|
213
|
+
re.VERBOSE,
|
214
|
+
)
|
215
|
+
|
216
|
+
@classmethod
|
217
|
+
def parse(cls, message):
|
218
|
+
"""Parse auth log message from pluggable authentication modules (PAM)."""
|
219
|
+
if not (match := cls.RE_PAM_UNIX.search(message)):
|
220
|
+
return {}
|
221
|
+
|
222
|
+
additional_fields = {}
|
223
|
+
for key, value in match.groupdict().items():
|
224
|
+
if value and value.isdigit():
|
225
|
+
value = int(value)
|
226
|
+
additional_fields[key] = value
|
227
|
+
|
228
|
+
return additional_fields
|
229
|
+
|
230
|
+
|
231
|
+
class AuthLogRecordBuilder:
|
232
|
+
"""Class for dynamically creating auth log records."""
|
233
|
+
|
234
|
+
RECORD_NAME = "linux/log/auth"
|
235
|
+
SERVICES: dict[str, BaseService] = {
|
236
|
+
"su": SuService,
|
237
|
+
"sudo": SudoService,
|
238
|
+
"sshd": SshdService,
|
239
|
+
"systemd-logind": SystemdLogindService,
|
240
|
+
"pkexec": PkexecService,
|
241
|
+
}
|
242
|
+
|
243
|
+
def __init__(self, target: Target):
|
244
|
+
self._create_event_descriptor = lru_cache(4096)(self._create_event_descriptor)
|
245
|
+
self.target = target
|
246
|
+
|
247
|
+
def _parse_additional_fields(self, service: str | None, message: str) -> dict[str, Any]:
|
248
|
+
"""Parse additional fields in the message based on the service."""
|
249
|
+
if "pam_unix(" in message:
|
250
|
+
return PamUnixService.parse(message)
|
251
|
+
|
252
|
+
if service not in self.SERVICES:
|
253
|
+
self.target.log.debug("Service %s is not recognised, no additional fields could be parsed", service)
|
254
|
+
return {}
|
255
|
+
|
256
|
+
try:
|
257
|
+
return self.SERVICES[service].parse(message)
|
258
|
+
except Exception as e:
|
259
|
+
self.target.log.warning("Parsing additional fields in message '%s' for service %s failed", message, service)
|
260
|
+
self.target.log.debug("", exc_info=e)
|
261
|
+
raise e
|
262
|
+
|
263
|
+
def build_record(self, ts: datetime, source: Path, line: str) -> TargetRecordDescriptor:
|
264
|
+
"""Builds an ``AuthLog`` event record."""
|
265
|
+
|
266
|
+
record_fields = [
|
267
|
+
("datetime", "ts"),
|
268
|
+
("path", "source"),
|
269
|
+
("string", "service"),
|
270
|
+
("varint", "pid"),
|
271
|
+
("string", "message"),
|
272
|
+
]
|
273
|
+
|
274
|
+
record_values = {
|
275
|
+
"ts": ts,
|
276
|
+
"message": line,
|
277
|
+
"service": None,
|
278
|
+
"pid": None,
|
279
|
+
"source": source,
|
280
|
+
"_target": self.target,
|
281
|
+
}
|
282
|
+
|
283
|
+
match = RE_LINE.search(line)
|
284
|
+
if match:
|
285
|
+
record_values.update(match.groupdict())
|
286
|
+
|
287
|
+
for key, value in self._parse_additional_fields(record_values["service"], line).items():
|
288
|
+
record_type = "string"
|
289
|
+
if isinstance(value, int):
|
290
|
+
record_type = "varint"
|
291
|
+
|
292
|
+
record_fields.append((record_type, key))
|
293
|
+
record_values[key] = value
|
294
|
+
|
295
|
+
# tuple conversion here is needed for lru_cache
|
296
|
+
desc = self._create_event_descriptor(tuple(record_fields))
|
297
|
+
return desc(**record_values)
|
298
|
+
|
299
|
+
def _create_event_descriptor(self, record_fields) -> TargetRecordDescriptor:
|
300
|
+
return TargetRecordDescriptor(self.RECORD_NAME, record_fields)
|
22
301
|
|
23
302
|
|
24
303
|
class AuthPlugin(Plugin):
|
25
|
-
"""Unix
|
304
|
+
"""Unix authentication log plugin."""
|
305
|
+
|
306
|
+
def __init__(self, target: Target):
|
307
|
+
super().__init__(target)
|
308
|
+
self._auth_log_builder = AuthLogRecordBuilder(target)
|
26
309
|
|
27
310
|
def check_compatible(self) -> None:
|
28
311
|
var_log = self.target.fs.path("/var/log")
|
29
312
|
if not any(var_log.glob("auth.log*")) and not any(var_log.glob("secure*")):
|
30
313
|
raise UnsupportedPluginError("No auth log files found")
|
31
314
|
|
32
|
-
@
|
33
|
-
|
34
|
-
|
35
|
-
|
315
|
+
@alias("securelog")
|
316
|
+
@export(record=DynamicDescriptor(["datetime", "path", "string"]))
|
317
|
+
def authlog(self) -> Iterator[Any]:
|
318
|
+
"""Yield contents of ``/var/log/auth.log*`` and ``/var/log/secure*`` files.
|
319
|
+
|
320
|
+
Order of returned events is not guaranteed to be chronological because of year
|
321
|
+
rollover detection efforts for log files without a year in the timestamp.
|
322
|
+
|
323
|
+
The following timestamp formats are recognised automatically. This plugin
|
324
|
+
assumes that no custom ``date_format`` template is set in ``syslog-ng`` or ``systemd``
|
325
|
+
configuration (defaults to ``M d H:M:S``).
|
326
|
+
|
327
|
+
ISO formatted authlog entries are parsed as can be found in Ubuntu 24.04 and later.
|
36
328
|
|
37
|
-
|
38
|
-
def authlog(self) -> Iterator[AuthLogRecord]:
|
39
|
-
"""Return contents of /var/log/auth.log* and /var/log/secure*."""
|
329
|
+
.. code-block:: text
|
40
330
|
|
41
|
-
|
42
|
-
|
43
|
-
|
331
|
+
CentOS format: Jan 12 13:37:00 hostname daemon: message
|
332
|
+
Debian format: Jan 12 13:37:00 hostname daemon[pid]: pam_unix(daemon:session): message
|
333
|
+
Ubuntu 24.04: 2024-01-12T13:37:00.000000+02:00 hostname daemon[pid]: pam_unix(daemon:session): message
|
334
|
+
|
335
|
+
Resources:
|
336
|
+
- https://help.ubuntu.com/community/LinuxLogFiles
|
337
|
+
"""
|
44
338
|
|
45
339
|
tzinfo = self.target.datetime.tzinfo
|
46
340
|
|
47
341
|
var_log = self.target.fs.path("/var/log")
|
48
342
|
for auth_file in chain(var_log.glob("auth.log*"), var_log.glob("secure*")):
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
343
|
+
if is_iso_fmt(auth_file):
|
344
|
+
iterable = iso_readlines(auth_file)
|
345
|
+
else:
|
346
|
+
iterable = year_rollover_helper(auth_file, RE_TS, "%b %d %H:%M:%S", tzinfo)
|
347
|
+
|
348
|
+
for ts, line in iterable:
|
349
|
+
yield self._auth_log_builder.build_record(ts, auth_file, line)
|
350
|
+
|
351
|
+
|
352
|
+
def iso_readlines(file: Path) -> Iterator[tuple[datetime, str]]:
|
353
|
+
"""Iterator reading the provided auth log file in ISO format. Mimics ``year_rollover_helper`` behaviour."""
|
354
|
+
with open_decompress(file, "rt") as fh:
|
355
|
+
for line in fh:
|
356
|
+
if not (match := RE_TS_ISO.match(line)):
|
357
|
+
log.warning("No timestamp found in one of the lines in %s!", file)
|
358
|
+
log.debug("Skipping line: %s", line)
|
359
|
+
continue
|
360
|
+
|
361
|
+
try:
|
362
|
+
ts = datetime.strptime(match[0], "%Y-%m-%dT%H:%M:%S.%f%z")
|
363
|
+
except ValueError as e:
|
364
|
+
log.warning("Unable to parse ISO timestamp in line: %s", line)
|
365
|
+
log.debug("", exc_info=e)
|
366
|
+
continue
|
367
|
+
|
368
|
+
yield ts, line
|
369
|
+
|
370
|
+
|
371
|
+
def is_iso_fmt(file: Path) -> bool:
|
372
|
+
"""Determine if the provided auth log file uses new ISO format logging or not."""
|
373
|
+
return any(itertools.islice(iso_readlines(file), 0, 2))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dissect.target
|
3
|
-
Version: 3.20.
|
3
|
+
Version: 3.20.dev47
|
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
|
@@ -268,7 +268,7 @@ dissect/target/plugins/os/unix/locate/plocate.py,sha256=0p7ibPPrDlQuJNjJbV4rU0fJ
|
|
268
268
|
dissect/target/plugins/os/unix/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
269
269
|
dissect/target/plugins/os/unix/log/atop.py,sha256=zjG5eKS-X0mpBXs-Sg2f7RfQvtjt0T8JcteNd9DB_ok,16361
|
270
270
|
dissect/target/plugins/os/unix/log/audit.py,sha256=rZwxC90Q0FOB5BZxplTJwCTIp0hdVpaps1e3C1fRYaM,3754
|
271
|
-
dissect/target/plugins/os/unix/log/auth.py,sha256=
|
271
|
+
dissect/target/plugins/os/unix/log/auth.py,sha256=9NJvlo7Vbsp_ENJFpKd04PH_sUuOy6ueSBwQqY0MtKo,14546
|
272
272
|
dissect/target/plugins/os/unix/log/journal.py,sha256=xe8p8MM_95uYjFNzNSP5IsoIthJtxwFEDicYR42RYAI,17681
|
273
273
|
dissect/target/plugins/os/unix/log/lastlog.py,sha256=Wr3-2n1-GwckN9mSx-yM55N6_L0PQyx6TGHoEvuc6c0,2515
|
274
274
|
dissect/target/plugins/os/unix/log/messages.py,sha256=XtjZ0a2budgQm_K5JT3fMf7JcjuD0AelcD3zOFN2xpI,5732
|
@@ -378,10 +378,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
|
|
378
378
|
dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
|
379
379
|
dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
|
380
380
|
dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
|
381
|
-
dissect.target-3.20.
|
382
|
-
dissect.target-3.20.
|
383
|
-
dissect.target-3.20.
|
384
|
-
dissect.target-3.20.
|
385
|
-
dissect.target-3.20.
|
386
|
-
dissect.target-3.20.
|
387
|
-
dissect.target-3.20.
|
381
|
+
dissect.target-3.20.dev47.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
|
382
|
+
dissect.target-3.20.dev47.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
383
|
+
dissect.target-3.20.dev47.dist-info/METADATA,sha256=28qUe6AIKvwvWI58yI9VPiy1U86ihuVGMhzR-avNrtc,12897
|
384
|
+
dissect.target-3.20.dev47.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
385
|
+
dissect.target-3.20.dev47.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
|
386
|
+
dissect.target-3.20.dev47.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
387
|
+
dissect.target-3.20.dev47.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.20.dev45.dist-info → dissect.target-3.20.dev47.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|