dissect.target 3.14.dev8__py3-none-any.whl → 3.14.dev10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,10 +45,12 @@ class Filesystem:
45
45
  # This has the added benefit of having a readily available "pretty name" for each implementation
46
46
  __type__: str = None
47
47
  """A short string identifying the type of filesystem."""
48
+ __multi_volume__: bool = False
49
+ """Whether this filesystem supports multiple volumes (disks)."""
48
50
 
49
51
  def __init__(
50
52
  self,
51
- volume: Optional[BinaryIO] = None,
53
+ volume: Optional[Union[BinaryIO, list[BinaryIO]]] = None,
52
54
  alt_separator: str = "",
53
55
  case_sensitive: bool = True,
54
56
  ) -> None:
@@ -56,7 +58,7 @@ class Filesystem:
56
58
 
57
59
  Args:
58
60
  volume: A volume or other file-like object associated with the filesystem.
59
- case_sensitive: Defines if the paths in the Filesystem are case sensitive or not.
61
+ case_sensitive: Defines if the paths in the filesystem are case sensitive or not.
60
62
  alt_separator: The alternative separator used to distingish between directories in a path.
61
63
 
62
64
  Raises:
@@ -82,7 +84,7 @@ class Filesystem:
82
84
  return cls.__type__
83
85
 
84
86
  def path(self, *args) -> fsutil.TargetPath:
85
- """Get a specific path from the filesystem."""
87
+ """Instantiate a new path-like object on this filesystem."""
86
88
  return fsutil.TargetPath(self, *args)
87
89
 
88
90
  @classmethod
@@ -125,6 +127,52 @@ class Filesystem:
125
127
  """
126
128
  raise NotImplementedError()
127
129
 
130
+ @classmethod
131
+ def detect_id(cls, fh: BinaryIO) -> Optional[bytes]:
132
+ """Return a filesystem set identifier.
133
+
134
+ Only used in filesystems that support multiple volumes (disks) to find all volumes
135
+ belonging to a single filesystem.
136
+
137
+ Args:
138
+ fh: A file-like object, usually a disk or partition.
139
+ """
140
+ if not cls.__multi_volume__:
141
+ return None
142
+
143
+ offset = fh.tell()
144
+ try:
145
+ fh.seek(0)
146
+ return cls._detect_id(fh)
147
+ except NotImplementedError:
148
+ raise
149
+ except Exception as e:
150
+ log.warning("Failed to detect ID on %s filesystem", cls.__fstype__)
151
+ log.debug("", exc_info=e)
152
+ finally:
153
+ fh.seek(offset)
154
+
155
+ return None
156
+
157
+ @staticmethod
158
+ def _detect_id(fh: BinaryIO) -> Optional[bytes]:
159
+ """Return a filesystem set identifier.
160
+
161
+ This method should be implemented by subclasses of filesystems that support multiple volumes (disks).
162
+ The position of ``fh`` is guaranteed to be ``0``.
163
+
164
+ Args:
165
+ fh: A file-like object, usually a disk or partition.
166
+
167
+ Returns:
168
+ An identifier that can be used to combine the given ``fh`` with others beloning to the same set.
169
+ """
170
+ raise NotImplementedError()
171
+
172
+ def iter_subfs(self) -> Iterator[Filesystem]:
173
+ """Yield possible sub-filesystems."""
174
+ yield from ()
175
+
128
176
  def get(self, path: str) -> FilesystemEntry:
129
177
  """Retrieve a :class:`FilesystemEntry` from the filesystem.
130
178
 
@@ -1461,6 +1509,18 @@ def register(module: str, class_name: str, internal: bool = True) -> None:
1461
1509
  FILESYSTEMS.append(getattr(import_lazy(module), class_name))
1462
1510
 
1463
1511
 
1512
+ def is_multi_volume_filesystem(fh: BinaryIO) -> bool:
1513
+ for filesystem in FILESYSTEMS:
1514
+ try:
1515
+ if filesystem.__multi_volume__ and filesystem.detect(fh):
1516
+ return True
1517
+ except ImportError as e:
1518
+ log.info("Failed to import %s", filesystem)
1519
+ log.debug("", exc_info=e)
1520
+
1521
+ return False
1522
+
1523
+
1464
1524
  def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1465
1525
  offset = fh.tell()
1466
1526
  fh.seek(0)
@@ -1469,10 +1529,7 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1469
1529
  for filesystem in FILESYSTEMS:
1470
1530
  try:
1471
1531
  if filesystem.detect(fh):
1472
- instance = filesystem(fh, *args, **kwargs)
1473
- instance.volume = fh
1474
-
1475
- return instance
1532
+ return filesystem(fh, *args, **kwargs)
1476
1533
  except ImportError as e:
1477
1534
  log.info("Failed to import %s", filesystem)
