dissect.target 3.16.dev44__py3-none-any.whl → 3.17__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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: