dissect.target 3.18.dev15__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 (95) 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/shellbags.py +8 -5
  74. dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
  75. dissect/target/plugins/os/windows/regf/usb.py +179 -114
  76. dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
  77. dissect/target/plugins/os/windows/wua_history.py +1073 -0
  78. dissect/target/target.py +4 -3
  79. dissect/target/tools/fs.py +53 -15
  80. dissect/target/tools/fsutils.py +243 -0
  81. dissect/target/tools/info.py +11 -4
  82. dissect/target/tools/query.py +2 -2
  83. dissect/target/tools/shell.py +505 -333
  84. dissect/target/tools/utils.py +23 -2
  85. dissect/target/tools/yara.py +65 -0
  86. dissect/target/volumes/md.py +2 -2
  87. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
  88. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/RECORD +94 -75
  89. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
  90. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
  91. dissect/target/helpers/ssh.py +0 -177
  92. /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
  93. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
  94. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
  95. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import logging
5
+ from io import BytesIO
1
6
  from pathlib import Path
7
+ from typing import Iterator
8
+
9
+ from dissect.target.helpers import hashutil
2
10
 
3
11
  try:
4
12
  import yara
13
+
14
+ HAS_YARA = True
15
+
5
16
  except ImportError:
6
- raise ImportError("Please install 'yara-python' to use 'target-query -f yara'.")
17
+ HAS_YARA = False
7
18
 
8
- from dissect.target.exceptions import FileNotFoundError
19
+ from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
9
20
  from dissect.target.helpers.record import TargetRecordDescriptor
10
21
  from dissect.target.plugin import Plugin, arg, export
11
22
 
23
+ log = logging.getLogger(__name__)
24
+
12
25
  YaraMatchRecord = TargetRecordDescriptor(
13
26
  "filesystem/yara/match",
14
27
  [
@@ -16,48 +29,156 @@ YaraMatchRecord = TargetRecordDescriptor(
16
29
  ("digest", "digest"),
17
30
  ("string", "rule"),
18
31
  ("string[]", "tags"),
32
+ ("string", "namespace"),
19
33
  ],
20
34
  )
21
35
 
36
+ DEFAULT_MAX_SCAN_SIZE = 10 * 1024 * 1024
37
+
22
38
 
23
39
  class YaraPlugin(Plugin):
24
40
  """Plugin to scan files against a local YARA rules file."""
25
41
 
26
- DEFAULT_MAX_SIZE = 10 * 1024 * 1024
27
-
28
42
  def check_compatible(self) -> None:
29
- pass
43
+ if not HAS_YARA:
44
+ raise UnsupportedPluginError("Please install 'yara-python' to use the yara plugin.")
30
45
 
31
- @arg("--rule-files", "-r", type=Path, nargs="+", required=True, help="path to YARA rule file")
32
- @arg("--scan-path", default="/", help="path to recursively scan")
33
- @arg("--max-size", "-m", default=DEFAULT_MAX_SIZE, help="maximum file size in bytes to scan")
46
+ @arg("-r", "--rules", required=True, nargs="*", help="path(s) to YARA rule file(s) or folder(s)")
47
+ @arg("-p", "--path", default="/", help="path on target(s) to recursively scan")
48
+ @arg("-m", "--max-size", type=int, default=DEFAULT_MAX_SCAN_SIZE, help="maximum file size in bytes to scan")
49
+ @arg("-c", "--check", default=False, action="store_true", help="check if every YARA rule is valid")
34
50
  @export(record=YaraMatchRecord)
35
- def yara(self, rule_files, scan_path="/", max_size=DEFAULT_MAX_SIZE):
36
- """Scan files up to a given maximum size with a local YARA rule file.
51
+ def yara(
52
+ self,
53
+ rules: list[str | Path],
54
+ path: str = "/",
55
+ max_size: int = DEFAULT_MAX_SCAN_SIZE,
56
+ check: bool = False,
57
+ ) -> Iterator[YaraMatchRecord]:
58
+ """Scan files inside the target up to a given maximum size with YARA rule file(s).
59
+
60
+ Args:
61
+ rules: ``list`` of strings or ``Path`` objects pointing to rule files to use.
62
+ path: ``string`` of absolute target path to scan.
63
+ max_size: Files larger than this size will not be scanned.
64
+ check: Check if provided rules are valid, only compiles valid rules.
37
65
 
38
- Example:
39
- target-query <TARGET> -f yara --rule-file /path/to/yara_sigs.rule
66
+ Returns:
67
+ Iterator yields ``YaraMatchRecord``.
40
68
  """
41
69
 
42
- rule_data = "\n".join([rule_file.read_text() for rule_file in rule_files])
70
+ compiled_rules = process_rules(rules, check)
43
71
 
44
- rules = yara.compile(source=rule_data)
45
- for _, _, files in self.target.fs.walk_ext(scan_path):
46
- for file_entry in files:
47
- path = self.target.fs.path(file_entry.path)
72
+ if not rules:
73
+ self.target.log.error("No working rules found in '%s'", ",".join(rules))
74
+ return
75
+
76
+ if hasattr(compiled_rules, "warnings") and (num_warns := len(compiled_rules.warnings)) > 0:
77
+ self.target.log.warning("YARA generated %s warnings while compiling rules", num_warns)
78
+ for warning in compiled_rules.warnings:
79
+ self.target.log.info(warning)
80
+
81
+ self.target.log.warning("Will not scan files larger than %s MB", max_size // 1024 // 1024)
82
+
83
+ for _, _, files in self.target.fs.walk_ext(path):
84
+ for file in files:
48
85
  try:
49
- if path.stat().st_size > max_size:
86
+ if (file_size := file.stat().st_size) > max_size:
87
+ self.target.log.info("Not scanning file of %s MB: '%s'", (file_size // 1024 // 1024), file)
50
88
  continue
51
89
 
52
- for match in rules.match(data=path.read_bytes()):
90
+ buf = file.open().read()
91
+ for match in compiled_rules.match(data=buf):
53
92
  yield YaraMatchRecord(
54
- path=path,
55
- digest=path.get().hash(),
93
+ path=self.target.fs.path(file.path),
94
+ digest=hashutil.common(BytesIO(buf)),
56
95
  rule=match.rule,
57
96
  tags=match.tags,
97
+ namespace=match.namespace,
58
98
  _target=self.target,
59
99
  )
100
+
60
101
  except FileNotFoundError:
61
102
  continue
62
- except Exception:
63
- self.target.log.exception("Error scanning file: %s", path)
103
+ except RuntimeWarning as e:
104
+ self.target.log.warning("Runtime warning while scanning file '%s': %s", file, e)
105
+ except Exception as e:
106
+ self.target.log.error("Exception scanning file '%s'", file)
107
+ self.target.log.debug("", exc_info=e)
108
+
109
+
110
+ def process_rules(paths: list[str | Path], check: bool = False) -> yara.Rules | None:
111
+ """Generate compiled YARA rules from the given path(s).
112
+
113
+ Provide path to one (compiled) YARA file or directory containing YARA files.
114
+
115
+ Args:
116
+ paths: Path to file(s) or folder(s) containing YARA files.
117
+ check: Attempt to compile every rule file before appending to rules.
118
+
119
+ Returns:
120
+ Compiled YARA rules or None.
121
+ """
122
+ files = set()
123
+ compiled_rules = None
124
+
125
+ for rules_path in paths:
126
+ if isinstance(rules_path, str):
127
+ rules_path = Path(rules_path)
128
+
129
+ if not rules_path.exists():
130
+ log.warning("File %s does not exist!", rules_path)
131
+ continue
132
+
133
+ if rules_path.is_dir():
134
+ for file in rules_path.rglob("*"):
135
+ if not file.is_file():
136
+ continue
137
+ files.add(file)
138
+ else:
139
+ files.add(rules_path)
140
+
141
+ for file in set(files):
142
+ with file.open("rb") as fh:
143
+ magic = fh.read(4)
144
+
145
+ if magic == b"YARA":
146
+ if len(files) > 1:
147
+ log.error("Providing multiple compiled YARA files is not supported. Did not add %s", file)
148
+ continue
149
+ else:
150
+ log.info("Adding single compiled YARA file %s", file)
151
+ compiled_rules = compile_yara(file, is_compiled=True)
152
+ break
153
+
154
+ elif check and not is_valid_yara({"check_namespace": file}):
155
+ log.warning("File %s contains invalid rule(s)!", file)
156
+ files.remove(file)
157
+ continue
158
+
159
+ if files and not compiled_rules:
160
+ try:
161
+ compiled_rules = compile_yara({hashlib.md5(file.as_posix().encode()).hexdigest(): file for file in files})
162
+ except yara.Error as e:
163
+ log.error("Failed to compile YARA file(s): %s", e)
164
+
165
+ return compiled_rules
166
+
167
+
168
+ def compile_yara(files: dict[str, Path] | Path, is_compiled: bool = False) -> yara.Rules | None:
169
+ """Compile or load the given YARA file(s) to rules."""
170
+ if is_compiled and isinstance(files, Path):
171
+ return yara.load(files.as_posix())
172
+ else:
173
+ return yara.compile(filepaths={ns: Path(path).as_posix() for ns, path in files.items()})
174
+
175
+
176
+ def is_valid_yara(files: dict[str, Path] | Path, is_compiled: bool = False) -> bool:
177
+ """Determine if the given YARA file(s) compile without errors or warnings."""
178
+ try:
179
+ compile_yara(files, is_compiled)
180
+ return True
181
+
182
+ except (yara.SyntaxError, yara.WarningError, yara.Error) as e:
183
+ log.debug("Rule file(s) '%s' invalid: %s", files, e)
184
+ return False
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterator, Union
4
+
5
+ from flow.record.fieldtypes.net import IPAddress, IPNetwork
6
+
7
+ from dissect.target.helpers.record import (
8
+ MacInterfaceRecord,
9
+ UnixInterfaceRecord,
10
+ WindowsInterfaceRecord,
11
+ )
12
+ from dissect.target.plugin import Plugin, export, internal
13
+ from dissect.target.target import Target
14
+
15
+ InterfaceRecord = Union[UnixInterfaceRecord, WindowsInterfaceRecord, MacInterfaceRecord]
16
+
17
+
18
+ class NetworkPlugin(Plugin):
19
+ __namespace__ = "network"
20
+
21
+ def __init__(self, target: Target):
22
+ super().__init__(target)
23
+ self._interface_list: list[InterfaceRecord] | None = None
24
+
25
+ def check_compatible(self) -> None:
26
+ pass
27
+
28
+ def _interfaces(self) -> Iterator[InterfaceRecord]:
29
+ yield from ()
30
+
31
+ def _get_record_type(self, field_name: str) -> Iterator[Any]:
32
+ for record in self.interfaces():
33
+ if (output := getattr(record, field_name, None)) is None:
34
+ continue
35
+
36
+ if isinstance(output, list):
37
+ yield from output
38
+ else:
39
+ yield output
40
+
41
+ @export(record=InterfaceRecord)
42
+ def interfaces(self) -> Iterator[InterfaceRecord]:
43
+ # Only search for the interfaces once
44
+ if self._interface_list is None:
45
+ self._interface_list = list(self._interfaces())
46
+
47
+ yield from self._interface_list
48
+
49
+ @export
50
+ def ips(self) -> list[IPAddress]:
51
+ return list(self._get_record_type("ip"))
52
+
53
+ @export
54
+ def gateways(self) -> list[IPAddress]:
55
+ return list(self._get_record_type("gateway"))
56
+
57
+ @export
58
+ def macs(self) -> list[str]:
59
+ return list(self._get_record_type("mac"))
60
+
61
+ @export
62
+ def dns(self) -> list[str]:
63
+ return list(self._get_record_type("dns"))
64
+
65
+ @internal
66
+ def with_ip(self, ip_addr: str) -> Iterator[InterfaceRecord]:
67
+ for interface in self.interfaces():
68
+ if ip_addr in interface.ip:
69
+ yield interface
70
+
71
+ @internal
72
+ def with_mac(self, mac: str) -> Iterator[InterfaceRecord]:
73
+ for interface in self.interfaces():
74
+ if interface.mac == mac:
75
+ yield interface
76
+
77
+ @internal
78
+ def in_cidr(self, cidr: str) -> Iterator[InterfaceRecord]:
79
+ cidr = IPNetwork(cidr)
80
+ for interface in self.interfaces():
81
+ if any(ip_addr in cidr for ip_addr in interface.ip):
82
+ yield interface
@@ -1,5 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from functools import lru_cache
2
- from typing import Generator, NamedTuple, Optional, Union
4
+ from typing import Iterator, NamedTuple, Union
3
5
 
4
6
  from dissect.target import Target
5
7
  from dissect.target.exceptions import UnsupportedPluginError
@@ -7,10 +9,12 @@ from dissect.target.helpers.fsutil import TargetPath
7
9
  from dissect.target.helpers.record import UnixUserRecord, WindowsUserRecord
8
10
  from dissect.target.plugin import InternalPlugin
9
11
 
12
+ UserRecord = Union[UnixUserRecord, WindowsUserRecord]
13
+
10
14
 
11
15
  class UserDetails(NamedTuple):
12
- user: Union[UnixUserRecord, WindowsUserRecord]
13
- home_path: Optional[TargetPath]
16
+ user: UserRecord
17
+ home_path: TargetPath | None
14
18
 
15
19
 
16
20
  class UsersPlugin(InternalPlugin):
@@ -28,11 +32,11 @@ class UsersPlugin(InternalPlugin):
28
32
 
29
33
  def find(
30
34
  self,
31
- sid: Optional[str] = None,
32
- uid: Optional[str] = None,
33
- username: Optional[str] = None,
35
+ sid: str | None = None,
36
+ uid: str | None = None,
37
+ username: str | None = None,
34
38
  force_case_sensitive: bool = False,
35
- ) -> Optional[UserDetails]:
39
+ ) -> UserDetails | None:
36
40
  """Find User record matching provided sid, uid or username and return UserDetails object"""
37
41
  if all(map(lambda x: x is None, [sid, uid, username])):
38
42
  raise ValueError("Either sid or uid or username is expected")
@@ -52,7 +56,7 @@ class UsersPlugin(InternalPlugin):
52
56
  ):
53
57
  return self.get(user)
54
58
 
55
- def get(self, user: Union[UnixUserRecord, WindowsUserRecord]) -> UserDetails:
59
+ def get(self, user: UserRecord) -> UserDetails:
56
60
  """Return additional details about the user"""
57
61
  # Resolving the user home can not use the user's environment variables,
58
62
  # as those depend on the user's home to be known first. So we resolve
@@ -60,12 +64,12 @@ class UsersPlugin(InternalPlugin):
60
64
  home_path = self.target.fs.path(self.target.resolve(str(user.home))) if user.home else None
61
65
  return UserDetails(user=user, home_path=home_path)
62
66
 
63
- def all(self) -> Generator[UserDetails, None, None]:
67
+ def all(self) -> Iterator[UserDetails]:
64
68
  """Return UserDetails objects for all users found"""
65
69
  for user in self.target.users():
66
70
  yield self.get(user)
67
71
 
68
- def all_with_home(self) -> Generator[UserDetails, None, None]:
72
+ def all_with_home(self) -> Iterator[UserDetails]:
69
73
  """Return UserDetails objects for users that have existing directory set as home directory"""
70
74
  for user in self.target.users():
71
75
  if user.home:
@@ -40,12 +40,18 @@ class UnixPlugin(OSPlugin):
40
40
  @export(record=UnixUserRecord)
41
41
  @arg("--sessions", action="store_true", help="Parse syslog for recent user sessions")
42
42
  def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
43
- """Recover users from /etc/passwd, /etc/master.passwd or /var/log/syslog session logins."""
43
+ """Yield unix user records from passwd files or syslog session logins.
44
+
45
+ Resources:
46
+ - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
47
+ """
48
+
49
+ PASSWD_FILES = ["/etc/passwd", "/etc/passwd-", "/etc/master.passwd"]
44
50
 
45
51
  seen_users = set()
46
52
 
47
53
  # Yield users found in passwd files.
48
- for passwd_file in ["/etc/passwd", "/etc/master.passwd"]:
54
+ for passwd_file in PASSWD_FILES:
49
55
  if (path := self.target.fs.path(passwd_file)).exists():
50
56
  for line in path.open("rt"):
51
57
  line = line.strip()
@@ -53,7 +59,12 @@ class UnixPlugin(OSPlugin):
53
59
  continue
54
60
 
55
61
  pwent = dict(enumerate(line.split(":")))
56
- seen_users.add((pwent.get(0), pwent.get(5), pwent.get(6)))
62
+
63
+ current_user = (pwent.get(0), pwent.get(5), pwent.get(6))
64
+ if current_user in seen_users:
65
+ continue
66
+
67
+ seen_users.add(current_user)
57
68
  yield UnixUserRecord(
58
69
  name=pwent.get(0),
59
70
  passwd=pwent.get(1),
@@ -202,11 +213,13 @@ class UnixPlugin(OSPlugin):
202
213
  fs_id = None
203
214
  fs_subvol = None
204
215
  fs_subvolid = None
205
- fs_volume_name = fs.volume.name if fs.volume and not isinstance(fs.volume, list) else None
206
216
  fs_last_mount = None
217
+ fs_volume_name = None
218
+ vol_volume_name = fs.volume.name if fs.volume and not isinstance(fs.volume, list) else None
207
219
 
208
220
  if fs.__type__ == "xfs":
209
221
  fs_id = fs.xfs.uuid
222
+ fs_volume_name = fs.xfs.name
210
223
  elif fs.__type__ == "ext":
211
224
  fs_id = fs.extfs.uuid
212
225
  fs_last_mount = fs.extfs.last_mount
@@ -224,8 +237,9 @@ class UnixPlugin(OSPlugin):
224
237
 
225
238
  if (
226
239
  (fs_id and (fs_id == dev_id and (subvol == fs_subvol or subvolid == fs_subvolid)))
227
- or (fs_volume_name and (fs_volume_name == volume_name))
228
240
  or (fs_last_mount and (fs_last_mount == mount_point))
241
+ or (fs_volume_name and (fs_volume_name == volume_name))
242
+ or (vol_volume_name and (vol_volume_name == volume_name))
229
243
  ):
230
244
  self.target.log.debug("Mounting %s (%s) at %s", fs, fs.volume, mount_point)
231
245
  self.target.fs.mount(mount_point, fs)
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional
4
-
5
3
  from dissect.target.filesystem import Filesystem
6
4
  from dissect.target.plugin import export
7
5
  from dissect.target.plugins.os.unix.bsd._os import BsdPlugin
@@ -14,13 +12,13 @@ class FreeBsdPlugin(BsdPlugin):
14
12
  self._os_release = self._parse_os_release("/bin/freebsd-version*")
15
13
 
16
14
  @classmethod
17
- def detect(cls, target: Target) -> Optional[Filesystem]:
15
+ def detect(cls, target: Target) -> Filesystem | None:
18
16
  for fs in target.filesystems:
19
- if fs.exists("/net") or fs.exists("/.sujournal"):
17
+ if fs.exists("/net") and (fs.exists("/.sujournal") or fs.exists("/entropy")):
20
18
  return fs
21
19
 
22
20
  return None
23
21
 
24
22
  @export(property=True)
25
- def version(self) -> Optional[str]:
23
+ def version(self) -> str | None:
26
24
  return self._os_release.get("USERLAND_VERSION")
@@ -8,7 +8,7 @@ import subprocess
8
8
  from configparser import ConfigParser
9
9
  from configparser import Error as ConfigParserError
10
10
  from io import BytesIO
11
- from typing import Any, BinaryIO, Iterator, Optional, TextIO
11
+ from typing import Any, BinaryIO, Iterator, TextIO
12
12
 
13
13
  from defusedxml import ElementTree
14
14
  from dissect.hypervisor.util import vmtar
@@ -17,9 +17,14 @@ from dissect.sql import sqlite3
17
17
  from dissect.target.helpers.fsutil import TargetPath
18
18
 
19
19
  try:
20
- from dissect.hypervisor.util.envelope import Envelope, KeyStore
20
+ from dissect.hypervisor.util.envelope import (
21
+ HAS_PYCRYPTODOME,
22
+ HAS_PYSTANDALONE,
23
+ Envelope,
24
+ KeyStore,
25
+ )
21
26
 
22
- HAS_ENVELOPE = True
27
+ HAS_ENVELOPE = HAS_PYCRYPTODOME or HAS_PYSTANDALONE
23
28
  except ImportError:
24
29
  HAS_ENVELOPE = False
25
30
 
@@ -68,9 +73,10 @@ class ESXiPlugin(UnixPlugin):
68
73
  if configstore.exists():
69
74
  self._configstore = parse_config_store(configstore.open())
70
75
 
71
- def _cfg(self, path: str) -> Optional[str]:
76
+ def _cfg(self, path: str) -> str | None:
72
77
  if not self._config:
73
- raise ValueError("No ESXi config!")
78
+ self.target.log.warning("No ESXi config!")
79
+ return None
74
80
 
75
81
  value_name = path.strip("/").split("/")[-1]
76
82
  obj = _traverse(path, self._config)
@@ -81,7 +87,7 @@ class ESXiPlugin(UnixPlugin):
81
87
  return obj.get(value_name) if obj else None
82
88
 
83
89
  @classmethod
84
- def detect(cls, target: Target) -> Optional[Filesystem]:
90
+ def detect(cls, target: Target) -> Filesystem | None:
85
91
  bootbanks = [
86
92
  fs for fs in target.filesystems if fs.path("boot.cfg").exists() and list(fs.path("/").glob("*.v00"))
87
93
  ]
@@ -95,13 +101,13 @@ class ESXiPlugin(UnixPlugin):
95
101
  def create(cls, target: Target, sysvol: Filesystem) -> ESXiPlugin:
96
102
  cfg = parse_boot_cfg(sysvol.path("boot.cfg").open("rt"))
97
103
 
104
+ # Mount all the visor tars in individual filesystem layers
105
+ _mount_modules(target, sysvol, cfg)
106
+
98
107
  # Create a root layer for the "local state" filesystem
99
108
  # This stores persistent configuration data
100
109
  local_layer = target.fs.append_layer()
101
110
 
102
- # Mount all the visor tars in individual filesystem layers
103
- _mount_modules(target, sysvol, cfg)
104
-
105
111
  # Mount the local.tgz to the local state layer
106
112
  _mount_local(target, local_layer)
107
113
 
@@ -122,7 +128,7 @@ class ESXiPlugin(UnixPlugin):
122
128
  return "localhost"
123
129
 
124
130
  @export(property=True)
125
- def domain(self) -> Optional[str]:
131
+ def domain(self) -> str | None:
126
132
  if hostname := self._cfg("/adv/Misc/HostName"):
127
133
  return hostname.partition(".")[2]
128
134
 
@@ -140,7 +146,7 @@ class ESXiPlugin(UnixPlugin):
140
146
  return list(result)
141
147
 
142
148
  @export(property=True)
143
- def version(self) -> Optional[str]:
149
+ def version(self) -> str | None:
144
150
  boot_cfg = self.target.fs.path("/bootbank/boot.cfg")
145
151
  if not boot_cfg.exists():
146
152
  return None
@@ -181,11 +187,11 @@ class ESXiPlugin(UnixPlugin):
181
187
  return self._configstore
182
188
 
183
189
  @export(property=True)
184
- def os(self):
190
+ def os(self) -> str:
185
191
  return OperatingSystem.ESXI.value
186
192
 
187
193
 
188
- def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
194
+ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]) -> None:
189
195
  modules = [m.strip() for m in cfg["modules"].split("---")]
190
196
 
191
197
  for module in modules:
@@ -212,20 +218,22 @@ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
212
218
  target.fs.append_layer().mount("/", tfs)
213
219
 
214
220
 
215
- def _mount_local(target: Target, local_layer: VirtualFilesystem):
221
+ def _mount_local(target: Target, local_layer: VirtualFilesystem) -> None:
216
222
  local_tgz = target.fs.path("local.tgz")
223
+ local_tgz_ve = target.fs.path("local.tgz.ve")
217
224
  local_fs = None
218
225
 
219
226
  if local_tgz.exists():
220
227
  local_fs = tar.TarFilesystem(local_tgz.open())
221
- else:
222
- local_tgz_ve = target.fs.path("local.tgz.ve")
228
+ elif local_tgz_ve.exists():
223
229
  # In the case "encryption.info" does not exist, but ".#encryption.info" does
224
230
  encryption_info = next(target.fs.path("/").glob("*encryption.info"), None)
225
231
  if not local_tgz_ve.exists() or not encryption_info.exists():
226
232
  raise ValueError("Unable to find valid configuration archive")
227
233
 
228
234
  local_fs = _create_local_fs(target, local_tgz_ve, encryption_info)
235
+ else:
236
+ target.log.warning("No local.tgz or local.tgz.ve found, skipping local state")
229
237
 
230
238
  if local_fs:
231
239
  local_layer.mount("/", local_fs)
@@ -239,7 +247,7 @@ def _decrypt_envelope(local_tgz_ve: TargetPath, encryption_info: TargetPath) ->
239
247
  return local_tgz
240
248
 
241
249
 
242
- def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> Optional[BytesIO]:
250
+ def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> BytesIO | None:
243
251
  """Decrypt ``local.tgz.ve`` using ESXi ``crypto-util``.
244
252
 
245
253
  We write to stdout, but this results in ``crypto-util`` exiting with a non-zero return code
@@ -258,9 +266,7 @@ def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> Optional[BytesIO]:
258
266
  return BytesIO(result.stdout)
259
267
 
260
268
 
261
- def _create_local_fs(
262
- target: Target, local_tgz_ve: TargetPath, encryption_info: TargetPath
263
- ) -> Optional[tar.TarFilesystem]:
269
+ def _create_local_fs(target: Target, local_tgz_ve: TargetPath, encryption_info: TargetPath) -> tar.TarFilesystem | None:
264
270
  local_tgz = None
265
271
 
266
272
  if HAS_ENVELOPE:
@@ -286,7 +292,7 @@ def _create_local_fs(
286
292
  return tar.TarFilesystem(local_tgz)
287
293
 
288
294
 
289
- def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
295
+ def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]) -> None:
290
296
  version = cfg["build"]
