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,3 +1,5 @@
1
+ from typing import Iterator
2
+
1
3
  from dissect.target.exceptions import UnsupportedPluginError
2
4
  from dissect.target.helpers.record import ChildTargetRecord
3
5
  from dissect.target.plugin import ChildTargetPlugin
@@ -12,7 +14,7 @@ class ESXiChildTargetPlugin(ChildTargetPlugin):
12
14
  if self.target.os != "esxi":
13
15
  raise UnsupportedPluginError("Not an ESXi operating system")
14
16
 
15
- def list_children(self):
17
+ def list_children(self) -> Iterator[ChildTargetRecord]:
16
18
  for vm in self.target.vm_inventory():
17
19
  yield ChildTargetRecord(
18
20
  type=self.__type__,
@@ -0,0 +1,68 @@
1
+ from pathlib import Path
2
+ from typing import Iterator
3
+
4
+ from dissect.target.exceptions import UnsupportedPluginError
5
+ from dissect.target.helpers.fsutil import TargetPath
6
+ from dissect.target.helpers.record import ChildTargetRecord
7
+ from dissect.target.plugin import ChildTargetPlugin
8
+ from dissect.target.target import Target
9
+
10
+ PARALLELS_USER_PATHS = [
11
+ "Parallels",
12
+ "Documents/Parallels",
13
+ "Library/Group Containers/*.com.parallels.desktop.appstore/Shared/Parallels",
14
+ ]
15
+
16
+ PARALLELS_SYSTEM_PATHS = [
17
+ "/Users/Shared/Parallels",
18
+ ]
19
+
20
+
21
+ def find_pvms(target: Target) -> Iterator[TargetPath]:
22
+ """Finds virtual machines located in default folders on a macOS target.
23
+
24
+ Resources:
25
+ - https://kb.parallels.com/117333
26
+ """
27
+ for user_details in target.user_details.all_with_home():
28
+ for parallels_path in PARALLELS_SYSTEM_PATHS:
29
+ if (path := target.fs.path(parallels_path)).exists():
30
+ yield from iter_vms(path)
31
+
32
+ for parallels_path in PARALLELS_USER_PATHS:
33
+ if "*" in parallels_path:
34
+ start_path, pattern = parallels_path.split("*", 1)
35
+ for path in user_details.home_path.joinpath(start_path).rglob("*" + pattern):
36
+ yield from iter_vms(path)
37
+ else:
38
+ if (path := user_details.home_path.joinpath(parallels_path)).exists():
39
+ yield from iter_vms(path)
40
+
41
+
42
+ def iter_vms(path: Path) -> Iterator[TargetPath]:
43
+ """Glob for .pvm folders in the provided folder."""
44
+ for file in path.rglob("*.pvm"):
45
+ if file.is_dir():
46
+ yield file
47
+
48
+
49
+ class ParallelsChildTargetPlugin(ChildTargetPlugin):
50
+ """Child target plugin that yields Parallels Desktop VM files."""
51
+
52
+ __type__ = "parallels"
53
+
54
+ def __init__(self, target: Target):
55
+ super().__init__(target)
56
+ self.pvms = list(find_pvms(target))
57
+
58
+ def check_compatible(self) -> None:
59
+ if not self.pvms:
60
+ raise UnsupportedPluginError("No Parallels pvm file(s) found")
61
+
62
+ def list_children(self) -> Iterator[ChildTargetRecord]:
63
+ for pvm in self.pvms:
64
+ yield ChildTargetRecord(
65
+ type=self.__type__,
66
+ path=pvm,
67
+ _target=self.target,
68
+ )
@@ -0,0 +1,23 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.helpers.record import ChildTargetRecord
5
+ from dissect.target.plugin import ChildTargetPlugin
6
+
7
+
8
+ class ProxmoxChildTargetPlugin(ChildTargetPlugin):
9
+ """Child target plugin that yields from the VM listing."""
10
+
11
+ __type__ = "proxmox"
12
+
13
+ def check_compatible(self) -> None:
14
+ if self.target.os != "proxmox":
15
+ raise UnsupportedPluginError("Not a Proxmox operating system")
16
+
17
+ def list_children(self) -> Iterator[ChildTargetRecord]:
18
+ for vm in self.target.vmlist():
19
+ yield ChildTargetRecord(
20
+ type=self.__type__,
21
+ path=vm.path,
22
+ _target=self.target,
23
+ )
@@ -1,3 +1,5 @@
1
+ from typing import Iterator
2
+
1
3
  from dissect.target.exceptions import UnsupportedPluginError
2
4
  from dissect.target.helpers.record import ChildTargetRecord
3
5
  from dissect.target.plugin import ChildTargetPlugin
@@ -9,13 +11,15 @@ class VirtuozzoChildTargetPlugin(ChildTargetPlugin):
9
11
  Virtuozzo conatiners are by default registered in the folder ``vz/root/$VEID``,
10
12
  where VEID will be substituted with the actual container UUID.
11
13
 
12
- /
13
- etc/
14
- var/
15
- vz/
16
- root/
17
- <container-uuid>/
18
- <container-uuid>/
14
+ .. code-block::
15
+
16
+ /
17
+ etc/
18
+ var/
19
+ vz/
20
+ root/
21
+ <container-uuid>/
22
+ <container-uuid>/
19
23
 
20
24
  References:
21
25
  - https://docs.virtuozzo.com/virtuozzo_hybrid_server_7_command_line_reference/managing-system/configuration-files.html
@@ -29,7 +33,7 @@ class VirtuozzoChildTargetPlugin(ChildTargetPlugin):
29
33
  if not self.target.fs.path(self.PATH).exists():
30
34
  raise UnsupportedPluginError("No Virtuozzo path found")
31
35
 
32
- def list_children(self):
36
+ def list_children(self) -> Iterator[ChildTargetRecord]:
33
37
  for container in self.target.fs.path(self.PATH).iterdir():
34
38
  yield ChildTargetRecord(
35
39
  type=self.__type__,
@@ -1,29 +1,44 @@
1
+ from typing import Iterator
2
+
1
3
  from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.helpers.fsutil import TargetPath
2
5
  from dissect.target.helpers.record import ChildTargetRecord
3
6
  from dissect.target.plugin import ChildTargetPlugin
7
+ from dissect.target.target import Target
8
+
9
+ INVENTORY_PATHS = [
10
+ # Windows
11
+ "AppData/Roaming/VMware/inventory.vmls",
12
+ # Linux
13
+ ".vmware/inventory.vmls",
14
+ ]
15
+
16
+
17
+ def find_vm_inventory(target: Target) -> Iterator[TargetPath]:
18
+ """Search for inventory.vmls files in user home folders.
4
19
 
20
+ Does not support older vmAutoStart.xml or vmInventory.xml formats."""
5
21
 
6
- def find_vm_inventory(target):
7
22
  for user_details in target.user_details.all_with_home():
8
- inv_file = user_details.home_path.joinpath("AppData/Roaming/VMware/inventory.vmls")
9
- if inv_file.exists():
10
- yield inv_file
23
+ for inv_path in INVENTORY_PATHS:
24
+ if (inv_file := user_details.home_path.joinpath(inv_path)).exists():
25
+ yield inv_file
11
26
 
12
27
 
13
- class WorkstationChildTargetPlugin(ChildTargetPlugin):
28
+ class VmwareWorkstationChildTargetPlugin(ChildTargetPlugin):
14
29
  """Child target plugin that yields from VMware Workstation VM inventory."""
15
30
 
16
31
  __type__ = "vmware_workstation"
17
32
 
18
- def __init__(self, target):
33
+ def __init__(self, target: Target):
19
34
  super().__init__(target)
20
35
  self.inventories = list(find_vm_inventory(target))
21
36
 
22
37
  def check_compatible(self) -> None:
23
- if not len(self.inventories):
38
+ if not self.inventories:
24
39
  raise UnsupportedPluginError("No VMWare inventories found")
25
40
 
26
- def list_children(self):
41
+ def list_children(self) -> Iterator[ChildTargetRecord]:
27
42
  for inv in self.inventories:
28
43
  for line in inv.open("rt"):
29
44
  line = line.strip()
@@ -1,5 +1,6 @@
1
1
  import csv
2
2
  import gzip
3
+ from typing import Iterator
3
4
 
4
5
  from dissect.target.exceptions import UnsupportedPluginError
5
6
  from dissect.target.helpers.record import TargetRecordDescriptor
@@ -27,7 +28,7 @@ class AcquireHashPlugin(Plugin):
27
28
  raise UnsupportedPluginError("No hash file found")
28
29
 
29
30
  @export(record=AcquireHashRecord)
30
- def acquire_hashes(self):
31
+ def acquire_hashes(self) -> Iterator[AcquireHashRecord]:
31
32
  """Return file hashes collected by Acquire.
32
33
 
33
34
  An Acquire file container contains a file hashes csv when the hashes module was used. The content of this csv
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import shutil
2
4
  import sys
3
5
 
@@ -27,24 +29,26 @@ class ICatPlugin(Plugin):
27
29
  )
28
30
  @arg("--ads", type=str, default="", help="Alternate Data Stream name")
29
31
  @export(output="none")
30
- def icat(self, inum, fs, ads):
32
+ def icat(self, inum: int, fs: int | None, ads: str) -> None:
31
33
  """Output the contents of a file based on its MFT segment or inode number. Supports Alternate Data Streams
32
34
 
33
35
  Example:
34
- # outputs contents of segment defaults to 'sysvol'
35
- target-query <TARGET> -f icat --segment 96997
36
+ .. code-block::
37
+
38
+ # outputs contents of segment defaults to 'sysvol'
39
+ target-query <TARGET> -f icat --segment 96997
36
40
 
37
- # outputs contents of inode defaults to '/'
38
- target-query <TARGET> -f icat --inode 50947
41
+ # outputs contents of inode defaults to '/'
42
+ target-query <TARGET> -f icat --inode 50947
39
43
 
40
- # outputs contents of segment's ADS
41
- target-query <TARGET> -f icat --segment 96997 --ads Zone.Identifier
44
+ # outputs contents of segment's ADS
45
+ target-query <TARGET> -f icat --segment 96997 --ads Zone.Identifier
42
46
 
43
- # outputs contents of segment in filesystem 3 of target
44
- target-query <TARGET> -f icat --fs 3 --segment 96997
47
+ # outputs contents of segment in filesystem 3 of target
48
+ target-query <TARGET> -f icat --fs 3 --segment 96997
45
49
 
46
- # outputs contents of inode in filesystem 2 of target
47
- target-query <TARGET> -f icat --fs 2 --inode 50947
50
+ # outputs contents of inode in filesystem 2 of target
51
+ target-query <TARGET> -f icat --fs 2 --inode 50947
48
52
  """
49
53
 
50
54
  open_as = None
@@ -105,6 +105,7 @@ FilesystemMACBRecord = TargetRecordDescriptor(
105
105
  ("filesize", "filesize"),
106
106
  ("boolean", "resident"),
107
107
  ("boolean", "inuse"),
108
+ ("boolean", "ads"),
108
109
  ("string", "volume_uuid"),
109
110
  ],
110
111
  )
@@ -122,6 +123,8 @@ COMPACT_RECORD_TYPES = {
122
123
 
123
124
 
124
125
  class MftPlugin(Plugin):
126
+ """NTFS MFT plugin."""
127
+
125
128
  def __init__(self, target):
126
129
  super().__init__(target)
127
130
  self.ntfs_filesystems = {index: fs for index, fs in enumerate(self.target.filesystems) if fs.__type__ == "ntfs"}
@@ -151,7 +154,7 @@ class MftPlugin(Plugin):
151
154
  "--macb",
152
155
  group="fmt",
153
156
  action="store_true",
154
- help="compacts the MFT entry timestamps into aggregated records with MACB bitfield",
157
+ help="compacts MFT timestamps into MACB bitfield (format: MACB[standard|ads]/MACB[filename])",
155
158
  )
156
159
  def mft(
157
160
  self, compact: bool = False, fs: int | None = None, start: int = 0, end: int = -1, macb: bool = False
@@ -342,12 +345,13 @@ def macb_aggr(records: list[Record]) -> Iterator[Record]:
342
345
  for record in records:
343
346
  found = False
344
347
 
345
- offset_std = int(record._desc.name == "filesystem/ntfs/mft/std") * 5
346
- offset_ads = (int(record.ads) * 10) if offset_std == 0 else 0
348
+ offset = 0
349
+ if not getattr(record, "ads", False):
350
+ offset = int(record._desc.name == "filesystem/ntfs/mft/filename") * 5
347
351
 
348
- field = "MACB".find(record.ts_type) + offset_std + offset_ads
352
+ field = "MACB".find(record.ts_type) + offset
349
353
  for macb in macbs:
350
- if macb.ts == record.ts:
354
+ if macb.ts == record.ts and macb.path == record.path:
351
355
  macb.macb = macb_set(macb.macb, field, record.ts_type)
352
356
  found = True
353
357
  break
@@ -356,7 +360,7 @@ def macb_aggr(records: list[Record]) -> Iterator[Record]:
356
360
  continue
357
361
 
358
362
  macb = FilesystemMACBRecord.init_from_record(record)
359
- macb.macb = "..../..../...."
363
+ macb.macb = "..../...."
360
364
  macb.macb = macb_set(macb.macb, field, record.ts_type)
361
365
 
362
366
  macbs.append(macb)
@@ -93,6 +93,8 @@ def format_info(
93
93
 
94
94
 
95
95
  class MftTimelinePlugin(Plugin):
96
+ """NTFS MFT timeline plugin."""
97
+
96
98
  def check_compatible(self) -> None:
97
99
  ntfs_filesystems = [fs for fs in self.target.filesystems if fs.__type__ == "ntfs"]
98
100
  if not len(ntfs_filesystems):
@@ -100,7 +102,7 @@ class MftTimelinePlugin(Plugin):
100
102
 
101
103
  @export(output="yield")
102
104
  @arg("--ignore-dos", action="store_true", help="ignore DOS file names")
103
- def mft_timeline(self, ignore_dos: bool = False):
105
+ def mft_timeline(self, ignore_dos: bool = False) -> Iterator[str]:
104
106
  """Return the MFT records of all NTFS filesystems in a human readable format (unsorted).
105
107
 
106
108
  The Master File Table (MFT) contains metadata about every file and folder on a NFTS filesystem.
@@ -24,6 +24,8 @@ UsnjrnlRecord = TargetRecordDescriptor(
24
24
 
25
25
 
26
26
  class UsnjrnlPlugin(Plugin):
27
+ """NFTS UsnJrnl plugin."""
28
+
27
29
  def check_compatible(self) -> None:
28
30
  pass
29
31
 
@@ -13,12 +13,14 @@ DRIVE_LETTER_RE = re.compile(r"[a-zA-Z]:")
13
13
 
14
14
 
15
15
  class InformationType(Enum):
16
+ """Valid information types"""
17
+
16
18
  STANDARD_INFORMATION = auto()
17
19
  FILE_INFORMATION = auto()
18
20
  ALTERNATE_DATA_STREAM = auto()
19
21
 
20
22
 
21
- def get_drive_letter(target: Target, filesystem: NtfsFilesystem):
23
+ def get_drive_letter(target: Target, filesystem: NtfsFilesystem) -> str:
22
24
  """Retrieve the drive letter from the loaded mounts
23
25
 
24
26
  When the drive letter is not available for that filesystem it returns empty.
@@ -1,4 +1,5 @@
1
1
  import stat
2
+ from typing import Iterator
2
3
 
3
4
  from dissect.target.exceptions import UnsupportedPluginError
4
5
  from dissect.target.helpers.record import TargetRecordDescriptor
@@ -12,12 +13,14 @@ SuidRecord = TargetRecordDescriptor(
12
13
 
13
14
 
14
15
  class SuidPlugin(Plugin):
16
+ """Unix SUID binary plugin."""
17
+
15
18
  def check_compatible(self) -> None:
16
19
  if not self.target.has_function("walkfs") or self.target.os == "windows":
17
20
  raise UnsupportedPluginError("Unsupported plugin")
18
21
 
19
22
  @export(record=SuidRecord)
20
- def suid_binaries(self):
23
+ def suid_binaries(self) -> Iterator[SuidRecord]:
21
24
  """Return all SUID binaries.
22
25
 
23
26
  A SUID binary allows all users to run it with the permissions of its owner. This means that a
@@ -27,6 +27,8 @@ FilesystemRecord = TargetRecordDescriptor(
27
27
 
28
28
 
29
29
  class WalkFSPlugin(Plugin):
30
+ """Filesystem agnostic walkfs plugin."""
31
+
30
32
  def check_compatible(self) -> None:
31
33
  if not len(self.target.filesystems):
32
34
  raise UnsupportedPluginError("No filesystems to walk")
@@ -69,7 +69,7 @@ class ExamplePlugin(Plugin):
69
69
  """
70
70
  pass
71
71
 
72
- @export
72
+ @export(output="default")
73
73
  @arg("--flag", action="store_true", help="optional example flag")
74
74
  def example(self, flag: bool = False) -> str:
75
75
  """Example plugin function.
@@ -117,7 +117,7 @@ class ExamplePlugin(Plugin):
117
117
  To include registry or user information in a record, you must create a new record descriptor using
118
118
  :func:`~dissect.target.helpers.record.create_extended_descriptor` with
119
119
  :class:`~dissect.target.helpers.descriptor_extensions.RegistryRecordDescriptorExtension` and/or
120
- :class:`~dissect.target.helpers.descriptor_extensions.UserRecordDescriptorExtension as extensions.
120
+ :class:`~dissect.target.helpers.descriptor_extensions.UserRecordDescriptorExtension` as extensions.
121
121
  """
122
122
  for key in self.target.registry.keys("HKCU\\SOFTWARE"):
123
123
  user = self.target.registry.get_user(key)
@@ -1,6 +1,8 @@
1
+ import json
2
+
1
3
  from dissect.target.helpers.docs import INDENT_STEP, get_docstring
2
4
  from dissect.target.loader import LOADERS_BY_SCHEME
3
- from dissect.target.plugin import Plugin, export
5
+ from dissect.target.plugin import Plugin, arg, export
4
6
 
5
7
 
6
8
  class LoaderListPlugin(Plugin):
@@ -10,7 +12,12 @@ class LoaderListPlugin(Plugin):
10
12
  pass
11
13
 
12
14
  @export(output="none")
13
- def loaders(self):
15
+ # NOTE: We would prefer to re-use arguments across plugins from argparse in query.py, but that is not possible yet.
16
+ # For now we use --as-json, but in the future this should be changed to inherit --json from target-query.
17
+ # https://github.com/fox-it/dissect.target/pull/841
18
+ # https://github.com/fox-it/dissect.target/issues/889
19
+ @arg("--as-json", dest="as_json", action="store_true")
20
+ def loaders(self, as_json: bool = False) -> None:
14
21
  """List the available loaders."""
15
22
 
16
23
  loaders_info = {}
@@ -21,6 +28,12 @@ class LoaderListPlugin(Plugin):
21
28
  except ImportError:
22
29
  continue
23
30
 
24
- print("Available loaders:")
25
- for loader_name, loader_description in sorted(loaders_info.items()):
26
- print(f"{INDENT_STEP}{loader_name} - {loader_description}")
31
+ loaders = sorted(loaders_info.items())
32
+
33
+ if as_json:
34
+ print(json.dumps([{"name": name, "description": desc} for name, desc in loaders]), end="")
35
+
36
+ else:
37
+ print("Available loaders:")
38
+ for loader_name, loader_description in loaders:
39
+ print(f"{INDENT_STEP}{loader_name} - {loader_description}")
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import Any, Iterator, Union
4
4
 
5
5
  from flow.record.fieldtypes.net import IPAddress, IPNetwork
6
+ from flow.record.fieldtypes.net.ipv4 import Address, addr_long, addr_str, mask_to_bits
6
7
 
7
8
  from dissect.target.helpers.record import (
8
9
  MacInterfaceRecord,
@@ -16,6 +17,8 @@ InterfaceRecord = Union[UnixInterfaceRecord, WindowsInterfaceRecord, MacInterfac
16
17
 
17
18
 
18
19
  class NetworkPlugin(Plugin):
20
+ """Generic implementation for network interfaces plugin."""
21
+
19
22
  __namespace__ = "network"
20
23
 
21
24
  def __init__(self, target: Target):
@@ -40,6 +43,7 @@ class NetworkPlugin(Plugin):
40
43
 
41
44
  @export(record=InterfaceRecord)
42
45
  def interfaces(self) -> Iterator[InterfaceRecord]:
46
+ """Yield interfaces."""
43
47
  # Only search for the interfaces once
44
48
  if self._interface_list is None:
45
49
  self._interface_list = list(self._interfaces())
@@ -48,19 +52,23 @@ class NetworkPlugin(Plugin):
48
52
 
49
53
  @export
50
54
  def ips(self) -> list[IPAddress]:
51
- return list(self._get_record_type("ip"))
55
+ """Return IP addresses as list of :class:`IPAddress`."""
56
+ return list(set(self._get_record_type("ip")))
52
57
 
53
58
  @export
54
59
  def gateways(self) -> list[IPAddress]:
55
- return list(self._get_record_type("gateway"))
60
+ """Return gateways as list of :class:`IPAddress`."""
61
+ return list(set(self._get_record_type("gateway")))
56
62
 
57
63
  @export
58
64
  def macs(self) -> list[str]:
59
- return list(self._get_record_type("mac"))
65
+ """Return MAC addresses as list of :class:`str`."""
66
+ return list(set(self._get_record_type("mac")))
60
67
 
61
68
  @export
62
- def dns(self) -> list[str]:
63
- return list(self._get_record_type("dns"))
69
+ def dns(self) -> list[str | IPAddress]:
70
+ """Return DNS addresses as list of :class:`str`."""
71
+ return list(set(self._get_record_type("dns")))
64
72
 
65
73
  @internal
66
74
  def with_ip(self, ip_addr: str) -> Iterator[InterfaceRecord]:
@@ -80,3 +88,10 @@ class NetworkPlugin(Plugin):
80
88
  for interface in self.interfaces():
81
89
  if any(ip_addr in cidr for ip_addr in interface.ip):
82
90
  yield interface
91
+
92
+ def calculate_network(self, ips: int | Address, subnets: int | Address) -> Iterator[str]:
93
+ for ip, subnet_mask in zip(ips, subnets):
94
+ subnet_mask_int = addr_long(subnet_mask)
95
+ cidr = mask_to_bits(subnet_mask_int)
96
+ network_address = addr_str(addr_long(ip) & subnet_mask_int)
97
+ yield f"{network_address}/{cidr}"
@@ -22,6 +22,7 @@ class OSInfoPlugin(plugin.Plugin):
22
22
 
23
23
  @plugin.export(record=OSInfoRecord)
24
24
  def osinfo(self) -> Iterator[Union[OSInfoRecord, GroupedRecord]]:
25
+ """Yield grouped records with target OS info."""
25
26
  for os_func in self.target._os.__functions__:
26
27
  if os_func in ["is_compatible", "get_all_records"]:
27
28
  continue
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import json
1
4
  import textwrap
2
- from typing import Dict, List, Type, Union
5
+ from typing import Iterator, Type
3
6
 
4
7
  from dissect.target import plugin
5
8
  from dissect.target.helpers.docs import INDENT_STEP, get_plugin_overview
@@ -23,7 +26,8 @@ def categorize_plugins(plugins_selection: list[dict] = None) -> dict:
23
26
  return output_dict
24
27
 
25
28
 
26
- def get_exported_plugins():
29
+ def get_exported_plugins() -> list:
30
+ """Returns list of exported plugins."""
27
31
  return [p for p in plugin.plugins() if len(p["exports"])]
28
32
 
29
33
 
@@ -50,10 +54,10 @@ def update_dict_recursive(source_dict: dict, updated_dict: dict) -> dict:
50
54
 
51
55
 
52
56
  def output_plugin_description_recursive(
53
- structure_dict: Union[Dict, Plugin],
57
+ structure_dict: dict | Plugin,
54
58
  print_docs: bool,
55
- indentation_step=0,
56
- ) -> List[str]:
59
+ indentation_step: int = 0,
60
+ ) -> list[str]:
57
61
  """Create plugin overview with identations."""
58
62
 
59
63
  if isinstance(structure_dict, type) and issubclass(structure_dict, Plugin):
@@ -78,10 +82,10 @@ def get_plugin_description(
78
82
 
79
83
 
80
84
  def get_description_dict(
81
- structure_dict: Dict,
85
+ structure_dict: dict,
82
86
  print_docs: bool,
83
87
  indentation_step: int,
84
- ) -> List[str]:
88
+ ) -> list[str]:
85
89
  """Returns a list of indented descriptions."""
86
90
 
87
91
  output_descriptions = []
@@ -98,13 +102,24 @@ def get_description_dict(
98
102
 
99
103
 
100
104
  class PluginListPlugin(Plugin):
105
+ """Plugin list plugin (so meta)."""
106
+
101
107
  def check_compatible(self) -> None:
102
108
  pass
103
109
 
104
110
  @export(output="none", cache=False)
105
111
  @arg("--docs", dest="print_docs", action="store_true")
106
- def plugins(self, plugins: list[dict] = None, print_docs: bool = False) -> None:
107
- categorized_plugins = dict(sorted(categorize_plugins(plugins).items()))
112
+ # NOTE: We would prefer to re-use arguments across plugins from argparse in query.py, but that is not possible yet.
113
+ # For now we use --as-json, but in the future this should be changed to inherit --json from target-query.
114
+ # https://github.com/fox-it/dissect.target/pull/841
115
+ # https://github.com/fox-it/dissect.target/issues/889
116
+ @arg("--as-json", dest="as_json", action="store_true")
117
+ def plugins(self, plugins: list[Plugin] = None, print_docs: bool = False, as_json: bool = False) -> None:
118
+ """Print all available plugins."""
119
+
120
+ dict_plugins = list({p.path: p.plugin_desc for p in plugins}.values())
121
+ categorized_plugins = dict(sorted(categorize_plugins(dict_plugins).items()))
122
+
108
123
  plugin_descriptions = output_plugin_description_recursive(categorized_plugins, print_docs)
109
124
 
110
125
  plugins_list = textwrap.indent(
@@ -138,4 +153,32 @@ class PluginListPlugin(Plugin):
138
153
  "Failed to load:",
139
154
  failed_list,
140
155
  ]
141
- print("\n".join(output_lines))
156
+
157
+ if as_json:
158
+ out = {"loaded": list(generate_plugins_json(plugins))}
159
+
160
+ if failed_plugins := plugin.failed():
161
+ out["failed"] = [
162
+ {"module": p["module"], "stacktrace": "".join(p["stacktrace"])} for p in failed_plugins
163
+ ]
164
+
165
+ print(json.dumps(out), end="")
166
+
167
+ else:
168
+ print("\n".join(output_lines))
169
+
170
+
171
+ def generate_plugins_json(plugins: list[Plugin]) -> Iterator[dict]:
172
+ """Generates JSON output of a list of :class:`Plugin`s."""
173
+
174
+ for p in plugins:
175
+ func = getattr(p.class_object, p.method_name)
176
+ description = getattr(func, "__doc__", None)
177
+ summary = description.split("\n\n", 1)[0].strip() if description else None
178
+
179
+ yield {
180
+ "name": p.name,
181
+ "output": p.output_type,
182
+ "description": summary,
183
+ "path": p.path,
184
+ }