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
@@ -3,8 +3,10 @@ from __future__ import annotations
3
3
  import logging
4
4
  import re
5
5
  import uuid
6
- from struct import unpack
7
- from typing import Iterator, Optional, Union
6
+ from pathlib import Path
7
+ from typing import Iterator
8
+
9
+ from flow.record.fieldtypes import posix_path
8
10
 
9
11
  from dissect.target.filesystem import Filesystem
10
12
  from dissect.target.helpers.fsutil import TargetPath
@@ -16,16 +18,36 @@ from dissect.target.target import Target
16
18
  log = logging.getLogger(__name__)
17
19
 
18
20
 
21
+ # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#ISA
22
+ ARCH_MAP = {
23
+ 0x00: "unknown",
24
+ 0x02: "sparc",
25
+ 0x03: "x86",
26
+ 0x08: "mips",
27
+ 0x14: "powerpc32",
28
+ 0x15: "powerpc64",
29
+ 0x16: "s390", # and s390x
30
+ 0x28: "aarch32", # armv7
31
+ 0x2A: "superh",
32
+ 0x32: "ia-64",
33
+ 0x3E: "x86_64",
34
+ 0xB7: "aarch64", # armv8
35
+ 0xF3: "riscv64",
36
+ 0xF7: "bpf",
37
+ }
38
+
39
+
19
40
  class UnixPlugin(OSPlugin):
20
41
  def __init__(self, target: Target):
21
42
  super().__init__(target)
22
43
  self._add_mounts()
44
+ self._add_devices()
23
45
  self._hostname_dict = self._parse_hostname_string()
24
46
  self._hosts_dict = self._parse_hosts_string()
25
47
  self._os_release = self._parse_os_release()
26
48
 
27
49
  @classmethod
28
- def detect(cls, target: Target) -> Optional[Filesystem]:
50
+ def detect(cls, target: Target) -> Filesystem | None:
29
51
  for fs in target.filesystems:
30
52
  if fs.exists("/var") and fs.exists("/etc"):
31
53
  return fs
@@ -71,7 +93,7 @@ class UnixPlugin(OSPlugin):
71
93
  uid=pwent.get(2),
72
94
  gid=pwent.get(3),
73
95
  gecos=pwent.get(4),
74
- home=self.target.fs.path(pwent.get(5)),
96
+ home=posix_path(pwent.get(5)),
75
97
  shell=pwent.get(6),
76
98
  source=passwd_file,
77
99
  _target=self.target,
@@ -115,23 +137,23 @@ class UnixPlugin(OSPlugin):
115
137
 
116
138
  yield UnixUserRecord(
117
139
  name=user["name"],
118
- home=user["home"],
140
+ home=posix_path(user["home"]),
119
141
  shell=user["shell"],
120
142
  source="/var/log/syslog",
121
143
  _target=self.target,
122
144
  )
123
145
 
124
146
  @export(property=True)
125
- def architecture(self) -> Optional[str]:
147
+ def architecture(self) -> str | None:
126
148
  return self._get_architecture(self.os)
127
149
 
128
150
  @export(property=True)
129
- def hostname(self) -> Optional[str]:
151
+ def hostname(self) -> str | None:
130
152
  hosts_string = self._hosts_dict.get("hostname", "localhost")
131
153
  return self._hostname_dict.get("hostname", hosts_string)
132
154
 
133
155
  @export(property=True)
134
- def domain(self) -> Optional[str]:
156
+ def domain(self) -> str | None:
135
157
  domain = self._hostname_dict.get("domain", "localhost")
136
158
  if domain == "localhost":
137
159
  domain = self._hosts_dict["hostname", "localhost"]
@@ -143,7 +165,7 @@ class UnixPlugin(OSPlugin):
143
165
  def os(self) -> str:
144
166
  return OperatingSystem.UNIX.value
145
167
 
146
- def _parse_rh_legacy(self, path):
168
+ def _parse_rh_legacy(self, path: Path) -> str | None:
147
169
  hostname = None
148
170
  file_contents = path.open("rt").readlines()
149
171
  for line in file_contents:
@@ -152,7 +174,7 @@ class UnixPlugin(OSPlugin):
152
174
  _, _, hostname = line.rstrip().partition("=")