291
297
 
292
298
  osdata_fs = None
@@ -365,7 +371,7 @@ def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
365
371
  target.fs.symlink(f"/vmfs/volumes/LOCKER-{locker_fs.vmfs.uuid}", "/locker")
366
372
 
367
373
 
368
- def _link_log_dir(target: Target, cfg: dict[str, str], plugin_obj: ESXiPlugin):
374
+ def _link_log_dir(target: Target, cfg: dict[str, str], plugin_obj: ESXiPlugin) -> None:
369
375
  version = cfg["build"]
370
376
 
371
377
  # Don't really know how ESXi does this, but let's just take a shortcut for now
@@ -435,7 +441,7 @@ def parse_esx_conf(fh: TextIO) -> dict[str, Any]:
435
441
  return config
436
442
 
437
443
 
438
- def _traverse(path: str, obj: dict[str, Any], create: bool = False):
444
+ def _traverse(path: str, obj: dict[str, Any], create: bool = False) -> dict[str, Any] | None:
439
445
  parts = path.strip("/").split("/")
440
446
  path_parts = parts[:-1]
441
447
  for part in path_parts:
@@ -1,5 +1,4 @@
1
1
  import fnmatch
2
- import logging
3
2
  import re
4
3
  from pathlib import Path
5
4
  from typing import Iterator
