dissect.target 3.16.dev44__py3-none-any.whl → 3.17__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.
Files changed (63) hide show
  1. dissect/target/container.py +1 -0
  2. dissect/target/containers/fortifw.py +190 -0
  3. dissect/target/filesystem.py +192 -67
  4. dissect/target/filesystems/dir.py +14 -1
  5. dissect/target/filesystems/overlay.py +103 -0
  6. dissect/target/helpers/compat/path_common.py +19 -5
  7. dissect/target/helpers/configutil.py +30 -7
  8. dissect/target/helpers/network_managers.py +101 -73
  9. dissect/target/helpers/record_modifier.py +4 -1
  10. dissect/target/loader.py +3 -1
  11. dissect/target/loaders/dir.py +23 -5
  12. dissect/target/loaders/itunes.py +3 -3
  13. dissect/target/loaders/mqtt.py +309 -0
  14. dissect/target/loaders/overlay.py +31 -0
  15. dissect/target/loaders/target.py +12 -9
  16. dissect/target/loaders/vb.py +2 -2
  17. dissect/target/loaders/velociraptor.py +5 -4
  18. dissect/target/plugin.py +1 -1
  19. dissect/target/plugins/apps/browser/brave.py +10 -0
  20. dissect/target/plugins/apps/browser/browser.py +43 -0
  21. dissect/target/plugins/apps/browser/chrome.py +10 -0
  22. dissect/target/plugins/apps/browser/chromium.py +234 -12
  23. dissect/target/plugins/apps/browser/edge.py +10 -0
  24. dissect/target/plugins/apps/browser/firefox.py +512 -19
  25. dissect/target/plugins/apps/browser/iexplore.py +2 -2
  26. dissect/target/plugins/apps/container/docker.py +24 -4
  27. dissect/target/plugins/apps/ssh/openssh.py +4 -0
  28. dissect/target/plugins/apps/ssh/putty.py +45 -14
  29. dissect/target/plugins/apps/ssh/ssh.py +40 -0
  30. dissect/target/plugins/apps/vpn/openvpn.py +115 -93
  31. dissect/target/plugins/child/docker.py +24 -0
  32. dissect/target/plugins/filesystem/ntfs/mft.py +1 -1
  33. dissect/target/plugins/filesystem/walkfs.py +2 -2
  34. dissect/target/plugins/general/users.py +6 -0
  35. dissect/target/plugins/os/unix/bsd/__init__.py +0 -0
  36. dissect/target/plugins/os/unix/esxi/_os.py +2 -2
  37. dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +1 -1
  38. dissect/target/plugins/os/unix/linux/fortios/_os.py +9 -9
  39. dissect/target/plugins/os/unix/linux/services.py +1 -0
  40. dissect/target/plugins/os/unix/linux/sockets.py +2 -2
  41. dissect/target/plugins/os/unix/log/messages.py +53 -8
  42. dissect/target/plugins/os/windows/_os.py +10 -1
  43. dissect/target/plugins/os/windows/catroot.py +178 -63
  44. dissect/target/plugins/os/windows/credhist.py +210 -0
  45. dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
  46. dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
  47. dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
  48. dissect/target/plugins/os/windows/regf/runkeys.py +6 -4
  49. dissect/target/plugins/os/windows/sam.py +10 -1
  50. dissect/target/target.py +1 -1
  51. dissect/target/tools/dump/run.py +23 -28
  52. dissect/target/tools/dump/state.py +11 -8
  53. dissect/target/tools/dump/utils.py +5 -4
  54. dissect/target/tools/query.py +3 -15
  55. dissect/target/tools/shell.py +48 -8
  56. dissect/target/tools/utils.py +23 -0
  57. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
  58. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/RECORD +63 -56
  59. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
  60. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
  61. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
  62. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
  63. {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ import base64
1
2
  import re
2
3
  from itertools import product
3
4
  from pathlib import Path
@@ -14,6 +15,7 @@ from dissect.target.plugins.apps.ssh.ssh import (
14
15
  PrivateKeyRecord,
15
16
  PublicKeyRecord,
16
17
  SSHPlugin,
18
+ calculate_fingerprints,
17
19
  )
18
20
 
19
21
 
@@ -143,12 +145,14 @@ class OpenSSHPlugin(SSHPlugin):
143
145
  continue
144
146
 
145
147
  key_type, public_key, comment = parse_ssh_public_key_file(file_path)
148
+ fingerprints = calculate_fingerprints(base64.b64decode(public_key))
146
149
 
147
150
  yield PublicKeyRecord(
148
151
  mtime_ts=file_path.stat().st_mtime,
149
152
  key_type=key_type,
150
153
  public_key=public_key,
151
154
  comment=comment,
155
+ fingerprint=fingerprints,
152
156
  path=file_path,
153
157
  _target=self.target,
154
158
  _user=user,
@@ -1,9 +1,16 @@
1
1
  import logging
2
+ from base64 import b64decode
2
3
  from datetime import datetime
3
4
  from pathlib import Path
4
5
  from typing import Iterator, Optional, Union
5
6
 
6
- from Crypto.PublicKey import ECC, RSA
7
+ try:
8
+ from Crypto.PublicKey import ECC, RSA
9
+
10
+ HAS_CRYPTO = True
11
+ except ImportError:
12
+ HAS_CRYPTO = False
13
+
7
14
  from flow.record.fieldtypes import posix_path, windows_path
8
15
 
9
16
  from dissect.target.exceptions import RegistryKeyNotFoundError, UnsupportedPluginError
@@ -12,7 +19,11 @@ from dissect.target.helpers.fsutil import TargetPath, open_decompress
12
19
  from dissect.target.helpers.record import create_extended_descriptor
13
20
  from dissect.target.helpers.regutil import RegistryKey
14
21
  from dissect.target.plugin import export
15
- from dissect.target.plugins.apps.ssh.ssh import KnownHostRecord, SSHPlugin
22
+ from dissect.target.plugins.apps.ssh.ssh import (
23
+ KnownHostRecord,
24
+ SSHPlugin,
25
+ calculate_fingerprints,
26
+ )
16
27
  from dissect.target.plugins.general.users import UserDetails
17
28
 
18
29
  log = logging.getLogger(__name__)
@@ -96,12 +107,15 @@ class PuTTYPlugin(SSHPlugin):
96
107
  key_type, host = entry.name.split("@")
97
108
  port, host = host.split(":")
98
109
 
110
+ public_key, fingerprints = construct_public_key(key_type, entry.value)
111
+
99
112
  yield KnownHostRecord(
100
113
  mtime_ts=ssh_host_keys.ts,
101
114
  host=host,
102
115
  port=port,
103
116
  key_type=key_type,
104
- public_key=construct_public_key(key_type, entry.value),
117
+ public_key=public_key,
118
+ fingerprint=fingerprints,
105
119
  comment="",
106
120
  marker=None,
107
121
  path=windows_path(ssh_host_keys.path),
@@ -121,12 +135,15 @@ class PuTTYPlugin(SSHPlugin):
121
135
  key_type, host = parts[0].split("@")
122
136
  port, host = host.split(":")
123
137
 
138
+ public_key, fingerprints = construct_public_key(key_type, parts[1])
139
+
124
140
  yield KnownHostRecord(
125
141
  mtime_ts=ts,
126
142
  host=host,
127
143
  port=port,
128
144
  key_type=key_type,
129
- public_key=construct_public_key(key_type, parts[1]),
145
+ public_key=public_key,
146
+ fingerprint=fingerprints,
130
147
  comment="",
131
148
  marker=None,
132
149
  path=posix_path(ssh_host_keys_path),
@@ -197,8 +214,8 @@ def parse_host_user(host: str, user: str) -> tuple[str, str]:
197
214
  return host, user
198
215
 
199
216
 
200
- def construct_public_key(key_type: str, iv: str) -> str:
201
- """Returns OpenSSH format public key calculated from PuTTY SshHostKeys format.
217
+ def construct_public_key(key_type: str, iv: str) -> tuple[str, tuple[str, str, str]]:
218
+ """Returns OpenSSH format public key calculated from PuTTY SshHostKeys format and set of fingerprints.
202
219
 
203
220
  PuTTY stores raw public key components instead of OpenSSH-formatted public keys
204
221
  or fingerprints. With RSA public keys the exponent and modulus are stored.
@@ -206,9 +223,7 @@ def construct_public_key(key_type: str, iv: str) -> str:
206
223
 
207
224
  Currently supports ``ssh-ed25519``, ``ecdsa-sha2-nistp256`` and ``rsa2`` key types.
208
225
 
209
- NOTE:
210
- - Sha256 fingerprints of the reconstructed public keys are currently not generated.
211
- - More key types could be supported in the future.
226
+ NOTE: More key types could be supported in the future.
212
227
 
213
228
  Resources:
214
229
  - https://github.com/github/putty/blob/master/contrib/kh2reg.py
@@ -216,21 +231,37 @@ def construct_public_key(key_type: str, iv: str) -> str:
216
231
  - https://pycryptodome.readthedocs.io/en/latest/src/public_key/ecc.html
217
232
  - https://github.com/mkorthof/reg2kh
218
233
  """
234
+ if not HAS_CRYPTO:
235
+ log.warning("Could not reconstruct public key: missing pycryptodome dependency")
236
+ return iv
237
+
238
+ if not isinstance(key_type, str) or not isinstance(iv, str):
239
+ raise ValueError("Invalid key_type or iv")
240
+
241
+ key = None
219
242
 
220
243
  if key_type == "ssh-ed25519":
221
244
  x, y = iv.split(",")
222
245
  key = ECC.construct(curve="ed25519", point_x=int(x, 16), point_y=int(y, 16))
223
- return key.public_key().export_key(format="OpenSSH").split()[-1]
224
246
 
225
247
  if key_type == "ecdsa-sha2-nistp256":
226
248
  _, x, y = iv.split(",")
227
249
  key = ECC.construct(curve="NIST P-256", point_x=int(x, 16), point_y=int(y, 16))
228
- return key.public_key().export_key(format="OpenSSH").split()[-1]
229
250
 
230
251
  if key_type == "rsa2":
231
252
  exponent, modulus = iv.split(",")
232
253
  key = RSA.construct((int(modulus, 16), int(exponent, 16)))
233
- return key.public_key().export_key(format="OpenSSH").decode("utf-8").split()[-1]
234
254
 
235
- log.warning("Could not reconstruct public key: type %s not implemented.", key_type)
236
- return iv
255
+ if key is None:
256
+ log.warning("Could not reconstruct public key: type %s not implemented", key_type)
257
+ return iv, (None, None, None)
258
+
259
+ openssh_public_key = key.public_key().export_key(format="OpenSSH")
260
+
261
+ if isinstance(openssh_public_key, bytes):
262
+ # RSA's export_key() returns bytes
263
+ openssh_public_key = openssh_public_key.decode()
264
+
265
+ key_part = openssh_public_key.split()[-1]
266
+ fingerprints = calculate_fingerprints(b64decode(key_part))
267
+ return key_part, fingerprints
@@ -1,3 +1,6 @@
1
+ import base64
2
+ from hashlib import md5, sha1, sha256
3
+
1
4
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
2
5
  from dissect.target.helpers.record import create_extended_descriptor
3
6
  from dissect.target.plugin import NamespacePlugin
@@ -29,6 +32,7 @@ KnownHostRecord = OpenSSHUserRecordDescriptor(
29
32
  ("varint", "port"),
30
33
  ("string", "public_key"),
31
34
  ("string", "marker"),
35
+ ("digest", "fingerprint"),
32
36
  ],
33
37
  )
34
38
 
@@ -50,9 +54,45 @@ PublicKeyRecord = OpenSSHUserRecordDescriptor(
50
54
  ("datetime", "mtime_ts"),
51
55
  *COMMON_ELLEMENTS,
52
56
  ("string", "public_key"),
57
+ ("digest", "fingerprint"),
53
58
  ],
54
59
  )
55
60
 
56
61
 
57
62
  class SSHPlugin(NamespacePlugin):
58
63
  __namespace__ = "ssh"
64
+
65
+
66
+ def calculate_fingerprints(public_key_decoded: bytes, ssh_keygen_format: bool = False) -> tuple[str, str, str]:
67
+ """Calculate the MD5, SHA1 and SHA256 digest of the given decoded public key.
68
+
69
+ Adheres as much as possible to the output provided by ssh-keygen when ``ssh_keygen_format``
70
+ parameter is set to ``True``. When set to ``False`` (default) hexdigests are calculated
71
+ instead for ``sha1``and ``sha256``.
72
+
73
+ Resources:
74
+ - https://en.wikipedia.org/wiki/Public_key_fingerprint
75
+ - https://man7.org/linux/man-pages/man1/ssh-keygen.1.html
76
+ - ``ssh-keygen -l -E <alg> -f key.pub``
77
+ """
78
+ if not public_key_decoded:
79
+ raise ValueError("No decoded public key provided")
80
+
81
+ if not isinstance(public_key_decoded, bytes):
82
+ raise ValueError("Provided public key should be bytes")
83
+
84
+ if public_key_decoded[0:3] != b"\x00\x00\x00":
85
+ raise ValueError("Provided value does not look like a public key")
86
+
87
+ digest_md5 = md5(public_key_decoded).digest()
88
+ digest_sha1 = sha1(public_key_decoded).digest()
89
+ digest_sha256 = sha256(public_key_decoded).digest()
90
+
91
+ if ssh_keygen_format:
92
+ fingerprint_sha1 = base64.b64encode(digest_sha1).rstrip(b"=").decode()
93
+ fingerprint_sha256 = base64.b64encode(digest_sha256).rstrip(b"=").decode()
94
+ else:
95
+ fingerprint_sha1 = digest_sha1.hex()
96
+ fingerprint_sha256 = digest_sha256.hex()
97
+
98
+ return digest_md5.hex(), fingerprint_sha1, fingerprint_sha256
@@ -1,12 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import io
1
4
  import itertools
2
- import re
3
- from os.path import basename
4
- from typing import Iterator, Union
5
+ from itertools import product
6
+ from typing import Iterable, Iterator, Optional, Union
5
7
 
6
- from dissect.target.exceptions import UnsupportedPluginError
8
+ from dissect.target.exceptions import ConfigurationParsingError, UnsupportedPluginError
7
9
  from dissect.target.helpers import fsutil
10
+ from dissect.target.helpers.configutil import Default, ListUnwrapper, _update_dictionary
8
11
  from dissect.target.helpers.record import TargetRecordDescriptor
9
- from dissect.target.plugin import OperatingSystem, Plugin, export
12
+ from dissect.target.plugin import OperatingSystem, Plugin, arg, export
10
13
 
11
14
  COMMON_ELEMENTS = [
12
15
  ("string", "name"), # basename of .conf file
@@ -15,6 +18,7 @@ COMMON_ELEMENTS = [
15
18
  ("string", "ca"),
16
19
  ("string", "cert"),
17
20
  ("string", "key"),
21
+ ("boolean", "redacted_key"),
18
22
  ("string", "tls_auth"),
19
23
  ("string", "status"),
20
24
  ("string", "log"),
@@ -46,7 +50,60 @@ OpenVPNClient = TargetRecordDescriptor(
46
50
  )
47
51
 
48
52
 
49
- CONFIG_COMMENT_SPLIT_REGEX = re.compile("(#|;)")
53
+ class OpenVPNParser(Default):
54
+ def __init__(self, *args, **kwargs):
55
+ boolean_fields = OpenVPNServer.getfields("boolean") + OpenVPNClient.getfields("boolean")
56
+ self.boolean_field_names = set(field.name.replace("_", "-") for field in boolean_fields)
57
+
58
+ super().__init__(*args, separator=(r"\s",), collapse=["key", "ca", "cert"], **kwargs)
59
+
60
+ def parse_file(self, fh: io.TextIOBase) -> None:
61
+ root = {}
62
+ iterator = self.line_reader(fh)
63
+ for line in iterator:
64
+ if line.startswith("<"):
65
+ key = line.strip().strip("<>")
66
+ value = self._read_blob(iterator)
67
+ _update_dictionary(root, key, value)
68
+ continue
69
+
70
+ self._parse_line(root, line)
71
+
72
+ self.parsed_data = ListUnwrapper.unwrap(root)
73
+
74
+ def _read_blob(self, lines: Iterable[str]) -> str | list[dict]:
75
+ """Read the whole section between <data></data> sections"""
76
+ output = ""
77
+ with io.StringIO() as buffer:
78
+ for line in lines:
79
+ if "</" in line:
80
+ break
81
+
82
+ buffer.write(line)
83
+ output = buffer.getvalue()
84
+
85
+ # Check for connection profile blocks
86
+ if not output.startswith("-----"):
87
+ profile_dict = dict()
88
+ for line in output.splitlines():
89
+ self._parse_line(profile_dict, line)
90
+
91
+ # We put it as a list as _update_dictionary appends data in a list.
92
+ output = [profile_dict]
93
+
94
+ return output
95
+
96
+ def _parse_line(self, root: dict, line: str) -> None:
97
+ key, *value = self.SEPARATOR.split(line, 1)
98
+ # Unquote data
99
+ value = value[0].strip() if value else ""
100
+
101
+ value = value.strip("'\"")
102
+
103
+ if key in self.boolean_field_names:
104
+ value = True
105
+
106
+ _update_dictionary(root, key, value)
50
107
 
51
108
 
52
109
  class OpenVPNPlugin(Plugin):
@@ -61,133 +118,98 @@ class OpenVPNPlugin(Plugin):
61
118
  config_globs = [
62
119
  # This catches openvpn@, openvpn-client@, and openvpn-server@ systemd configurations
63
120
  # Linux
64
- "/etc/openvpn/*.conf",
65
- "/etc/openvpn/server/*.conf",
66
- "/etc/openvpn/client/*.conf",
121
+ "/etc/openvpn/",
67
122
  # Windows
68
- "sysvol/Program Files/OpenVPN/config/*.conf",
123
+ "sysvol/Program Files/OpenVPN/config/",
69
124
  ]
70
125
 
71
126
  user_config_paths = {
72
- OperatingSystem.WINDOWS.value: ["OpenVPN/config/*.conf"],
73
- OperatingSystem.OSX.value: ["Library/Application Support/OpenVPN Connect/profiles/*.conf"],
127
+ OperatingSystem.WINDOWS.value: ["OpenVPN/config/"],
128
+ OperatingSystem.OSX.value: ["Library/Application Support/OpenVPN Connect/profiles/"],
74
129
  }
75
130
 
76
131
  def __init__(self, target) -> None:
77
132
  super().__init__(target)
78
133
  self.configs: list[fsutil.TargetPath] = []
79
- for path in self.config_globs:
80
- self.configs.extend(self.target.fs.path().glob(path.lstrip("/")))
134
+ for base, glob in product(self.config_globs, ["*.conf", "*.ovpn"]):
135
+ self.configs.extend(self.target.fs.path(base).rglob(glob))
81
136
 
82
137
  user_paths = self.user_config_paths.get(target.os, [])
83
- for path, user_details in itertools.product(user_paths, self.target.user_details.all_with_home()):
84
- self.configs.extend(user_details.home_path.glob(path))
138
+ for path, glob, user_details in itertools.product(
139
+ user_paths, ["*.conf", "*.ovpn"], self.target.user_details.all_with_home()
140
+ ):
141
+ self.configs.extend(user_details.home_path.joinpath(path).rglob(glob))
85
142
 
86
143
  def check_compatible(self) -> None:
87
144
  if not self.configs:
88
145
  raise UnsupportedPluginError("No OpenVPN configuration files found")
89
146
 
147
+ def _load_config(self, parser: OpenVPNParser, config_path: fsutil.TargetPath) -> Optional[dict]:
148
+ with config_path.open("rt") as file:
149
+ try:
150
+ parser.parse_file(file)
151
+ except ConfigurationParsingError as e:
152
+ # Couldn't parse file, continue
153
+ self.target.log.info("An issue occurred during parsing of %s, continuing", config_path)
154
+ self.target.log.debug("", exc_info=e)
155
+ return None
156
+
157
+ return parser.parsed_data
158
+
90
159
  @export(record=[OpenVPNServer, OpenVPNClient])
91
- def config(self) -> Iterator[Union[OpenVPNServer, OpenVPNClient]]:
160
+ @arg("--export-key", action="store_true")
161
+ def config(self, export_key: bool = False) -> Iterator[Union[OpenVPNServer, OpenVPNClient]]:
92
162
  """Parses config files from openvpn interfaces."""
163
+ # We define the parser here so we can reuse it
164
+ parser = OpenVPNParser()
93
165
 
94
166
  for config_path in self.configs:
95
- config = _parse_config(config_path.read_text())
96
-
97
- name = basename(config_path).replace(".conf", "")
98
- proto = config.get("proto", "udp") # Default is UDP
99
- dev = config.get("dev")
100
- ca = _unquote(config.get("ca"))
101
- cert = _unquote(config.get("cert"))
102
- key = _unquote(config.get("key"))
167
+ config = self._load_config(parser, config_path)
168
+
169
+ common_elements = {
170
+ "name": config_path.stem,
171
+ "proto": config.get("proto", "udp"), # Default is UDP
172
+ "dev": config.get("dev"),
173
+ "ca": config.get("ca"),
174
+ "cert": config.get("cert"),
175
+ "key": config.get("key"),
176
+ "status": config.get("status"),
177
+ "log": config.get("log"),
178
+ "source": config_path,
179
+ "_target": self.target,
180
+ }
181
+
182
+ if not export_key and "PRIVATE KEY" in common_elements.get("key"):
183
+ common_elements.update({"key": None})
184
+ common_elements.update({"redacted_key": True})
185
+
103
186
  tls_auth = config.get("tls-auth", "")
104
187
  # The format of tls-auth is 'tls-auth ta.key <NUM>'.
105
188
  # NUM is either 0 or 1 depending on whether the configuration
106
189
  # is for the client or server, and that does not interest us
107
190
  # This gets rid of the number at the end, while still supporting spaces
108
- tls_auth = _unquote(" ".join(tls_auth.split(" ")[:-1]))
109
- status = config.get("status")
110
- log = config.get("log")
191
+ tls_auth = " ".join(tls_auth.split(" ")[:-1]).strip("'\"")
192
+
193
+ common_elements.update({"tls_auth": tls_auth})
111
194
 
112
195
  if "client" in config:
113
196
  remote = config.get("remote", [])
114
- # In cases when there is only a single remote,
115
- # we want to return it as its own list
116
- if isinstance(remote, str):
117
- remote = [remote]
118
197
 
119
198
  yield OpenVPNClient(
120
- name=name,
121
- proto=proto,
122
- dev=dev,
123
- ca=ca,
124
- cert=cert,
125
- key=key,
126
- tls_auth=tls_auth,
127
- status=status,
128
- log=log,
199
+ **common_elements,
129
200
  remote=remote,
130
- source=config_path,
131
- _target=self.target,
132
201
  )
133
202
  else:
134
- pushed_options = config.get("push", [])
135
- # In cases when there is only a single push,
136
- # we want to return it as its own list
137
- if isinstance(pushed_options, str):
138
- pushed_options = [pushed_options]
139
- pushed_options = [_unquote(opt) for opt in pushed_options]
140
203
  # Defaults here are taken from `man (8) openvpn`
141
204
  yield OpenVPNServer(
142
- name=name,
143
- proto=proto,
144
- dev=dev,
145
- ca=ca,
146
- cert=cert,
147
- key=key,
148
- tls_auth=tls_auth,
149
- status=status,
150
- log=log,
205
+ **common_elements,
151
206
  local=config.get("local", "0.0.0.0"),
152
207
  port=int(config.get("port", "1194")),
153
- dh=_unquote(config.get("dh")),
208
+ dh=config.get("dh"),
154
209
  topology=config.get("topology"),
155
210
  server=config.get("server"),
156
211
  ifconfig_pool_persist=config.get("ifconfig-pool-persist"),
157
- pushed_options=pushed_options,
212
+ pushed_options=config.get("push", []),
158
213
  client_to_client=config.get("client-to-client", False),
159
214
  duplicate_cn=config.get("duplicate-cn", False),
160
- source=config_path,
161
- _target=self.target,
162
215
  )
163
-
164
-
165
- def _parse_config(content: str) -> dict[str, Union[str, list[str]]]:
166
- """Parses Openvpn config files"""
167
- lines = content.splitlines()
168
- res = {}
169
- boolean_fields = OpenVPNServer.getfields("boolean") + OpenVPNClient.getfields("boolean")
170
- boolean_field_names = set(field.name for field in boolean_fields)
171
-
172
- for line in lines:
173
- # As per man (8) openvpn, lines starting with ; or # are comments
174
- if line and not line.startswith((";", "#")):
175
- key, *value = line.split(" ", 1)
176
- value = value[0] if value else ""
177
- # This removes all text after the first comment
178
- value = CONFIG_COMMENT_SPLIT_REGEX.split(value, 1)[0].strip()
179
-
180
- if key in boolean_field_names and value == "":
181
- value = True
182
-
183
- if old_value := res.get(key):
184
- if not isinstance(old_value, list):
185
- old_value = [old_value]
186
- res[key] = old_value + [value]
187
- else:
188
- res[key] = value
189
- return res
190
-
191
-
192
- def _unquote(content: str) -> str:
193
- return content.strip("\"'")
@@ -0,0 +1,24 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.helpers.record import ChildTargetRecord
5
+ from dissect.target.plugin import ChildTargetPlugin
6
+
7
+
8
+ class DockerChildTargetPlugin(ChildTargetPlugin):
9
+ """Child target plugin that yields from Docker overlay2fs containers."""
10
+
11
+ __type__ = "docker"
12
+
13
+ def check_compatible(self) -> None:
14
+ if not self.target.has_function("docker"):
15
+ raise UnsupportedPluginError("No Docker data root folder(s) found!")
16
+
17
+ def list_children(self) -> Iterator[ChildTargetRecord]:
18
+ for container in self.target.docker.containers():
19
+ if container.mount_path:
20
+ yield ChildTargetRecord(
21
+ type=self.__type__,
22
+ path=container.mount_path,
23
+ _target=self.target,
24
+ )
@@ -154,7 +154,7 @@ class MftPlugin(Plugin):
154
154
  try:
155
155
  inuse = bool(record.header.Flags & FILE_RECORD_SEGMENT_IN_USE)
156
156
  owner, _ = get_owner_and_group(record, fs)
157
- resident = None
157
+ resident = False
158
158
  size = None
159
159
 
160
160
  if not record.is_dir():
@@ -3,7 +3,7 @@ from typing import Iterable
3
3
  from dissect.util.ts import from_unix
4
4
 
5
5
  from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
6
- from dissect.target.filesystem import RootFilesystemEntry
6
+ from dissect.target.filesystem import LayerFilesystemEntry
7
7
  from dissect.target.helpers.fsutil import TargetPath
8
8
  from dissect.target.helpers.record import TargetRecordDescriptor
9
9
  from dissect.target.plugin import Plugin, export
@@ -50,7 +50,7 @@ def generate_record(target: Target, path: TargetPath) -> FilesystemRecord:
50
50
  stat = path.lstat()
51
51
  btime = from_unix(stat.st_birthtime) if stat.st_birthtime else None
52
52
  entry = path.get()
53
- if isinstance(entry, RootFilesystemEntry):
53
+ if isinstance(entry, LayerFilesystemEntry):
54
54
  fs_types = [sub_entry.fs.__type__ for sub_entry in entry.entries]
55
55
  else:
56
56
  fs_types = [entry.fs.__type__]
@@ -1,5 +1,7 @@
1
+ from functools import lru_cache
1
2
  from typing import Generator, NamedTuple, Optional, Union
2
3
 
4
+ from dissect.target import Target
3
5
  from dissect.target.exceptions import UnsupportedPluginError
4
6
  from dissect.target.helpers.fsutil import TargetPath
5
7
  from dissect.target.helpers.record import UnixUserRecord, WindowsUserRecord
@@ -16,6 +18,10 @@ class UsersPlugin(InternalPlugin):
16
18
 
17
19
  __namespace__ = "user_details"
18
20
 
21
+ def __init__(self, target: Target):
22
+ super().__init__(target)
23
+ self.find = lru_cache(32)(self.find)
24
+
19
25
  def check_compatible(self) -> None:
20
26
  if not hasattr(self.target, "users"):
21
27
  raise UnsupportedPluginError("Unsupported Plugin")
File without changes
@@ -97,7 +97,7 @@ class ESXiPlugin(UnixPlugin):
97
97
 
98
98
  # Create a root layer for the "local state" filesystem
99
99
  # This stores persistent configuration data
100
- local_layer = target.fs.add_layer()
100
+ local_layer = target.fs.append_layer()
101
101
 
102
102
  # Mount all the visor tars in individual filesystem layers
103
103
  _mount_modules(target, sysvol, cfg)
@@ -209,7 +209,7 @@ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
209
209
  tfs = tar.TarFilesystem(cfile, tarinfo=vmtar.VisorTarInfo)
210
210
 
211
211
  if tfs:
212
- target.fs.add_layer().mount("/", tfs)
212
+ target.fs.append_layer().mount("/", tfs)
213
213
 
214
214
 
215
215
  def _mount_local(target: Target, local_layer: VirtualFilesystem):
@@ -24,7 +24,7 @@ class VyosPlugin(LinuxPlugin):
24
24
  self._version, rootpath = latest
25
25
 
26
26
  # VyOS does some additional magic with base system files
27
- layer = target.fs.add_layer()
27
+ layer = target.fs.append_layer()
28
28
  layer.map_file_entry("/", target.fs.root.get(f"/boot/{self._version}/{rootpath}"))
29
29
  super().__init__(target)
30
30
 
@@ -23,9 +23,9 @@ from dissect.target.target import Target
23
23
  try:
24
24
  from Crypto.Cipher import AES, ChaCha20
25
25
 
26
- HAS_PYCRYPTODOME = True
26
+ HAS_CRYPTO = True
27
27
  except ImportError:
28
- HAS_PYCRYPTODOME = False
28
+ HAS_CRYPTO = False
29
29
 
30
30
  FortiOSUserRecord = TargetRecordDescriptor(
31
31
  "fortios/user",
@@ -113,7 +113,7 @@ class FortiOSPlugin(LinuxPlugin):
113
113
 
114
114
  # FortiGate
115
115
  if (datafs_tar := sysvol.path("/datafs.tar.gz")).exists():
116
- target.fs.add_layer().mount("/data", TarFilesystem(datafs_tar.open("rb")))
116
+ target.fs.append_layer().mount("/data", TarFilesystem(datafs_tar.open("rb")))
117
117
 
118
118
  # Additional FortiGate or FortiManager tars with corrupt XZ streams
119
119
  target.log.warning("Attempting to load XZ files, this can take a while.")
@@ -127,11 +127,11 @@ class FortiOSPlugin(LinuxPlugin):
127
127
  ):
128
128
  if (tar := target.fs.path(path)).exists() or (tar := sysvol.path(path)).exists():
129
129
  fh = xz.repair_checksum(tar.open("rb"))
130
- target.fs.add_layer().mount("/", TarFilesystem(fh))
130
+ target.fs.append_layer().mount("/", TarFilesystem(fh))
131
131
 
132
132
  # FortiAnalyzer and FortiManager
133
133
  if (rootfs_ext_tar := sysvol.path("rootfs-ext.tar.xz")).exists():
134
- target.fs.add_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb")))
134
+ target.fs.append_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb")))
135
135
 
136
136
  # Filesystem mounts can be discovered in the FortiCare debug report
137
137
  # or using ``fnsysctl ls`` and ``fnsysctl df`` in the cli.
@@ -442,8 +442,8 @@ def decrypt_password(input: str) -> str:
442
442
  - https://www.fortiguard.com/psirt/FG-IR-19-007
443
443
  """
444
444
 
445
- if not HAS_PYCRYPTODOME:
446
- raise RuntimeError("PyCryptodome module not available")
445
+ if not HAS_CRYPTO:
446
+ raise RuntimeError("Missing pycryptodome dependency")
447
447
 
448
448
  if input[:3] in ["SH2", "AK1"]:
449
449
  raise ValueError("Password is a hash (SHA-256 or SHA-1) and cannot be decrypted.")
@@ -511,8 +511,8 @@ def decrypt_rootfs(fh: BinaryIO, key: bytes, iv: bytes) -> BinaryIO:
511
511
  RuntimeError: When PyCryptodome is not available.
512
512
  """
513
513
 
514
- if not HAS_PYCRYPTODOME:
515
- raise RuntimeError("PyCryptodome module not available")
514
+ if not HAS_CRYPTO:
515
+ raise RuntimeError("Missing pycryptodome dependency")
516
516
 
517
517
  # First 8 bytes = counter, last 8 bytes = nonce
518
518
  # PyCryptodome interally divides this seek by 64 to get a (position, offset) tuple
@@ -63,6 +63,7 @@ class ServicesPlugin(Plugin):
63
63
  for key, value in configuration.items():
64
64
  _value = value or None
65
65
  _key = f"{segment}_{key}"
66
+ _key = _key.replace("-", "_")
66
67
  types.append(("string", _key))
67
68
  config.update({_key: _value})
68
69
  except FileNotFoundError: