dissect.target 3.17.dev13__py3-none-any.whl → 3.17.dev17__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -258,3 +258,4 @@ register("vdi", "VdiContainer")
258
258
  register("hdd", "HddContainer")
259
259
  register("hds", "HdsContainer")
260
260
  register("split", "SplitContainer")
261
+ register("fortifw", "FortiFirmwareContainer")
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ import io
5
+ import logging
6
+ import zlib
7
+ from itertools import cycle, islice
8
+ from pathlib import Path
9
+ from typing import BinaryIO
10
+
11
+ from dissect.util.stream import AlignedStream, RangeStream, RelativeStream
12
+
13
+ from dissect.target.container import Container
14
+ from dissect.target.tools.utils import catch_sigpipe
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ def find_xor_key(fh: BinaryIO) -> bytes:
20
+ """Find the XOR key for the firmware file by using known plaintext of zeros.
21
+
22
+ File-like object ``fh`` must be seeked to the correct offset where it should decode to all zeroes (0x00).
23
+
24
+ Arguments:
25
+ fh: File-like object to read from.
26
+
27
+ Returns:
28
+ bytes: XOR key, note that the XOR key is not validated and may be incorrect.
29
+ """
30
+ key = bytearray()
31
+
32
+ pos = fh.tell()
33
+ buf = fh.read(32)
34
+ fh.seek(pos)
35
+
36
+ if pos % 512 == 0:
37
+ xor_char = 0xFF
38
+ else:
39
+ fh.seek(pos - 1)
40
+ xor_char = ord(fh.read(1))
41
+
42
+ for i, k_char in enumerate(buf):
43
+ idx = (i + pos) & 0x1F
44
+ key.append((xor_char ^ k_char ^ idx) & 0xFF)
45
+ xor_char = k_char
46
+
47
+ # align xor key
48
+ koffset = 32 - (pos & 0x1F)
49
+ key = islice(cycle(key), koffset, koffset + 32)
50
+ return bytes(key)
51
+
52
+
53
+ class FortiFirmwareFile(AlignedStream):
54
+ """Fortinet firmware file, handles transparant decompression and deobfuscation of the firmware file."""
55
+
56
+ def __init__(self, fh: BinaryIO):
57
+ self.fh = fh
58
+ self.trailer_offset = None
59
+ self.trailer_data = None
60
+ self.xor_key = None
61
+ self.is_gzipped = False
62
+
63
+ size = None
64
+
65
+ # Check if the file is gzipped
66
+ self.fh.seek(0)
67
+ header = self.fh.read(4)
68
+ if header.startswith(b"\x1f\x8b"):
69
+ self.is_gzipped = True
70
+
71
+ # Find the extra metadata behind the gzip compressed data
72
+ # as a bonus we can also calculate the size of the firmware here
73
+ dec = zlib.decompressobj(wbits=16 + zlib.MAX_WBITS)
74
+ self.fh.seek(0)
75
+ size = 0
76
+ while True:
77
+ data = self.fh.read(io.DEFAULT_BUFFER_SIZE)
78
+ if not data:
79
+ break
80
+ d = dec.decompress(dec.unconsumed_tail + data)
81
+ size += len(d)
82
+
83
+ # Ignore the trailer data of the gzip file if we have any
84
+ if dec.unused_data:
85
+ self.trailer_offset = self.fh.seek(-len(dec.unused_data), io.SEEK_END)
86
+ self.trailer_data = self.fh.read()
87
+ log.info("Found trailer offset: %d, data: %r", self.trailer_offset, self.trailer_data)
88
+ self.fh = RangeStream(self.fh, 0, self.trailer_offset)
89
+
90
+ self.fh.seek(0)
91
+ self.fh = gzip.GzipFile(fileobj=self.fh)
92
+
93
+ # Find the xor key based on known offsets where the firmware should decode to zero bytes
94
+ for zero_offset in (0x30, 0x40, 0x400):
95
+ self.fh.seek(zero_offset)
96
+ xor_key = find_xor_key(self.fh)
97
+ if xor_key.isalnum():
98
+ self.xor_key = xor_key
99
+ log.info("Found key %r @ offset %s", self.xor_key, zero_offset)
100
+ break
101
+ else:
102
+ log.info("No xor key found")
103
+
104
+ # Determine the size of the firmware file if we didn't calculate it yet
105
+ if size is None:
106
+ size = self.fh.seek(0, io.SEEK_END)
107
+
108
+ log.info("firmware size: %s", size)
109
+ log.info("xor key: %r", self.xor_key)
110
+ log.info("gzipped: %s", self.is_gzipped)
111
+ self.fh.seek(0)
112
+
113
+ # Align the stream to 512 bytes which simplifies the XOR deobfuscation code
114
+ super().__init__(size=size, align=512)
115
+
116
+ def _read(self, offset: int, length: int) -> bytes:
117
+ self.fh.seek(offset)
118
+ buf = self.fh.read(length)
119
+
120
+ if not self.xor_key:
121
+ return buf
122
+
123
+ buf = bytearray(buf)
124
+ xor_char = 0xFF
125
+ for i, cur_char in enumerate(buf):
126
+ if (i + offset) % 512 == 0:
127
+ xor_char = 0xFF
128
+ idx = (i + offset) & 0x1F
129
+ buf[i] = ((self.xor_key[idx] ^ cur_char ^ xor_char) - idx) & 0xFF
130
+ xor_char = cur_char
131
+
132
+ return bytes(buf)
133
+
134
+
135
+ class FortiFirmwareContainer(Container):
136
+ __type__ = "fortifw"
137
+
138
+ def __init__(self, fh: BinaryIO | Path, *args, **kwargs):
139
+ if not hasattr(fh, "read"):
140
+ fh = fh.open("rb")
141
+
142
+ # Open the firmware file
143
+ self.ff = FortiFirmwareFile(fh)
144
+
145
+ # seek to MBR
146
+ self.fw = RelativeStream(self.ff, 0x200)
147
+ super().__init__(self.fw, self.ff.size, *args, **kwargs)
148
+
149
+ @staticmethod
150
+ def detect_fh(fh: BinaryIO, original: list | BinaryIO) -> bool:
151
+ return False
152
+
153
+ @staticmethod
154
+ def detect_path(path: Path, original: list | BinaryIO) -> bool:
155
+ # all Fortinet firmware files end with `-FORTINET.out`
156
+ return str(path).lower().endswith("-fortinet.out")
157
+
158
+ def read(self, length: int) -> bytes:
159
+ return self.fw.read(length)
160
+
161
+ def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
162
+ return self.fw.seek(offset, whence)
163
+
164
+ def tell(self) -> int:
165
+ return self.fw.tell()
166
+
167
+ def close(self) -> None:
168
+ pass
169
+
170
+
171
+ @catch_sigpipe
172
+ def main(argv: list[str] | None = None) -> None:
173
+ import argparse
174
+ import shutil
175
+ import sys
176
+
177
+ parser = argparse.ArgumentParser(description="Decompress and deobfuscate Fortinet firmware file to stdout.")
178
+ parser.add_argument("file", type=argparse.FileType("rb"), help="Fortinet firmware file")
179
+ parser.add_argument("--verbose", "-v", action="store_true", help="verbose output")
180
+ args = parser.parse_args(argv)
181
+
182
+ if args.verbose:
183
+ logging.basicConfig(level=logging.INFO)
184
+
185
+ ff = FortiFirmwareFile(args.file)
186
+ shutil.copyfileobj(ff, sys.stdout.buffer)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
dissect/target/loader.py CHANGED
@@ -77,7 +77,7 @@ class Loader:
77
77
  raise NotImplementedError()