1478
1535
  log.debug("", exc_info=e)
@@ -1482,12 +1539,35 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1482
1539
  raise FilesystemError(f"Failed to open filesystem for {fh}")
1483
1540
 
1484
1541
 
1542
+ def open_multi_volume(fhs: list[BinaryIO], *args, **kwargs) -> Filesystem:
1543
+ for filesystem in FILESYSTEMS:
1544
+ try:
1545
+ if not filesystem.__multi_volume__:
1546
+ continue
1547
+
1548
+ volumes = defaultdict(list)
1549
+ for fh in fhs:
1550
+ if not filesystem.detect(fh):
1551
+ continue
1552
+
1553
+ identifier = filesystem.detect_id(fh)
1554
+ volumes[identifier].append(fh)
1555
+
1556
+ for vols in volumes.values():
1557
+ yield filesystem(vols, *args, **kwargs)
1558
+
1559
+ except ImportError as e:
1560
+ log.info("Failed to import %s", filesystem)
1561
+ log.debug("", exc_info=e)
1562
+
1563
+
1485
1564
  register("ntfs", "NtfsFilesystem")
1486
1565
  register("extfs", "ExtFilesystem")
1487
1566
  register("xfs", "XfsFilesystem")
1488
1567
  register("fat", "FatFilesystem")
1489
1568
  register("ffs", "FfsFilesystem")
1490
1569
  register("vmfs", "VmfsFilesystem")
1570
+ register("btrfs", "BtrfsFilesystem")
1491
1571
  register("exfat", "ExfatFilesystem")
1492
1572
  register("squashfs", "SquashFSFilesystem")
