dissect.target 3.19.dev57__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.dev57.dist-info → dissect.target-3.20.dist-info}/METADATA +6 -2
  170. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/RECORD +176 -160
  171. {dissect.target-3.19.dev57.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.dev57.dist-info → dissect.target-3.20.dist-info}/COPYRIGHT +0 -0
  178. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/LICENSE +0 -0
  179. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/entry_points.txt +0 -0
  180. {dissect.target-3.19.dev57.dist-info → dissect.target-3.20.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,17 @@
1
- import gzip
1
+ from __future__ import annotations
2
+
2
3
  import ipaddress
3
4
  import struct
4
5
  from collections import namedtuple
5
6
  from typing import Iterator
6
7
 
7
8
  from dissect.cstruct import cstruct
8
- from dissect.util.stream import BufferedStream
9
9
  from dissect.util.ts import from_unix
10
10
 
11
11
  from dissect.target.exceptions import UnsupportedPluginError
12
- from dissect.target.helpers.fsutil import TargetPath
12
+ from dissect.target.helpers.fsutil import TargetPath, open_decompress
13
13
  from dissect.target.helpers.record import TargetRecordDescriptor
14
- from dissect.target.plugin import OperatingSystem, Plugin, export
14
+ from dissect.target.plugin import Plugin, alias, export
15
15
  from dissect.target.target import Target
16
16
 
17
17
  UTMP_FIELDS = [
@@ -27,16 +27,12 @@ UTMP_FIELDS = [
27
27
 
28
28
  BtmpRecord = TargetRecordDescriptor(
29
29
  "linux/log/btmp",
30
- [
31
- *UTMP_FIELDS,
32
- ],
30
+ UTMP_FIELDS,
33
31
  )
34
32
 
35
33
  WtmpRecord = TargetRecordDescriptor(
36
34
  "linux/log/wtmp",
37
- [
38
- *UTMP_FIELDS,
39
- ],
35
+ UTMP_FIELDS,
40
36
  )
41
37
 
42
38
  utmp_def = """
@@ -104,24 +100,13 @@ UTMP_ENTRY = namedtuple(
104
100
  class UtmpFile:
105
101
  """utmp maintains a full accounting of the current status of the system"""
106
102
 
107
- def __init__(self, target: Target, path: TargetPath):
108
- self.fh = target.fs.path(path).open()
109
-
110
- if "gz" in path:
111
- self.compressed = True
112
- else:
113
- self.compressed = False
103
+ def __init__(self, path: TargetPath):
104
+ self.fh = open_decompress(path, "rb")
114
105
 
115
106
  def __iter__(self):
116
- if self.compressed:
117
- gzip_entry = BufferedStream(gzip.open(self.fh, mode="rb"))
118
- byte_stream = gzip_entry
119
- else:
120
- byte_stream = self.fh
121
-
122
107
  while True:
123
108
  try:
124
- entry = c_utmp.entry(byte_stream)
109
+ entry = c_utmp.entry(self.fh)
125
110
 
126
111
  r_type = ""
127
112
  if entry.ut_type in c_utmp.Type:
@@ -151,7 +136,7 @@ class UtmpFile:
151
136
  # ut_addr_v6 is parsed as IPv4 address. This could not lead to incorrect results.
152
137
  ut_addr = ipaddress.ip_address(struct.pack("<i", entry.ut_addr_v6[0]))
153
138
 
154
- utmp_entry = UTMP_ENTRY(
139
+ yield UTMP_ENTRY(
155
140
  ts=from_unix(entry.ut_tv.tv_sec),
156
141
  ut_type=r_type,
157
142
  ut_pid=entry.ut_pid,
@@ -162,25 +147,37 @@ class UtmpFile:
162
147
  ut_addr=ut_addr,
163
148
  )
164
149
 
165
- yield utmp_entry
166
150
  except EOFError:
167
151
  break
168
152
 
169
153
 
170
154
  class UtmpPlugin(Plugin):
171
- WTMP_GLOB = "/var/log/wtmp*"
172
- BTMP_GLOB = "/var/log/btmp*"
155
+ """Unix utmp log plugin."""
156
+
157
+ def __init__(self, target: Target):
158
+ super().__init__(target)
159
+ self.btmp_paths = list(self.target.fs.path("/").glob("var/log/btmp*"))
160
+ self.wtmp_paths = list(self.target.fs.path("/").glob("var/log/wtmp*"))
161
+ self.utmp_paths = list(self.target.fs.path("/").glob("var/run/utmp*"))
173
162
 
174
163
  def check_compatible(self) -> None:
175
- if not self.target.os == OperatingSystem.LINUX and not any(
176
- [
177
- list(self.target.fs.glob(self.BTMP_GLOB)),
178
- list(self.target.fs.glob(self.WTMP_GLOB)),
179
- ]
180
- ):
181
- raise UnsupportedPluginError("No WTMP or BTMP log files found")
182
-
183
- @export(record=[BtmpRecord])
164
+ if not any(self.btmp_paths + self.wtmp_paths + self.utmp_paths):
165
+ raise UnsupportedPluginError("No wtmp and/or btmp log files found")
166
+
167
+ def _build_record(self, record: TargetRecordDescriptor, entry: UTMP_ENTRY) -> Iterator[BtmpRecord | WtmpRecord]:
168
+ return record(
169
+ ts=entry.ts,
170
+ ut_type=entry.ut_type,
171
+ ut_pid=entry.ut_pid,
172
+ ut_user=entry.ut_user,
173
+ ut_line=entry.ut_line,
174
+ ut_id=entry.ut_id,
175
+ ut_host=entry.ut_host,
176
+ ut_addr=entry.ut_addr,
177
+ _target=self.target,
178
+ )
179
+
180
+ @export(record=BtmpRecord)
184
181
  def btmp(self) -> Iterator[BtmpRecord]:
185
182
  """Return failed login attempts stored in the btmp file.
186
183
 
@@ -190,26 +187,18 @@ class UtmpPlugin(Plugin):
190
187
  - https://en.wikipedia.org/wiki/Utmp
191
188
  - https://www.thegeekdiary.com/what-is-the-purpose-of-utmp-wtmp-and-btmp-files-in-linux/
192
189
  """
193
- btmp_paths = self.target.fs.glob(self.BTMP_GLOB)
194
- for btmp_path in btmp_paths:
195
- btmp = UtmpFile(self.target, btmp_path)
196
-
197
- for entry in btmp:
198
- yield BtmpRecord(
199
- ts=entry.ts,
200
- ut_type=entry.ut_type,
201
- ut_pid=entry.ut_pid,
202
- ut_user=entry.ut_user,
203
- ut_line=entry.ut_line,
204
- ut_id=entry.ut_id,
205
- ut_host=entry.ut_host,
206
- ut_addr=entry.ut_addr,
207
- _target=self.target,
208
- )
190
+ for path in self.btmp_paths:
191
+ if not path.is_file():
192
+ self.target.log.warning("Unable to parse btmp file: %s is not a file", path)
193
+ continue
209
194
 
210
- @export(record=[WtmpRecord])
195
+ for entry in UtmpFile(path):
196
+ yield self._build_record(BtmpRecord, entry)
197
+
198
+ @alias("utmp")
199
+ @export(record=WtmpRecord)
211
200
  def wtmp(self) -> Iterator[WtmpRecord]:
212
- """Return the content of the wtmp log files.
201
+ """Yield contents of wtmp log files.
213
202
 
214
203
  The wtmp file contains the historical data of the utmp file. The utmp file contains information about users
215
204
  logins at which terminals, logouts, system events and current status of the system, system boot time
@@ -218,19 +207,11 @@ class UtmpPlugin(Plugin):
218
207
  References:
219
208
  - https://www.thegeekdiary.com/what-is-the-purpose-of-utmp-wtmp-and-btmp-files-in-linux/
220
209
  """
221
- wtmp_paths = self.target.fs.glob(self.WTMP_GLOB)
222
- for wtmp_path in wtmp_paths:
223
- wtmp = UtmpFile(self.target, wtmp_path)
224
-
225
- for entry in wtmp:
226
- yield WtmpRecord(
227
- ts=entry.ts,
228
- ut_type=entry.ut_type,
229
- ut_pid=entry.ut_pid,
230
- ut_user=entry.ut_user,
231
- ut_line=entry.ut_line,
232
- ut_id=entry.ut_id,
233
- ut_host=entry.ut_host,
234
- ut_addr=entry.ut_addr,
235
- _target=self.target,
236
- )
210
+
211
+ for path in self.wtmp_paths + self.utmp_paths:
212
+ if not path.is_file():
213
+ self.target.log.warning("Unable to parse wtmp file: %s is not a file", path)
214
+ continue
215
+
216
+ for entry in UtmpFile(path):
217
+ yield self._build_record(WtmpRecord, entry)
@@ -1,15 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import Enum
4
- from typing import Iterator
5
4
 
6
- from dissect.target import Target
7
- from dissect.target.exceptions import UnsupportedPluginError
8
5
  from dissect.target.helpers.record import TargetRecordDescriptor
9
- from dissect.target.plugin import Plugin, export
6
+ from dissect.target.plugin import NamespacePlugin
10
7
 
11
8
  PackageManagerLogRecord = TargetRecordDescriptor(
12
- "unix/log/packagemanager",
9
+ "unix/packagemanager/log",
13
10
  [
14
11
  ("datetime", "ts"),
15
12
  ("string", "package_manager"),
@@ -22,6 +19,8 @@ PackageManagerLogRecord = TargetRecordDescriptor(
22
19
 
23
20
 
24
21
  class OperationTypes(Enum):
22
+ """Valid operation types."""
23
+
25
24
  Install = "install"
26
25
  Update = "update"
27
26
  Downgrade = "downgrade"
@@ -45,37 +44,5 @@ class OperationTypes(Enum):
45
44
  return OperationTypes.Other
46
45
 
47
46
 
48
- class PackageManagerPlugin(Plugin):
47
+ class PackageManagerPlugin(NamespacePlugin):
49
48
  __namespace__ = "packagemanager"
50
- __findable__ = False
51
-
52
- TOOLS = [
53
- "apt",
54
- "yum",
55
- "zypper",
56
- ]
57
-
58
- def __init__(self, target: Target):
59
- super().__init__(target)
60
- self._plugins = []
61
- for entry in self.TOOLS:
62
- try:
63
- self._plugins.append(getattr(self.target, entry))
64
- except Exception:
65
- target.log.exception(f"Failed to load tool plugin: {entry}")
66
-
67
- def check_compatible(self) -> None:
68
- if not len(self._plugins):
69
- raise UnsupportedPluginError("No compatible plugins found")
70
-
71
- def _func(self, f: str) -> Iterator[PackageManagerLogRecord]:
72
- for p in self._plugins:
73
- try:
74
- yield from getattr(p, f)()
75
- except Exception:
76
- self.target.log.exception("Failed to execute package manager plugin: %s.%s", p._name, f)
77
-
78
- @export(record=PackageManagerLogRecord)
79
- def logs(self) -> Iterator[PackageManagerLogRecord]:
80
- """Returns logs from all available Unix package managers."""
81
- yield from self._func("logs")
@@ -25,6 +25,8 @@ UnixShadowRecord = TargetRecordDescriptor(
25
25
 
26
26
 
27
27
  class ShadowPlugin(Plugin):
28
+ """Unix shadow passwords plugin."""
29
+
28
30
  def check_compatible(self) -> None:
29
31
  if not self.target.fs.path("/etc/shadow").exists():
30
32
  raise UnsupportedPluginError("No shadow file found")
@@ -36,7 +38,7 @@ class ShadowPlugin(Plugin):
36
38
  """Yield shadow records from /etc/shadow files.
37
39
 
38
40
  Resources:
39
- - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html#file:/etc/shadow
41
+ - https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
40
42
  """
41
43
 
42
44
  seen_hashes = set()
@@ -0,0 +1,132 @@
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.descriptor_extensions import UserRecordDescriptorExtension
6
+ from dissect.target.helpers.fsutil import TargetPath
7
+ from dissect.target.helpers.record import create_extended_descriptor
8
+ from dissect.target.plugin import Plugin, alias, export
9
+ from dissect.target.plugins.general.users import UserDetails
10
+ from dissect.target.target import Target
11
+
12
+ TrashRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
13
+ "linux/filesystem/recyclebin",
14
+ [
15
+ ("datetime", "ts"),
16
+ ("path", "path"),
17
+ ("filesize", "filesize"),
18
+ ("path", "deleted_path"),
19
+ ("path", "source"),
20
+ ],
21
+ )
22
+
23
+
24
+ class GnomeTrashPlugin(Plugin):
25
+ """Linux GNOME Trash plugin."""
26
+
27
+ PATHS = [
28
+ # Default $XDG_DATA_HOME/Trash
29
+ ".local/share/Trash",
30
+ ]
31
+
32
+ def __init__(self, target: Target):
33
+ super().__init__(target)
34
+ self.trashes = list(self._garbage_collector())
35
+
36
+ def _garbage_collector(self) -> Iterator[tuple[UserDetails, TargetPath]]:
37
+ """it aint much, but its honest work"""
38
+ for user_details in self.target.user_details.all_with_home():
39
+ for trash_path in self.PATHS:
40
+ if (path := user_details.home_path.joinpath(trash_path)).exists():
41
+ yield user_details, path
42
+
43
+ def check_compatible(self) -> None:
44
+ if not self.trashes:
45
+ raise UnsupportedPluginError("No Trash folder(s) found")
46
+
47
+ @export(record=TrashRecord)
48
+ @alias(name="recyclebin")
49
+ def trash(self) -> Iterator[TrashRecord]:
50
+ """Yield deleted files from GNOME Trash folders.
51
+
52
+ Recovers deleted files and artifacts from ``$HOME/.local/share/Trash``.
53
+ Probably also works with other desktop interfaces as long as they follow the Trash specification from FreeDesktop.
54
+
55
+ Currently does not parse media trash locations such as ``/media/$Label/.Trash-1000/*``.
56
+
57
+ Resources:
58
+ - https://specifications.freedesktop.org/trash-spec/latest/
59
+ - https://github.com/GNOME/glib/blob/main/gio/glocalfile.c
60
+ - https://specifications.freedesktop.org/basedir-spec/latest/
61
+
62
+ Yields ``TrashRecord`` records with the following fields:
63
+
64
+ .. code-block:: text
65
+
66
+ ts (datetime): timestamp when the file was deleted or for expunged files when it could not be permanently deleted
67
+ path (path): path where the file was located before it was deleted
68
+ filesize (filesize): size in bytes of the deleted file
69
+ deleted_path (path): path to the current location of the deleted file
70
+ source (path): path to the .trashinfo file
71
+ """ # noqa: E501
72
+
73
+ for user_details, trash in self.trashes:
74
+ for trash_info_file in trash.glob("info/*.trashinfo"):
75
+ trash_info = configutil.parse(trash_info_file, hint="ini").get("Trash Info", {})
76
+ original_path = self.target.fs.path(trash_info.get("Path", ""))
77
+
78
+ # We use the basename of the .trashinfo file and not the Path variable inside the
79
+ # ini file. This way we can keep duplicate basenames of trashed files separated correctly.
80
+ deleted_path = trash / "files" / trash_info_file.name.replace(".trashinfo", "")
81
+
82
+ if deleted_path.exists():
83
+ deleted_files = [deleted_path]
84
+
85
+ if deleted_path.is_dir():
86
+ for child in deleted_path.rglob("*"):
87
+ deleted_files.append(child)
88
+
89
+ for file in deleted_files:
90
+ # NOTE: We currently do not 'fix' the original_path of files inside deleted directories.
91
+ # This would require guessing where the parent folder starts, which is impossible without
92
+ # making assumptions.
93
+ yield TrashRecord(
94
+ ts=trash_info.get("DeletionDate", 0),
95
+ path=original_path,
96
+ filesize=file.lstat().st_size if file.is_file() else None,
97
+ deleted_path=file,
98
+ source=trash_info_file,
99
+ _user=user_details.user,
100
+ _target=self.target,
101
+ )
102
+
103
+ # We cannot determine if the deleted entry is a directory since the path does
104
+ # not exist at $TRASH/files, so we work with what we have instead.
105
+ else:
106
+ self.target.log.warning(f"Expected trashed file(s) at {deleted_path}")
107
+ yield TrashRecord(
108
+ ts=trash_info.get("DeletionDate", 0),
109
+ path=original_path,
110
+ filesize=0,
111
+ deleted_path=deleted_path,
112
+ source=trash_info_file,
113
+ _user=user_details.user,
114
+ _target=self.target,
115
+ )
116
+
117
+ # We also iterate expunged folders, they can contain files that could not be
118
+ # deleted when the user pressed the "empty trash" button in the file manager.
119
+ # Resources:
120
+ # - https://gitlab.gnome.org/GNOME/glib/-/issues/1665
121
+ # - https://bugs.launchpad.net/ubuntu/+source/nautilus/+bug/422012
122
+ for item in (trash / "expunged").rglob("*/*"):
123
+ stat = item.lstat()
124
+ yield TrashRecord(
125
+ ts=stat.st_mtime, # NOTE: This is the timestamp at which the file failed to delete
126
+ path=None, # We do not know the original file path
127
+ filesize=stat.st_size if item.is_file() else None,
128
+ deleted_path=item,
129
+ source=trash / "expunged",
130
+ _user=user_details.user,
131
+ _target=self.target,
132
+ )
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import operator
4
4
  import struct
5
- from typing import Any, Iterator, Optional
5
+ from typing import Any, Iterator
6
+
7
+ from flow.record.fieldtypes import windows_path
6
8
 
7
9
  from dissect.target.exceptions import RegistryError, RegistryValueNotFoundError
8
10
  from dissect.target.filesystem import Filesystem
@@ -10,6 +12,14 @@ from dissect.target.helpers.record import WindowsUserRecord
10
12
  from dissect.target.plugin import OperatingSystem, OSPlugin, export
11
13
  from dissect.target.target import Target
12
14
 
15
+ ARCH_MAP = {
16
+ "x86": 32,
17
+ "IA64": 64,
18
+ "ARM64": 64,
19
+ "EM64T": 64,
20
+ "AMD64": 64,
21
+ }
22
+
13
23
 
14
24
  class WindowsPlugin(OSPlugin):
15
25
  CURRENT_VERSION_KEY = "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion"
@@ -26,7 +36,7 @@ class WindowsPlugin(OSPlugin):
26
36
  )
27
37
 
28
38
  @classmethod
29
- def detect(cls, target: Target) -> Optional[Filesystem]:
39
+ def detect(cls, target: Target) -> Filesystem | None:
30
40
  for fs in target.filesystems:
31
41
  if fs.exists("/windows/system32") or fs.exists("/winnt"):
32
42
  return fs
@@ -90,7 +100,7 @@ class WindowsPlugin(OSPlugin):
90
100
  self.target.log.warning("Unknown drive letter for sysvol")
91
101
 
92
102
  @export(property=True)
93
- def hostname(self) -> Optional[str]:
103
+ def hostname(self) -> str | None:
94
104
  key = "HKLM\\SYSTEM\\ControlSet001\\Control\\Computername\\Computername"
95
105
  try:
96
106
  return self.target.registry.value(key, "Computername").value
@@ -99,28 +109,7 @@ class WindowsPlugin(OSPlugin):
99
109
 
100
110
  @export(property=True)
101
111
  def ips(self) -> list[str]:
102
- key = "HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"
103
- fields = ["IPAddress", "DhcpIPAddress"]
104
- ips = set()
105
-
106
- for r in self.target.registry.keys(key):
107
- for s in r.subkeys():
108
- for field in fields:
109
- try:
110
- ip = s.value(field).value
111
- except RegistryValueNotFoundError:
112
- continue
113
-
114
- if isinstance(ip, str):
115
- ip = [ip]
116
-
117
- for i in ip:
118
- if i == "0.0.0.0":
119
- continue
120
-
121
- ips.add(i)
122
-
123
- return list(ips)
112
+ return list(set(map(str, self.target.network.ips())))
124
113
 
125
114
  def _get_version_reg_value(self, value_name: str) -> Any:
126
115
  try:
@@ -130,7 +119,7 @@ class WindowsPlugin(OSPlugin):
130
119
 
131
120
  return value
132
121
 
133
- def _legacy_current_version(self) -> Optional[str]:
122
+ def _legacy_current_version(self) -> str | None:
134
123
  """Returns the NT version as used up to and including NT 6.3.
135
124
 
136
125
  This corresponds with Windows 8 / Windows 2012 Server.
@@ -142,7 +131,7 @@ class WindowsPlugin(OSPlugin):
142
131
  """
143
132
  return self._get_version_reg_value("CurrentVersion")
144
133
 
145
- def _major_version(self) -> Optional[int]:
134
+ def _major_version(self) -> int | None:
146
135
  """Return the NT major version number (starting from NT 10.0 / Windows 10).
147
136
 
148
137
  Returns:
@@ -152,7 +141,7 @@ class WindowsPlugin(OSPlugin):
152
141
  """
153
142
  return self._get_version_reg_value("CurrentMajorVersionNumber")
154
143
 
155
- def _minor_version(self) -> Optional[int]:
144
+ def _minor_version(self) -> int | None:
156
145
  """Return the NT minor version number (starting from NT 10.0 / Windows 10).
157
146
 
158
147
  Returns:
@@ -162,7 +151,7 @@ class WindowsPlugin(OSPlugin):
162
151
  """
163
152
  return self._get_version_reg_value("CurrentMinorVersionNumber")
164
153
 
165
- def _nt_version(self) -> Optional[int]:
154
+ def _nt_version(self) -> int | None:
166
155
  """Return the Windows NT version in x.y format.
167
156
 
168
157
  For systems up to and including NT 6.3 (Win 8 / Win 2012 Server) this
@@ -190,7 +179,7 @@ class WindowsPlugin(OSPlugin):
190
179
  return version
191
180
 
192
181
  @export(property=True)
193
- def version(self) -> Optional[str]:
182
+ def version(self) -> str | None:
194
183
  """Return a string representation of the Windows version of the target.
195
184
 
196
185
  For Windows versions before Windows 10 this looks like::
@@ -276,7 +265,7 @@ class WindowsPlugin(OSPlugin):
276
265
  return version_string
277
266
 
278
267
  @export(property=True)
279
- def architecture(self) -> Optional[str]:
268
+ def architecture(self) -> str | None:
280
269
  """
281
270
  Returns a dict containing the architecture and bitness of the system
282
271
 
@@ -284,19 +273,11 @@ class WindowsPlugin(OSPlugin):
284
273
  Dict: arch: architecture, bitness: bits
285
274
  """
286
275
 
287
- arch_strings = {
288
- "x86": 32,
289
- "IA64": 64,
290
- "ARM64": 64,
291
- "EM64T": 64,
292
- "AMD64": 64,
293
- }
294
-
295
276
  key = "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment"
296
277
 
297
278
  try:
298
279
  arch = self.target.registry.key(key).value("PROCESSOR_ARCHITECTURE").value
299
- bits = arch_strings.get(arch)
280
+ bits = ARCH_MAP.get(arch)
300
281
 
301
282
  # return {"arch": arch, "bitness": bits}
302
283
  if bits == 64:
@@ -333,7 +314,7 @@ class WindowsPlugin(OSPlugin):
333
314
  yield WindowsUserRecord(
334
315
  sid=subkey.name,
335
316
  name=name,
336
- home=home,
317
+ home=windows_path(home),
337
318
  _target=self.target,
338
319
  )
339
320
 
@@ -1,3 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Iterator
5
+
1
6
  from dissect.sql import sqlite3
2
7
  from dissect.util.ts import from_unix
3
8
 
@@ -42,8 +47,8 @@ class ActivitiesCachePlugin(Plugin):
42
47
  """Plugin that parses the ActivitiesCache.db on newer Windows 10 machines.
43
48
 
44
49
  References:
45
- https://www.cclsolutionsgroup.com/resources/technical-papers
46
- https://salt4n6.com/2018/05/03/windows-10-timeline-forensic-artefacts/
50
+ - https://www.cclsolutionsgroup.com/resources/technical-papers
51
+ - https://salt4n6.com/2018/05/03/windows-10-timeline-forensic-artefacts/
47
52
  """
48
53
 
49
54
  def __init__(self, target):
@@ -62,7 +67,7 @@ class ActivitiesCachePlugin(Plugin):
62
67
  raise UnsupportedPluginError("No ActiviesCache.db files found")
63
68
 
64
69
  @export(record=ActivitiesCacheRecord)
65
- def activitiescache(self):
70
+ def activitiescache(self) -> Iterator[ActivitiesCacheRecord]:
66
71
  """Return ActivitiesCache.db database content.
67
72
 
68
73
  The Windows Activities Cache database keeps track of activity on a device, such as application and services
@@ -143,7 +148,7 @@ class ActivitiesCachePlugin(Plugin):
143
148
  )
144
149
 
145
150
 
146
- def mkts(ts):
151
+ def mkts(ts: int) -> datetime | None:
147
152
  """Timestamps inside ActivitiesCache.db are stored in a Unix-like format.
148
153
 
149
154
  Source: https://salt4n6.com/2018/05/03/windows-10-timeline-forensic-artefacts/
@@ -1,4 +1,5 @@
1
1
  from struct import unpack
2
+ from typing import Iterator
2
3
 
3
4
  from defusedxml import ElementTree
4
5
  from dissect.cstruct import cstruct
@@ -90,7 +91,7 @@ class ADPolicyPlugin(Plugin):
90
91
  self.target.log.warning("Unable to read XML policy file: %s", error)
91
92
 
92
93
  @export(record=ADPolicyRecord)
93
- def adpolicy(self):
94
+ def adpolicy(self) -> Iterator[ADPolicyRecord]:
94
95
  """Return all AD policies (also known as GPOs or Group Policy Objects).
95
96
 
96
97
  An Active Directory (AD) maintains global policies that should be adhered by all systems in the domain.