dissect.target 3.19.dev58__py3-none-any.whl → 3.20__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. dissect/target/container.py +1 -1
  2. dissect/target/exceptions.py +6 -5
  3. dissect/target/filesystem.py +2 -2
  4. dissect/target/filesystems/btrfs.py +14 -5
  5. dissect/target/filesystems/config.py +5 -1
  6. dissect/target/filesystems/extfs.py +5 -4
  7. dissect/target/filesystems/fat.py +22 -16
  8. dissect/target/filesystems/ffs.py +11 -4
  9. dissect/target/filesystems/jffs.py +12 -7
  10. dissect/target/filesystems/ntfs.py +22 -6
  11. dissect/target/filesystems/overlay.py +14 -4
  12. dissect/target/filesystems/smb.py +3 -3
  13. dissect/target/filesystems/squashfs.py +4 -4
  14. dissect/target/filesystems/vmfs.py +4 -4
  15. dissect/target/filesystems/xfs.py +15 -8
  16. dissect/target/helpers/compat/path_common.py +5 -5
  17. dissect/target/helpers/configutil.py +128 -32
  18. dissect/target/helpers/cyber.py +2 -0
  19. dissect/target/helpers/data/windowsZones.xml +19 -23
  20. dissect/target/helpers/docs.py +1 -1
  21. dissect/target/helpers/keychain.py +2 -0
  22. dissect/target/helpers/mount.py +2 -1
  23. dissect/target/helpers/record.py +29 -2
  24. dissect/target/helpers/record_modifier.py +5 -1
  25. dissect/target/helpers/regutil.py +56 -26
  26. dissect/target/loader.py +1 -1
  27. dissect/target/loaders/mqtt.py +104 -9
  28. dissect/target/loaders/proxmox.py +68 -0
  29. dissect/target/loaders/vma.py +1 -1
  30. dissect/target/loaders/xva.py +1 -1
  31. dissect/target/plugin.py +24 -21
  32. dissect/target/plugins/apps/av/mcafee.py +2 -0
  33. dissect/target/plugins/apps/av/sophos.py +2 -0
  34. dissect/target/plugins/apps/av/trendmicro.py +2 -0
  35. dissect/target/plugins/apps/browser/chromium.py +27 -6
  36. dissect/target/plugins/apps/container/docker.py +48 -32
  37. dissect/target/plugins/apps/editor/__init__.py +0 -0
  38. dissect/target/plugins/apps/editor/editor.py +23 -0
  39. dissect/target/plugins/apps/{texteditor → editor}/windowsnotepad.py +40 -31
  40. dissect/target/plugins/apps/other/__init__.py +0 -0
  41. dissect/target/plugins/apps/other/env.py +56 -0
  42. dissect/target/plugins/apps/shell/powershell.py +6 -2
  43. dissect/target/plugins/apps/shell/wget.py +91 -0
  44. dissect/target/plugins/apps/ssh/openssh.py +2 -0
  45. dissect/target/plugins/apps/ssh/opensshd.py +2 -0
  46. dissect/target/plugins/apps/virtualization/__init__.py +0 -0
  47. dissect/target/plugins/apps/virtualization/vmware_workstation.py +61 -0
  48. dissect/target/plugins/apps/vpn/wireguard.py +9 -9
  49. dissect/target/plugins/apps/webhosting/cpanel.py +2 -0
  50. dissect/target/plugins/apps/webserver/caddy.py +2 -0
  51. dissect/target/plugins/apps/webserver/nginx.py +2 -0
  52. dissect/target/plugins/child/esxi.py +3 -1
  53. dissect/target/plugins/child/parallels.py +68 -0
  54. dissect/target/plugins/child/proxmox.py +23 -0
  55. dissect/target/plugins/child/virtuozzo.py +12 -8
  56. dissect/target/plugins/child/vmware_workstation.py +23 -8
  57. dissect/target/plugins/filesystem/acquire_hash.py +2 -1
  58. dissect/target/plugins/filesystem/icat.py +15 -11
  59. dissect/target/plugins/filesystem/ntfs/mft.py +10 -6
  60. dissect/target/plugins/filesystem/ntfs/mft_timeline.py +3 -1
  61. dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -0
  62. dissect/target/plugins/filesystem/ntfs/utils.py +3 -1
  63. dissect/target/plugins/filesystem/unix/suid.py +4 -1
  64. dissect/target/plugins/filesystem/walkfs.py +2 -0
  65. dissect/target/plugins/general/example.py +2 -2
  66. dissect/target/plugins/general/loaders.py +18 -5
  67. dissect/target/plugins/general/network.py +20 -5
  68. dissect/target/plugins/general/osinfo.py +1 -0
  69. dissect/target/plugins/general/plugins.py +53 -10
  70. dissect/target/plugins/os/unix/_os.py +70 -44
  71. dissect/target/plugins/os/unix/applications.py +78 -0
  72. dissect/target/plugins/os/unix/bsd/citrix/history.py +2 -0
  73. dissect/target/plugins/os/unix/bsd/osx/_os.py +4 -21
  74. dissect/target/plugins/os/unix/bsd/osx/network.py +92 -0
  75. dissect/target/plugins/os/unix/bsd/osx/user.py +4 -0
  76. dissect/target/plugins/os/unix/cronjobs.py +8 -4
  77. dissect/target/plugins/os/unix/etc/etc.py +4 -0
  78. dissect/target/plugins/os/unix/generic.py +2 -0
  79. dissect/target/plugins/os/unix/history.py +27 -25
  80. dissect/target/plugins/os/unix/linux/_os.py +8 -10
  81. dissect/target/plugins/os/unix/linux/cmdline.py +2 -0
  82. dissect/target/plugins/os/unix/linux/debian/apt.py +4 -1
  83. dissect/target/plugins/os/unix/linux/debian/dpkg.py +3 -3
  84. dissect/target/plugins/os/unix/linux/debian/proxmox/__init__.py +0 -0
  85. dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py +141 -0
  86. dissect/target/plugins/os/unix/linux/debian/proxmox/vm.py +29 -0
  87. dissect/target/plugins/os/unix/linux/debian/snap.py +79 -0
  88. dissect/target/plugins/os/unix/linux/environ.py +2 -0
  89. dissect/target/plugins/os/unix/linux/fortios/_os.py +74 -63
  90. dissect/target/plugins/os/unix/linux/fortios/generic.py +2 -0
  91. dissect/target/plugins/os/unix/linux/fortios/locale.py +2 -0
  92. dissect/target/plugins/os/unix/linux/modules.py +2 -0
  93. dissect/target/plugins/os/unix/linux/netstat.py +2 -0
  94. dissect/target/{helpers → plugins/os/unix/linux}/network_managers.py +11 -9
  95. dissect/target/plugins/os/unix/linux/processes.py +2 -0
  96. dissect/target/plugins/os/unix/linux/redhat/yum.py +4 -1
  97. dissect/target/plugins/os/unix/linux/services.py +5 -3
  98. dissect/target/plugins/os/unix/linux/sockets.py +2 -0
  99. dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -1
  100. dissect/target/plugins/os/unix/locale.py +2 -0
  101. dissect/target/plugins/os/unix/locate/gnulocate.py +4 -2
  102. dissect/target/plugins/os/unix/locate/mlocate.py +2 -0
  103. dissect/target/plugins/os/unix/locate/plocate.py +3 -1
  104. dissect/target/plugins/os/unix/log/atop.py +2 -0
  105. dissect/target/plugins/os/unix/log/audit.py +3 -1
  106. dissect/target/plugins/os/unix/log/auth.py +351 -38
  107. dissect/target/plugins/os/unix/log/journal.py +123 -101
  108. dissect/target/plugins/os/unix/log/lastlog.py +5 -3
  109. dissect/target/plugins/os/unix/log/messages.py +51 -27
  110. dissect/target/plugins/os/unix/log/utmp.py +52 -71
  111. dissect/target/plugins/os/unix/packagemanager.py +5 -38
  112. dissect/target/plugins/os/unix/shadow.py +3 -1
  113. dissect/target/plugins/os/unix/trash.py +132 -0
  114. dissect/target/plugins/os/windows/_os.py +22 -41
  115. dissect/target/plugins/os/windows/activitiescache.py +9 -4
  116. dissect/target/plugins/os/windows/adpolicy.py +2 -1
  117. dissect/target/plugins/os/windows/amcache.py +16 -13
  118. dissect/target/plugins/os/windows/defender.py +4 -3
  119. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +3 -0
  120. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +3 -0
  121. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +3 -0
  122. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +3 -0
  123. dissect/target/plugins/os/windows/env.py +1 -2
  124. dissect/target/plugins/os/windows/exchange/exchange.py +6 -4
  125. dissect/target/plugins/os/windows/generic.py +68 -19
  126. dissect/target/plugins/os/windows/lnk.py +2 -0
  127. dissect/target/plugins/os/windows/locale.py +9 -3
  128. dissect/target/plugins/os/windows/log/etl.py +5 -4
  129. dissect/target/plugins/os/windows/log/evt.py +12 -8
  130. dissect/target/plugins/os/windows/log/evtx.py +9 -7
  131. dissect/target/plugins/os/windows/log/mssql.py +103 -0
  132. dissect/target/plugins/os/windows/log/pfro.py +2 -1
  133. dissect/target/plugins/os/windows/network.py +380 -0
  134. dissect/target/plugins/os/windows/notifications.py +6 -4
  135. dissect/target/plugins/os/windows/prefetch.py +7 -2
  136. dissect/target/plugins/os/windows/regf/7zip.py +9 -1
  137. dissect/target/plugins/os/windows/regf/applications.py +62 -0
  138. dissect/target/plugins/os/windows/regf/auditpol.py +2 -1
  139. dissect/target/plugins/os/windows/regf/bam.py +3 -1
  140. dissect/target/plugins/os/windows/regf/cit.py +14 -12
  141. dissect/target/plugins/os/windows/regf/clsid.py +6 -3
  142. dissect/target/plugins/os/windows/regf/firewall.py +2 -1
  143. dissect/target/plugins/os/windows/regf/mru.py +9 -8
  144. dissect/target/plugins/os/windows/regf/nethist.py +6 -3
  145. dissect/target/plugins/os/windows/regf/recentfilecache.py +3 -1
  146. dissect/target/plugins/os/windows/regf/regf.py +5 -1
  147. dissect/target/plugins/os/windows/regf/shellbags.py +351 -345
  148. dissect/target/plugins/os/windows/regf/shimcache.py +1 -1
  149. dissect/target/plugins/os/windows/regf/usb.py +2 -1
  150. dissect/target/plugins/os/windows/regf/userassist.py +2 -1
  151. dissect/target/plugins/os/windows/registry.py +11 -0
  152. dissect/target/plugins/os/windows/services.py +3 -2
  153. dissect/target/plugins/os/windows/startupinfo.py +7 -2
  154. dissect/target/plugins/os/windows/syscache.py +5 -2
  155. dissect/target/plugins/os/windows/tasks.py +1 -1
  156. dissect/target/plugins/os/windows/thumbcache.py +11 -5
  157. dissect/target/plugins/os/windows/ual.py +12 -9
  158. dissect/target/plugins/os/windows/wer.py +21 -6
  159. dissect/target/plugins/os/windows/wua_history.py +0 -1
  160. dissect/target/target.py +13 -8
  161. dissect/target/tools/dump/utils.py +4 -0
  162. dissect/target/tools/fsutils.py +1 -1
  163. dissect/target/tools/info.py +1 -1
  164. dissect/target/tools/mount.py +15 -5
  165. dissect/target/tools/query.py +15 -9
  166. dissect/target/tools/shell.py +98 -9
  167. dissect/target/tools/utils.py +7 -7
  168. dissect/target/volume.py +4 -4
  169. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/METADATA +6 -2
  170. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/RECORD +176 -160
  171. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/WHEEL +1 -1
  172. dissect/target/helpers/targetd.py +0 -58
  173. dissect/target/loaders/targetd.py +0 -223
  174. dissect/target/plugins/apps/texteditor/texteditor.py +0 -13
  175. dissect/target/plugins/os/unix/etc.py +0 -9
  176. /dissect/target/plugins/apps/{texteditor → database}/__init__.py +0 -0
  177. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/COPYRIGHT +0 -0
  178. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/LICENSE +0 -0
  179. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/entry_points.txt +0 -0
  180. {dissect.target-3.19.dev58.dist-info → dissect.target-3.20.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,13 @@
1
- """ Registry related abstractions """
1
+ """Registry related abstractions"""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import fnmatch
5
6
  import re
6
- import struct
7
7
  from collections import defaultdict
8
8
  from datetime import datetime
9
+ from enum import IntEnum
10
+ from functools import cached_property
9
11
  from io import BytesIO
10
12
  from pathlib import Path
11
13
  from typing import BinaryIO, Iterator, Optional, TextIO, Union
@@ -28,6 +30,19 @@ ValueType = Union[int, str, bytes, list[str]]
28
30
  """The possible value types that can be returned from the registry."""
29
31
 
30
32
 
33
+ class RegistryValueType(IntEnum):
34
+ NONE = regf.REG_NONE
35
+ SZ = regf.REG_SZ
36
+ EXPAND_SZ = regf.REG_EXPAND_SZ
37
+ BINARY = regf.REG_BINARY
38
+ DWORD = regf.REG_DWORD
39
+ DWORD_BIG_ENDIAN = regf.REG_DWORD_BIG_ENDIAN
40
+ MULTI_SZ = regf.REG_MULTI_SZ
41
+ FULL_RESOURCE_DESCRIPTOR = regf.REG_FULL_RESOURCE_DESCRIPTOR
42
+ RESOURCE_REQUIREMENTS_LIST = regf.REG_RESOURCE_REQUIREMENTS_LIST
43
+ QWORD = regf.REG_QWORD
44
+
45
+
31
46
  class RegistryHive:
32
47
  """Base class for registry hives."""
33
48
 
@@ -296,6 +311,7 @@ class VirtualKey(RegistryKey):
296
311
  self._class_name = class_name
297
312
  self._values: dict[str, RegistryValue] = {}
298
313
  self._subkeys: dict[str, RegistryKey] = {}
314
+ self._timestamp: datetime = None
299
315
  self.top: RegistryKey = None
300
316
  super().__init__(hive=hive)
301
317
 
@@ -325,11 +341,19 @@ class VirtualKey(RegistryKey):
325
341
  return self._path
326
342
 
327
343
  @property
328
- def timestamp(self) -> datetime:
344
+ def timestamp(self) -> datetime | None:
329
345
  if self.top:
330
346
  return self.top.timestamp
347
+
348
+ if self._timestamp:
349
+ return self._timestamp
350
+
331
351
  return None
332
352
 
353
+ @timestamp.setter
354
+ def timestamp(self, ts: datetime) -> None:
355
+ self._timestamp = ts
356
+
333
357
  def subkey(self, subkey: str) -> RegistryKey:
334
358
  try:
335
359
  return self._subkeys[subkey.lower()]
@@ -405,8 +429,8 @@ class VirtualValue(RegistryValue):
405
429
  return self._value
406
430
 
407
431
  @property
408
- def type(self) -> int:
409
- return None
432
+ def type(self) -> RegistryValueType:
433
+ return RegistryValueType.NONE
410
434
 
411
435
 
412
436
  class HiveCollection(RegistryHive):
@@ -622,7 +646,7 @@ class RegfHive(RegistryHive):
622
646
  try:
623
647
  return RegfKey(self, self.hive.open(key))
624
648
  except regf.RegistryKeyNotFoundError as e:
625
- raise RegistryKeyNotFoundError(key, cause=e)
649
+ raise RegistryKeyNotFoundError(key) from e
626
650
 
627
651
 
628
652
  class RegfKey(RegistryKey):
@@ -652,7 +676,7 @@ class RegfKey(RegistryKey):
652
676
  try:
653
677
  return RegfKey(self.hive, self.key.subkey(subkey))
654
678
  except regf.RegistryKeyNotFoundError as e:
655
- raise RegistryKeyNotFoundError(subkey, cause=e)
679
+ raise RegistryKeyNotFoundError(subkey) from e
656
680
 
657
681
  def subkeys(self) -> list[RegistryKey]:
658
682
  return [RegfKey(self.hive, k) for k in self.key.subkeys()]
@@ -661,7 +685,7 @@ class RegfKey(RegistryKey):
661
685
  try:
662
686
  return RegfValue(self.hive, self.key.value(value))
663
687
  except regf.RegistryValueNotFoundError as e:
664
- raise RegistryValueNotFoundError(value, cause=e)
688
+ raise RegistryValueNotFoundError(value) from e
665
689
 
666
690
  def values(self) -> list[RegistryValue]:
667
691
  return [RegfValue(self.hive, v) for v in self.key.values()]
@@ -683,8 +707,8 @@ class RegfValue(RegistryValue):
683
707
  return self.kv.value
684
708
 
685
709
  @property
686
- def type(self) -> int:
687
- return self.kv.type
710
+ def type(self) -> RegistryValueType:
711
+ return RegistryValueType(self.kv.type)
688
712
 
689
713
 
690
714
  class RegFlex:
@@ -750,17 +774,22 @@ class RegFlexKey(VirtualKey):
750
774
 
751
775
  class RegFlexValue(VirtualValue):
752
776
  def __init__(self, hive: RegistryHive, name: str, value: ValueType):
753
- self._parsed_value = None
754
777
  super().__init__(hive, name, value)
755
778
 
779
+ @cached_property
780
+ def _parse(self) -> tuple[RegistryValueType, ValueType]:
781
+ return parse_flex_value(self._value)
782
+
756
783
  @property
757
784
  def value(self) -> ValueType:
758
- if not self._parsed_value:
759
- self._parsed_value = parse_flex_value(self._value)
760
- return self._parsed_value
785
+ return self._parse[1]
786
+
787
+ @property
788
+ def type(self) -> RegistryValueType:
789
+ return self._parse[0]
761
790
 
762
791
 
763
- def parse_flex_value(value: str) -> ValueType:
792
+ def parse_flex_value(value: str) -> tuple[RegistryValueType, ValueType]:
764
793
  """Parse values from text registry exports.
765
794
 
766
795
  Args:
@@ -770,31 +799,31 @@ def parse_flex_value(value: str) -> ValueType:
770
799
  NotImplementedError: If ``value`` is not of a supported type for parsing.
771
800
  """
772
801
  if value.startswith('"'):
773
- return value.strip('"')
802
+ return RegistryValueType.SZ, value.strip('"')
774
803
 
775
804
  vtype, _, value = value.partition(":")
776
805
  if vtype == "dword":
777
- return struct.unpack(">i", bytes.fromhex(value))[0]
806
+ return RegistryValueType.DWORD, int.from_bytes(bytes.fromhex(value), "big", signed=True)
778
807
  elif "hex" in vtype:
779
808
  value = bytes.fromhex(value.replace(",", ""))
780
809
  if vtype == "hex":
781
- return value
810
+ return RegistryValueType.BINARY, value
782
811
 
783
812
  # hex(T)
784
813
  # These values match regf type values
785
814
  vtype = int(vtype[4:5], 16)
786
815
  if vtype == regf.REG_NONE:
787
- return value if value else None
816
+ decoded = value if value else None
788
817
  elif vtype == regf.REG_SZ:
789
- return regf.try_decode_sz(value)
818
+ decoded = regf.try_decode_sz(value)
790
819
  elif vtype == regf.REG_EXPAND_SZ:
791
- return regf.try_decode_sz(value)
820
+ decoded = regf.try_decode_sz(value)
792
821
  elif vtype == regf.REG_BINARY:
793
- return value
822
+ decoded = value
794
823
  elif vtype == regf.REG_DWORD:
795
- return struct.unpack("<I", value)[0]
824
+ decoded = int.from_bytes(value, "little", signed=False)
796
825
  elif vtype == regf.REG_DWORD_BIG_ENDIAN:
797
- return struct.unpack(">I", value)[0]
826
+ decoded = int.from_bytes(value, "big", signed=False)
798
827
  elif vtype == regf.REG_MULTI_SZ:
799
828
  d = BytesIO(value)
800
829
 
@@ -806,11 +835,12 @@ def parse_flex_value(value: str) -> ValueType:
806
835
 
807
836
  r.append(s)
808
837
 
809
- return r
838
+ decoded = r
810
839
  elif vtype == regf.REG_QWORD:
811
- return struct.unpack(">Q", value)[0]
840
+ decoded = int.from_bytes(value, "big", signed=False)
812
841
  else:
813
842
  raise NotImplementedError(f"Registry flex value type {vtype}")
843
+ return RegistryValueType(vtype), decoded
814
844
 
815
845
 
816
846
  def has_glob_magic(pattern: str) -> bool:
dissect/target/loader.py CHANGED
@@ -177,7 +177,6 @@ def open(item: Union[str, Path], *args, **kwargs) -> Loader:
177
177
  register("local", "LocalLoader")
178
178
  register("remote", "RemoteLoader")
179
179
  register("mqtt", "MQTTLoader")
180
- register("targetd", "TargetdLoader")
181
180
  register("asdf", "AsdfLoader")
182
181
  register("tar", "TarLoader")
183
182
  register("vmx", "VmxLoader")
@@ -206,4 +205,5 @@ register("velociraptor", "VelociraptorLoader")
206
205
  register("smb", "SmbLoader")
207
206
  register("cb", "CbLoader")
208
207
  register("cyber", "CyberLoader")
208
+ register("proxmox", "ProxmoxLoader")
209
209
  register("multiraw", "MultiRawLoader") # Should be last
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import argparse
3
4
  import atexit
4
5
  import logging
5
6
  import math
6
7
  import os
8
+ import re
7
9
  import ssl
8
10
  import sys
9
11
  import time
@@ -280,7 +282,7 @@ class Broker:
280
282
  factor = 1
281
283
 
282
284
  def __init__(
283
- self, broker: Broker, port: str, key: str, crt: str, ca: str, case: str, username: str, password: str, **kwargs
285
+ self, broker: str, port: str, key: str, crt: str, ca: str, case: str, username: str, password: str, **kwargs
284
286
  ):
285
287
  self.broker_host = broker
286
288
  self.broker_port = int(port)
@@ -341,9 +343,19 @@ class Broker:
341
343
  self.mqtt_client.subscribe(f"{self.case}/{host}/DISKS")
342
344
  self.mqtt_client.subscribe(f"{self.case}/{host}/READ/#")
343
345
  if self.command is not None:
346
+ self.mqtt_client.subscribe(f"{self.case}/{host}/CALLID")
344
347
  self.mqtt_client.publish(f"{self.case}/{host}/COMM", self.command.encode("utf-8"))
345
348
  time.sleep(1)
346
349
 
350
+ def _on_call_id(self, hostname: str, payload: bytes) -> None:
351
+ try:
352
+ decoded_payload = payload.decode("utf-8")
353
+ except UnicodeDecodeError as e:
354
+ log.error(f"Failed to decode payload for hostname {hostname}: {e}")
355
+ return
356
+
357
+ print(decoded_payload)
358
+
347
359
  def _on_log(self, client: mqtt.Client, userdata: Any, log_level: int, message: str) -> None:
348
360
  log.debug(message)
349
361
 
@@ -365,6 +377,8 @@ class Broker:
365
377
  self._on_read(hostname, tokens, msg.payload)
366
378
  elif response == "ID":
367
379
  self._on_id(hostname, msg.payload)
380
+ elif response == "CALLID":
381
+ self._on_call_id(hostname, msg.payload)
368
382
 
369
383
  def seek(self, host: str, disk_id: int, offset: int, flength: int, optimization_strategy: int) -> None:
370
384
  length = int(flength / self.factor)
@@ -410,16 +424,97 @@ class Broker:
410
424
  self.mqtt_client.loop_start()
411
425
 
412
426
 
413
- @arg("--mqtt-peers", type=int, dest="peers", help="minimum number of peers to await for first alias")
414
- @arg("--mqtt-case", dest="case", help="case name (broker will determine if you are allowed to access this data)")
415
- @arg("--mqtt-port", type=int, dest="port", help="broker connection port")
416
- @arg("--mqtt-broker", dest="broker", help="broker ip-address")
417
- @arg("--mqtt-key", dest="key", help="private key file")
418
- @arg("--mqtt-crt", dest="crt", help="client certificate file")
419
- @arg("--mqtt-ca", dest="ca", help="certificate authority file")
427
+ def strictly_positive(value: str) -> int:
428
+ """
429
+ Validates that the provided value is a strictly positive integer.
430
+
431
+ This function is intended to be used as a type for argparse arguments.
432
+
433
+ Args:
434
+ value (str): The value to validate.
435
+
436
+ Returns:
437
+ int: The validated integer value.
438
+
439
+ Raises:
440
+ argparse.ArgumentTypeError: If the value is not a strictly positive integer.
441
+ """
442
+ try:
443
+ strictly_positive_value = int(value)
444
+ if strictly_positive_value < 1:
445
+ raise argparse.ArgumentTypeError("Value must be larger than or equal to 1.")
446
+ return strictly_positive_value
447
+ except ValueError:
448
+ raise argparse.ArgumentTypeError(f"Invalid integer value specified: '{value}'")
449
+
450
+
451
+ def port(value: str) -> int:
452
+ """
453
+ Convert a string value to an integer representing a valid port number.
454
+
455
+ This function is intended to be used as a type for argparse arguments.
456
+
457
+ Args:
458
+ value (str): The string representation of the port number.
459
+ Returns:
460
+ int: The port number as an integer.
461
+ Raises:
462
+ argparse.ArgumentTypeError: If the port number is not an integer or out of the valid range (1-65535).
463
+ """
464
+
465
+ try:
466
+ port = int(value)
467
+ if port < 1 or port > 65535:
468
+ raise argparse.ArgumentTypeError("Port number must be between 1 and 65535.")
469
+ return port
470
+ except ValueError:
471
+ raise argparse.ArgumentTypeError(f"Invalid port number specified: '{value}'")
472
+
473
+
474
+ def case(value: str) -> str:
475
+ """
476
+ Validates that the given value is a valid case name consisting of
477
+ alphanumeric characters and underscores only.
478
+
479
+ This function is intended to be used as a type for argparse arguments.
480
+
481
+ Args:
482
+ value (str): The case name to validate.
483
+
484
+ Returns:
485
+ str: The validated case name if it matches the required pattern.
486
+
487
+ Raises:
488
+ argparse.ArgumentTypeError: If the case name does not match the required pattern.
489
+ """
490
+
491
+ if re.match(r"^[a-zA-Z0-9_]+$", value):
492
+ return value
493
+
494
+ raise argparse.ArgumentTypeError(f"Invalid case name specified: '{value}'")
495
+
496
+
497
+ @arg(
498
+ "--mqtt-peers",
499
+ type=strictly_positive,
500
+ dest="peers",
501
+ default=1,
502
+ help="minimum number of peers to await for first alias",
503
+ )
504
+ @arg(
505
+ "--mqtt-case",
506
+ type=case,
507
+ dest="case",
508
+ help="case name (broker will determine if you are allowed to access this data)",
509
+ )
510
+ @arg("--mqtt-port", type=port, dest="port", default=443, help="broker connection port")
511
+ @arg("--mqtt-broker", default="localhost", dest="broker", help="broker ip-address")
512
+ @arg("--mqtt-key", type=Path, dest="key", required=True, help="private key file")
513
+ @arg("--mqtt-crt", type=Path, dest="crt", required=True, help="client certificate file")
514
+ @arg("--mqtt-ca", type=Path, dest="ca", required=True, help="certificate authority file")
420
515
  @arg("--mqtt-command", dest="command", help="direct command to client(s)")
421
516
  @arg("--mqtt-diag", action="store_true", dest="diag", help="show MQTT diagnostic information")
422
- @arg("--mqtt-username", dest="username", help="Username for connection")
517
+ @arg("--mqtt-username", dest="username", default="mqtt-loader", help="Username for connection")
423
518
  @arg("--mqtt-password", action="store_true", dest="password", help="Ask for password before connecting")
424
519
  class MQTTLoader(Loader):
425
520
  """Load remote targets through a broker."""
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ from dissect.target import container
7
+ from dissect.target.loader import Loader
8
+ from dissect.target.target import Target
9
+
10
+ RE_VOLUME_ID = re.compile(r"(?:file=)?([^:]+):([^,]+)")
11
+
12
+
13
+ class ProxmoxLoader(Loader):
14
+ """Loader for Proxmox VM configuration files.
15
+
16
+ Proxmox uses volume identifiers in the format of ``storage_id:volume_id``. The ``storage_id`` maps to a
17
+ storage configuration in ``/etc/pve/storage.cfg``. The ``volume_id`` is the name of the volume within
18
+ that configuration.
19
+
20
+ This loader currently does not support parsing the storage configuration, so it will attempt to open the
21
+ volume directly from the same directory as the configuration file, or from ``/dev/pve/`` (default LVM config).
22
+ If the volume is not found, it will log a warning.
23
+ """
24
+
25
+ def __init__(self, path: Path, **kwargs):
26
+ path = path.resolve()
27
+ super().__init__(path)
28
+ self.base_dir = path.parent
29
+
30
+ @staticmethod
31
+ def detect(path: Path) -> bool:
32
+ if path.suffix.lower() != ".conf":
33
+ return False
34
+
35
+ with path.open("rb") as fh:
36
+ lines = fh.read(512).split(b"\n")
37
+ needles = [b"cpu:", b"memory:", b"name:"]
38
+ return all(any(needle in line for line in lines) for needle in needles)
39
+
40
+ def map(self, target: Target) -> None:
41
+ with self.path.open("rt") as fh:
42
+ for line in fh:
43
+ if not (line := line.strip()):
44
+ continue
45
+
46
+ key, value = line.split(":", 1)
47
+ value = value.strip()
48
+
49
+ if key.startswith(("scsi", "sata", "ide", "virtio")) and key[-1].isdigit():
50
+ # https://pve.proxmox.com/wiki/Storage
51
+ if match := RE_VOLUME_ID.match(value):
52
+ storage_id, volume_id = match.groups()
53
+
54
+ # TODO: parse the storage information from /etc/pve/storage.cfg
55
+ # For now, let's try a few assumptions
56
+ disk_path = None
57
+ if (path := self.base_dir.joinpath(volume_id)).exists():
58
+ disk_path = path
59
+ elif (path := self.base_dir.joinpath("/dev/pve/").joinpath(volume_id)).exists():
60
+ disk_path = path
61
+
62
+ if disk_path:
63
+ try:
64
+ target.disks.add(container.open(disk_path))
65
+ except Exception:
66
+ target.log.exception("Failed to open disk: %s", disk_path)
67
+ else:
68
+ target.log.warning("Unable to find disk: %s:%s", storage_id, volume_id)
@@ -1,4 +1,4 @@
1
- from dissect.hypervisor import vma
1
+ from dissect.archive import vma
2
2
 
3
3
  from dissect.target.containers.raw import RawContainer
4
4
  from dissect.target.loader import Loader
@@ -1,4 +1,4 @@
1
- from dissect.hypervisor import xva
1
+ from dissect.archive import xva
2
2
 
3
3
  from dissect.target.containers.raw import RawContainer
4
4
  from dissect.target.loader import Loader
dissect/target/plugin.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Dissect plugin system.
2
2
 
3
- See dissect/target/plugins/general/example.py for an example plugin.
3
+ See ``dissect/target/plugins/general/example.py`` for an example plugin.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -57,17 +57,18 @@ log = logging.getLogger(__name__)
57
57
 
58
58
 
59
59
  class OperatingSystem(StrEnum):
60
- LINUX = "linux"
61
- WINDOWS = "windows"
62
- ESXI = "esxi"
60
+ ANDROID = "android"
63
61
  BSD = "bsd"
62
+ CITRIX = "citrix-netscaler"
63
+ ESXI = "esxi"
64
+ FORTIOS = "fortios"
65
+ IOS = "ios"
66
+ LINUX = "linux"
64
67
  OSX = "osx"
68
+ PROXMOX = "proxmox"
65
69
  UNIX = "unix"
66
- ANDROID = "android"
67
70
  VYOS = "vyos"
68
- IOS = "ios"
69
- FORTIOS = "fortios"
70
- CITRIX = "citrix-netscaler"
71
+ WINDOWS = "windows"
71
72
 
72
73
 
73
74
  def export(*args, **kwargs) -> Callable:
@@ -76,18 +77,19 @@ def export(*args, **kwargs) -> Callable:
76
77
  Supported keyword arguments:
77
78
  property (bool): Whether this export should be regarded as a property.
78
79
  Properties are implicitly cached.
80
+
79
81
  cache (bool): Whether the result of this function should be cached.
82
+
80
83
  record (RecordDescriptor): The :class:`flow.record.RecordDescriptor` for the records that this function yields.
81
84
  If the records are dynamically made, use DynamicRecord instead.
82
- output (str): The output type of this function. Can be one of:
83
85
 
86
+ output (str): The output type of this function. Must be one of:
84
87
  - default: Single return value
85
88
  - record: Yields records. Implicit when record argument is given.
86
89
  - yield: Yields printable values.
87
90
  - none: No return value. Plugin is responsible for output formatting and should return ``None``.
88
91
 
89
92
  The ``export`` decorator adds some additional private attributes to an exported method or property:
90
-
91
93
  - ``__output__``: The output type to expect for this function, this is the same as ``output``.
92
94
  - ``__record__``: The type of record to expect, this value is the same as ``record``.
93
95
  - ``__exported__``: set to ``True`` to indicate the method or property is exported.
@@ -173,7 +175,7 @@ class Plugin:
173
175
  class attribute. Namespacing results in your plugin needing to be prefixed
174
176
  with this namespace when being called. For example, if your plugin has
175
177
  specified ``test`` as namespace and a function called ``example``, you must
176
- call your plugin with ``test.example``::
178
+ call your plugin with ``test.example``.
177
179
 
178
180
  A ``Plugin`` class has the following private class attributes:
179
181
 
@@ -430,15 +432,13 @@ def register(plugincls: Type[Plugin]) -> None:
430
432
  """Register a plugin, and put related data inside :attr:`PLUGINS`.
431
433
 
432
434
  This function uses the following private attributes that are set using decorators:
433
-
434
- - ``__exported__``: Set in :func:`export`.
435
- - ``__internal__``: Set in :func:`internal`.
435
+ - ``__exported__``: Set in :func:`export`.
436
+ - ``__internal__``: Set in :func:`internal`.
436
437
 
437
438
  Additionally, ``register`` sets the following private attributes on the `plugincls`:
438
-
439
- - ``__plugin__``: Always set to ``True``.
440
- - ``__functions__``: A list of all the methods and properties that are ``__internal__`` or ``__exported__``.
441
- - ``__exports__``: A list of all the methods or properties that were explicitly exported.
439
+ - ``__plugin__``: Always set to ``True``.
440
+ - ``__functions__``: A list of all the methods and properties that are ``__internal__`` or ``__exported__``.
441
+ - ``__exports__``: A list of all the methods or properties that were explicitly exported.
442
442
 
443
443
  Args:
444
444
  plugincls: A plugin class to register.
@@ -807,7 +807,7 @@ def load(plugin_desc: PluginDescriptor) -> Type[Plugin]:
807
807
  module = importlib.import_module(module)
808
808
  return getattr(module, plugin_desc["class"])
809
809
  except Exception as e:
810
- raise PluginError(f"An exception occurred while trying to load a plugin: {module}", cause=e)
810
+ raise PluginError(f"An exception occurred while trying to load a plugin: {module}") from e
811
811
 
812
812
 
813
813
  def failed() -> list[dict[str, Any]]:
@@ -1138,6 +1138,9 @@ class PluginFunction:
1138
1138
  method_name: str
1139
1139
  plugin_desc: PluginDescriptor = field(hash=False)
1140
1140
 
1141
+ def __repr__(self) -> str:
1142
+ return self.path
1143
+
1141
1144
 
1142
1145
  def plugin_function_index(target: Optional[Target]) -> tuple[dict[str, PluginDescriptor], set[str]]:
1143
1146
  """Returns an index-list for plugins.
@@ -1292,7 +1295,7 @@ def find_plugin_functions(
1292
1295
  path=index_name,
1293
1296
  class_object=loaded_plugin_object,
1294
1297
  method_name=method_name,
1295
- output_type=getattr(fobject, "__output__", "text"),
1298
+ output_type=getattr(fobject, "__output__", "none"),
1296
1299
  plugin_desc=func,
1297
1300
  )
1298
1301
  )