1493
1573
  register("zip", "ZipFilesystem")
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import BinaryIO, Iterator, Optional, Union
4
+
5
+ import dissect.btrfs as btrfs
6
+ from dissect.btrfs.c_btrfs import c_btrfs
7
+
8
+ from dissect.target.exceptions import (
9
+ FileNotFoundError,
10
+ FilesystemError,
11
+ IsADirectoryError,
12
+ NotADirectoryError,
13
+ NotASymlinkError,
14
+ )
15
+ from dissect.target.filesystem import Filesystem, FilesystemEntry
16
+ from dissect.target.helpers import fsutil
17
+
18
+
19
+ class BtrfsFilesystem(Filesystem):
20
+ __fstype__ = "btrfs"
21
+ __multi_volume__ = True
22
+
23
+ def __init__(self, fh: Union[BinaryIO, list[BinaryIO]], *args, **kwargs):
24
+ super().__init__(fh, *args, **kwargs)
25
+ self.btrfs = btrfs.Btrfs(fh)
26
+ self.subfs = self.open_subvolume()
27
+ self.subvolume = self.subfs.subvolume
28
+
29
+ @staticmethod
30
+ def _detect(fh: BinaryIO) -> bool:
31
+ fh.seek(c_btrfs.BTRFS_SUPER_INFO_OFFSET)
32
+ block = fh.read(4096)
33
+ magic = int.from_bytes(block[64:72], "little")
34
+
35
+ return magic == c_btrfs.BTRFS_MAGIC
36
+
37
+ @staticmethod
38
+ def _detect_id(fh: BinaryIO) -> Optional[bytes]:
39
+ # First field is csum, followed by fsid
40
+ fh.seek(c_btrfs.BTRFS_SUPER_INFO_OFFSET + c_btrfs.BTRFS_CSUM_SIZE)
41
+ return fh.read(c_btrfs.BTRFS_FSID_SIZE)
42
+
43
+ def iter_subfs(self) -> Iterator[BtrfsSubvolumeFilesystem]:
44
+ for subvol in self.btrfs.subvolumes():
45
+ if subvol.objectid == self.subfs.subvolume.objectid:
46
+ # Skip the default volume as it's already opened by the main filesystem
47
+ continue
48
+ yield self.open_subvolume(subvolid=subvol.objectid)
49
+
50
+ def open_subvolume(self, subvol: Optional[str] = None, subvolid: Optional[int] = None) -> BtrfsSubvolumeFilesystem:
51
+ return BtrfsSubvolumeFilesystem(self, subvol, subvolid)
52
+
53
+ def get(self, path: str) -> FilesystemEntry:
54
+ return self.subfs.get(path)
55
+
56
+
57
+ class BtrfsSubvolumeFilesystem(Filesystem):
58
+ __fstype__ = "btrfs"
59
+
60
+ def __init__(self, fs: BtrfsFilesystem, subvol: Optional[str] = None, subvolid: Optional[int] = None):
61
+ super().__init__(fs.volume, alt_separator=fs.alt_separator, case_sensitive=fs.case_sensitive)
62
+ if subvol is not None and subvolid is not None:
63
+ raise ValueError("Only one of subvol or subvolid is allowed")
64
+
65
+ self.fs = fs
66
+ self.btrfs = fs.btrfs
67
+ if subvol:
68
+ self.subvolume = self.btrfs.find_subvolume(subvol)
69
+ elif subvolid:
70
+ self.subvolume = self.btrfs.open_subvolume(subvolid)
71
+ else:
72
+ self.subvolume = self.btrfs.default_subvolume
73
+
74
+ def get(self, path: str) -> FilesystemEntry:
75
+ return BtrfsFilesystemEntry(self, path, self._get_node(path))
76
+
77
+ def _get_node(self, path: str, node: Optional[btrfs.INode] = None) -> btrfs.INode:
78
+ try:
79
+ return self.subvolume.get(path, node)
80
+ except btrfs.FileNotFoundError as e:
81
+ raise FileNotFoundError(path, cause=e)
82
+ except btrfs.NotADirectoryError as e:
83
+ raise NotADirectoryError(path, cause=e)
84
+ except btrfs.NotASymlinkError as e:
85
+ raise NotASymlinkError(path, cause=e)
86
+ except btrfs.Error as e:
87
+ raise FileNotFoundError(path, cause=e)
88
+
89
+
90
+ class BtrfsFilesystemEntry(FilesystemEntry):
91
+ fs: BtrfsFilesystem
92
+ entry: btrfs.INode
93
+
94
+ def get(self, path: str) -> FilesystemEntry:
95
+ entry_path = fsutil.join(self.path, path, alt_separator=self.fs.alt_separator)
96
+ entry = self.fs._get_node(path, self.entry)
97
+ return BtrfsFilesystemEntry(self.fs, entry_path, entry)
98
+
99
+ def open(self) -> BinaryIO:
100
+ if self.is_dir():
101
+ raise IsADirectoryError(self.path)
102
+ return self._resolve().entry.open()
103
+
104
+ def _iterdir(self) -> Iterator[btrfs.INode]:
105
+ if not self.is_dir():
106
+ raise NotADirectoryError(self.path)
107
+
108
+ if self.is_symlink():
109
+ for entry in self.readlink_ext().iterdir():
110
+ yield entry
111
+ else:
112
+ for name, entry in self.entry.iterdir():
113
+ if name in (".", ".."):
114
+ continue
115
+
116
+ yield name, entry
117
+
118
+ def iterdir(self) -> Iterator[str]:
119
+ for name, _ in self._iterdir():
120
+ yield name
121
+
122
+ def scandir(self) -> Iterator[FilesystemEntry]:
123
+ for name, entry in self._iterdir():
124
+ entry_path = fsutil.join(self.path, name, alt_separator=self.fs.alt_separator)
125
+ yield BtrfsFilesystemEntry(self.fs, entry_path, entry)
126
+
127
+ def is_dir(self, follow_symlinks: bool = True) -> bool:
128
+ try:
129
+ return self._resolve(follow_symlinks=follow_symlinks).entry.is_dir()
130
+ except FilesystemError:
131
+ return False
132
+
133
+ def is_file(self, follow_symlinks: bool = True) -> bool:
134
+ try:
135
+ return self._resolve(follow_symlinks=follow_symlinks).entry.is_file()
136
+ except FilesystemError:
137
+ return False
138
+
139
+ def is_symlink(self) -> bool:
140
+ return self.entry.is_symlink()
141
+
142
+ def readlink(self) -> str:
143
+ if not self.is_symlink():
144
+ raise NotASymlinkError()
145
+
146
+ return self.entry.link
147
+
148
+ def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
149
+ return self._resolve(follow_symlinks=follow_symlinks).lstat()
150
+
151
+ def lstat(self) -> fsutil.stat_result:
152
+ entry = self.entry
153
+ node = self.entry.inode
154
+
155
+ # mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime
156
+ st_info = st_info = fsutil.stat_result(
157
+ [
158
+ entry.mode,
159
+ entry.inum,
160
+ 0,
161
+ node.nlink,
162
+ entry.uid,
163
+ entry.gid,
164
+ entry.size,
165
+ # timestamp() returns a float which will fill both the integer and float fields
166
+ entry.atime.timestamp(),
167
+ entry.mtime.timestamp(),
168
+ entry.ctime.timestamp(),
169
+ ]
170
+ )
171
+
172
+ # Set the nanosecond resolution separately
173
+ st_info.st_atime_ns = entry.atime_ns
174
+ st_info.st_mtime_ns = entry.mtime_ns
175
+ st_info.st_ctime_ns = entry.ctime_ns
176
+
177
+ # Btrfs has a birth time, called otime
178
+ st_info.st_birthtime = entry.otime.timestamp()
179
+
180
+ return st_info
@@ -4,7 +4,7 @@ import urllib.parse
4
4
  from datetime import datetime, timezone, tzinfo
