dissect.target 3.18.dev15__py3-none-any.whl → 3.19__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. dissect/target/filesystem.py +44 -25
  2. dissect/target/filesystems/config.py +32 -21
  3. dissect/target/filesystems/extfs.py +4 -0
  4. dissect/target/filesystems/itunes.py +1 -1
  5. dissect/target/filesystems/tar.py +1 -1
  6. dissect/target/filesystems/zip.py +81 -46
  7. dissect/target/helpers/config.py +22 -7
  8. dissect/target/helpers/configutil.py +69 -5
  9. dissect/target/helpers/cyber.py +4 -2
  10. dissect/target/helpers/fsutil.py +32 -4
  11. dissect/target/helpers/loaderutil.py +26 -7
  12. dissect/target/helpers/network_managers.py +22 -7
  13. dissect/target/helpers/record.py +37 -0
  14. dissect/target/helpers/record_modifier.py +23 -4
  15. dissect/target/helpers/shell_application_ids.py +732 -0
  16. dissect/target/helpers/utils.py +11 -0
  17. dissect/target/loader.py +1 -0
  18. dissect/target/loaders/ab.py +285 -0
  19. dissect/target/loaders/libvirt.py +40 -0
  20. dissect/target/loaders/mqtt.py +14 -1
  21. dissect/target/loaders/tar.py +8 -4
  22. dissect/target/loaders/utm.py +3 -0
  23. dissect/target/loaders/velociraptor.py +6 -6
  24. dissect/target/plugin.py +60 -3
  25. dissect/target/plugins/apps/browser/chrome.py +1 -0
  26. dissect/target/plugins/apps/browser/chromium.py +7 -5
  27. dissect/target/plugins/apps/browser/edge.py +1 -0
  28. dissect/target/plugins/apps/browser/firefox.py +82 -36
  29. dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
  30. dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
  31. dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
  32. dissect/target/plugins/apps/ssh/openssh.py +1 -1
  33. dissect/target/plugins/apps/ssh/ssh.py +177 -0
  34. dissect/target/plugins/apps/texteditor/__init__.py +0 -0
  35. dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
  36. dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
  37. dissect/target/plugins/child/qemu.py +21 -0
  38. dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
  39. dissect/target/plugins/filesystem/unix/capability.py +102 -87
  40. dissect/target/plugins/filesystem/walkfs.py +32 -21
  41. dissect/target/plugins/filesystem/yara.py +144 -23
  42. dissect/target/plugins/general/network.py +82 -0
  43. dissect/target/plugins/general/users.py +14 -10
  44. dissect/target/plugins/os/unix/_os.py +19 -5
  45. dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
  46. dissect/target/plugins/os/unix/esxi/_os.py +29 -23
  47. dissect/target/plugins/os/unix/etc/etc.py +5 -8
  48. dissect/target/plugins/os/unix/history.py +3 -7
  49. dissect/target/plugins/os/unix/linux/_os.py +15 -14
  50. dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
  51. dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
  52. dissect/target/plugins/os/unix/locale.py +17 -6
  53. dissect/target/plugins/os/unix/shadow.py +47 -31
  54. dissect/target/plugins/os/windows/_os.py +4 -4
  55. dissect/target/plugins/os/windows/adpolicy.py +4 -1
  56. dissect/target/plugins/os/windows/catroot.py +1 -11
  57. dissect/target/plugins/os/windows/credential/__init__.py +0 -0
  58. dissect/target/plugins/os/windows/credential/lsa.py +174 -0
  59. dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
  60. dissect/target/plugins/os/windows/defender.py +6 -3
  61. dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
  62. dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
  63. dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
  64. dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
  65. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
  66. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
  67. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
  68. dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
  69. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
  70. dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
  71. dissect/target/plugins/os/windows/jumplist.py +292 -0
  72. dissect/target/plugins/os/windows/lnk.py +96 -93
  73. dissect/target/plugins/os/windows/regf/shellbags.py +8 -5
  74. dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
  75. dissect/target/plugins/os/windows/regf/usb.py +179 -114
  76. dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
  77. dissect/target/plugins/os/windows/wua_history.py +1073 -0
  78. dissect/target/target.py +4 -3
  79. dissect/target/tools/fs.py +53 -15
  80. dissect/target/tools/fsutils.py +243 -0
  81. dissect/target/tools/info.py +11 -4
  82. dissect/target/tools/query.py +2 -2
  83. dissect/target/tools/shell.py +505 -333
  84. dissect/target/tools/utils.py +23 -2
  85. dissect/target/tools/yara.py +65 -0
  86. dissect/target/volumes/md.py +2 -2
  87. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
  88. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/RECORD +94 -75
  89. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
  90. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
  91. dissect/target/helpers/ssh.py +0 -177
  92. /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
  93. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
  94. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
  95. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