78
78
 
79
79
  @staticmethod
80
- def find_all(path: Path) -> Iterator[Path]:
80
+ def find_all(path: Path, **kwargs) -> Iterator[Path]:
81
81
  """Finds all targets to load from ``path``.
82
82
 
83
83
  This can be used to open multiple targets from a target path that doesn't necessarily map to files on a disk.
@@ -6,17 +6,15 @@ import time
6
6
  import urllib
7
7
  from dataclasses import dataclass
8
8
  from functools import lru_cache
9
- from io import BytesIO
10
9
  from pathlib import Path
11
10
  from struct import pack, unpack_from
12
- from typing import Any, Callable, Optional, Union
11
+ from typing import Any, Callable, Iterator, Optional, Union
13
12
 
14
13
  import paho.mqtt.client as mqtt
15
14
  from dissect.util.stream import AlignedStream
16
15
 
17
16
  from dissect.target.containers.raw import RawContainer
18
17
  from dissect.target.exceptions import LoaderError
19
- from dissect.target.filesystem import VirtualFilesystem
20
18
  from dissect.target.loader import Loader
21
19
  from dissect.target.plugin import arg
22
20
  from dissect.target.target import Target
@@ -74,7 +72,7 @@ class MQTTConnection:
74
72
  self.info = lru_cache(128)(self.info)
75
73
  self.read = lru_cache(128)(self.read)
76
74
 
77
- def topo(self, peers: int):
75
+ def topo(self, peers: int) -> list[str]:
78
76
  self.broker.topology(self.host)
79
77
 
80
78
  while len(self.broker.peers(self.host)) < peers:
@@ -148,7 +146,7 @@ class Broker:
148
146
  def disk(self, host: str) -> DiskMessage:
149
147
  return self.diskinfo[host]
150
148
 
151
- def peers(self, host: str) -> int:
149
+ def peers(self, host: str) -> list[str]:
152
150
  return self.topo[host]
153
151
 
154
152
  def _on_disk(self, hostname: str, payload: bytes) -> None:
@@ -252,9 +250,6 @@ class Broker:
252
250
  class MQTTLoader(Loader):
253
251
  """Load remote targets through a broker."""
254
252
 
255
- PATH = "/remote/data/hosts.txt"
256
- FOLDER = "/remote/hosts"
257
-
258
253
  connection = None
259
254
  broker = None
260
255
  peers = []
@@ -262,10 +257,15 @@ class MQTTLoader(Loader):
262
257
  def __init__(self, path: Union[Path, str], **kwargs):
263
258
  super().__init__(path)
264
259
  cls = MQTTLoader
260
+ self.broker = cls.broker
261
+ self.connection = MQTTConnection(self.broker, path)
265
262
 
266
- if str(path).startswith("/remote/hosts/host"):
267
- self.path = path.read_text() # update path to reflect the resolved host
263
+ @staticmethod
264
+ def detect(path: Path) -> bool:
265
+ return False
268
266
 
267
+ def find_all(path: Path, **kwargs) -> Iterator[str]:
268
+ cls = MQTTLoader
269
269
  num_peers = 1
270
270
  if cls.broker is None:
271
271
  if (uri := kwargs.get("parsed_path")) is None:
@@ -275,26 +275,10 @@ class MQTTLoader(Loader):
275
275
  cls.broker.connect()
276
276
  num_peers = int(options.get("peers", 1))
277
277
 
278
- self.broker = cls.broker
279
- self.connection = MQTTConnection(self.broker, self.path)
280
- self.peers = self.connection.topo(num_peers)
278
+ cls.connection = MQTTConnection(cls.broker, path)
279
+ cls.peers = cls.connection.topo(num_peers)
280
+ yield from cls.peers
281
281
 
282
282
  def map(self, target: Target) -> None:
283
- if len(self.peers) == 1 and self.peers[0] == str(self.path):
284
- target.path = Path(str(self.path))
285
- for disk in self.connection.info():
286
- target.disks.add(RawContainer(disk))
287
- else:
288
- target.props["mqtt"] = True
289
-
290
- vfs = VirtualFilesystem()
291
- vfs.map_file_fh(self.PATH, BytesIO("\n".join(self.peers).encode("utf-8")))
292
- for index, peer in enumerate(self.peers):
293
- vfs.map_file_fh(f"{self.FOLDER}/host{index}-{peer}", BytesIO(peer.encode("utf-8")))
294
-
295
- target.fs.mount("/data", vfs)
296
- target.filesystems.add(vfs)
297
-
298
- @staticmethod
299
- def detect(path: Path) -> bool:
300
- return str(path).startswith("/remote/hosts/host")
283
+ for disk in self.connection.info():
284
+ target.disks.add(RawContainer(disk))
@@ -17,9 +17,9 @@ NetSocketRecord = TargetRecordDescriptor(
17
17
  ("uint32", "rx_queue"),
18
18
  ("uint32", "tx_queue"),
19
19
  ("string", "local_ip"),
20
- ("string", "local_port"),
20
+ ("uint16", "local_port"),
21
21
  ("string", "remote_ip"),
22
- ("string", "remote_port"),
22
+ ("uint16", "remote_port"),
23
23
  ("string", "state"),
24
24
  ("string", "owner"),
25
25
  ("uint32", "inode"),
dissect/target/target.py CHANGED
@@ -280,7 +280,7 @@ class Target:
280
280
  continue
281
281
 
282
282
  getlogger(entry).debug("Attempting to use loader: %s", loader_cls)
283
- for sub_entry in loader_cls.find_all(entry):
283
+ for sub_entry in loader_cls.find_all(entry, parsed_path=parsed_path):
284
284
  try:
285
285
  ldr = loader_cls(sub_entry, parsed_path=parsed_path)
286
286
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev13
3
+ Version: 3.17.dev17
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
@@ -1,15 +1,16 @@
1
1
  dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
2
- dissect/target/container.py,sha256=9ixufT1_0WhraqttBWwQjG80caToJqvCX8VjFk8d5F0,9307
2
+ dissect/target/container.py,sha256=0YcwcGmfJjhPXUB6DEcjWEoSuAtTDxMDpoTviMrLsxM,9353
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=HVNHcNhLixbxHl8glbEEC4njkp85hzj0Qdn4of1EnDY,7288
5
+ dissect/target/loader.py,sha256=_mrMOzKdpb7nlZJpLENOLuU4Ty92PzJem9GFDuo0PK4,7298
6
6
  dissect/target/plugin.py,sha256=HAN8maaDt-Rlqt8Rr1IW7gXQpzNQZjCVz-i4aSPphSw,48677
7
7
  dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
8
- dissect/target/target.py,sha256=xNJdecZSt2oHcZwf775kOSTFRA-c_hKoScXaDuK-8FI,32155
8
+ dissect/target/target.py,sha256=jq0Ii8073GOfwfqRj7UMuJT5jTVvQ_FD9Vrl9TMGpVc,32180
9
9
  dissect/target/volume.py,sha256=aQZAJiny8jjwkc9UtwIRwy7nINXjCxwpO-_UDfh6-BA,15801
10
10
  dissect/target/containers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  dissect/target/containers/asdf.py,sha256=DJp0QEFwUjy2MFwKYcYqIR_BS1fQT1Yi9Kcmqt0aChM,1366
12
12
  dissect/target/containers/ewf.py,sha256=FTEPZpogDzymrbAeSnLuHNNStifLzNVhUvtbEMOyo0E,1342
13
+ dissect/target/containers/fortifw.py,sha256=2Ga89c0qPguHAPigcte8wptgF2aM9qfPTZHddkfQ8J4,5874
13
14
  dissect/target/containers/hdd.py,sha256=Y1qYpk3GePCpq2HZIyqyoGch7nzN8aeI3zWG3UGhf5o,1069
14
15
  dissect/target/containers/hds.py,sha256=xijSUSRM392Ckc9QsOsvjx7PMyeoR4qOlWGG0w4nqUU,1145
15
16
  dissect/target/containers/qcow2.py,sha256=FtXLZA-Xkegbv--dStusQntUiDqM1idSFWMtJRiL7eM,1128
@@ -84,7 +85,7 @@ dissect/target/loaders/itunes.py,sha256=69aMTQiiGYpmD_EYSmf9mO1re8C3jAZIEStmwlMx
84
85
  dissect/target/loaders/kape.py,sha256=t5TfrGLqPeIpUUpXzIl6aHsqXMEGDqJ5YwDCs07DiBA,1237
85
86
  dissect/target/loaders/local.py,sha256=Ul-LCd_fY7SyWOVR6nH-NqbkuNpxoZVmffwrkvQElU8,16453
86
87
  dissect/target/loaders/log.py,sha256=cCkDIRS4aPlX3U-n_jUKaI2FPSV3BDpfqKceaU7rBbo,1507
87
- dissect/target/loaders/mqtt.py,sha256=J5RG35EwsshJtpFGkwMA6IOm638Xkl5df3SolO_BvCE,10225
88
+ dissect/target/loaders/mqtt.py,sha256=b0VrQ75_tmc4POkcfnUwKJoj1qmcjm1OKsVBQ9MjgqI,9552
88
89
  dissect/target/loaders/multiraw.py,sha256=4a3ZST0NwjnfPDxHkcEfAcX2ddUlT_C-rcrMHNg1wp4,1046
89
90
  dissect/target/loaders/ova.py,sha256=6h4O-7i87J394C6KgLsPkdXRAKNwtPubzLNS3vBGs7U,744
90
91
  dissect/target/loaders/ovf.py,sha256=ELMq6J2y6cPKbp7pjWAqMMnFYefWxXNqzIiAQdvGGXQ,1061
@@ -151,7 +152,6 @@ dissect/target/plugins/apps/webserver/webserver.py,sha256=a7a2lLrhsa9c1AXnwiLP-t
151
152
  dissect/target/plugins/child/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
152
153
  dissect/target/plugins/child/esxi.py,sha256=GfgQzxntcHcyxAE2QjMJ-TrFhklweSXLbYh0uuv-klg,693
153
154
  dissect/target/plugins/child/hyperv.py,sha256=R2qVeu4p_9V53jO-65znN0LwX9v3FVA-9jbbtOQcEz8,2236
154
- dissect/target/plugins/child/mqtt.py,sha256=8nLCm71FhQarqPMQ8JL-I-jjxSB9ct2pZGbq4FJO7ao,1189
155
155
  dissect/target/plugins/child/virtuozzo.py,sha256=Mx4ZxEl21g7IYkzraw4FBZup5EfrkFDv4WuTE3hxguw,1206
156
156
  dissect/target/plugins/child/vmware_workstation.py,sha256=8wkA_tSufvBUyp4XQHzRzFETf5ROlyyO_MVS3TExyfw,1570
157
157
  dissect/target/plugins/child/wsl.py,sha256=IssQgYET1T-XR5ZX2lGlNFJ_u_3QECpMF_7kXu09HTE,2469
@@ -216,7 +216,7 @@ dissect/target/plugins/os/unix/linux/netstat.py,sha256=MAC4ZdeNqcKpxT2ZMh1-7rjt4
216
216
  dissect/target/plugins/os/unix/linux/proc.py,sha256=jm35fAasnNbObN2tpflwQuCfVYLDkTP2EDrzYG42ZSk,23354
217
217
  dissect/target/plugins/os/unix/linux/processes.py,sha256=sTQqZYPW-_gs7Z3f0wwsV6clUX4NK44GGyMiZToBIrg,1936
218
218
  dissect/target/plugins/os/unix/linux/services.py,sha256=-d2y073mOXUM3XCzRgDVCRFR9eTLoVuN8FsZVewHzRg,4075
219
- dissect/target/plugins/os/unix/linux/sockets.py,sha256=l7Zq4J_xioyl8V7-Q9GLStOyYLNrcpKoWWWSzJaSIZQ,9765
219
+ dissect/target/plugins/os/unix/linux/sockets.py,sha256=11de73KiF2D2s1eyPBA4EWDpNsEzOunbj3YqSlMYZ2Y,9765
220
220
  dissect/target/plugins/os/unix/linux/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
221
221
  dissect/target/plugins/os/unix/linux/android/_os.py,sha256=trmESlpHdwVu7wV18RevEhh_TsVyfKPFCd5Usb5-fSU,2056
222
222
  dissect/target/plugins/os/unix/linux/debian/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -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.dev13.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
- dissect.target-3.17.dev13.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
- dissect.target-3.17.dev13.dist-info/METADATA,sha256=r2xQoOApd3QGiaNTpuo27XM3t1YkKLaNSLkywaWejJ8,11300
342
- dissect.target-3.17.dev13.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
- dissect.target-3.17.dev13.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
- dissect.target-3.17.dev13.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
- dissect.target-3.17.dev13.dist-info/RECORD,,
339
+ dissect.target-3.17.dev17.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
+ dissect.target-3.17.dev17.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
+ dissect.target-3.17.dev17.dist-info/METADATA,sha256=BAALMIovnkuqmnNGuxrQv3CM7ptX_oBXUyLi_LRWAPs,11300
342
+ dissect.target-3.17.dev17.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
+ dissect.target-3.17.dev17.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
+ dissect.target-3.17.dev17.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
+ dissect.target-3.17.dev17.dist-info/RECORD,,
@@ -1,35 +0,0 @@
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.props.get("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
- )