5
5
  from enum import Enum
6
6
  from pathlib import Path
7
- from typing import BinaryIO, Iterator, Union
7
+ from typing import BinaryIO, Callable, Iterator, Optional, Union
8
8
 
9
9
  from dissect.util.ts import from_unix
10
10
 
@@ -17,7 +17,7 @@ class StrEnum(str, Enum):
17
17
  """Sortable and serializible string-based enum"""
18
18
 
19
19
 
20
- def list_to_frozen_set(function):
20
+ def list_to_frozen_set(function: Callable) -> Callable:
21
21
  def wrapper(*args):
22
22
  args = [frozenset(x) if isinstance(x, list) else x for x in args]
23
23
  return function(*args)
@@ -25,7 +25,7 @@ def list_to_frozen_set(function):
25
25
  return wrapper
26
26
 
27
27
 
28
- def parse_path_uri(path):
28
+ def parse_path_uri(path: Path) -> tuple[Optional[str], Optional[str], Optional[str]]:
29
29
  if path is None:
30
30
  return None, None, None
31
31
  parsed_path = urllib.parse.urlparse(str(path))
@@ -33,6 +33,17 @@ def parse_path_uri(path):
33
33
  return parsed_path.scheme, parsed_path.path, parsed_query
34
34
 
35
35
 
36
+ def parse_options_string(options: str) -> dict[str, Union[str, bool]]:
37
+ result = {}
38
+ for opt in options.split(","):
39
+ if "=" in opt:
40
+ key, _, value = opt.partition("=")
41
+ result[key] = value
42
+ else:
43
+ result[opt] = True
44
+ return result
45
+
46
+
36
47
  SLUG_RE = re.compile(r"[/\\ ]")
37
48
 
38
49
 
@@ -199,10 +199,9 @@ class SymantecPlugin(Plugin):
199
199
 
200
200
  def check_compatible(self) -> None:
201
201
  for log_file in self.LOGS:
202
- if self.target.fs.glob(log_file):
203
- break
204
- else:
205
- raise UnsupportedPluginError("No Symantec SEP logs found")
202
+ if list(self.target.fs.glob(log_file)):
203
+ return
204
+ raise UnsupportedPluginError("No Symantec SEP logs found")
206
205
 
207
206
  def _fw_cell(self, line: list, cell_id: int) -> str:
208
207
  return line[cell_id].decode("utf-8")
@@ -11,6 +11,7 @@ from flow.record.fieldtypes import posix_path
11
11
  from dissect.target.filesystem import Filesystem
12
12
  from dissect.target.helpers.fsutil import TargetPath
13
13
  from dissect.target.helpers.record import UnixUserRecord
14
+ from dissect.target.helpers.utils import parse_options_string
14
15
  from dissect.target.plugin import OperatingSystem, OSPlugin, arg, export
15
16
  from dissect.target.target import Target
16
17
 
@@ -177,33 +178,41 @@ class UnixPlugin(OSPlugin):
177
178
  def _add_mounts(self) -> None:
178
179
  fstab = self.target.fs.path("/etc/fstab")
179
180
 
180
- volumes_to_mount = [v for v in self.target.volumes if v.fs]
181
-
182
- for dev_id, volume_name, _, mount_point in parse_fstab(fstab, self.target.log):
183
- for volume in volumes_to_mount:
181
+ for dev_id, volume_name, mount_point, _, options in parse_fstab(fstab, self.target.log):
182
+ opts = parse_options_string(options)
183
+ subvol = opts.get("subvol", None)
184
+ subvolid = opts.get("subvolid", None)
185
+ for fs in self.target.filesystems:
184
186
  fs_id = None
187
+ fs_subvol = None
188
+ fs_subvolid = None
189
+ fs_volume_name = fs.volume.name if fs.volume and not isinstance(fs.volume, list) else None
185
190
  last_mount = None
186
191
 
187
192
  if dev_id:
188
- if volume.fs.__type__ == "xfs":
189
- fs_id = volume.fs.xfs.uuid
190
- elif volume.fs.__type__ == "ext":
191
- fs_id = volume.fs.extfs.uuid
192
- last_mount = volume.fs.extfs.last_mount
193
- elif volume.fs.__type__ == "fat":
194
- fs_id = volume.fs.fatfs.volume_id
193
+ if fs.__type__ == "xfs":
194
+ fs_id = fs.xfs.uuid
195
+ elif fs.__type__ == "ext":
196
+ fs_id = fs.extfs.uuid
197
+ last_mount = fs.extfs.last_mount
198
+ elif fs.__type__ == "btrfs":
199
+ fs_id = fs.btrfs.uuid
200
+ fs_subvol = fs.subvolume.path
201
+ fs_subvolid = fs.subvolume.objectid
202
+ elif fs.__type__ == "fat":
203
+ fs_id = fs.fatfs.volume_id
195
204
  # This normalizes fs_id to comply with libblkid generated UUIDs
196
205
  # This is needed because FAT filesystems don't have a real UUID,
197
206
  # but instead a volume_id which is not case-sensitive
198
207
  fs_id = fs_id[:4].upper() + "-" + fs_id[4:].upper()
199
208
 
200
209
  if (
201
- (fs_id and (fs_id == dev_id))
202
- or (volume.name and (volume.name == volume_name))
210
+ (fs_id and (fs_id == dev_id and (subvol == fs_subvol or subvolid == fs_subvolid)))
211
+ or (fs_volume_name and (fs_volume_name == volume_name))
203
212
  or (last_mount and (last_mount == mount_point))
204
213
  ):
205
- self.target.log.debug("Mounting %s at %s", volume, mount_point)
206
- self.target.fs.mount(mount_point, volume.fs)
214
+ self.target.log.debug("Mounting %s (%s) at %s", fs, fs.volume, mount_point)
215
+ self.target.fs.mount(mount_point, fs)
207
216
 
208
217
  def _parse_os_release(self, glob: Optional[str] = None) -> dict[str, str]:
209
218
  """Parse files containing Unix version information.
@@ -283,7 +292,7 @@ class UnixPlugin(OSPlugin):
283
292
  def parse_fstab(
284
293
  fstab: TargetPath,
285
294
  log: logging.Logger = log,
286
- ) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str]]:
295
+ ) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str, str]]:
287
296
  """Parse fstab file and return a generator that streams the details of entries,
288
297
  with unsupported FS types and block devices filtered away.