@@ -1337,7 +1340,7 @@ def find_plugin_functions(
1337
1340
  path=f"{description['module']}.{funcname}",
1338
1341
  class_object=loaded_plugin_object,
1339
1342
  method_name=funcname,
1340
- output_type=getattr(fobject, "__output__", "text"),
1343
+ output_type=getattr(fobject, "__output__", "none"),
1341
1344
  plugin_desc=description,
1342
1345
  )
1343
1346
  )
@@ -40,6 +40,8 @@ re_strip_tags = re.compile(r"<[^!][^>]*>")
40
40
 
41
41
 
42
42
  class McAfeePlugin(Plugin):
43
+ """McAfee antivirus plugin."""
44
+
43
45
  __namespace__ = "mcafee"
44
46
 
45
47
  DIRS = [
@@ -30,6 +30,8 @@ SophosLogRecord = TargetRecordDescriptor(
30
30
 
31
31
 
32
32
  class SophosPlugin(Plugin):
33
+ """Sophos antivirus plugin."""
34
+
33
35
  __namespace__ = "sophos"
34
36
 
35
37
  LOG_SOPHOS_HOME = "sysvol/ProgramData/Sophos/Clean/Logs/Clean.log"
@@ -51,6 +51,8 @@ c_pfwlog = cstruct().load(pfwlog_def)
51
51
 
52
52
 
53
53
  class TrendMicroPlugin(Plugin):
54
+ """TrendMicro antivirus plugin."""
55
+
54
56
  __namespace__ = "trendmicro"
55
57
 
56
58
  LOG_FOLDER = "sysvol/Program Files (x86)/Trend Micro/Security Agent"
@@ -608,6 +608,15 @@ def remove_padding(decrypted: bytes) -> bytes:
608
608
 
609
609
 
610
610
  def decrypt_v10(encrypted_password: bytes) -> str:
611
+ """Decrypt a version 10 encrypted password.
612
+
613
+ Args:
614
+ encrypted_password: The encrypted password bytes.
615
+
616
+ Returns:
617
+ Decrypted password string.
618
+ """
619
+
611
620
  if not HAS_CRYPTO:
612
621
  raise ValueError("Missing pycryptodome dependency for AES operation")
613
622
 
@@ -625,12 +634,24 @@ def decrypt_v10(encrypted_password: bytes) -> str:
625
634
 
626
635
 
627
636
  def decrypt_v10_2(encrypted_password: bytes, key: bytes) -> str:
628
- """
629
- struct chrome_pass {
630
- byte signature[3] = 'v10';
631
- byte iv[12];
632
- byte ciphertext[EOF];
633
- }
637
+ """Decrypt a version 10 type 2 password.
638
+
639
+ References:
640
+
641
+ .. code-block::
642
+
643
+ struct chrome_pass {
644
+ byte signature[3] = 'v10';
645
+ byte iv[12];
646
+ byte ciphertext[EOF];
647
+ }
648
+
649
+ Args:
650
+ encrypted_password: The encrypted password bytes.
651
+ key: The encryption key.
652
+
653
+ Returns:
654
+ Decrypted password string.
634
655
  """
635
656
 
636
657
  if not HAS_CRYPTO: