dissect.target 3.17.dev3__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/loader.py CHANGED
@@ -176,6 +176,7 @@ def open(item: Union[str, Path], *args, **kwargs) -> Loader:
176
176
 
177
177
  register("local", "LocalLoader")
178
178
  register("remote", "RemoteLoader")
179
+ register("mqtt", "MQTTLoader")
179
180
  register("targetd", "TargetdLoader")
180
181
  register("asdf", "AsdfLoader")
181
182
  register("tar", "TarLoader")
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import ssl
5
+ import time
6
+ import urllib
7
+ from dataclasses import dataclass
8
+ from functools import lru_cache
9
+ from io import BytesIO
10
+ from pathlib import Path
11
+ from struct import pack, unpack_from
12
+ from typing import Any, Callable, Optional, Union
13
+
14
+ import paho.mqtt.client as mqtt
15
+ from dissect.util.stream import AlignedStream
16
+
17
+ from dissect.target.containers.raw import RawContainer
18
+ from dissect.target.exceptions import LoaderError
19
+ from dissect.target.filesystem import VirtualFilesystem
20
+ from dissect.target.loader import Loader
21
+ from dissect.target.plugin import arg
22
+ from dissect.target.target import Target
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+ DISK_INDEX_OFFSET = 9
27
+
28
+
29
+ def suppress(func: Callable) -> Callable:
30
+ def suppressed(*args, **kwargs):
31
+ try:
32
+ return func(*args, **kwargs)
33
+ except Exception:
34
+ return
35
+
36
+ return suppressed
37
+
38
+
39
+ @dataclass
40
+ class InfoMessage:
41
+ disks: list[DiskMessage]
42
+
43
+
44
+ @dataclass
45
+ class DiskMessage:
46
+ index: int = 0
47
+ sector_size: int = 0
48
+ total_size: int = 0
49
+
50
+
51
+ @dataclass
52
+ class SeekMessage:
53
+ data: bytes = b""
54
+
55
+
56
+ class MQTTStream(AlignedStream):
57
+ def __init__(self, stream: MQTTConnection, disk_id: int, size: Optional[int] = None):
58
+ self.stream = stream
59
+ self.disk_id = disk_id
60
+ super().__init__(size)
61
+
62
+ def _read(self, offset: int, length: int, optimization_strategy: int = 0) -> bytes:
63
+ data = self.stream.read(self.disk_id, offset, length, optimization_strategy)
64
+ return data
65
+
66
+
67
+ class MQTTConnection:
68
+ broker = None
69
+ host = None
70
+
71
+ def __init__(self, broker: Broker, host: str):
72
+ self.broker = broker
73
+ self.host = str(host)
74
+ self.info = lru_cache(128)(self.info)
75
+ self.read = lru_cache(128)(self.read)
76
+
77
+ def topo(self, peers: int):
78
+ self.broker.topology(self.host)
79
+
80
+ while len(self.broker.peers(self.host)) < peers:
81
+ self.broker.topology(self.host)
82
+ time.sleep(1)
83
+ return self.broker.peers(self.host)
84
+
85
+ def info(self) -> list[MQTTStream]:
86
+ disks = []
87
+ self.broker.info(self.host)
88
+
89
+ message = None
90
+ while message is None:
91
+ message = self.broker.disk(self.host)
92
+
93
+ for idx, disk in enumerate(message.disks):
94
+ disks.append(MQTTStream(self, idx, disk.total_size))
95
+
96
+ return disks
97
+
98
+ def read(self, disk_id: int, offset: int, length: int, optimization_strategy: int) -> bytes:
99
+ message = None
100
+ self.broker.seek(self.host, disk_id, offset, length, optimization_strategy)
101
+
102
+ attempts = 0
103
+ while True:
104
+ message = self.broker.read(self.host, disk_id, offset, length)
105
+ # don't waste time with sleep if we have a response
106
+ if message:
107
+ break
108
+
109
+ attempts += 1
110
+ time.sleep(0.01)
111
+ if attempts > 100:
112
+ # message might have not reached agent, resend...
113
+ self.broker.seek(self.host, disk_id, offset, length, optimization_strategy)
114
+ attempts = 0
115
+
116
+ return message.data
117
+
118
+
119
+ class Broker:
120
+ broker_host = None
121
+ broker_port = None
122
+ private_key_file = None
123
+ certificate_file = None
124
+ cacert_file = None
125
+ mqtt_client = None
126
+ connected = False
127
+ case = None
128
+
129
+ diskinfo = {}
130
+ index = {}
131
+ topo = {}
132
+
133
+ def __init__(self, broker: Broker, port: str, key: str, crt: str, ca: str, case: str, **kwargs):
134
+ self.broker_host = broker
135
+ self.broker_port = int(port)
136
+ self.private_key_file = key
137
+ self.certificate_file = crt
138
+ self.cacert_file = ca
139
+ self.case = case
140
+
141
+ @suppress
142
+ def read(self, host: str, disk_id: int, seek_address: int, read_length: int) -> SeekMessage:
143
+ key = f"{host}-{disk_id}-{seek_address}-{read_length}"
144
+ return self.index.pop(key)
145
+
146
+ @suppress
147
+ def disk(self, host: str) -> DiskMessage:
148
+ return self.diskinfo[host]
149
+
150
+ def peers(self, host: str) -> int:
151
+ return self.topo[host]
152
+
153
+ def _on_disk(self, hostname: str, payload: bytes) -> None:
154
+ (num_of_disks,) = unpack_from("<B", payload, offset=0)
155
+ disks = []
156
+ for disk_index in range(num_of_disks):
157
+ (
158
+ sector_size,
159
+ total_size,
160
+ ) = unpack_from("<IQ", payload, offset=1 + (disk_index * DISK_INDEX_OFFSET))
161
+ disks.append(DiskMessage(index=disk_index, sector_size=sector_size, total_size=total_size))
162
+
163
+ self.diskinfo[hostname] = InfoMessage(disks=disks)
164
+
165
+ def _on_read(self, hostname: str, tokens: list[str], payload: bytes) -> None:
166
+ disk_id = tokens[3]
167
+ seek_address = int(tokens[4], 16)
168
+ read_length = int(tokens[5], 16)
169
+ msg = SeekMessage(data=payload)
170
+
171
+ key = f"{hostname}-{disk_id}-{seek_address}-{read_length}"
172
+
173
+ if key in self.index:
174
+ return
175
+
176
+ self.index[key] = msg
177
+
178
+ def _on_id(self, hostname: str, payload: bytes) -> None:
179
+ key = hostname
180
+ host = payload.decode("utf-8")
181
+ if host not in self.topo[key]:
182
+ self.topo[key].append(payload.decode("utf-8"))
183
+ self.mqtt_client.subscribe(f"{self.case}/{host}/DISKS")
184
+ self.mqtt_client.subscribe(f"{self.case}/{host}/READ/#")
185
+ time.sleep(1)
186
+
187
+ def _on_log(self, client: mqtt.Client, userdata: Any, log_level: int, message: str) -> None:
188
+ log.debug(message)
189
+
190
+ def _on_connect(self, client: mqtt.Client, userdata: Any, flags: dict, rc: int) -> None:
191
+ self.connected = True
192
+
193
+ def _on_message(self, client: mqtt.Client, userdata: Any, msg: mqtt.client.MQTTMessage) -> None:
194
+ tokens = msg.topic.split("/")
195
+ casename, hostname, response, *_ = tokens
196
+ if casename != self.case:
197
+ return
198
+
199
+ if response == "DISKS":
200
+ self._on_disk(hostname, msg.payload)
201
+ elif response == "READ":
202
+ self._on_read(hostname, tokens, msg.payload)
203
+ elif response == "ID":
204
+ self._on_id(hostname, msg.payload)
205
+
206
+ def seek(self, host: str, disk_id: int, offset: int, length: int, optimization_strategy: int) -> None:
207
+ self.mqtt_client.publish(
208
+ f"{self.case}/{host}/SEEK/{disk_id}/{hex(offset)}/{hex(length)}", pack("<I", optimization_strategy)
209
+ )
210
+
211
+ def info(self, host: str) -> None:
212
+ self.mqtt_client.publish(f"{self.case}/{host}/INFO")
213
+
214
+ def topology(self, host: str) -> None:
215
+ self.topo[host] = []
216
+ self.mqtt_client.subscribe(f"{self.case}/{host}/ID")
217
+ time.sleep(1) # need some time to avoid race condition, i.e. MQTT might react too fast
218
+ self.mqtt_client.publish(f"{self.case}/{host}/TOPO")
219
+
220
+ def connect(self) -> None:
221
+ self.mqtt_client = mqtt.Client(
222
+ client_id="", clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp"
223
+ )
224
+ self.mqtt_client.tls_set(
225
+ ca_certs=self.cacert_file,
226
+ certfile=self.certificate_file,
227
+ keyfile=self.private_key_file,
228
+ cert_reqs=ssl.CERT_REQUIRED,
229
+ tls_version=ssl.PROTOCOL_TLS,
230
+ ciphers=None,
231
+ )
232
+ self.mqtt_client.tls_insecure_set(True) # merely having the correct cert is ok
233
+ self.mqtt_client.on_connect = self._on_connect
234
+ self.mqtt_client.on_message = self._on_message
235
+ if log.getEffectiveLevel() == logging.DEBUG:
236
+ self.mqtt_client.on_log = self._on_log
237
+ self.mqtt_client.connect(self.broker_host, port=self.broker_port, keepalive=60)
238
+ self.mqtt_client.loop_start()
239
+
240
+
241
+ @arg("--mqtt-peers", type=int, dest="peers", help="minimum number of peers to await for first alias")
242
+ @arg("--mqtt-case", dest="case", help="case name (broker will determine if you are allowed to access this data)")
243
+ @arg("--mqtt-port", type=int, dest="port", help="broker connection port")
244
+ @arg("--mqtt-broker", dest="broker", help="broker ip-address")
245
+ @arg("--mqtt-key", dest="key", help="private key file")
246
+ @arg("--mqtt-crt", dest="crt", help="client certificate file")
247
+ @arg("--mqtt-ca", dest="ca", help="certificate authority file")
248
+ class MQTTLoader(Loader):
249
+ """Load remote targets through a broker."""
250
+
251
+ PATH = "/remote/data/hosts.txt"
252
+ FOLDER = "/remote/hosts"
253
+
254
+ connection = None
255
+ broker = None
256
+ peers = []
257
+
258
+ def __init__(self, path: Union[Path, str], **kwargs):
259
+ super().__init__(path)
260
+ cls = MQTTLoader
261
+
262
+ if str(path).startswith("/remote/hosts/host"):
263
+ self.path = path.read_text() # update path to reflect the resolved host
264
+
265
+ num_peers = 1
266
+ if cls.broker is None:
267
+ if (uri := kwargs.get("parsed_path")) is None:
268
+ raise LoaderError("No URI connection details has been passed.")
269
+ options = dict(urllib.parse.parse_qsl(uri.query, keep_blank_values=True))
270
+ cls.broker = Broker(**options)
271
+ cls.broker.connect()
272
+ num_peers = int(options.get("peers", 1))
273
+
274
+ self.broker = cls.broker
275
+ self.connection = MQTTConnection(self.broker, self.path)
276
+ self.peers = self.connection.topo(num_peers)
277
+
278
+ def map(self, target: Target) -> None:
279
+ if len(self.peers) == 1 and self.peers[0] == str(self.path):
280
+ target.path = Path(str(self.path))
281
+ for disk in self.connection.info():
282
+ target.disks.add(RawContainer(disk))
283
+ else:
284
+ target.mqtt = True
285
+ vfs = VirtualFilesystem()
286
+ vfs.map_file_fh(self.PATH, BytesIO("\n".join(self.peers).encode("utf-8")))
287
+ for index, peer in enumerate(self.peers):
288
+ vfs.map_file_fh(f"{self.FOLDER}/host{index}-{peer}", BytesIO(peer.encode("utf-8")))
289
+
290
+ target.fs.mount("/data", vfs)
291
+ target.filesystems.add(vfs)
292
+
293
+ @staticmethod
294
+ def detect(path: Path) -> bool:
295
+ return str(path).startswith("/remote/hosts/host")
@@ -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,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Iterator
4
+
5
+ from flow.record.fieldtypes import posix_path
6
+
7
+ from dissect.target.exceptions import UnsupportedPluginError
8
+ from dissect.target.helpers.record import ChildTargetRecord
9
+ from dissect.target.plugin import ChildTargetPlugin
10
+
11
+ if TYPE_CHECKING:
12
+ from dissect.target.target import Target
13
+
14
+
15
+ class MQTT(ChildTargetPlugin):
16
+ """Child target plugin that yields from remote broker."""
17
+
18
+ __type__ = "mqtt"
19
+
20
+ PATH = "/remote/data/hosts.txt"
21
+ FOLDER = "/remote/hosts"
22
+
23
+ def __init__(self, target: Target):
24
+ super().__init__(target)
25
+
26
+ def check_compatible(self) -> None:
27
+ if not self.target.mqtt or not self.target.fs.path(self.PATH).exists():
28
+ raise UnsupportedPluginError("No remote children.txt file found.")
29
+
30
+ def list_children(self) -> Iterator[ChildTargetRecord]:
31
+ hosts = self.target.fs.path(self.PATH).read_text(encoding="utf-8").split("\n")
32
+ for index, host in enumerate(hosts):
33
+ yield ChildTargetRecord(
34
+ type=self.__type__, path=posix_path(f"{self.FOLDER}/host{index}-{host}"), _target=self.target
35
+ )
@@ -37,6 +37,7 @@ from dissect.target.plugin import arg
37
37
  from dissect.target.target import Target