289
298
  """
@@ -310,7 +319,7 @@ def parse_fstab(
310
319
  if len(entry_parts) != 6:
311
320
  continue
312
321
 
313
- dev, mount_point, fs_type, _, _, _ = entry_parts
322
+ dev, mount_point, fs_type, options, _, _ = entry_parts
314
323
 
315
324
  if fs_type in SKIP_FS_TYPES:
316
325
  log.warning("Skipped FS type: %s, %s, %s", fs_type, dev, mount_point)
@@ -341,4 +350,4 @@ def parse_fstab(
341
350
  except ValueError:
342
351
  pass
343
352
 
344
- yield dev_id, volume_name, fs_type, mount_point
353
+ yield dev_id, volume_name, mount_point, fs_type, options
dissect/target/target.py CHANGED
@@ -155,6 +155,7 @@ class Target:
155
155
  """Resolve all disks, volumes and filesystems and load an operating system on the current ``Target``."""
156
156
  self.disks.apply()
157
157
  self.volumes.apply()
158
+ self.filesystems.apply()
158
159
  self._init_os()
159
160
  self._applied = True
160
161
 
@@ -712,19 +713,12 @@ class DiskCollection(Collection[container.Container]):
712
713
 
713
714
 
714
715
  class VolumeCollection(Collection[volume.Volume]):
715
- def open(self, vol: volume.Volume) -> None:
716
- try:
717
- if not hasattr(vol, "fs") or vol.fs is None:
718
- vol.fs = filesystem.open(vol)
719
- self.target.log.debug("Opened filesystem: %s on %s", vol.fs, vol)
720
- self.target.filesystems.add(vol.fs)
721
- except FilesystemError as e:
722
- self.target.log.warning("Can't identify filesystem: %s", vol)
723
- self.target.log.debug("", exc_info=e)
724
-
725
716
  def apply(self) -> None:
726
717
  # We don't want later additions to modify the todo, so make a copy
727
718
  todo = self.entries[:]
719
+ fs_volumes = []
720
+ lvm_volumes = []
721
+ encrypted_volumes = []
728
722
 
729
723
  while todo:
730
724
  new_volumes = []
@@ -751,19 +745,19 @@ class VolumeCollection(Collection[volume.Volume]):
751
745
  if vol.offset == 0 and vol.vs and vol.vs.__type__ == "disk":
752
746
  # We are going to re-open a volume system on itself, bail out
753
747
  self.target.log.info("Found volume with offset 0, opening as raw volume instead")
754
- self.open(vol)
748
+ fs_volumes.append(vol)
755
749
  continue
756
750
 
757
751
  try:
758
752
  vs = volume.open(vol)
759
753
  except Exception:
760
754
  # If opening a volume system fails, there's likely none, so open as a filesystem instead
761
- self.open(vol)
755
+ fs_volumes.append(vol)
762
756
  continue
763
757
 
764
758
  if not len(vs.volumes):
765
759
  # We opened an empty volume system, discard
766
- self.open(vol)
760
+ fs_volumes.append(vol)
767
761
  continue
768
762
 
769
763
  self.entries.extend(vs.volumes)
@@ -786,20 +780,30 @@ class VolumeCollection(Collection[volume.Volume]):
786
780
 
787
781
  todo = new_volumes
788
782
 
789
- # ASDF - getting the correct starting system volume
790
- start_fs = None
791
- start_vol = None
792
- for idx, vol in enumerate(self.entries):
793
- if start_fs is None and (vol.name is None):
794
- start_fs = idx
783
+ mv_fs_volumes = []
784
+ for vol in fs_volumes:
785
+ try:
786
+ if getattr(vol, "fs", None) is None:
787
+ if filesystem.is_multi_volume_filesystem(vol):
788
+ mv_fs_volumes.append(vol)
789
+ else:
790
+ vol.fs = filesystem.open(vol)
791
+ self.target.log.debug("Opened filesystem: %s on %s", vol.fs, vol)
795
792
 
796
- if start_vol is None and start_fs is not None and (vol.name is not None and vol.fs is None):
797
- start_vol = idx
793
+ if getattr(vol, "fs", None) is not None:
794
+ self.target.filesystems.add(vol.fs)
795
+ except FilesystemError as e:
796
+ self.target.log.warning("Can't identify filesystem: %s", vol)
797
+ self.target.log.debug("", exc_info=e)
798
798
 
799
- if start_fs is not None and start_vol is not None and (vol.name is not None and vol.fs is None):
800
- rel_vol = idx - start_vol
801
- vol.fs = self.entries[start_fs + rel_vol].fs
799
+ for fs in filesystem.open_multi_volume(mv_fs_volumes):
800
+ self.target.filesystems.add(fs)
801
+ for vol in fs.volume:
802
+ vol.fs = fs
802
803
 
803
804
 
804
805
  class FilesystemCollection(Collection[filesystem.Filesystem]):
805
- pass
806
+ def apply(self) -> None:
807
+ for fs in self.entries:
808
+ for subfs in fs.iter_subfs():
809
+ self.add(subfs)
@@ -3,6 +3,7 @@ import logging
3
3
  from typing import Union
4
4
 
5
5
  from dissect.target import Target, filesystem
6
+ from dissect.target.helpers.utils import parse_options_string
6
7
  from dissect.target.tools.utils import (
7
8
  catch_sigpipe,
8
9
  configure_generic_arguments,
@@ -70,7 +71,7 @@ def main():
70
71
  vfs.mount(fname, fs)
71
72
 
72
73
  # This is kinda silly because fusepy will convert this back into string arguments
73
- options = _parse_options(args.options) if args.options else {}
74
+ options = parse_options_string(args.options) if args.options else {}
74
75
 
75
76
  options["nothreads"] = True
76
77
  options["allow_other"] = True
@@ -83,17 +84,6 @@ def main():
83
84
  parser.exit("FUSE error")
84
85
 
85
86
 
86
- def _parse_options(options: str) -> dict[str, Union[str, bool]]:
87
- result = {}
88
- for opt in options.split(","):
89
- if "=" in opt:
90
- key, _, value = opt.partition("=")
91
- result[key] = value
92
- else:
93
- result[opt] = True
94
- return result
95
-
96
-
97
87
  def _format_options(options: dict[str, Union[str, bool]]) -> str:
98
88
  return ",".join([key if value is True else f"{key}={value}" for key, value in options.items()])
99
89
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.14.dev8
3
+ Version: 3.14.dev10
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -38,6 +38,7 @@ Requires-Dist: dissect.target[full] ; extra == 'cb'
38
38
  Requires-Dist: carbon-black-cloud-sdk-python ~=1.4.3 ; extra == 'cb'
39
39
  Provides-Extra: full
40
40
  Requires-Dist: asn1crypto ; extra == 'full'
41
+ Requires-Dist: dissect.btrfs <2.0.dev,>=1.0.dev ; extra == 'full'
41
42
  Requires-Dist: dissect.cim <4.0.dev,>=3.0.dev ; extra == 'full'
42
43
  Requires-Dist: dissect.clfs <2.0.dev,>=1.0.dev ; extra == 'full'
43
44
  Requires-Dist: dissect.esedb <4.0.dev,>=3.0.dev ; extra == 'full'
@@ -1,11 +1,11 @@
1
1
  dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
2
2
  dissect/target/container.py,sha256=9ixufT1_0WhraqttBWwQjG80caToJqvCX8VjFk8d5F0,9307
3
3
  dissect/target/exceptions.py,sha256=VVW_Rq_vQinapz-2mbJ3UkxBEZpb2pE_7JlhMukdtrY,2877
4
- dissect/target/filesystem.py,sha256=iENm42dxq3H-AlI3LN5qFud14P0FcR5ANTGPS5LDhCI,50857
4
+ dissect/target/filesystem.py,sha256=_7hxYD34P5Dxnc7WZZsAHvQcF8KxGUJfGgc1gCyQZA8,53412
5
5
  dissect/target/loader.py,sha256=4ZdX-QJY83NPswTyNG31LUwYXdV1tuByrR2vKKg7d5k,7214
6
6
  dissect/target/plugin.py,sha256=7Gss9pofcWKemwwfeAJ7E6nmJSNnZkBkxTcxUY2wzmk,40526
7
7
  dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
8
- dissect/target/target.py,sha256=tvMIHh-lWhXK_7_PHCs6Bu-tJEopdjDtPR0WyAMayqM,31348
8
+ dissect/target/target.py,sha256=zARjjT1tSMW4opble1KfDIISt3pcxPa50cTwhWC_90c,31431
9
9
  dissect/target/volume.py,sha256=aQZAJiny8jjwkc9UtwIRwy7nINXjCxwpO-_UDfh6-BA,15801
10
10
  dissect/target/containers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  dissect/target/containers/asdf.py,sha256=DJp0QEFwUjy2MFwKYcYqIR_BS1fQT1Yi9Kcmqt0aChM,1366
@@ -22,6 +22,7 @@ dissect/target/containers/vmdk.py,sha256=5fQGkJy4esXONXrKLbhpkQDt8Fwx19YENK2mOm7
22
22
  dissect/target/data/autocompletion/target_bash_completion.sh,sha256=wrOQ_ED-h8WFcjCmY6n4qKl84tWJv9l8ShFHDfJqJyA,3592
23
23
  dissect/target/filesystems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  dissect/target/filesystems/ad1.py,sha256=nEPzaaRsb6bL4ItFo0uLdmdLvrmK9BjqHeD3FOp3WQI,2413
25
+ dissect/target/filesystems/btrfs.py,sha256=X8HUZJHzArpo04v_A42LM4SQIfQt07TZJJNsYiSvXOE,6247
25
26
  dissect/target/filesystems/cb.py,sha256=kFyZ7oFMkICSEGGFna2vhKnZx9KQKZ2ZSG_ckEWTIBE,5329
26
27
  dissect/target/filesystems/config.py,sha256=Xih61MoEup1rCwQu66SezmTDG4XLOn1hC8ECn_WxX7I,5949
27
28
  dissect/target/filesystems/dir.py,sha256=1RVT0-lomceRLQG5sXHpiyii4Vu7S1MGeQSyT-7BCPY,3851
@@ -56,7 +57,7 @@ dissect/target/helpers/regutil.py,sha256=kX-sSZbW8Qkg29Dn_9zYbaQrwLumrr4Y8zJ1EhH
56
57
  dissect/target/helpers/shell_folder_ids.py,sha256=Behhb8oh0kMxrEk6YYKYigCDZe8Hw5QS6iK_d2hTs2Y,24978
57
58
  dissect/target/helpers/ssh.py,sha256=LPssHXyfL8QYmLi2vpa3wElsGboLG_A1Y8kvOehpUr4,6338
58
59
  dissect/target/helpers/targetd.py,sha256=ELhUulzQ4OgXgHsWhsLgM14vut8Wm6btr7qTynlwKaE,1812
59
- dissect/target/helpers/utils.py,sha256=0v0HkVKzzYvUVMuMWF0rifXS-k9PomJU-DKAZTCiQzA,3636
60
+ dissect/target/helpers/utils.py,sha256=r36Bn0UL0E6Z8ajmQrHzC6RyUxTRdwJ1PNsd904Lmzs,4027
60
61
  dissect/target/helpers/data/windowsZones.xml,sha256=4OijeR7oxI0ZwPTSwCkmtcofOsUCjSnbZ4dQxVOM_4o,50005
61
62
  dissect/target/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
63
  dissect/target/loaders/ad1.py,sha256=k0bkY0L6NKuQBboua-jM_KgeyIcXAo59NjExDZHMy0o,572
@@ -96,7 +97,7 @@ dissect/target/plugins/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
96
97
  dissect/target/plugins/apps/av/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
98
  dissect/target/plugins/apps/av/mcafee.py,sha256=4lro9iwcL2Vl9Lyy69Sk1D9JWSRTXv5yjpV6NJbbZXE,5409
98
99
  dissect/target/plugins/apps/av/sophos.py,sha256=6a4N44VPwrXucGcTspwkCEhKPw5cfmqi-AQrXXDBKps,4140
99
- dissect/target/plugins/apps/av/symantec.py,sha256=MTs9wL0byS_GDq5JVbE4sWpoAOQMVUwxNSBvphXgdfw,14120
100
+ dissect/target/plugins/apps/av/symantec.py,sha256=RFLyNW6FyuoGcirJ4xHbQM8oGjua9W4zXmC7YDF-H20,14109
100
101
  dissect/target/plugins/apps/av/trendmicro.py,sha256=SDSFcjbabP3IORuVovoZNxTEpMw_rPxbm_JQ5Yg9xeQ,4649
101
102
  dissect/target/plugins/apps/browser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
103
  dissect/target/plugins/apps/browser/browser.py,sha256=LpGs4IXpO5t7A3QDxP3zv5Cb0DQXwh3h1vcBKAWT2HY,2187
@@ -159,7 +160,7 @@ dissect/target/plugins/general/scrape.py,sha256=Fz7BNXflvuxlnVulyyDhLpyU8D_hJdH6
159
160
  dissect/target/plugins/general/users.py,sha256=IOqopQ9Y7CKGkALRUr16y8DwxsidYC5tcPErGZCXxyA,2845
160
161
  dissect/target/plugins/os/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
161
162
  dissect/target/plugins/os/unix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
- dissect/target/plugins/os/unix/_os.py,sha256=7EMlRzSvziov2DnkTiFj07VzQLmgmop6JeTj0xolIKA,12840
163
+ dissect/target/plugins/os/unix/_os.py,sha256=33xAyHYG5vqpMupoIk_vbwsxAXBBRV1hqwkOOU5Gvg8,13409
163
164
  dissect/target/plugins/os/unix/cronjobs.py,sha256=tVRnUxN0w2IqhOKs68hNeovRUu_ag0W35j9PziPprgQ,3508
164
165
  dissect/target/plugins/os/unix/datetime.py,sha256=gKfBdPyUirt3qmVYfOJ1oZXRPn8wRzssbZxR_ARrtk8,1518
165
166
  dissect/target/plugins/os/unix/etc.py,sha256=HoPEC1hxqurSnAXQAK-jf_HxdBIDe-1z_qSw_n-ViI4,258
@@ -286,7 +287,7 @@ dissect/target/tools/dd.py,sha256=Nlh2CFOCV0ksxyedFp7BuyoQ3tBFi6rK6UO0_k5GR_8,17
286
287
  dissect/target/tools/fs.py,sha256=IL71ntXA_oS92l0NPpqyOVrHOZ-bf3qag1amZzAaeHc,3548
287
288
  dissect/target/tools/info.py,sha256=LcR0WLBo2w0QiOmM0llQfcF0a_hfrqImbJ_QNW1mtbo,5174
288
289
  dissect/target/tools/logging.py,sha256=5ZnumtMWLyslxfrUGZ4ntRyf3obOOhmn8SBjKfdLcEg,4174
289
- dissect/target/tools/mount.py,sha256=jZn5U4CqabxDSu8q8tGggHvZl7eLUateywFJDpcpkAU,2796
290
+ dissect/target/tools/mount.py,sha256=m6Ise8H82jgIW2FN0hXKO4l9t3emKiOi55O4LyEqvxk,2581
290
291
  dissect/target/tools/query.py,sha256=qbQI2kAeFP0_1CxT3UbTIZZ1EZIhotD0rRNXqihZcy4,14926
291
292
  dissect/target/tools/reg.py,sha256=ZB5WDmKfiDvs988kHZVyzF-RIoDLXcXuvdLjuEYvDi4,2181
292
293
  dissect/target/tools/shell.py,sha256=zgabhrXMBFA36g7Rc-8VkF5BL6xc4148vblYnjfI88I,42281
@@ -303,10 +304,10 @@ dissect/target/volumes/luks.py,sha256=v_mHW05KM5iG8JDe47i2V4Q9O0r4rnAMA9m_qc9cYw
303
304
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
304
305
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
305
306
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
306
- dissect.target-3.14.dev8.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
307
- dissect.target-3.14.dev8.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
308
- dissect.target-3.14.dev8.dist-info/METADATA,sha256=yZF0uFs7bUJukmL46OHuJvl7HliEDAszKA4x3O-2iRg,10975
309
- dissect.target-3.14.dev8.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
310
- dissect.target-3.14.dev8.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
311
- dissect.target-3.14.dev8.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
312
- dissect.target-3.14.dev8.dist-info/RECORD,,
307
+ dissect.target-3.14.dev10.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
308
+ dissect.target-3.14.dev10.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
309
+ dissect.target-3.14.dev10.dist-info/METADATA,sha256=k9I3m8_nSD3iQchZl7cyxC4H3_Hu6EFQAoszbRjdevg,11042
310
+ dissect.target-3.14.dev10.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
311
+ dissect.target-3.14.dev10.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
312
+ dissect.target-3.14.dev10.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
313
+ dissect.target-3.14.dev10.dist-info/RECORD,,