153
175
  return hostname
154
176
 
155
- def _parse_hostname_string(self, paths: Optional[list[str]] = None) -> Optional[dict[str, str]]:
177
+ def _parse_hostname_string(self, paths: list[str] | None = None) -> dict[str, str] | None:
156
178
  """
157
179
  Returns a dict containing the hostname and domain name portion of the path(s) specified
158
180
 
@@ -184,7 +206,7 @@ class UnixPlugin(OSPlugin):
184
206
  break # break whenever a valid hostname is found
185
207
  return hostname_dict
186
208
 
187
- def _parse_hosts_string(self, paths: Optional[list[str]] = None) -> dict[str, str]:
209
+ def _parse_hosts_string(self, paths: list[str] | None = None) -> dict[str, str]:
188
210
  paths = paths or ["/etc/hosts"]
189
211
  hosts_string = {"ip": None, "hostname": None}
190
212
 
@@ -244,7 +266,18 @@ class UnixPlugin(OSPlugin):
244
266
  self.target.log.debug("Mounting %s (%s) at %s", fs, fs.volume, mount_point)
245
267
  self.target.fs.mount(mount_point, fs)
246
268
 
247
- def _parse_os_release(self, glob: Optional[str] = None) -> dict[str, str]:
269
+ def _add_devices(self) -> None:
270
+ """Add some virtual block devices to the target.
271
+
272
+ Currently only adds LVM devices.
273
+ """
274
+ vfs = self.target.fs.append_layer()
275
+
276
+ for volume in self.target.volumes:
277
+ if volume.vs and volume.vs.__type__ == "lvm":
278
+ vfs.map_file_fh(f"/dev/{volume.raw.vg.name}/{volume.raw.name}", volume)
279
+
280
+ def _parse_os_release(self, glob: str | None = None) -> dict[str, str]:
248
281
  """Parse files containing Unix version information.
249
282
 
250
283
  Not all these files are equal. Generally speaking these files are
@@ -286,43 +319,36 @@ class UnixPlugin(OSPlugin):
286
319
  continue
287
320
  return os_release
288
321
 
289
- def _get_architecture(self, os: str = "unix", path: str = "/bin/ls") -> Optional[str]:
290
- arch_strings = {
291
- 0x00: "Unknown",
292
- 0x02: "SPARC",
293
- 0x03: "x86",
294
- 0x08: "MIPS",
295
- 0x14: "PowerPC",
296
- 0x16: "S390",
297
- 0x28: "ARM",
298
- 0x2A: "SuperH",
299
- 0x32: "IA-64",
300
- 0x3E: "x86_64",
301
- 0xB7: "AArch64",
302
- 0xF3: "RISC-V",
303
- }
304
-
305
- for fs in self.target.filesystems:
306
- if fs.exists(path):
307
- fh = fs.open(path)
308
- fh.seek(4)
309
- # ELF - e_ident[EI_CLASS]
310
- bits = unpack("B", fh.read(1))[0]
311
- fh.seek(18)
312
- # ELF - e_machine
313
- arch = unpack("H", fh.read(2))[0]
314
- arch = arch_strings.get(arch)
315
-
316
- if bits == 1: # 32 bit system
317
- return f"{arch}_32-{os}"
318
- else:
319
- return f"{arch}-{os}"
322
+ def _get_architecture(self, os: str = "unix", path: Path | str = "/bin/ls") -> str | None:
323
+ """Determine architecture by reading an ELF header of a binary on the target.
324
+
325
+ Resources:
326
+ - https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#ISA
327
+ """
328
+
329
+ if not isinstance(path, TargetPath):
330
+ for fs in [self.target.fs, *self.target.filesystems]:
331
+ if (path := fs.path(path)).exists():
332
+ break
333
+
334
+ if not path.exists():
335
+ return
336
+
337
+ fh = path.open("rb")
338
+ fh.seek(4) # ELF - e_ident[EI_CLASS]
339
+ bits = fh.read(1)[0]
340
+
341
+ fh.seek(18) # ELF - e_machine
342
+ e_machine = int.from_bytes(fh.read(2), "little")
343
+ arch = ARCH_MAP.get(e_machine, "unknown")
344
+
345
+ return f"{arch}_32-{os}" if bits == 1 and not arch[-2:] == "32" else f"{arch}-{os}"
320
346
 
321
347
 
322
348
  def parse_fstab(
323
349
  fstab: TargetPath,
324
350
  log: logging.Logger = log,
325
- ) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str, str]]:
351
+ ) -> Iterator[tuple[uuid.UUID | str, str, str, str, str]]:
326
352
  """Parse fstab file and return a generator that streams the details of entries,
327
353
  with unsupported FS types and block devices filtered away.
328
354
  """
@@ -0,0 +1,78 @@
1
+ from typing import Iterator
2
+
3
+ from dissect.target.exceptions import UnsupportedPluginError
4
+ from dissect.target.helpers import configutil
5
+ from dissect.target.helpers.fsutil import TargetPath
6
+ from dissect.target.helpers.record import UnixApplicationRecord
7
+ from dissect.target.plugin import Plugin, export
8
+ from dissect.target.target import Target
9
+
10
+
11
+ class UnixApplicationsPlugin(Plugin):
12
+ """Unix Applications plugin."""
13
+
14
+ SYSTEM_PATHS = [
15
+ "/usr/share/applications/",
16
+ "/usr/local/share/applications/",
17
+ "/var/lib/snapd/desktop/applications/",
18
+ "/var/lib/flatpak/exports/share/applications/",
19
+ ]
20
+
21
+ USER_PATHS = [
22
+ ".local/share/applications/",
23
+ ]
24
+
25
+ SYSTEM_APPS = ("org.gnome.",)
26
+
27
+ def __init__(self, target: Target):
28
+ super().__init__(target)
29
+ self.desktop_files = list(self._find_desktop_files())
30
+
31
+ def _find_desktop_files(self) -> Iterator[TargetPath]:
32
+ for dir in self.SYSTEM_PATHS:
33
+ for file in self.target.fs.path(dir).glob("*.desktop"):
34
+ yield file
35
+
36
+ for user_details in self.target.user_details.all_with_home():
37
+ for dir in self.USER_PATHS:
38
+ for file in user_details.home_path.joinpath(dir).glob("*.desktop"):
39
+ yield file
40
+
41
+ def check_compatible(self) -> None:
42
+ if not self.desktop_files:
43
+ raise UnsupportedPluginError("No application .desktop files found")
44
+
45
+ @export(record=UnixApplicationRecord)
46
+ def applications(self) -> Iterator[UnixApplicationRecord]:
47
+ """Yield installed Unix GUI applications from GNOME and XFCE.
48
+
49
+ Resources:
50
+ - https://wiki.archlinux.org/title/Desktop_entries
51
+ - https://specifications.freedesktop.org/desktop-entry-spec/latest/
52
+ - https://unix.stackexchange.com/questions/582928/where-gnome-apps-are-installed
53
+
54
+ Yields ``UnixApplicationRecord`` records with the following fields:
55
+
56
+ .. code-block:: text
57
+
58
+ ts_modified (datetime): timestamp when the installation was modified
59
+ ts_installed (datetime): timestamp when the application was installed on the system
60
+ name (string): name of the application
61
+ version (string): version of the application
62
+ author (string): author of the application
63
+ type (string): type of the application, either user or system
64
+ path (string): path to the desktop file entry of the application
65
+ """
66
+ for file in self.desktop_files:
67
+ config = configutil.parse(file, hint="ini").get("Desktop Entry") or {}
68
+ stat = file.lstat()
69
+
70
+ yield UnixApplicationRecord(
71
+ ts_modified=stat.st_mtime,
72
+ ts_installed=stat.st_btime if hasattr(stat, "st_btime") else None,
73
+ name=config.get("Name"),
74
+ version=config.get("Version"),
75
+ path=config.get("Exec"),
76
+ type="system" if config.get("Icon", "").startswith(self.SYSTEM_APPS) else "user",
77
+ _target=self.target,
78
+ )
@@ -44,6 +44,8 @@ CITRIX_NETSCALER_BASH_HISTORY_RE = re.compile(
44
44
 
45
45
 
46
46
  class CitrixCommandHistoryPlugin(CommandHistoryPlugin):
47
+ """Citrix command history plugin."""
48
+
47
49
  COMMAND_HISTORY_ABSOLUTE_PATHS = (("citrix-netscaler-bash", "/var/log/bash.log*"),)
48
50
  COMMAND_HISTORY_RELATIVE_PATHS = CommandHistoryPlugin.COMMAND_HISTORY_RELATIVE_PATHS + (
49
51
  ("citrix-netscaler-cli", ".nscli_history"),
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import plistlib
4
4
  from typing import Iterator, Optional
5
5
 
6
+ from flow.record.fieldtypes import posix_path
7
+
6
8
  from dissect.target.filesystem import Filesystem
7
9
  from dissect.target.helpers.record import UnixUserRecord
8
10
  from dissect.target.plugin import OperatingSystem, export
@@ -39,26 +41,7 @@ class MacPlugin(BsdPlugin):
39
41
 
40
42
  @export(property=True)
41
43
  def ips(self) -> Optional[list[str]]:
42
- ips = set()
43
-
44
- # Static configured IP-addresses
45
- if (preferences := self.target.fs.path(self.SYSTEM)).exists():
46
- network = plistlib.load(preferences.open()).get("NetworkServices")
47
-
48
- for interface in network.values():
49
- for addresses in [interface.get("IPv4"), interface.get("IPv6")]:
50
- ips.update(addresses.get("Addresses", []))
51
-
52
- # IP-addresses configured by DHCP
53
- if (dhcp := self.target.fs.path("/private/var/db/dhcpclient/leases")).exists():
54
- for lease in dhcp.iterdir():
55
- if lease.is_file():
56
- lease = plistlib.load(lease.open())
57
-
58
- if ip := lease.get("IPAddress"):
59
- ips.add(ip)
60
-
61
- return list(ips)
44
+ return list(set(map(str, self.target.network.ips())))
62
45
 
63
46
  @export(property=True)
64
47
  def version(self) -> Optional[str]:
@@ -87,7 +70,7 @@ class MacPlugin(BsdPlugin):
87
70
  uid=user.get("uid", [None])[0],
88
71
  gid=user.get("gid", [None])[0],
89
72
  gecos=user.get("realname", [None])[0],
90
- home=home_dir,
73
+ home=posix_path(home_dir) if home_dir else None,
91
74
  shell=user.get("shell", [None])[0],
92
75
  source=path,
93
76
  )
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import plistlib
4
+ from functools import cache, lru_cache
5
+ from typing import Iterator
6
+
7
+ from dissect.target.helpers.record import MacInterfaceRecord
8
+ from dissect.target.plugins.general.network import NetworkPlugin
9
+ from dissect.target.target import Target
10
+
11
+
12
+ class MacNetworkPlugin(NetworkPlugin):
13
+ """macOS network interface plugin."""
14
+
15
+ def __init__(self, target: Target):
16
+ super().__init__(target)
17
+ self._plistnetwork = cache(self._plistnetwork)
18
+ self._plistlease = lru_cache(32)(self._plistlease)
19
+
20
+ def _plistlease(self, devname: str) -> dict:
21
+ for lease in self.target.fs.glob_ext(f"/private/var/db/dhcpclient/leases/{devname}*"):
22
+ return plistlib.load(lease.open())
23
+ return {}
24
+
25
+ def _plistnetwork(self) -> dict:
26
+ if (preferences := self.target.fs.path("/Library/Preferences/SystemConfiguration/preferences.plist")).exists():
27
+ return plistlib.load(preferences.open())
28
+
29
+ def _interfaces(self) -> Iterator[MacInterfaceRecord]:
30
+ plistnetwork = self._plistnetwork()
31
+ current_set = plistnetwork.get("CurrentSet")
32
+ sets = plistnetwork.get("Sets", {})
33
+ for name, _set in sets.items():
34
+ if f"/Sets/{name}" == current_set:
35
+ item = _set
36
+ for key in ["Network", "Global", "IPv4", "ServiceOrder"]:
37
+ item = item.get(key, {})
38
+ service_order = item
39
+ break
40
+
41
+ network = plistnetwork.get("NetworkServices", {})
42
+ vlans = plistnetwork.get("VirtualNetworkInterfaces", {}).get("VLAN", {})
43
+
44
+ vlan_lookup = {key: vlan.get("Tag") for key, vlan in vlans.items()}
45
+
46
+ for _id, interface in network.items():
47
+ dns = set()
48
+ gateways = set()
49
+ ips = set()
50
+ device = interface.get("Interface", {})
51
+ name = device.get("DeviceName")
52
+ _type = device.get("Type")
53
+ vlan = vlan_lookup.get(name)
54
+ dhcp = False
55
+ subnetmask = []
56
+ network = []
57
+ interface_service_order = service_order.index(_id) if _id in service_order else None
58
+ try:
59
+ for addr in interface.get("DNS", {}).get("ServerAddresses", {}):
60
+ dns.add(addr)
61
+ for addresses in [interface.get("IPv4", {}), interface.get("IPv6", {})]:
62
+ subnetmask += filter(lambda mask: mask != "", addresses.get("SubnetMasks", []))
63
+ if router := addresses.get("Router"):
64
+ gateways.add(router)
65
+ if addresses.get("ConfigMethod", "") == "DHCP":
66
+ ips.add(self._plistlease(name).get("IPAddress"))
67
+ dhcp = True
68
+ else:
69
+ for addr in addresses.get("Addresses", []):
70
+ ips.add(addr)
71
+
72
+ if subnetmask:
73
+ network = self.calculate_network(ips, subnetmask)
74
+
75
+ yield MacInterfaceRecord(
76
+ name=name,
77
+ type=_type,
78
+ enabled=not interface.get("__INACTIVE__", False),
79
+ dns=list(dns),
80
+ ip=list(ips),
81
+ gateway=list(gateways),
82
+ source="NetworkServices",
83
+ vlan=vlan,
84
+ network=network,
85
+ interface_service_order=interface_service_order,
86
+ dhcp=dhcp,
87
+ _target=self.target,
88
+ )
89
+
90
+ except Exception as e:
91
+ self.target.log.warning("Error reading configuration for network device %s: %s", name, e)
92
+ continue
@@ -20,6 +20,8 @@ AccountPolicyRecord = create_extended_descriptor([UserRecordDescriptorExtension]
20
20
 
21
21
 
22
22
  class UserPlugin(Plugin):
23
+ """MacOS / OSX user plugin."""
24
+
23
25
  # TODO: Parse additional user data like: HeimdalSRPKey, KerberosKeys, ShadowHashData, LinkedIdentity,
24
26
  # inputSources, smartCardSecureTokenData, smartCardSecureTokenUUID, unlockOptions, smb_sid
25
27
 
@@ -31,6 +33,8 @@ class UserPlugin(Plugin):
31
33
 
32
34
  @export(record=AccountPolicyRecord)
33
35
  def account_policy(self) -> Iterator[AccountPolicyRecord]:
36
+ """Yield user account policy information"""
37
+
34
38
  # The data is not retrieved from the home folder of the user
35
39
  for user_details in self.target.user_details.all():
36
40
  user = plistlib.load(self.target.fs.path(user_details.user.source).open())
@@ -1,4 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
4
+ from typing import Iterator
2
5
 
3
6
  from dissect.target.helpers.record import TargetRecordDescriptor
4
7
  from dissect.target.plugin import Plugin, export
@@ -28,10 +31,12 @@ EnvironmentVariableRecord = TargetRecordDescriptor(
28
31
 
29
32
 
30
33
  class CronjobPlugin(Plugin):
34
+ """Unix cronjob plugin."""
35
+
31
36
  def check_compatible(self) -> None:
32
37
  pass
33
38
 
34
- def parse_crontab(self, file_path):
39
+ def parse_crontab(self, file_path) -> Iterator[CronjobRecord | EnvironmentVariableRecord]:
35
40
  for line in file_path.open("rt"):
36
41
  line = line.strip()
37
42
  if line.startswith("#") or not len(line):
@@ -67,9 +72,8 @@ class CronjobPlugin(Plugin):
67
72
  )
68
73
 
69
74
  @export(record=[CronjobRecord, EnvironmentVariableRecord])
70
- def cronjobs(self):
71
- """
72
- Return all cronjobs.
75
+ def cronjobs(self) -> Iterator[CronjobRecord | EnvironmentVariableRecord]:
76
+ """Yield cronjobs on the unix system.
73
77
 
74
78
  A cronjob is a scheduled task/command on a Unix based system. Adversaries may use cronjobs to gain
75
79
  persistence on the system.
@@ -23,6 +23,8 @@ UnixConfigTreeRecord = TargetRecordDescriptor(
23
23
 
24
24
 
25
25
  class EtcTree(ConfigurationTreePlugin):
26
+ """Unix etc configuration tree plugin."""
27
+
26
28
  __namespace__ = "etc"
27
29
 
28
30
  def __init__(self, target: Target):
@@ -64,6 +66,8 @@ class EtcTree(ConfigurationTreePlugin):
64
66
  @arg("--glob", dest="pattern", required=False, default="*", type=str, help="Glob-style pattern to search for")
65
67
  @arg("--root", dest="root", required=False, default="/", type=str, help="Path to use as root for search")
66
68
  def etc(self, pattern: str, root: str) -> Iterator[UnixConfigTreeRecord]:
69
+ """Yield etc configuration records."""
70
+
67
71
  for entry, subs, items in self.config_fs.walk(root):
68
72
  for item in items:
69
73
  try:
@@ -9,6 +9,8 @@ from dissect.target.plugin import Plugin, export
9
9
 
10
10
 
11
11
  class GenericPlugin(Plugin):
12
+ """Generic plugin for Unix targets."""
13
+
12
14
  def check_compatible(self) -> None:
13
15
  pass
14
16
 
@@ -26,6 +26,8 @@ RE_FISH = re.compile(r"- cmd: (?P<command>.+?)\s+when: (?P<ts>\d+)")
26
26
 
27
27
 
28
28
  class CommandHistoryPlugin(Plugin):
29
+ """Unix command history plugin."""
30
+
29
31
  COMMAND_HISTORY_RELATIVE_PATHS = (
30
32
  ("bash", ".bash_history"),
31
33
  ("fish", ".local/share/fish/fish_history"),
@@ -59,7 +61,7 @@ class CommandHistoryPlugin(Plugin):
59
61
 
60
62
  @alias("bashhistory")
61
63
  @export(record=CommandHistoryRecord)
62
- def commandhistory(self):
64
+ def commandhistory(self) -> Iterator[CommandHistoryRecord]:
63
65
  """Return shell history for all users.
64
66
 
65
67
  When using a shell, history of the used commands is kept on the system.
@@ -81,12 +83,12 @@ class CommandHistoryPlugin(Plugin):
81
83
  def parse_generic_history(self, file, user: UnixUserRecord, shell: str) -> Iterator[CommandHistoryRecord]:
82
84
  """Parse bash_history contents.
83
85
 
84
- Regular .bash_history files contain one plain command per line.
85
- An extended .bash_history file may look like this:
86
- ```
87
- #1648598339
88
- echo "this is a test"
89
- ```
86
+ Regular .bash_history files contain one plain command per line. Extended ``.bash_history`` files look like this:
87
+
88
+ .. code-block::
89
+
90
+ #1648598339
91
+ echo "this is a test"
90
92
 
91
93
  Resources:
92
94
  - http://git.savannah.gnu.org/cgit/bash.git/tree/bashhist.c
@@ -121,12 +123,12 @@ class CommandHistoryPlugin(Plugin):
121
123
  def parse_zsh_history(self, file, user: UnixUserRecord) -> Iterator[CommandHistoryRecord]:
122
124
  """Parse zsh_history contents.
123
125
 
124
- Regular .zsh_history lines are just the plain commands.
125
- Extended .zsh_history files may look like this:
126
- ```
127
- : 1673860722:0;sudo apt install sl
128
- : :;
129
- ```
126
+ Regular ``.zsh_history`` lines are just the plain commands. Extended ``.zsh_history`` files look like this:
127
+
128
+ .. code-block::
129
+
130
+ : 1673860722:0;sudo apt install sl
131
+ : :;
130
132
 
131
133
  Resources:
132
134
  - https://sourceforge.net/p/zsh/code/ci/master/tree/Src/hist.c
@@ -157,18 +159,18 @@ class CommandHistoryPlugin(Plugin):
157
159
  def parse_fish_history(self, history_file: TargetPath, user: UnixUserRecord) -> Iterator[CommandHistoryRecord]:
158
160
  """Parses the history file of the fish shell.
159
161
 
160
- The fish history file is formatted as pseudo-YAML.
161
- An example of such a file:
162
- ```
163
- - cmd: ls
164
- when: 1688642435
165
- - cmd: cd home/
166
- when: 1688642441
167
- paths:
168
- - home/
169
- - cmd: echo "test: test"
170
- when: 1688986629
171
- ```
162
+ The fish history file is formatted as pseudo-YAML. An example of such a file:
163
+
164
+ .. code-block::
165
+
166
+ - cmd: ls
167
+ when: 1688642435
168
+ - cmd: cd home/
169
+ when: 1688642441
170
+ paths:
171
+ - home/
172
+ - cmd: echo "test: test"
173
+ when: 1688986629
172
174
 
173
175
  Note that the last `- cmd: echo "test: test"` is not valid YAML,
174
176
  which is why we cannot safely use the Python yaml module.
@@ -3,13 +3,13 @@ from __future__ import annotations
3
3
  import logging
4
4
 
5
5
  from dissect.target.filesystem import Filesystem
6
- from dissect.target.helpers.network_managers import (
7
- LinuxNetworkManager,
8
- parse_unix_dhcp_log_messages,
9
- )
10
6
  from dissect.target.plugin import OperatingSystem, export
11
7
  from dissect.target.plugins.os.unix._os import UnixPlugin
12
8
  from dissect.target.plugins.os.unix.bsd.osx._os import MacPlugin
9
+ from dissect.target.plugins.os.unix.linux.network_managers import (
10
+ LinuxNetworkManager,
11
+ parse_unix_dhcp_log_messages,
12
+ )
13
13
  from dissect.target.plugins.os.windows._os import WindowsPlugin
14
14
  from dissect.target.target import Target
15
15
 
@@ -34,17 +34,15 @@ class LinuxPlugin(UnixPlugin, LinuxNetworkManager):
34
34
  @export(property=True)
35
35
  def ips(self) -> list[str]:
36
36
  """Returns a list of static IP addresses and DHCP lease IP addresses found on the host system."""
37
- ips = []
37
+ ips = set()
38
38
 
39
39
  for ip_set in self.network_manager.get_config_value("ips"):
40
- for ip in ip_set:
41
- ips.append(ip)
40
+ ips.update(ip_set)
42
41
 
43
42
  for ip in parse_unix_dhcp_log_messages(self.target, iter_all=False):
44
- if ip not in ips:
45
- ips.append(ip)
43
+ ips.add(ip)
46
44
 
47
- return ips
45
+ return list(ips)
48
46
 
49
47
  @export(property=True)
50
48
  def dns(self) -> list[str]:
@@ -16,6 +16,8 @@ CmdlineRecord = TargetRecordDescriptor(
16
16
 
17
17
 
18
18
  class CmdlinePlugin(Plugin):
19
+ """Linux volatile proc commandline plugin."""
20
+
19
21
  def check_compatible(self) -> None:
20
22
  self.target.proc
21
23
 
@@ -10,13 +10,16 @@ from dissect.target.helpers.fsutil import open_decompress
10
10
  from dissect.target.plugins.os.unix.packagemanager import (
11
11
  OperationTypes,
12
12
  PackageManagerLogRecord,
13
+ PackageManagerPlugin,
13
14
  )
14
15
 
15
16
  APT_LOG_OPERATIONS = ["Install", "Reinstall", "Upgrade", "Downgrade", "Remove", "Purge"]
16
17
  REGEX_PACKAGE_NAMES = re.compile(r"(.*?\)),?")
17
18
 
18
19
 
19
- class AptPlugin(plugin.Plugin):
20
+ class AptPlugin(PackageManagerPlugin):
21
+ """Apt package manager plugin."""
22
+
20
23
  __namespace__ = "apt"
21
24
 
22
25
  LOG_DIR_PATH = "/var/log/apt"