38
38
  from dissect.target.tools.info import print_target_info
39
39
  from dissect.target.tools.utils import (
40
+ args_to_uri,
40
41
  catch_sigpipe,
41
42
  configure_generic_arguments,
42
43
  generate_argparse_for_bound_method,
@@ -1223,10 +1224,17 @@ def main() -> None:
1223
1224
  parser.add_argument("targets", metavar="TARGETS", nargs="*", help="targets to load")
1224
1225
  parser.add_argument("-p", "--python", action="store_true", help="(I)Python shell")
1225
1226
  parser.add_argument("-r", "--registry", action="store_true", help="registry shell")
1227
+ parser.add_argument(
1228
+ "-L",
1229
+ "--loader",
1230
+ action="store",
1231
+ default=None,
1232
+ help="select a specific loader (i.e. vmx, raw)",
1233
+ )
1226
1234
 
1227
1235
  configure_generic_arguments(parser)
1228
- args = parser.parse_args()
1229
-
1236
+ args, rest = parser.parse_known_args()
1237
+ args.targets = args_to_uri(args.targets, args.loader, rest) if args.loader else args.targets
1230
1238
  process_generic_arguments(args)
1231
1239
 
1232
1240
  # For the shell tool we want -q to log slightly more then just CRITICAL
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev3
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
@@ -58,6 +58,9 @@ Requires-Dist: fusepy ; extra == 'full'
58
58
  Requires-Dist: pycryptodome ; extra == 'full'
59
59
  Requires-Dist: pyyaml ; extra == 'full'
60
60
  Requires-Dist: zstandard ; extra == 'full'
61
+ Provides-Extra: mqtt
62
+ Requires-Dist: dissect.target[full] ; extra == 'mqtt'
63
+ Requires-Dist: paho-mqtt ==1.6.1 ; extra == 'mqtt'
61
64
  Provides-Extra: smb
62
65
  Requires-Dist: dissect.target[full] ; extra == 'smb'
63
66
  Requires-Dist: impacket ==0.10.0 ; extra == 'smb'
@@ -2,7 +2,7 @@ dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
2
2
  dissect/target/container.py,sha256=9ixufT1_0WhraqttBWwQjG80caToJqvCX8VjFk8d5F0,9307
3
3
  dissect/target/exceptions.py,sha256=VVW_Rq_vQinapz-2mbJ3UkxBEZpb2pE_7JlhMukdtrY,2877
4
4
  dissect/target/filesystem.py,sha256=VD1BA6hLqH_FPWFZ-wliEuCxnFrUK61S9VbGK7CtA5w,55597
5
- dissect/target/loader.py,sha256=0-LcZNi7S0qsXR7XGtrzxpuCh9BsLcqNR1T15O7SnBM,7257
5
+ dissect/target/loader.py,sha256=HVNHcNhLixbxHl8glbEEC4njkp85hzj0Qdn4of1EnDY,7288
6
6
  dissect/target/plugin.py,sha256=HAN8maaDt-Rlqt8Rr1IW7gXQpzNQZjCVz-i4aSPphSw,48677
7
7
  dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
8
8
  dissect/target/target.py,sha256=xNJdecZSt2oHcZwf775kOSTFRA-c_hKoScXaDuK-8FI,32155
@@ -84,6 +84,7 @@ dissect/target/loaders/itunes.py,sha256=69aMTQiiGYpmD_EYSmf9mO1re8C3jAZIEStmwlMx
84
84
  dissect/target/loaders/kape.py,sha256=t5TfrGLqPeIpUUpXzIl6aHsqXMEGDqJ5YwDCs07DiBA,1237
85
85
  dissect/target/loaders/local.py,sha256=Ul-LCd_fY7SyWOVR6nH-NqbkuNpxoZVmffwrkvQElU8,16453
86
86
  dissect/target/loaders/log.py,sha256=cCkDIRS4aPlX3U-n_jUKaI2FPSV3BDpfqKceaU7rBbo,1507
87
+ dissect/target/loaders/mqtt.py,sha256=yyjiRkVipMcUkUZijk-1mSJAwaWliryhE2CF1FoTwB8,9948
87
88
  dissect/target/loaders/multiraw.py,sha256=4a3ZST0NwjnfPDxHkcEfAcX2ddUlT_C-rcrMHNg1wp4,1046
88
89
  dissect/target/loaders/ova.py,sha256=6h4O-7i87J394C6KgLsPkdXRAKNwtPubzLNS3vBGs7U,744
89
90
  dissect/target/loaders/ovf.py,sha256=ELMq6J2y6cPKbp7pjWAqMMnFYefWxXNqzIiAQdvGGXQ,1061
@@ -136,7 +137,7 @@ dissect/target/plugins/apps/ssh/opensshd.py,sha256=DaXKdgGF3GYHHA4buEvphcm6FF4C8
136
137
  dissect/target/plugins/apps/ssh/putty.py,sha256=N8ssjutUVN50JNA5fEIVISbP5sJ7bGTFidRbX3uNG5Y,9404
137
138
  dissect/target/plugins/apps/ssh/ssh.py,sha256=uCaoWlT2bgKLUHA1aL6XymJDWJ8JmLsN8PB1C66eidY,1409
138
139
  dissect/target/plugins/apps/vpn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
139
- 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
140
141
  dissect/target/plugins/apps/vpn/wireguard.py,sha256=SoAMED_bwWJQ3nci5qEY-qV4wJKSSDZQ8K7DoJRYq0k,6521
141
142
  dissect/target/plugins/apps/webhosting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
143
  dissect/target/plugins/apps/webhosting/cpanel.py,sha256=OeFQnu9GmpffIlFyK-AR2Qf8tjyMhazWEAUyccDU5y0,2979
@@ -150,6 +151,7 @@ dissect/target/plugins/apps/webserver/webserver.py,sha256=a7a2lLrhsa9c1AXnwiLP-t
150
151
  dissect/target/plugins/child/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
151
152
  dissect/target/plugins/child/esxi.py,sha256=GfgQzxntcHcyxAE2QjMJ-TrFhklweSXLbYh0uuv-klg,693
152
153
  dissect/target/plugins/child/hyperv.py,sha256=R2qVeu4p_9V53jO-65znN0LwX9v3FVA-9jbbtOQcEz8,2236
154
+ dissect/target/plugins/child/mqtt.py,sha256=9tMxJ4GA4OUEtfDQmKd-gK1U7HTyJTPQ7_iYA9vMvSc,1176
153
155
  dissect/target/plugins/child/virtuozzo.py,sha256=Mx4ZxEl21g7IYkzraw4FBZup5EfrkFDv4WuTE3hxguw,1206
154
156
  dissect/target/plugins/child/vmware_workstation.py,sha256=8wkA_tSufvBUyp4XQHzRzFETf5ROlyyO_MVS3TExyfw,1570
155
157
  dissect/target/plugins/child/wsl.py,sha256=IssQgYET1T-XR5ZX2lGlNFJ_u_3QECpMF_7kXu09HTE,2469
@@ -320,7 +322,7 @@ dissect/target/tools/logging.py,sha256=5ZnumtMWLyslxfrUGZ4ntRyf3obOOhmn8SBjKfdLc
320
322
  dissect/target/tools/mount.py,sha256=L_0tSmiBdW4aSaF0vXjB0bAkTC0kmT2N1hrbW6s5Jow,3254
321
323
  dissect/target/tools/query.py,sha256=1LbvUKSmXOCMb4xqP3t86JkOgFzKlc7mLCqcczfLht8,16018
322
324
  dissect/target/tools/reg.py,sha256=FDsiBBDxjWVUBTRj8xn82vZe-J_d9piM-TKS3PHZCcM,3193
323
- dissect/target/tools/shell.py,sha256=EBRNKiIV3ljaXKAXraA6DmrIw8Cy5h9irAuwlblP3zo,43251
325
+ dissect/target/tools/shell.py,sha256=6GF-mr4EpzX34G9sqTNKcccFJXhNTlrUqriRYIW7P7o,43544
324
326
  dissect/target/tools/utils.py,sha256=bhVZ3-8YynpHkBl4m1T4IpSpCArAXnEjjYwAFGW5JPg,10595
325
327
  dissect/target/tools/dump/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
326
328
  dissect/target/tools/dump/run.py,sha256=yHn9xl_VjasgiuLpjtZdnLW32QCbkwHfnnTPY6Ck_aw,9689
@@ -334,10 +336,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
334
336
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
335
337
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
336
338
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
337
- dissect.target-3.17.dev3.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
338
- dissect.target-3.17.dev3.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
339
- dissect.target-3.17.dev3.dist-info/METADATA,sha256=tT1AlpqvMn7l4mtqaIyghmF6WcYv_dEllU2imku16RI,11099
340
- dissect.target-3.17.dev3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
341
- dissect.target-3.17.dev3.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
342
- dissect.target-3.17.dev3.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
343
- dissect.target-3.17.dev3.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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5