dissect.target 3.19.dev40__py3-none-any.whl → 3.19.dev42__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.
@@ -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
 
@@ -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:
@@ -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
@@ -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(),
@@ -8,7 +8,7 @@ from dissect.target.exceptions import UnsupportedPluginError
8
8
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
9
9
  from dissect.target.helpers.fsutil import TargetPath
10
10
  from dissect.target.helpers.record import UnixUserRecord, create_extended_descriptor
11
- from dissect.target.plugin import Plugin, export, internal
11
+ from dissect.target.plugin import Plugin, alias, export, internal
12
12
 
13
13
  CommandHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
14
14
  "unix/history",
@@ -36,6 +36,7 @@ class CommandHistoryPlugin(Plugin):
36
36
  ("sqlite", ".sqlite_history"),
37
37
  ("zsh", ".zsh_history"),
38
38
  ("ash", ".ash_history"),
39
+ ("dissect", ".dissect_history"), # wow so meta
39
40
  )
40
41
 
41
42
  def __init__(self, target: Target):
@@ -56,12 +57,7 @@ class CommandHistoryPlugin(Plugin):
56
57
  history_files.append((shell, history_path, user_details.user))
57
58
  return history_files
58
59
 
59
- @export(record=CommandHistoryRecord)
60
- def bashhistory(self):
61
- """Deprecated, use commandhistory function."""
62
- self.target.log.warn("Function 'bashhistory' is deprecated, use the 'commandhistory' function instead.")
63
- return self.commandhistory()
64
-
60
+ @alias("bashhistory")
65
61
  @export(record=CommandHistoryRecord)
66
62
  def commandhistory(self):
