dissect.target 3.17.dev4__py3-none-any.whl → 3.17.dev5__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.
@@ -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("\"'")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev4
3
+ Version: 3.17.dev5
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -137,7 +137,7 @@ dissect/target/plugins/apps/ssh/opensshd.py,sha256=DaXKdgGF3GYHHA4buEvphcm6FF4C8
137
137
  dissect/target/plugins/apps/ssh/putty.py,sha256=N8ssjutUVN50JNA5fEIVISbP5sJ7bGTFidRbX3uNG5Y,9404
138
138
  dissect/target/plugins/apps/ssh/ssh.py,sha256=uCaoWlT2bgKLUHA1aL6XymJDWJ8JmLsN8PB1C66eidY,1409
139
139
  dissect/target/plugins/apps/vpn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
140
- dissect/target/plugins/apps/vpn/openvpn.py,sha256=NZeFSFgGAifevGIQBusdbBRFOPxu0584Th8rKE-XSus,6875
140
+ dissect/target/plugins/apps/vpn/openvpn.py,sha256=d-DGINTIHP_bvv3T09ZwbezHXGctvCyAhJ482m2_-a0,7654
141
141
  dissect/target/plugins/apps/vpn/wireguard.py,sha256=SoAMED_bwWJQ3nci5qEY-qV4wJKSSDZQ8K7DoJRYq0k,6521
142
142
  dissect/target/plugins/apps/webhosting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
143
  dissect/target/plugins/apps/webhosting/cpanel.py,sha256=OeFQnu9GmpffIlFyK-AR2Qf8tjyMhazWEAUyccDU5y0,2979
@@ -336,10 +336,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
336
336
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
337
337
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
338
338
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
339
- dissect.target-3.17.dev4.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
- dissect.target-3.17.dev4.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
- dissect.target-3.17.dev4.dist-info/METADATA,sha256=66nmVpweOFOoauPHo_Ii1fS7BGJVk1kgezK_wdbjJfk,11225
342
- dissect.target-3.17.dev4.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
- dissect.target-3.17.dev4.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
- dissect.target-3.17.dev4.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
- dissect.target-3.17.dev4.dist-info/RECORD,,
339
+ dissect.target-3.17.dev5.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
+ dissect.target-3.17.dev5.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
+ dissect.target-3.17.dev5.dist-info/METADATA,sha256=qGpE_5-EiSeQqcnEOWCm0ftgBhIpfe6yMQPtXmQ1i_0,11225
342
+ dissect.target-3.17.dev5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
+ dissect.target-3.17.dev5.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
+ dissect.target-3.17.dev5.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
+ dissect.target-3.17.dev5.dist-info/RECORD,,