- from typing import Callable
4
+ from typing import Callable, Iterator
3
5
 
4
6
  from dissect.ntfs.attr import Attribute
5
7
  from dissect.ntfs.c_ntfs import FILE_RECORD_SEGMENT_IN_USE
6
8
  from dissect.ntfs.mft import MftRecord
9
+ from flow.record import Record
7
10
  from flow.record.fieldtypes import windows_path
8
11
 
9
12
  from dissect.target.exceptions import UnsupportedPluginError
13
+ from dissect.target.filesystems.ntfs import NtfsFilesystem
10
14
  from dissect.target.helpers.record import TargetRecordDescriptor
11
15
  from dissect.target.plugin import Plugin, arg, export
12
16
  from dissect.target.plugins.filesystem.ntfs.utils import (
@@ -53,7 +57,6 @@ FilesystemStdRecord = TargetRecordDescriptor(
53
57
  ],
54
58
  )
55
59
 
56
-
57
60
  FilesystemFilenameCompactRecord = TargetRecordDescriptor(
58
61
  "filesystem/ntfs/mft/filename/compact",
59
62
  [
@@ -90,6 +93,21 @@ FilesystemFilenameRecord = TargetRecordDescriptor(
90
93
  ],
91
94
  )
92
95
 
96
+ FilesystemMACBRecord = TargetRecordDescriptor(
97
+ "filesystem/ntfs/mft/macb",
98
+ [
99
+ ("datetime", "ts"),
100
+ ("string", "macb"),
101
+ ("uint32", "filename_index"),
102
+ ("uint32", "segment"),
103
+ ("path", "path"),
104
+ ("string", "owner"),
105
+ ("filesize", "filesize"),
106
+ ("boolean", "resident"),
107
+ ("boolean", "inuse"),
108
+ ("string", "volume_uuid"),
109
+ ],
110
+ )
93
111
 
94
112
  RECORD_TYPES = {
95
113
  InformationType.STANDARD_INFORMATION: FilesystemStdRecord,
@@ -104,9 +122,12 @@ COMPACT_RECORD_TYPES = {
104
122
 
105
123
 
106
124
  class MftPlugin(Plugin):
125
+ def __init__(self, target):
126
+ super().__init__(target)
127
+ self.ntfs_filesystems = {index: fs for index, fs in enumerate(self.target.filesystems) if fs.__type__ == "ntfs"}
128
+
107
129
  def check_compatible(self) -> None:
108
- ntfs_filesystems = [fs for fs in self.target.filesystems if fs.__type__ == "ntfs"]
109
- if not len(ntfs_filesystems):
130
+ if not len(self.ntfs_filesystems):
110
131
  raise UnsupportedPluginError("No NTFS filesystems found")
111
132
 
112
133
  @export(
@@ -117,8 +138,24 @@ class MftPlugin(Plugin):
117
138
  FilesystemFilenameCompactRecord,
118
139
  ]
119
140
  )
120
- @arg("--compact", action="store_true", help="compacts the MFT entry timestamps into a single record")
121
- def mft(self, compact: bool = False):
141
+ @arg(
142
+ "--compact",
143
+ group="fmt",
144
+ action="store_true",
145
+ help="compacts the MFT entry timestamps into a single record",
146
+ )
147
+ @arg("--fs", type=int, default=None, help="optional filesystem index, zero indexed")
148
+ @arg("--start", type=int, default=0, help="the first MFT segment number")
149
+ @arg("--end", type=int, default=-1, help="the last MFT segment number")
150
+ @arg(
151
+ "--macb",
152
+ group="fmt",
153
+ action="store_true",
154
+ help="compacts the MFT entry timestamps into aggregated records with MACB bitfield",
155
+ )
156
+ def mft(
157
+ self, compact: bool = False, fs: int | None = None, start: int = 0, end: int = -1, macb: bool = False
158
+ ) -> Iterator[Record]:
122
159
  """Return the MFT records of all NTFS filesystems.
123
160
 
124
161
  The Master File Table (MFT) contains primarily metadata about every file and folder on a NFTS filesystem.
@@ -133,41 +170,60 @@ class MftPlugin(Plugin):
133
170
  - https://docs.microsoft.com/en-us/windows/win32/fileio/master-file-table
134
171
  """
135
172
 
136
- if compact:
137
- record_formatter = compacted_formatter
138
- else:
139
- record_formatter = formatter
173
+ record_formatter = formatter
140
174
 
141
- for fs in self.target.filesystems:
142
- if fs.__type__ != "ntfs":
143
- continue
175
+ def noaggr(records: list[Record]) -> Iterator[Record]:
176
+ yield from records
144
177
 
145
- # If this filesystem is a "fake" NTFS filesystem, used to enhance a
146
- # VirtualFilesystem, The driveletter (more accurate mount point)
147
- # returned will be that of the VirtualFilesystem. This makes sure
148
- # the paths returned in the records are actually reachable.
149
- drive_letter = get_drive_letter(self.target, fs)
150
- volume_uuid = get_volume_identifier(fs)
178
+ aggr = noaggr
151
179
 
180
+ if compact:
181
+ record_formatter = compacted_formatter
182
+ elif macb:
183
+ aggr = macb_aggr
184
+
185
+ if fs is not None:
152
186
  try:
153
- for record in fs.ntfs.mft.segments():
154
- try:
155
- inuse = bool(record.header.Flags & FILE_RECORD_SEGMENT_IN_USE)
156
- owner, _ = get_owner_and_group(record, fs)
157
- resident = False
158
- size = None
159
-
160
- if not record.is_dir():
161
- for data_attribute in record.attributes.DATA:
162
- if data_attribute.name == "":
163
- resident = data_attribute.resident
164
- break
165
-
166
- size = get_record_size(record)
167
-
168
- for path in record.full_paths():
169
- path = f"{drive_letter}{path}"
170
- yield from self.mft_records(
187
+ filesystem = self.ntfs_filesystems[fs]
188
+ except KeyError:
189
+ self.target.log.error("NTFS filesystem with index number %s does not exist", fs)
190
+ return
191
+
192
+ yield from self.segments(filesystem, record_formatter, aggr, start, end)
193
+ else:
194
+ for filesystem in self.ntfs_filesystems.values():
195
+ yield from self.segments(filesystem, record_formatter, aggr, start, end)
196
+
197
+ def segments(
198
+ self, fs: NtfsFilesystem, record_formatter: Callable, aggr: Callable, start: int, end: int
199
+ ) -> Iterator[Record]:
200
+ # If this filesystem is a "fake" NTFS filesystem, used to enhance a
201
+ # VirtualFilesystem, The driveletter (more accurate mount point)
202
+ # returned will be that of the VirtualFilesystem. This makes sure
203
+ # the paths returned in the records are actually reachable.
204
+ drive_letter = get_drive_letter(self.target, fs)
205
+ volume_uuid = get_volume_identifier(fs)
206
+
207
+ try:
208
+ for record in fs.ntfs.mft.segments(start, end):
209
+ try:
210
+ inuse = bool(record.header.Flags & FILE_RECORD_SEGMENT_IN_USE)
211
+ owner, _ = get_owner_and_group(record, fs)
212
+ resident = None
213
+ size = None
214
+
215
+ if not record.is_dir():
216
+ for data_attribute in record.attributes.DATA:
217
+ if data_attribute.name == "":
218
+ resident = data_attribute.resident
219
+ break
220
+
221
+ size = get_record_size(record)
222
+
223
+ for path in record.full_paths():
224
+ path = f"{drive_letter}{path}"
225
+ yield from aggr(
226
+ self.mft_records(
171
227
  drive_letter=drive_letter,
172
228
  record=record,
173
229
  segment=record.segment,
@@ -179,12 +235,13 @@ class MftPlugin(Plugin):
179
235
  volume_uuid=volume_uuid,
180
236
  record_formatter=record_formatter,
181
237
  )
182
- except Exception as e:
183
- self.target.log.warning("An error occured parsing MFT segment %d: %s", record.segment, str(e))
184
- self.target.log.debug("", exc_info=e)
238
+ )
239
+ except Exception as e:
240
+ self.target.log.warning("An error occured parsing MFT segment %d: %s", record.segment, str(e))
241
+ self.target.log.debug("", exc_info=e)
185
242
 
186
- except Exception:
187
- log.exception("An error occured constructing FilesystemRecords")
243
+ except Exception:
244
+ log.exception("An error occured constructing FilesystemRecords")
188
245
 
189
246
  def mft_records(
190
247
  self,
@@ -198,7 +255,7 @@ class MftPlugin(Plugin):
198
255
  inuse: bool,
199
256
  volume_uuid: str,
200
257
  record_formatter: Callable,
201
- ):
258
+ ) -> Iterator[Record]:
202
259
  for attr in record.attributes.STANDARD_INFORMATION:
203
260
  yield from record_formatter(
204
261
  attr=attr,
@@ -255,7 +312,7 @@ class MftPlugin(Plugin):
255
312
  )
256
313
 
257
314
 
258
- def compacted_formatter(attr: Attribute, record_type: InformationType, **kwargs):
315
+ def compacted_formatter(attr: Attribute, record_type: InformationType, **kwargs) -> Iterator[Record]:
259
316
  record_desc = COMPACT_RECORD_TYPES.get(record_type)
260
317
  yield record_desc(
261
318
  creation_time=attr.creation_time,
@@ -266,7 +323,7 @@ def compacted_formatter(attr: Attribute, record_type: InformationType, **kwargs)
266
323
  )
267
324
 
268
325
 
269
- def formatter(attr: Attribute, record_type: InformationType, **kwargs):
326
+ def formatter(attr: Attribute, record_type: InformationType, **kwargs) -> Iterator[Record]:
270
327
  record_desc = RECORD_TYPES.get(record_type)
271
328
  for type, timestamp in [
272
329
  ("B", attr.creation_time),
@@ -275,3 +332,33 @@ def formatter(attr: Attribute, record_type: InformationType, **kwargs):
275
332
  ("A", attr.last_access_time),
276
333
  ]:
277
334
  yield record_desc(ts=timestamp, ts_type=type, **kwargs)
335
+
336
+
337
+ def macb_aggr(records: list[Record]) -> Iterator[Record]:
338
+ def macb_set(bitfield, index, letter):
339
+ return bitfield[:index] + letter + bitfield[index + 1 :]
340
+
341
+ macbs = []
342
+ for record in records:
343
+ found = False
344
+
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
347
+
348
+ field = "MACB".find(record.ts_type) + offset_std + offset_ads
349
+ for macb in macbs:
350
+ if macb.ts == record.ts:
351
+ macb.macb = macb_set(macb.macb, field, record.ts_type)
352
+ found = True
353
+ break
354
+
355
+ if found:
356
+ continue
357
+
358
+ macb = FilesystemMACBRecord.init_from_record(record)
359
+ macb.macb = "..../..../...."
360
+ macb.macb = macb_set(macb.macb, field, record.ts_type)
361
+
362
+ macbs.append(macb)
363
+
364
+ yield from macbs
@@ -1,20 +1,20 @@
1
- import struct
2
1
  from enum import IntEnum
3
2
  from io import BytesIO
3
+ from typing import Iterator
4
4
 
5
- from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
5
+ from dissect.target.exceptions import UnsupportedPluginError
6
6
  from dissect.target.helpers.record import TargetRecordDescriptor
7
7
  from dissect.target.plugin import Plugin, export
8
- from dissect.target.plugins.filesystem.walkfs import generate_record
9
8
 
10
9
  CapabilityRecord = TargetRecordDescriptor(
11
10
  "filesystem/unix/capability",
12
11
  [
13
- ("record", "record"),
12
+ ("datetime", "ts_mtime"),
13
+ ("path", "path"),
14
14
  ("string[]", "permitted"),
15
15
  ("string[]", "inheritable"),
16
16
  ("boolean", "effective"),
17
- ("uint32", "rootid"),
17
+ ("uint32", "root_id"),
18
18
  ],
19
19
  )
20
20
 
@@ -82,88 +82,103 @@ class CapabilityPlugin(Plugin):
82
82
  """Plugin to yield files with capabilites set."""
83
83
 
84
84
  def check_compatible(self) -> None:
85
- if not self.target.has_function("walkfs") or self.target.os == "windows":
86
- raise UnsupportedPluginError("Unsupported plugin")
85
+ if not self.target.has_function("walkfs"):
86
+ raise UnsupportedPluginError("Need walkfs plugin")
87
+
88
+ if not any(fs.__type__ in ("extfs", "xfs") for fs in self.target.filesystems):
89
+ raise UnsupportedPluginError("Capability plugin only works on EXT and XFS filesystems")
87
90
 
88
91
  @export(record=CapabilityRecord)
89
- def capability_binaries(self):
90
- """Find all files that have capabilities set."""
91
- for path_entries, _, files in self.target.fs.walk_ext("/"):
92
- entries = [path_entries[-1]] + files
93
- for entry in entries:
94
- path = self.target.fs.path(entry.path)
95
- try:
96
- record = generate_record(self.target, path)
97
- except FileNotFoundError:
98
- continue
92
+ def capability_binaries(self) -> Iterator[CapabilityRecord]:
93
+ """Find all files that have capabilities set on files.
94
+
95
+ Resources:
96
+ - https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h
97
+ """
98
+
99
+ for entry in self.target.fs.recurse("/"):
100
+ if not entry.is_file() or entry.is_symlink():
101
+ continue
102
+
103
+ try:
104
+ attrs = [attr for attr in entry.lattr() if attr.name == "security.capability"]
105
+ except Exception as e:
106
+ self.target.log.warning("Failed to get attrs for entry %s", entry)
107
+ self.target.log.debug("", exc_info=e)
108
+ continue
109
+
110
+ for attr in attrs:
99
111
  try:
100
- attrs = path.get().lattr()
101
- except TypeError:
102
- # Virtual(File|Directory|Symlink) instances don't have a functional lattr()
103
- continue
104
- except Exception:
105
- self.target.log.exception("Failed to get attrs for entry %s", entry)
106
- continue
107
-
108
- for attr in attrs:
109
- if attr.name != "security.capability":
110
- continue
111
-
112
- buf = BytesIO(attr.value)
113
-
114
- # Reference: https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h
115
- # The struct is small enough we can just use struct
116
- magic_etc = struct.unpack("<I", buf.read(4))[0]
117
- cap_revision = magic_etc & VFS_CAP_REVISION_MASK
118
-
119
- permitted_caps = []
120
- inheritable_caps = []
121
- rootid = None
122
-
123
- if cap_revision == VFS_CAP_REVISION_1:
124
- num_caps = VFS_CAP_U32_1
125
- data_len = (1 + 2 * VFS_CAP_U32_1) * 4
126
- elif cap_revision == VFS_CAP_REVISION_2:
127
- num_caps = VFS_CAP_U32_2
128
- data_len = (1 + 2 * VFS_CAP_U32_2) * 4
129
- elif cap_revision == VFS_CAP_REVISION_3:
130
- num_caps = VFS_CAP_U32_3
131
- data_len = (2 + 2 * VFS_CAP_U32_2) * 4
132
- else:
133
- self.target.log.error("Unexpected capability revision: %s", entry)
134
- continue
135
-
136
- if data_len != len(attr.value):
137
- self.target.log.error("Unexpected capability length: %s", entry)
138
- continue
139
-
140
- for _ in range(num_caps):
141
- permitted_val, inheritable_val = struct.unpack("<2I", buf.read(8))
142
- permitted_caps.append(permitted_val)
143
- inheritable_caps.append(inheritable_val)
144
-
145
- if cap_revision == VFS_CAP_REVISION_3:
146
- rootid = struct.unpack("<I", buf.read(4))[0]
147
-
148
- permitted = []
149
- inheritable = []
150
-
151
- for capability in Capabilities:
152
- for caps, results in [(permitted_caps, permitted), (inheritable_caps, inheritable)]:
153
- # CAP_TO_INDEX
154
- cap_index = capability.value >> 5
155
- if cap_index >= len(caps):
156
- # We loop over all capabilities, but might only have a version 1 caps list
157
- continue
158
-
159
- if caps[cap_index] & (1 << (capability.value & 31)) != 0:
160
- results.append(capability.name)
161
-
162
- yield CapabilityRecord(
163
- record=record,
164
- permitted=permitted,
165
- inheritable=inheritable,
166
- effective=magic_etc & VFS_CAP_FLAGS_EFFECTIVE != 0,
167
- rootid=rootid,
168
- _target=self.target,
169
- )
112
+ permitted, inheritable, effective, root_id = parse_attr(attr.value)
113
+ except ValueError as e:
114
+ self.target.log.warning("Could not parse attributes for entry %s: %s", entry, str(e.value))
115
+ self.target.log.debug("", exc_info=e)
116
+
117
+ yield CapabilityRecord(
118
+ ts_mtime=entry.lstat().st_mtime,
119
+ path=self.target.fs.path(entry.path),
120
+ permitted=permitted,
121
+ inheritable=inheritable,
122
+ effective=effective,
123
+ root_id=root_id,
124
+ _target=self.target,
125
+ )
126
+
127
+
128
+ def parse_attr(attr: bytes) -> tuple[list[str], list[str], bool, int]:
129
+ """Efficiently parse a Linux xattr capability struct.
130
+
131
+ Returns:
132
+ A tuple of permitted capability names, inheritable capability names, effective flag and ``root_id``.
133
+ """
134
+ buf = BytesIO(attr)
135
+
136
+ # The struct is small enough we can just use int.from_bytes
137
+ magic_etc = int.from_bytes(buf.read(4), "little")
138
+ effective = magic_etc & VFS_CAP_FLAGS_EFFECTIVE != 0
139
+ cap_revision = magic_etc & VFS_CAP_REVISION_MASK
140
+
141
+ permitted_caps = []
142
+ inheritable_caps = []
143
+ root_id = None
144
+
145
+ if cap_revision == VFS_CAP_REVISION_1:
146
+ num_caps = VFS_CAP_U32_1
147
+ data_len = (1 + 2 * VFS_CAP_U32_1) * 4
148
+
149
+ elif cap_revision == VFS_CAP_REVISION_2:
150
+ num_caps = VFS_CAP_U32_2
151
+ data_len = (1 + 2 * VFS_CAP_U32_2) * 4
152
+
153
+ elif cap_revision == VFS_CAP_REVISION_3:
154
+ num_caps = VFS_CAP_U32_3
155
+ data_len = (2 + 2 * VFS_CAP_U32_2) * 4
156
+
157
+ else:
158
+ raise ValueError("Unexpected capability revision '%s'" % cap_revision)
159
+
160
+ if data_len != (actual_len := len(attr)):
161
+ raise ValueError("Unexpected capability length (%s vs %s)", data_len, actual_len)
162
+
163
+ for _ in range(num_caps):
164
+ permitted_caps.append(int.from_bytes(buf.read(4), "little"))
165
+ inheritable_caps.append(int.from_bytes(buf.read(4), "little"))
166
+
167
+ if cap_revision == VFS_CAP_REVISION_3:
168
+ root_id = int.from_bytes(buf.read(4), "little")
169
+
170
+ permitted = []
171
+ inheritable = []
172
+
173
+ for capability in Capabilities:
174
+ for caps, results in [(permitted_caps, permitted), (inheritable_caps, inheritable)]:
175
+ # CAP_TO_INDEX
176
+ cap_index = capability.value >> 5
177
+ if cap_index >= len(caps):
178
+ # We loop over all capabilities, but might only have a version 1 caps list
179
+ continue
180
+
181
+ if caps[cap_index] & (1 << (capability.value & 31)) != 0:
182
+ results.append(capability.name)
183
+
184
+ return permitted, inheritable, effective, root_id
@@ -1,12 +1,11 @@
1
- from typing import Iterable
1
+ from typing import Iterator
2
2
 
3
3
  from dissect.util.ts import from_unix
4
4
 
5
5
  from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
6
- from dissect.target.filesystem import LayerFilesystemEntry
7
- from dissect.target.helpers.fsutil import TargetPath
6
+ from dissect.target.filesystem import FilesystemEntry, LayerFilesystemEntry
8
7
  from dissect.target.helpers.record import TargetRecordDescriptor
9
- from dissect.target.plugin import Plugin, export
8
+ from dissect.target.plugin import Plugin, arg, export
10
9
  from dissect.target.target import Target
11
10
 
12
11
  FilesystemRecord = TargetRecordDescriptor(
@@ -30,37 +29,49 @@ FilesystemRecord = TargetRecordDescriptor(
30
29
  class WalkFSPlugin(Plugin):
31
30
  def check_compatible(self) -> None:
32
31
  if not len(self.target.filesystems):
33
- raise UnsupportedPluginError("No filesystems found")
32
+ raise UnsupportedPluginError("No filesystems to walk")
34
33
 
35
34
  @export(record=FilesystemRecord)
36
- def walkfs(self) -> Iterable[FilesystemRecord]:
35
+ @arg("--walkfs-path", default="/", help="path to recursively walk")
36
+ def walkfs(self, walkfs_path: str = "/") -> Iterator[FilesystemRecord]:
37
37
  """Walk a target's filesystem and return all filesystem entries."""
38
- for path_entries, _, files in self.target.fs.walk_ext("/"):
39
- entries = [path_entries[-1]] + files
40
- for entry in entries:
41
- path = self.target.fs.path(entry.path)
42
- try:
43
- record = generate_record(self.target, path)
44
- except FileNotFoundError:
45
- continue
46
- yield record
38
+ for entry in self.target.fs.recurse(walkfs_path):
39
+ try:
40
+ yield generate_record(self.target, entry)
47
41
 
42
+ except FileNotFoundError as e:
43
+ self.target.log.warning("File not found: %s", entry)
44
+ self.target.log.debug("", exc_info=e)
45
+ except Exception as e:
46
+ self.target.log.warning("Exception generating record for: %s", entry)
47
+ self.target.log.debug("", exc_info=e)
48
+ continue
49
+
50
+
51
+ def generate_record(target: Target, entry: FilesystemEntry) -> FilesystemRecord:
52
+ """Generate a :class:`FilesystemRecord` from the given :class:`FilesystemEntry`.
53
+
54
+ Args:
55
+ target: :class:`Target` instance
56
+ entry: :class:`FilesystemEntry` instance
57
+
58
+ Returns:
59
+ Generated :class:`FilesystemRecord` for the given :class:`FilesystemEntry`.
60
+ """
61
+ stat = entry.lstat()
48
62
 
49
- def generate_record(target: Target, path: TargetPath) -> FilesystemRecord:
50
- stat = path.lstat()
51
- btime = from_unix(stat.st_birthtime) if stat.st_birthtime else None
52
- entry = path.get()
53
63
  if isinstance(entry, LayerFilesystemEntry):
54
64
  fs_types = [sub_entry.fs.__type__ for sub_entry in entry.entries]
55
65
  else:
56
66
  fs_types = [entry.fs.__type__]
67
+
57
68
  return FilesystemRecord(
58
69
  atime=from_unix(stat.st_atime),
59
70
  mtime=from_unix(stat.st_mtime),
60
71
  ctime=from_unix(stat.st_ctime),
61
- btime=btime,
72
+ btime=from_unix(stat.st_birthtime) if stat.st_birthtime else None,
62
73
  ino=stat.st_ino,
63
- path=path,
74
+ path=entry.path,
64
75
  size=stat.st_size,
65
76
  mode=stat.st_mode,
66
77
  uid=stat.st_uid,