67
63
  """Return shell history for all users.
@@ -5,6 +5,7 @@ from flow.record.fieldtypes import digest
5
5
 
6
6
  from dissect.target.exceptions import UnsupportedPluginError
7
7
  from dissect.target.helpers.record import TargetRecordDescriptor
8
+ from dissect.target.helpers.utils import findall
8
9
  from dissect.target.plugin import Plugin, export
9
10
 
10
11
  try:
@@ -36,17 +37,6 @@ CatrootRecord = TargetRecordDescriptor(
36
37
  )
37
38
 
38
39
 
39
- def findall(buf: bytes, needle: bytes) -> Iterator[int]:
40
- offset = 0
41
- while True:
42
- offset = buf.find(needle, offset)
43
- if offset == -1:
44
- break
45
-
46
- yield offset
47
- offset += 1
48
-
49
-
50
40
  def _get_package_name(sequence: Sequence) -> str:
51
41
  """Parse sequences within a sequence and return the 'PackageName' value if it exists."""
52
42
  for value in sequence.native.values():
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import logging
5
+ from struct import error as StructError
6
+ from typing import BinaryIO, Iterator
7
+
8
+ from dissect.cstruct import cstruct
9
+ from dissect.ole import OLE
10
+ from dissect.ole.exceptions import Error as OleError
11
+ from dissect.shellitem.lnk import Lnk
12
+
13
+ from dissect.target import Target
14
+ from dissect.target.exceptions import UnsupportedPluginError
15
+ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
16
+ from dissect.target.helpers.record import create_extended_descriptor
17
+ from dissect.target.helpers.shell_application_ids import APPLICATION_IDENTIFIERS
18
+ from dissect.target.helpers.utils import findall
19
+ from dissect.target.plugin import Plugin, export
20
+ from dissect.target.plugins.os.windows.lnk import LnkRecord, parse_lnk_file
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+ LNK_GUID = b"\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46"
25
+
26
+ JumpListRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
27
+ "windows/jumplist",
28
+ [
29
+ ("string", "type"),
30
+ ("string", "application_id"),
31
+ ("string", "application_name"),
32
+ *LnkRecord.target_fields,
33
+ ],
34
+ )
35
+
36
+
37
+ custom_destination_def = """
38
+ struct header {
39
+ int version;
40
+ int unknown1;
41
+ int unknown2;
42
+ int value_type;
43
+ }
44
+
45
+ struct header_end {
46
+ int number_of_entries;
47
+ }
48
+
49
+ struct header_end_0 {
50
+ uint16 name_length;
51
+ wchar name[name_length];
52
+ int number_of_entries;
53
+ }
54
+
55
+ struct footer {
56
+ char magic[4];
57
+ }
58
+ """
59
+
60
+ c_custom_destination = cstruct()
61
+ c_custom_destination.load(custom_destination_def)
62
+
63
+
64
+ class JumpListFile:
65
+ def __init__(self, fh: BinaryIO, file_name: str):
66
+ self.fh = fh
67
+ self.file_name = file_name
68
+
69
+ self.application_id, self.application_type = file_name.split(".")
70
+ self.application_type = self.application_type.split("-")[0]
71
+ self.application_name = APPLICATION_IDENTIFIERS.get(self.application_id)
72
+
73
+ def __iter__(self) -> Iterator[Lnk]:
74
+ raise NotImplementedError
75
+
76
+ @property
77
+ def name(self) -> str:
78
+ """Return the name of the application."""
79
+ return self.application_name
80
+
81
+ @property
82
+ def id(self) -> str:
83
+ """Return the application identifier."""
84
+ return self.application_id
85
+
86
+ @property
87
+ def type(self) -> str:
88
+ """Return the type of the Jump List file."""
89
+ return self.application_type
90
+
91
+
92
+ class AutomaticDestinationFile(JumpListFile):
93
+ """Parse Jump List AutomaticDestination file."""
94
+
95
+ def __init__(self, fh: BinaryIO, file_name: str):
96
+ super().__init__(fh, file_name)
97
+ self.ole = OLE(self.fh)
98
+
99
+ def __iter__(self) -> Iterator[Lnk]:
100
+ for dir_name in self.ole.root.listdir():
101
+ if dir_name == "DestList":
102
+ continue
103
+
104
+ dir = self.ole.get(dir_name)
105
+
106
+ for item in dir.open():
107
+ try:
108
+ yield Lnk(io.BytesIO(item))
109
+ except StructError:
110
+ continue
111
+ except Exception as e:
112
+ log.warning("Failed to parse LNK file from directory %s", dir_name)
113
+ log.debug("", exc_info=e)
114
+ continue
115
+
116
+
117
+ class CustomDestinationFile(JumpListFile):
118
+ """Parse Jump List CustomDestination file."""
119
+
120
+ MAGIC_FOOTER = 0xBABFFBAB
121
+ VERSIONS = [2]
122
+
123
+ def __init__(self, fh: BinaryIO, file_name: str):
124
+ super().__init__(fh, file_name)
125
+
126
+ self.fh.seek(-4, io.SEEK_END)
127
+ self.footer = c_custom_destination.footer(self.fh.read(4))
128
+ self.magic = int.from_bytes(self.footer.magic, "little")
129
+
130
+ self.fh.seek(0, io.SEEK_SET)
131
+ self.header = c_custom_destination.header(self.fh)
132
+ self.version = self.header.version
133
+
134
+ if self.header.value_type == 0:
135
+ self.header_end = c_custom_destination.header_end_0(self.fh)
136
+ elif self.header.value_type in [1, 2]:
137
+ self.header_end = c_custom_destination.header_end(self.fh)
138
+ else:
139
+ raise NotImplementedError(
140
+ f"The value_type ({self.header.value_type}) of the CustomDestination file is not implemented"
141
+ )
142
+
143
+ if self.version not in self.VERSIONS:
144
+ raise NotImplementedError(f"The CustomDestination file has an unsupported version: {self.version}")
145
+
146
+ if not self.MAGIC_FOOTER == self.magic:
147
+ raise ValueError(f"The CustomDestination file has an invalid magic footer: {self.magic}")
148
+
149
+ def __iter__(self) -> Iterator[Lnk]:
150
+ # Searches for all LNK GUID's because the number of entries in the header is not always correct.
151
+ buf = self.fh.read()
152
+
153
+ for offset in findall(buf, LNK_GUID):
154
+ try:
155
+ lnk = Lnk(io.BytesIO(buf[offset + len(LNK_GUID) :]))
156
+ yield lnk
157
+ except EOFError:
158
+ break
159
+ except Exception as e:
160
+ log.warning("Failed to parse LNK file from a CustomDestination file")
161
+ log.debug("", exc_info=e)
162
+ continue
163
+
164
+
165
+ class JumpListPlugin(Plugin):
166
+ """Jump List is a Windows feature introduced in Windows 7.
167
+
168
+ It stores information about recently accessed applications and files.
169
+
170
+ References:
171
+ - https://forensics.wiki/jump_lists
172
+ - https://github.com/libyal/dtformats/blob/main/documentation/Jump%20lists%20format.asciidoc
173
+ """
174
+
175
+ __namespace__ = "jumplist"
176
+
177
+ def __init__(self, target: Target):
178
+ super().__init__(target)
179
+ self.automatic_destinations = []
180
+ self.custom_destinations = []
181
+
182
+ for user_details in self.target.user_details.all_with_home():
183
+ for destination in user_details.home_path.glob(
184
+ "AppData/Roaming/Microsoft/Windows/Recent/CustomDestinations/*.customDestinations-ms"
185
+ ):
186
+ self.custom_destinations.append([destination, user_details.user])
187
+
188
+ for user_details in self.target.user_details.all_with_home():
189
+ for destination in user_details.home_path.glob(
190
+ "AppData/Roaming/Microsoft/Windows/Recent/AutomaticDestinations/*.automaticDestinations-ms"
191
+ ):
192
+ self.automatic_destinations.append([destination, user_details.user])
193
+
194
+ def check_compatible(self) -> None:
195
+ if not any([self.automatic_destinations, self.custom_destinations]):
196
+ raise UnsupportedPluginError("No Jump List files found")
197
+
198
+ @export(record=JumpListRecord)
199
+ def custom_destination(self) -> Iterator[JumpListRecord]:
200
+ """Return the content of CustomDestination Windows Jump Lists.
201
+
202
+ These are created when a user pins an application or a file in a Jump List.
203
+
204
+ Yields JumpListRecord with fields:
205
+
206
+ .. code-block:: text
207
+
208
+ type (string): Type of Jump List.
209
+ application_id (string): ID of the application.
210
+ application_name (string): Name of the application.
211
+ lnk_path (path): Path of the link (.lnk) file.
212
+ lnk_name (string): Name of the link (.lnk) file.
213
+ lnk_mtime (datetime): Modification time of the link (.lnk) file.
214
+ lnk_atime (datetime): Access time of the link (.lnk) file.
215
+ lnk_ctime (datetime): Creation time of the link (.lnk) file.
216
+ lnk_relativepath (path): Relative path of target file to the link (.lnk) file.
217
+ lnk_workdir (path): Path of the working directory the link (.lnk) file will execute from.
218
+ lnk_iconlocation (path): Path of the display icon used for the link (.lnk) file.
219
+ lnk_arguments (string): Command-line arguments passed to the target (linked) file.
220
+ local_base_path (string): Absolute path of the target (linked) file.
221
+ common_path_suffix (string): Suffix of the local_base_path.
222
+ lnk_full_path (string): Full path of the linked file. Made from local_base_path and common_path_suffix.
223
+ lnk_net_name (string): Specifies a server share path; for example, "\\\\server\\share".
224
+ lnk_device_name (string): Specifies a device; for example, the drive letter "D:"
225
+ machine_id (string): The NetBIOS name of the machine where the linked file was last known to reside.
226
+ target_mtime (datetime): Modification time of the target (linked) file.
227
+ target_atime (datetime): Access time of the target (linked) file.
228
+ target_ctime (datetime): Creation time of the target (linked) file.
229
+ """
230
+ yield from self._generate_records(self.custom_destinations, CustomDestinationFile)
231
+
232
+ @export(record=JumpListRecord)
233
+ def automatic_destination(self) -> Iterator[JumpListRecord]:
234
+ """Return the content of AutomaticDestination Windows Jump Lists.
235
+
236
+ These are created automatically when a user opens an application or file.
237
+
238
+ Yields JumpListRecord with fields:
239
+
240
+ .. code-block:: text
241
+
242
+ type (string): Type of Jump List.
243
+ application_id (string): ID of the application.
244
+ application_name (string): Name of the application.
245
+ lnk_path (path): Path of the link (.lnk) file.
246
+ lnk_name (string): Name of the link (.lnk) file.
247
+ lnk_mtime (datetime): Modification time of the link (.lnk) file.
248
+ lnk_atime (datetime): Access time of the link (.lnk) file.
249
+ lnk_ctime (datetime): Creation time of the link (.lnk) file.
250
+ lnk_relativepath (path): Relative path of target file to the link (.lnk) file.
251
+ lnk_workdir (path): Path of the working directory the link (.lnk) file will execute from.
252
+ lnk_iconlocation (path): Path of the display icon used for the link (.lnk) file.
253
+ lnk_arguments (string): Command-line arguments passed to the target (linked) file.
254
+ local_base_path (string): Absolute path of the target (linked) file.
255
+ common_path_suffix (string): Suffix of the local_base_path.
256
+ lnk_full_path (string): Full path of the linked file. Made from local_base_path and common_path_suffix.
257
+ lnk_net_name (string): Specifies a server share path; for example, "\\\\server\\share".
258
+ lnk_device_name (string): Specifies a device; for example, the drive letter "D:"
259
+ machine_id (string): The NetBIOS name of the machine where the linked file was last known to reside.
260
+ target_mtime (datetime): Modification time of the target (linked) file.
261
+ target_atime (datetime): Access time of the target (linked) file.
262
+ target_ctime (datetime): Creation time of the target (linked) file.
263
+ """
264
+ yield from self._generate_records(self.automatic_destinations, AutomaticDestinationFile)
265
+
266
+ def _generate_records(
267
+ self,
268
+ destinations: list,
269
+ destination_file: AutomaticDestinationFile | CustomDestinationFile,
270
+ ) -> Iterator[JumpListRecord]:
271
+ for destination, user in destinations:
272
+ fh = destination.open("rb")
273
+
274
+ try:
275
+ jumplist = destination_file(fh, destination.name)
276
+ except OleError:
277
+ continue
278
+ except Exception as e:
279
+ self.target.log.warning("Failed to parse Jump List file: %s", destination)
280
+ self.target.log.debug("", exc_info=e)
281
+ continue
282
+
283
+ for lnk in jumplist:
284
+ if lnk := parse_lnk_file(self.target, lnk, destination):
285
+ yield JumpListRecord(
286
+ type=jumplist.type,
287
+ application_name=jumplist.name,
288
+ application_id=jumplist.id,
289
+ **lnk._asdict(),
290
+ _user=user,
291
+ _target=self.target,
292
+ )