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.
- dissect/target/plugins/apps/vpn/openvpn.py +115 -93
- {dissect.target-3.17.dev4.dist-info → dissect.target-3.17.dev5.dist-info}/METADATA +1 -1
- {dissect.target-3.17.dev4.dist-info → dissect.target-3.17.dev5.dist-info}/RECORD +8 -8
- {dissect.target-3.17.dev4.dist-info → dissect.target-3.17.dev5.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.17.dev4.dist-info → dissect.target-3.17.dev5.dist-info}/LICENSE +0 -0
- {dissect.target-3.17.dev4.dist-info → dissect.target-3.17.dev5.dist-info}/WHEEL +0 -0
- {dissect.target-3.17.dev4.dist-info → dissect.target-3.17.dev5.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.17.dev4.dist-info → dissect.target-3.17.dev5.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,15 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import io
|
1
4
|
import itertools
|
2
|
-
import
|
3
|
-
from
|
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
|
-
|
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
|
65
|
-
"/etc/openvpn/server/*.conf",
|
66
|
-
"/etc/openvpn/client/*.conf",
|
121
|
+
"/etc/openvpn/",
|
67
122
|
# Windows
|
68
|
-
"sysvol/Program Files/OpenVPN/config
|
123
|
+
"sysvol/Program Files/OpenVPN/config/",
|
69
124
|
]
|
70
125
|
|
71
126
|
user_config_paths = {
|
72
|
-
OperatingSystem.WINDOWS.value: ["OpenVPN/config
|
73
|
-
OperatingSystem.OSX.value: ["Library/Application Support/OpenVPN Connect/profiles
|
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
|
80
|
-
self.configs.extend(self.target.fs.path().glob
|
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(
|
84
|
-
self.
|
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
|
-
|
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 =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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 =
|
109
|
-
|
110
|
-
|
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
|
-
|
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
|
-
|
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=
|
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=
|
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.
|
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=
|
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.
|
340
|
-
dissect.target-3.17.
|
341
|
-
dissect.target-3.17.
|
342
|
-
dissect.target-3.17.
|
343
|
-
dissect.target-3.17.
|
344
|
-
dissect.target-3.17.
|
345
|
-
dissect.target-3.17.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|