dissect.target 3.18.dev16__py3-none-any.whl → 3.19__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. dissect/target/filesystem.py +44 -25
  2. dissect/target/filesystems/config.py +32 -21
  3. dissect/target/filesystems/extfs.py +4 -0
  4. dissect/target/filesystems/itunes.py +1 -1
  5. dissect/target/filesystems/tar.py +1 -1
  6. dissect/target/filesystems/zip.py +81 -46
  7. dissect/target/helpers/config.py +22 -7
  8. dissect/target/helpers/configutil.py +69 -5
  9. dissect/target/helpers/cyber.py +4 -2
  10. dissect/target/helpers/fsutil.py +32 -4
  11. dissect/target/helpers/loaderutil.py +26 -7
  12. dissect/target/helpers/network_managers.py +22 -7
  13. dissect/target/helpers/record.py +37 -0
  14. dissect/target/helpers/record_modifier.py +23 -4
  15. dissect/target/helpers/shell_application_ids.py +732 -0
  16. dissect/target/helpers/utils.py +11 -0
  17. dissect/target/loader.py +1 -0
  18. dissect/target/loaders/ab.py +285 -0
  19. dissect/target/loaders/libvirt.py +40 -0
  20. dissect/target/loaders/mqtt.py +14 -1
  21. dissect/target/loaders/tar.py +8 -4
  22. dissect/target/loaders/utm.py +3 -0
  23. dissect/target/loaders/velociraptor.py +6 -6
  24. dissect/target/plugin.py +60 -3
  25. dissect/target/plugins/apps/browser/chrome.py +1 -0
  26. dissect/target/plugins/apps/browser/chromium.py +7 -5
  27. dissect/target/plugins/apps/browser/edge.py +1 -0
  28. dissect/target/plugins/apps/browser/firefox.py +82 -36
  29. dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
  30. dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
  31. dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
  32. dissect/target/plugins/apps/ssh/openssh.py +1 -1
  33. dissect/target/plugins/apps/ssh/ssh.py +177 -0
  34. dissect/target/plugins/apps/texteditor/__init__.py +0 -0
  35. dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
  36. dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
  37. dissect/target/plugins/child/qemu.py +21 -0
  38. dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
  39. dissect/target/plugins/filesystem/unix/capability.py +102 -87
  40. dissect/target/plugins/filesystem/walkfs.py +32 -21
  41. dissect/target/plugins/filesystem/yara.py +144 -23
  42. dissect/target/plugins/general/network.py +82 -0
  43. dissect/target/plugins/general/users.py +14 -10
  44. dissect/target/plugins/os/unix/_os.py +19 -5
  45. dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
  46. dissect/target/plugins/os/unix/esxi/_os.py +29 -23
  47. dissect/target/plugins/os/unix/etc/etc.py +5 -8
  48. dissect/target/plugins/os/unix/history.py +3 -7
  49. dissect/target/plugins/os/unix/linux/_os.py +15 -14
  50. dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
  51. dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
  52. dissect/target/plugins/os/unix/locale.py +17 -6
  53. dissect/target/plugins/os/unix/shadow.py +47 -31
  54. dissect/target/plugins/os/windows/_os.py +4 -4
  55. dissect/target/plugins/os/windows/adpolicy.py +4 -1
  56. dissect/target/plugins/os/windows/catroot.py +1 -11
  57. dissect/target/plugins/os/windows/credential/__init__.py +0 -0
  58. dissect/target/plugins/os/windows/credential/lsa.py +174 -0
  59. dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
  60. dissect/target/plugins/os/windows/defender.py +6 -3
  61. dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
  62. dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
  63. dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
  64. dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
  65. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
  66. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
  67. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
  68. dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
  69. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
  70. dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
  71. dissect/target/plugins/os/windows/jumplist.py +292 -0
  72. dissect/target/plugins/os/windows/lnk.py +96 -93
  73. dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
  74. dissect/target/plugins/os/windows/regf/usb.py +179 -114
  75. dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
  76. dissect/target/plugins/os/windows/wua_history.py +1073 -0
  77. dissect/target/target.py +4 -3
  78. dissect/target/tools/fs.py +53 -15
  79. dissect/target/tools/fsutils.py +243 -0
  80. dissect/target/tools/info.py +11 -4
  81. dissect/target/tools/query.py +2 -2
  82. dissect/target/tools/shell.py +505 -333
  83. dissect/target/tools/utils.py +23 -2
  84. dissect/target/tools/yara.py +65 -0
  85. dissect/target/volumes/md.py +2 -2
  86. {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
  87. {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/RECORD +93 -74
  88. {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
  89. {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
  90. dissect/target/helpers/ssh.py +0 -177
  91. /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
  92. {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
  93. {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
  94. {dissect.target-3.18.dev16.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,17 @@ from dissect.target.helpers import fsutil
13
13
  log = logging.getLogger(__name__)
14
14
 
15
15
 
16
+ def findall(buf: bytes, needle: bytes) -> Iterator[int]:
17
+ offset = 0
18
+ while True:
19
+ offset = buf.find(needle, offset)
20
+ if offset == -1:
21
+ break
22
+
23
+ yield offset
24
+ offset += 1
25
+
26
+
16
27
  class StrEnum(str, Enum):
17
28
  """Sortable and serializible string-based enum"""
18
29
 
dissect/target/loader.py CHANGED
@@ -195,6 +195,7 @@ register("vma", "VmaLoader")
195
195
  register("kape", "KapeLoader")
196
196
  register("tanium", "TaniumLoader")
197
197
  register("itunes", "ITunesLoader")
198
+ register("ab", "AndroidBackupLoader")
198
199
  register("target", "TargetLoader")
199
200
  register("log", "LogLoader")
200
201
  # Disabling ResLoader because of DIS-536
@@ -0,0 +1,285 @@
1
+ import hashlib
2
+ import io
3
+ import posixpath
4
+ import shutil
5
+ import struct
6
+ from pathlib import Path
7
+ from typing import BinaryIO
8
+
9
+ try:
10
+ from Crypto.Cipher import AES
11
+
12
+ HAS_PYCRYPTODOME = True
13
+ except ImportError:
14
+ HAS_PYCRYPTODOME = False
15
+
16
+ from dissect.util.stream import AlignedStream, RelativeStream, ZlibStream
17
+
18
+ from dissect.target.exceptions import LoaderError
19
+ from dissect.target.filesystem import VirtualFilesystem
20
+ from dissect.target.filesystems.tar import TarFilesystem
21
+ from dissect.target.helpers import keychain
22
+ from dissect.target.loader import Loader
23
+ from dissect.target.plugins.os.unix.linux.android._os import AndroidPlugin
24
+ from dissect.target.target import Target
25
+
26
+ DIRECTORY_MAPPING = {
27
+ "a": "/data/app/{id}",
28
+ "f": "/data/data/{id}/files",
29
+ "db": "/data/data/{id}/databases",
30
+ "ef": "/storage/emulated/0/Android/data/{id}",
31
+ "sp": "/data/data/{id}/shared_preferences",
32
+ "r": "/data/data/{id}",
33
+ "obb": "/storage/emulated/0/Android/obb/{id}",
34
+ }
35
+
36
+
37
+ class AndroidBackupLoader(Loader):
38
+ """Load Android backup files.
39
+
40
+ References:
41
+ - http://fileformats.archiveteam.org/wiki/Android_ADB_Backup
42
+ """
43
+
44
+ def __init__(self, path: Path, **kwargs):
45
+ super().__init__(path)
46
+ self.ab = AndroidBackup(path.open("rb"))
47
+
48
+ if self.ab.encrypted:
49
+ for key in keychain.get_keys_for_provider("ab") + keychain.get_keys_without_provider():
50
+ if key.key_type == keychain.KeyType.PASSPHRASE:
51
+ try:
52
+ self.ab.unlock(key.value)
53
+ break
54
+ except ValueError:
55
+ continue
56
+ else:
57
+ raise LoaderError(f"Missing password for encrypted Android Backup: {self.path}")
58
+
59
+ @staticmethod
60
+ def detect(path: Path) -> bool:
61
+ if path.suffix.lower() == ".ab":
62
+ return True
63
+
64
+ if path.is_file():
65
+ # The file extension can be chosen freely so let's also test for the file magic
66
+ with path.open("rb") as fh:
67
+ if fh.read(15) == b"ANDROID BACKUP\n":
68
+ return True
69
+
70
+ return False
71
+
72
+ def map(self, target: Target) -> None:
73
+ if self.ab.compressed or self.ab.encrypted:
74
+ if self.ab.compressed and not self.ab.encrypted:
75
+ word = "compressed"
76
+ elif self.ab.encrypted and not self.ab.compressed:
77
+ word = "encrypted"
78
+ else:
79
+ word = "compressed and encrypted"
80
+
81
+ target.log.warning(
82
+ f"Backup file is {word}, consider unwrapping with "
83
+ "`python -m dissect.target.loaders.ab <path/to/backup.ab>`"
84
+ )
85
+
86
+ vfs = VirtualFilesystem(case_sensitive=False)
87
+
88
+ fs = TarFilesystem(self.ab.open())
89
+ for app in fs.path("/apps").iterdir():
90
+ for subdir in app.iterdir():
91
+ if subdir.name not in DIRECTORY_MAPPING:
92
+ continue
93
+
94
+ path = DIRECTORY_MAPPING[subdir.name].format(id=app.name)
95
+
96
+ # TODO: Remove once we move towards "directory entries"
97
+ entry = subdir.get()
98
+ entry.name = posixpath.basename(path)
99
+
100
+ vfs.map_file_entry(path, entry)
101
+
102
+ target.filesystems.add(vfs)
103
+ target._os_plugin = AndroidPlugin.create(target, vfs)
104
+
105
+
106
+ class AndroidBackup:
107
+ def __init__(self, fh: BinaryIO):
108
+ self.fh = fh
109
+
110
+ size = fh.seek(0, io.SEEK_END)
111
+ fh.seek(0)
112
+
113
+ # Don't readline() straight away as we may be reading something other than a backup file
114
+ magic = fh.read(15)
115
+ if magic != b"ANDROID BACKUP\n":
116
+ raise ValueError("Not a valid Android Backup file")
117
+
118
+ self.version = int(fh.read(2)[:1])
119
+ self.compressed = bool(int(fh.read(2)[:1]))
120
+
121
+ self.encrypted = False
122
+ self.unlocked = True
123
+ self.encryption = fh.readline().strip().decode()
124
+
125
+ if self.encryption != "none":
126
+ self.encrypted = True
127
+ self.unlocked = False
128
+ self._user_salt = bytes.fromhex(fh.readline().strip().decode())
129
+ self._ck_salt = bytes.fromhex(fh.readline().strip().decode())
130
+ self._rounds = int(fh.readline().strip())
131
+ self._user_iv = bytes.fromhex(fh.readline().strip().decode())
132
+ self._master_key = bytes.fromhex(fh.readline().strip().decode())
133
+
134
+ self._mk = None
135
+ self._iv = None
136
+
137
+ self._data_offset = fh.tell()
138
+ self.size = size - self._data_offset
139
+
140
+ def unlock(self, password: str) -> None:
141
+ if not self.encrypted:
142
+ raise ValueError("Android Backup is not encrypted")
143
+
144
+ self._mk, self._iv = self._decrypt_mk(password)
145
+ self.unlocked = True
146
+
147
+ def _decrypt_mk(self, password: str) -> tuple[bytes, bytes]:
148
+ user_key = hashlib.pbkdf2_hmac("sha1", password.encode(), self._user_salt, self._rounds, 32)
149
+
150
+ blob = AES.new(user_key, AES.MODE_CBC, iv=self._user_iv).decrypt(self._master_key)
151
+ blob = blob[: -blob[-1]]
152
+
153
+ offset = 0
154
+ iv_len = blob[offset]
155
+ offset += 1
156
+ iv = blob[offset : offset + iv_len]
157
+
158
+ offset += iv_len
159
+ mk_len = blob[offset]
160
+ offset += 1
161
+ mk = blob[offset : offset + mk_len]
162
+
163
+ offset += mk_len
164
+ checksum_len = blob[offset]
165
+ offset += 1
166
+ checksum = blob[offset : offset + checksum_len]
167
+
168
+ ck_mk = _encode_bytes(mk) if self.version >= 2 else mk
169
+ our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32)
170
+ if our_checksum != checksum:
171
+ # Try reverse encoding for good measure
172
+ ck_mk = mk if self.version >= 2 else _encode_bytes(mk)
173
+ our_checksum = hashlib.pbkdf2_hmac("sha1", ck_mk, self._ck_salt, self._rounds, 32)
174
+
175
+ if our_checksum != checksum:
176
+ raise ValueError("Invalid password: master key checksum does not match")
177
+
178
+ return mk, iv
179
+
180
+ def open(self) -> BinaryIO:
181
+ fh = RelativeStream(self.fh, self._data_offset)
182
+
183
+ if self.encrypted:
184
+ if not self.unlocked:
185
+ raise ValueError("Missing password for encrypted Android Backup")
186
+ fh = CipherStream(fh, self._mk, self._iv, self.size)
187
+
188
+ if self.compressed:
189
+ fh = ZlibStream(fh)
190
+
191
+ return fh
192
+
193
+
194
+ class CipherStream(AlignedStream):
195
+ """Transparently AES-CBC decrypted stream."""
196
+
197
+ def __init__(self, fh: BinaryIO, key: bytes, iv: bytes, size: int):
198
+ self._fh = fh
199
+
200
+ self._key = key
201
+ self._iv = iv
202
+ self._cipher = None
203
+ self._cipher_offset = 0
204
+ self._reset_cipher()
205
+
206
+ super().__init__(size)
207
+
208
+ def _reset_cipher(self) -> None:
209
+ self._cipher = AES.new(self._key, AES.MODE_CBC, iv=self._iv)
210
+ self._cipher_offset = 0
211
+
212
+ def _seek_cipher(self, offset: int) -> None:
213
+ """CBC is dependent on previous blocks so to seek the cipher, decrypt and discard to the wanted offset."""
214
+ if offset < self._cipher_offset:
215
+ self._reset_cipher()
216
+ self._fh.seek(0)
217
+
218
+ while self._cipher_offset < offset:
219
+ read_size = min(offset - self._cipher_offset, self.align)
220
+ self._cipher.decrypt(self._fh.read(read_size))
221
+ self._cipher_offset += read_size
222
+
223
+ def _read(self, offset: int, length: int) -> bytes:
224
+ self._seek_cipher(offset)
225
+
226
+ data = self._cipher.decrypt(self._fh.read(length))
227
+ if offset + length >= self.size:
228
+ # Remove padding
229
+ data = data[: -data[-1]]
230
+
231
+ self._cipher_offset += len(data)
232
+
233
+ return data
234
+
235
+
236
+ def _encode_bytes(buf: bytes) -> bytes:
237
+ # Emulate byte[] -> char[] -> utf8 byte[] casting
238
+ return struct.pack(">32h", *struct.unpack(">32b", buf)).decode("utf-16-be").encode("utf-8")
239
+
240
+
241
+ def main() -> None:
242
+ import argparse
243
+
244
+ parser = argparse.ArgumentParser(description="Android Backup file unwrapper")
245
+ parser.add_argument("path", type=Path, help="source path")
246
+ parser.add_argument("-p", "--password", help="encryption password")
247
+ parser.add_argument("-t", "--tar", action="store_true", help="write a tar file instead of a plain Android Backup")
248
+ parser.add_argument("-o", "--output", type=Path, help="output path")
249
+ args = parser.parse_args()
250
+
251
+ if not args.path.is_file():
252
+ parser.exit("source path does not exist or is not a file")
253
+
254
+ ext = ".tar" if args.tar else ".plain.ab"
255
+ if args.output is None:
256
+ output = args.path.with_suffix(ext)
257
+ elif args.output.is_dir():
258
+ output = args.output.joinpath(args.path.name).with_suffix(ext)
259
+ else:
260
+ output = args.output
261
+
262
+ if output.exists():
263
+ parser.exit(f"output path already exists: {output}")
264
+
265
+ print(f"unwrapping {args.path} -> {output}")
266
+ with args.path.open("rb") as fh:
267
+ ab = AndroidBackup(fh)
268
+
269
+ if ab.encrypted:
270
+ if not args.password:
271
+ parser.exit("missing password for encrypted Android Backup")
272
+ ab.unlock(args.password)
273
+
274
+ with ab.open() as fhab, output.open("wb") as fhout:
275
+ if not args.tar:
276
+ fhout.write(b"ANDROID BACKUP\n") # header
277
+ fhout.write(b"5\n") # version
278
+ fhout.write(b"0\n") # compressed
279
+ fhout.write(b"none\n") # encryption
280
+
281
+ shutil.copyfileobj(fhab, fhout, 1024 * 1024 * 64)
282
+
283
+
284
+ if __name__ == "__main__":
285
+ main()
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+
3
+ from defusedxml import ElementTree
4
+
5
+ from dissect.target import container
6
+ from dissect.target.helpers import fsutil
7
+ from dissect.target.loader import Loader
8
+ from dissect.target.target import Target
9
+
10
+
11
+ class LibvirtLoader(Loader):
12
+ """Load libvirt xml configuration files."""
13
+
14
+ def __init__(self, path: Path, **kwargs):
15
+ path = path.resolve()
16
+ self.base_dir = path.parent
17
+ super().__init__(path)
18
+
19
+ @staticmethod
20
+ def detect(path: Path) -> bool:
21
+ if path.suffix.lower() != ".xml":
22
+ return False
23
+
24
+ with path.open("rb") as fh:
25
+ lines = fh.read(512).split(b"\n")
26
+ # From what I've seen, these are are always at the start of the file
27
+ # If its generated using virt-install
28
+ needles = [b"<domain", b"<name>", b"<uuid>"]
29
+ return all(any(needle in line for line in lines) for needle in needles)
30
+
31
+ def map(self, target: Target) -> None:
32
+ xml_data = ElementTree.fromstring(self.path.read_text())
33
+ for disk in xml_data.findall("devices/disk/source"):
34
+ if not (file := disk.get("file")):
35
+ continue
36
+
37
+ for part in [fsutil.basename(file), file]:
38
+ if (path := self.base_dir.joinpath(part)).exists():
39
+ target.disks.add(container.open(path))
40
+ break
@@ -10,6 +10,7 @@ import time
10
10
  import urllib
11
11
  from dataclasses import dataclass
12
12
  from functools import lru_cache
13
+ from getpass import getpass
13
14
  from pathlib import Path
14
15
  from struct import pack, unpack_from
15
16
  from threading import Thread
@@ -270,19 +271,25 @@ class Broker:
270
271
  case = None
271
272
  bytes_received = 0
272
273
  monitor = False
274
+ username = None
275
+ password = None
273
276
 
274
277
  diskinfo = {}
275
278
  index = {}
276
279
  topo = {}
277
280
  factor = 1
278
281
 
279
- def __init__(self, broker: Broker, port: str, key: str, crt: str, ca: str, case: str, **kwargs):
282
+ def __init__(
283
+ self, broker: Broker, port: str, key: str, crt: str, ca: str, case: str, username: str, password: str, **kwargs
284
+ ):
280
285
  self.broker_host = broker
281
286
  self.broker_port = int(port)
282
287
  self.private_key_file = key
283
288
  self.certificate_file = crt
284
289
  self.cacert_file = ca
285
290
  self.case = case
291
+ self.username = username
292
+ self.password = password
286
293
  self.command = kwargs.get("command", None)
287
294
 
288
295
  def clear_cache(self) -> None:
@@ -393,6 +400,7 @@ class Broker:
393
400
  tls_version=ssl.PROTOCOL_TLS,
394
401
  ciphers=None,
395
402
  )
403
+ self.mqtt_client.username_pw_set(self.username, self.password)
396
404
  self.mqtt_client.tls_insecure_set(True) # merely having the correct cert is ok
397
405
  self.mqtt_client.on_connect = self._on_connect
398
406
  self.mqtt_client.on_message = self._on_message
@@ -411,6 +419,8 @@ class Broker:
411
419
  @arg("--mqtt-ca", dest="ca", help="certificate authority file")
412
420
  @arg("--mqtt-command", dest="command", help="direct command to client(s)")
413
421
  @arg("--mqtt-diag", action="store_true", dest="diag", help="show MQTT diagnostic information")
422
+ @arg("--mqtt-username", dest="username", help="Username for connection")
423
+ @arg("--mqtt-password", action="store_true", dest="password", help="Ask for password before connecting")
414
424
  class MQTTLoader(Loader):
415
425
  """Load remote targets through a broker."""
416
426
 
@@ -435,7 +445,10 @@ class MQTTLoader(Loader):
435
445
  if cls.broker is None:
436
446
  if (uri := kwargs.get("parsed_path")) is None:
437
447
  raise LoaderError("No URI connection details have been passed.")
448
+
438
449
  options = dict(urllib.parse.parse_qsl(uri.query, keep_blank_values=True))
450
+ if options.get("password"):
451
+ options["password"] = getpass()
439
452
  cls.broker = Broker(**options)
440
453
  cls.broker.connect()
441
454
  num_peers = int(options.get("peers", 1))
@@ -1,8 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import re
3
5
  import tarfile
4
6
  from pathlib import Path
5
- from typing import Union
6
7
 
7
8
  from dissect.target import filesystem, target
8
9
  from dissect.target.filesystems.tar import (
@@ -21,22 +22,25 @@ ANON_FS_RE = re.compile(r"^fs[0-9]+$")
21
22
  class TarLoader(Loader):
22
23
  """Load tar files."""
23
24
 
24
- def __init__(self, path: Union[Path, str], **kwargs):
25
+ def __init__(self, path: Path | str, **kwargs):
25
26
  super().__init__(path)
26
27
 
28
+ if isinstance(path, str):
29
+ path = Path(path)
30
+
27
31
  if self.is_compressed(path):
28
32
  log.warning(
29
33
  f"Tar file {path!r} is compressed, which will affect performance. "
30
34
  "Consider uncompressing the archive before passing the tar file to Dissect."
31
35
  )
32
36
 
33
- self.tar = tarfile.open(path)
37
+ self.tar = tarfile.open(fileobj=path.open("rb"))
34
38
 
35
39
  @staticmethod
36
40
  def detect(path: Path) -> bool:
37
41
  return path.name.lower().endswith((".tar", ".tar.gz", ".tgz"))
38
42
 
39
- def is_compressed(self, path: Union[Path, str]) -> bool:
43
+ def is_compressed(self, path: Path | str) -> bool:
40
44
  return str(path).lower().endswith((".tar.gz", ".tgz"))
41
45
 
42
46
  def map(self, target: target.Target) -> None:
@@ -21,6 +21,9 @@ class UtmLoader(Loader):
21
21
  def map(self, target: Target) -> None:
22
22
  data_dir = self.path.joinpath("Data")
23
23
  for drive in self.config.get("Drive", []):
24
+ if drive.get("ImageType") != "Disk":
25
+ continue
26
+
24
27
  path = data_dir.joinpath(drive["ImageName"])
25
28
  try:
26
29
  target.disks.add(container.open(path))
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  import zipfile
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Optional
6
+ from typing import TYPE_CHECKING
7
7
 
8
8
  from dissect.target.loaders.dir import DirLoader, find_dirs, map_dirs
9
9
  from dissect.target.plugin import OperatingSystem
@@ -18,7 +18,7 @@ UNIX_ACCESSORS = ["file", "auto"]
18
18
  WINDOWS_ACCESSORS = ["mft", "ntfs", "lazy_ntfs", "ntfs_vss", "auto"]
19
19
 
20
20
 
21
- def find_fs_directories(path: Path) -> tuple[Optional[OperatingSystem], Optional[list[Path]]]:
21
+ def find_fs_directories(path: Path) -> tuple[OperatingSystem | None, list[Path] | None]:
22
22
  fs_root = path.joinpath(FILESYSTEMS_ROOT)
23
23
 
24
24
  # Unix
@@ -56,7 +56,7 @@ def find_fs_directories(path: Path) -> tuple[Optional[OperatingSystem], Optional
56
56
  return None, None
57
57
 
58
58
 
59
- def extract_drive_letter(name: str) -> Optional[str]:
59
+ def extract_drive_letter(name: str) -> str | None:
60
60
  # \\.\X: in URL encoding
61
61
  if len(name) == 14 and name.startswith("%5C%5C.%5C") and name.endswith("%3A"):
62
62
  return name[10].lower()
@@ -91,7 +91,7 @@ class VelociraptorLoader(DirLoader):
91
91
  f"Velociraptor target {path!r} is compressed, which will slightly affect performance. "
92
92
  "Consider uncompressing the archive and passing the uncompressed folder to Dissect."
93
93
  )
94
- self.root = zipfile.Path(path)
94
+ self.root = zipfile.Path(path.open("rb"))
95
95
  else:
96
96
  self.root = path
97
97
 
@@ -105,8 +105,8 @@ class VelociraptorLoader(DirLoader):
105
105
  # results/
106
106
  # uploads.json
107
107
  # [...] other files related to the collection
108
- if path.suffix == ".zip": # novermin
109
- path = zipfile.Path(path)
108
+ if path.exists() and path.suffix == ".zip": # novermin
109
+ path = zipfile.Path(path.open("rb"))
110
110
 
111
111
  if path.joinpath(FILESYSTEMS_ROOT).exists() and path.joinpath("uploads.json").exists():
112
112
  _, dirs = find_fs_directories(path)
dissect/target/plugin.py CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  See dissect/target/plugins/general/example.py for an example plugin.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import fnmatch
9
+ import functools
8
10
  import importlib
9
11
  import importlib.util
10
12
  import inspect
@@ -140,7 +142,7 @@ def get_nonprivate_attributes(cls: Type[Plugin]) -> list[Any]:
140
142
 
141
143
  def get_nonprivate_methods(cls: Type[Plugin]) -> list[Callable]:
142
144
  """Retrieve all public methods of a :class:`Plugin`."""
143
- return [attr for attr in get_nonprivate_attributes(cls) if not isinstance(attr, property)]
145
+ return [attr for attr in get_nonprivate_attributes(cls) if not isinstance(attr, property) and callable(attr)]
144
146
 
145
147
 
146
148
  def get_descriptors_on_nonprivate_methods(cls: Type[Plugin]) -> list[RecordDescriptor]:
@@ -196,6 +198,8 @@ class Plugin:
196
198
  The :func:`internal` decorator and :class:`InternalPlugin` set the ``__internal__`` attribute.
197
199
  Finally. :func:`args` decorator sets the ``__args__`` attribute.
198
200
 
201
+ The :func:`alias` decorator populates the ``__aliases__`` private attribute of :class:`Plugin` methods.
202
+
199
203
  Args:
200
204
  target: The :class:`~dissect.target.target.Target` object to load the plugin for.
201
205
  """
@@ -448,6 +452,11 @@ def register(plugincls: Type[Plugin]) -> None:
448
452
  exports = []
449
453
  functions = []
450
454
 
455
+ # First pass to resolve aliases
456
+ for attr in get_nonprivate_attributes(plugincls):
457
+ for alias in getattr(attr, "__aliases__", []):
458
+ clone_alias(plugincls, attr, alias)
459
+
451
460
  for attr in get_nonprivate_attributes(plugincls):
452
461
  if isinstance(attr, property):
453
462
  attr = attr.fget
@@ -542,6 +551,47 @@ def arg(*args, **kwargs) -> Callable:
542
551
  return decorator
543
552
 
544
553
 
554
+ def alias(*args, **kwargs: dict[str, Any]) -> Callable:
555
+ """Decorator to be used on :class:`Plugin` functions to register an alias of that function."""
556
+
557
+ if not kwargs.get("name") and not args:
558
+ raise ValueError("Missing argument 'name'")
559
+
560
+ def decorator(obj: Callable) -> Callable:
561
+ if not hasattr(obj, "__aliases__"):
562
+ obj.__aliases__ = []
563
+
564
+ if name := (kwargs.get("name") or args[0]):
565
+ obj.__aliases__.append(name)
566
+
567
+ return obj
568
+
569
+ return decorator
570
+
571
+
572
+ def clone_alias(cls: type, attr: Callable, alias: str) -> None:
573
+ """Clone the given attribute to an alias in the provided class."""
574
+
575
+ # Clone the function object
576
+ clone = type(attr)(attr.__code__, attr.__globals__, alias, attr.__defaults__, attr.__closure__)
577
+ clone.__kwdefaults__ = attr.__kwdefaults__
578
+
579
+ # Copy some attributes
580
+ functools.update_wrapper(clone, attr)
581
+ if wrapped := getattr(attr, "__wrapped__", None):
582
+ # update_wrapper sets a new wrapper, we want the original
583
+ clone.__wrapped__ = wrapped
584
+
585
+ # Update module path so we can fool inspect.getmodule with subclassed Plugin classes
586
+ clone.__module__ = cls.__module__
587
+
588
+ # Update the names
589
+ clone.__name__ = alias
590
+ clone.__qualname__ = f"{cls.__name__}.{alias}"
591
+
592
+ setattr(cls, alias, clone)
593
+
594
+
545
595
  def plugins(
546
596
  osfilter: Optional[type[OSPlugin]] = None,
547
597
  special_keys: set[str] = set(),
@@ -970,7 +1020,7 @@ class NamespacePlugin(Plugin):
970
1020
  continue
971
1021
 
972
1022
  # The method needs to output records
973
- if getattr(subplugin_func, "__output__", None) != "record":
1023
+ if getattr(subplugin_func, "__output__", None) not in ["record", "yield"]:
974
1024
  continue
975
1025
 
976
1026
  # The method may not be part of a parent class.
@@ -1056,6 +1106,9 @@ class NamespacePlugin(Plugin):
1056
1106
  cls.__init_subclass_namespace__(cls, **kwargs)
1057
1107
 
1058
1108
 
1109
+ __COMMON_PLUGIN_METHOD_NAMES__ = {attr.__name__ for attr in get_nonprivate_methods(Plugin)}
1110
+
1111
+
1059
1112
  class InternalPlugin(Plugin):
1060
1113
  """Parent class for internal plugins.
1061
1114
 
@@ -1065,13 +1118,17 @@ class InternalPlugin(Plugin):
1065
1118
 
1066
1119
  def __init_subclass__(cls, **kwargs):
1067
1120
  for method in get_nonprivate_methods(cls):
1068
- if callable(method):
1121
+ if callable(method) and method.__name__ not in __COMMON_PLUGIN_METHOD_NAMES__:
1069
1122
  method.__internal__ = True
1070
1123
 
1071
1124
  super().__init_subclass__(**kwargs)
1072
1125
  return cls
1073
1126
 
1074
1127
 
1128
+ class InternalNamespacePlugin(NamespacePlugin, InternalPlugin):
1129
+ pass
1130
+
1131
+
1075
1132
  @dataclass(frozen=True, eq=True)
1076
1133
  class PluginFunction:
1077
1134
  name: str
@@ -25,6 +25,7 @@ class ChromePlugin(ChromiumMixin, BrowserPlugin):
25
25
  DIRS = [
26
26
  # Windows
27
27
  "AppData/Local/Google/Chrome/User Data/Default",
28
+ "AppData/Local/Google/Chrome/User Data/Snapshots/*/Default",
28
29
  "AppData/Local/Google/Chrome/continuousUpdates/User Data/Default",
29
30
  "Local Settings/Application Data/Google/Chrome/User Data/Default",
30
31
  # Linux
@@ -79,11 +79,12 @@ class ChromiumMixin:
79
79
  users_dirs: list[tuple] = []
80
80
  for user_details in self.target.user_details.all_with_home():
81
81
  for d in hist_paths:
82
- cur_dir: TargetPath = user_details.home_path.joinpath(d)
83
- cur_dir = cur_dir.resolve()
84
- if not cur_dir.exists() or (user_details, cur_dir) in users_dirs:
85
- continue
86
- users_dirs.append((user_details, cur_dir))
82
+ home_dir: TargetPath = user_details.home_path
83
+ for cur_dir in home_dir.glob(d):
84
+ cur_dir = cur_dir.resolve()
85
+ if not cur_dir.exists() or (user_details.user, cur_dir) in users_dirs:
86
+ continue
87
+ users_dirs.append((user_details, cur_dir))
87
88
  return users_dirs
88
89
 
89
90
  def _iter_db(
@@ -278,6 +279,7 @@ class ChromiumMixin:
278
279
  is_http_only=bool(cookie.is_httponly),
279
280
  same_site=bool(cookie.samesite),
280
281
  source=db_file,
282
+ _target=self.target,
281
283
  _user=user.user,
282
284
  )
283
285
  except SQLError as e:
@@ -28,6 +28,7 @@ class EdgePlugin(ChromiumMixin, BrowserPlugin):
28
28
  ".var/app/com.microsoft.Edge/config/microsoft-edge/Default",
29
29
  # Windows
30
30
  "AppData/Local/Microsoft/Edge/User Data/Default",
31
+ "AppData/Local/Microsoft/Edge/User Data/Snapshots/*/Default",
31
32
  # Macos
32
33
  "Library/Application Support/Microsoft Edge/Default",
33
34
  ]