@@ -22,8 +21,6 @@ UnixConfigTreeRecord = TargetRecordDescriptor(
22
21
  ],
23
22
  )
24
23
 
25
- log = logging.getLogger(__name__)
26
-
27
24
 
28
25
  class EtcTree(ConfigurationTreePlugin):
29
26
  __namespace__ = "etc"
@@ -65,13 +62,13 @@ class EtcTree(ConfigurationTreePlugin):
65
62
 
66
63
  @export(record=UnixConfigTreeRecord)
67
64
  @arg("--glob", dest="pattern", required=False, default="*", type=str, help="Glob-style pattern to search for")
68
- def etc(self, pattern: str) -> Iterator[UnixConfigTreeRecord]:
69
- for entry, subs, items in self.config_fs.walk("/"):
65
+ @arg("--root", dest="root", required=False, default="/", type=str, help="Path to use as root for search")
66
+ def etc(self, pattern: str, root: str) -> Iterator[UnixConfigTreeRecord]:
67
+ for entry, subs, items in self.config_fs.walk(root):
70
68
  for item in items:
71
69
  try:
72
70
  config_object = self.get(str(Path(entry) / Path(item)))
73
- if isinstance(config_object, ConfigurationEntry):
74
- yield from self._sub(config_object, Path(entry) / Path(item), pattern)
71
+ yield from self._sub(config_object, Path(entry) / Path(item), pattern)
75
72
  except Exception:
76
- log.warning("Could not open configuration item: %s", item)
73
+ self.target.log.warning("Could not open configuration item: %s", item)
77
74
  pass
@@ -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.