dissect.target 3.13.dev26__py3-none-any.whl → 3.14__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. dissect/target/container.py +9 -1
  2. dissect/target/containers/asdf.py +2 -0
  3. dissect/target/containers/ewf.py +2 -0
  4. dissect/target/containers/hdd.py +2 -0
  5. dissect/target/containers/hds.py +2 -0
  6. dissect/target/containers/qcow2.py +2 -0
  7. dissect/target/containers/raw.py +2 -0
  8. dissect/target/containers/split.py +2 -0
  9. dissect/target/containers/vdi.py +2 -0
  10. dissect/target/containers/vhd.py +2 -0
  11. dissect/target/containers/vhdx.py +2 -0
  12. dissect/target/containers/vmdk.py +2 -0
  13. dissect/target/filesystem.py +108 -15
  14. dissect/target/filesystems/ad1.py +1 -1
  15. dissect/target/filesystems/btrfs.py +180 -0
  16. dissect/target/filesystems/cb.py +4 -4
  17. dissect/target/filesystems/config.py +161 -31
  18. dissect/target/filesystems/dir.py +1 -1
  19. dissect/target/filesystems/exfat.py +1 -1
  20. dissect/target/filesystems/extfs.py +5 -1
  21. dissect/target/filesystems/fat.py +1 -1
  22. dissect/target/filesystems/ffs.py +1 -1
  23. dissect/target/filesystems/itunes.py +1 -1
  24. dissect/target/filesystems/ntfs.py +1 -1
  25. dissect/target/filesystems/smb.py +1 -1
  26. dissect/target/filesystems/squashfs.py +1 -1
  27. dissect/target/filesystems/tar.py +1 -1
  28. dissect/target/filesystems/vmfs.py +1 -1
  29. dissect/target/filesystems/xfs.py +1 -1
  30. dissect/target/filesystems/zip.py +1 -1
  31. dissect/target/helpers/cache.py +2 -2
  32. dissect/target/helpers/configutil.py +283 -83
  33. dissect/target/helpers/fsutil.py +9 -6
  34. dissect/target/helpers/hashutil.py +20 -19
  35. dissect/target/helpers/utils.py +14 -3
  36. dissect/target/loaders/ad1.py +1 -1
  37. dissect/target/loaders/asdf.py +1 -1
  38. dissect/target/loaders/log.py +2 -2
  39. dissect/target/loaders/smb.py +23 -13
  40. dissect/target/loaders/targetd.py +12 -2
  41. dissect/target/loaders/vma.py +1 -1
  42. dissect/target/loaders/xva.py +1 -1
  43. dissect/target/plugin.py +14 -2
  44. dissect/target/plugins/apps/av/sophos.py +1 -2
  45. dissect/target/plugins/apps/av/symantec.py +3 -4
  46. dissect/target/plugins/apps/av/trendmicro.py +2 -3
  47. dissect/target/plugins/{browsers → apps/browser}/chrome.py +6 -3
  48. dissect/target/plugins/{browsers → apps/browser}/chromium.py +18 -13
  49. dissect/target/plugins/{browsers → apps/browser}/edge.py +6 -3
  50. dissect/target/plugins/{browsers → apps/browser}/firefox.py +3 -7
  51. dissect/target/plugins/{browsers → apps/browser}/iexplore.py +14 -4
  52. dissect/target/plugins/apps/remoteaccess/teamviewer.py +55 -27
  53. dissect/target/plugins/apps/ssh/opensshd.py +31 -30
  54. dissect/target/plugins/apps/{webservers → webserver}/apache.py +1 -1
  55. dissect/target/plugins/apps/{webservers → webserver}/caddy.py +1 -1
  56. dissect/target/plugins/apps/{webservers → webserver}/iis.py +1 -1
  57. dissect/target/plugins/apps/{webservers → webserver}/nginx.py +1 -1
  58. dissect/target/plugins/child/hyperv.py +1 -2
  59. dissect/target/plugins/child/vmware_workstation.py +1 -3
  60. dissect/target/plugins/filesystem/acquire_handles.py +2 -0
  61. dissect/target/plugins/filesystem/acquire_hash.py +1 -7
  62. dissect/target/plugins/filesystem/icat.py +5 -5
  63. dissect/target/plugins/filesystem/ntfs/mft.py +2 -2
  64. dissect/target/plugins/filesystem/ntfs/mft_timeline.py +2 -2
  65. dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -3
  66. dissect/target/plugins/filesystem/resolver.py +1 -1
  67. dissect/target/plugins/filesystem/unix/capability.py +77 -66
  68. dissect/target/plugins/filesystem/walkfs.py +25 -19
  69. dissect/target/plugins/filesystem/yara.py +20 -19
  70. dissect/target/plugins/general/config.py +28 -11
  71. dissect/target/plugins/os/unix/_os.py +28 -21
  72. dissect/target/plugins/os/unix/bsd/osx/user.py +1 -3
  73. dissect/target/plugins/os/unix/cronjobs.py +4 -16
  74. dissect/target/plugins/os/unix/{linux/esxi → esxi}/_os.py +5 -6
  75. dissect/target/plugins/os/unix/generic.py +5 -1
  76. dissect/target/plugins/os/unix/history.py +2 -1
  77. dissect/target/plugins/os/unix/linux/_os.py +12 -5
  78. dissect/target/plugins/os/unix/linux/services.py +112 -0
  79. dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -4
  80. dissect/target/plugins/os/unix/locale.py +3 -1
  81. dissect/target/plugins/os/unix/log/journal.py +7 -6
  82. dissect/target/plugins/os/unix/packagemanager.py +3 -3
  83. dissect/target/plugins/os/unix/shadow.py +1 -1
  84. dissect/target/plugins/os/windows/_os.py +2 -1
  85. dissect/target/plugins/os/windows/amcache.py +9 -10
  86. dissect/target/plugins/os/windows/catroot.py +2 -2
  87. dissect/target/plugins/os/windows/cim.py +5 -4
  88. dissect/target/plugins/os/windows/datetime.py +4 -1
  89. dissect/target/plugins/os/windows/defender.py +3 -3
  90. dissect/target/plugins/os/windows/generic.py +10 -11
  91. dissect/target/plugins/os/windows/lnk.py +6 -6
  92. dissect/target/plugins/os/windows/log/amcache.py +3 -5
  93. dissect/target/plugins/os/windows/log/pfro.py +1 -3
  94. dissect/target/plugins/os/windows/prefetch.py +5 -6
  95. dissect/target/plugins/os/windows/recyclebin.py +3 -4
  96. dissect/target/plugins/os/windows/regf/7zip.py +2 -4
  97. dissect/target/plugins/os/windows/regf/bam.py +1 -2
  98. dissect/target/plugins/os/windows/regf/cit.py +4 -5
  99. dissect/target/plugins/os/windows/regf/mru.py +6 -2
  100. dissect/target/plugins/os/windows/regf/muicache.py +1 -3
  101. dissect/target/plugins/os/windows/regf/recentfilecache.py +1 -2
  102. dissect/target/plugins/os/windows/regf/shimcache.py +1 -2
  103. dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
  104. dissect/target/plugins/os/windows/regf/userassist.py +1 -2
  105. dissect/target/plugins/os/windows/services.py +2 -4
  106. dissect/target/plugins/os/windows/sru.py +4 -4
  107. dissect/target/plugins/os/windows/startupinfo.py +5 -6
  108. dissect/target/plugins/os/windows/syscache.py +2 -3
  109. dissect/target/target.py +65 -32
  110. dissect/target/tools/info.py +2 -1
  111. dissect/target/tools/mount.py +2 -12
  112. dissect/target/tools/shell.py +3 -2
  113. dissect/target/volume.py +10 -9
  114. dissect/target/volumes/bde.py +1 -1
  115. dissect/target/volumes/ddf.py +2 -0
  116. dissect/target/volumes/disk.py +2 -0
  117. dissect/target/volumes/luks.py +1 -1
  118. dissect/target/volumes/lvm.py +2 -0
  119. dissect/target/volumes/md.py +2 -0
  120. dissect/target/volumes/vmfs.py +2 -0
  121. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/METADATA +2 -1
  122. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/RECORD +137 -136
  123. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/WHEEL +1 -1
  124. dissect/target/plugins/os/unix/services.py +0 -151
  125. /dissect/target/plugins/apps/{containers → browser}/__init__.py +0 -0
  126. /dissect/target/plugins/{browsers → apps/browser}/browser.py +0 -0
  127. /dissect/target/plugins/apps/{vpns → container}/__init__.py +0 -0
  128. /dissect/target/plugins/apps/{containers → container}/docker.py +0 -0
  129. /dissect/target/plugins/apps/{webservers → vpn}/__init__.py +0 -0
  130. /dissect/target/plugins/apps/{vpns → vpn}/openvpn.py +0 -0
  131. /dissect/target/plugins/apps/{vpns → vpn}/wireguard.py +0 -0
  132. /dissect/target/plugins/{browsers → apps/webserver}/__init__.py +0 -0
  133. /dissect/target/plugins/apps/{webservers/webservers.py → webserver/webserver.py} +0 -0
  134. /dissect/target/plugins/os/unix/{linux/esxi → esxi}/__init__.py +0 -0
  135. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/COPYRIGHT +0 -0
  136. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/LICENSE +0 -0
  137. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/entry_points.txt +0 -0
  138. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/top_level.txt +0 -0
@@ -34,6 +34,11 @@ class Container(io.IOBase):
34
34
  vs: An optional shorthand to set the underlying volume system, usually set later.
35
35
  """
36
36
 
37
+ # Due to lazy importing we generally can't use isinstance(), so we add a short identifying string to each class
38
+ # This has the added benefit of having a readily available "pretty name" for each implementation
39
+ __type__: str = None
40
+ """A short string identifying the type of container."""
41
+
37
42
  def __init__(self, fh: Union[BinaryIO, Path], size: int, vs: VolumeSystem = None):
38
43
  self.fh = fh
39
44
  self.size = size
@@ -41,7 +46,10 @@ class Container(io.IOBase):
41
46
  # Shorthand access to vs
42
47
  self.vs = vs
43
48
 
44
- def __repr__(self):
49
+ if self.__type__ is None:
50
+ raise NotImplementedError(f"{self.__class__.__name__} must define __type__")
51
+
52
+ def __repr__(self) -> str:
45
53
  return f"<{self.__class__.__name__} size={self.size} vs={self.vs}>"
46
54
 
47
55
  @classmethod
@@ -9,6 +9,8 @@ from dissect.target.container import Container
9
9
 
10
10
 
11
11
  class AsdfContainer(Container):
12
+ __type__ = "asdf"
13
+
12
14
  def __init__(self, fh: BinaryIO, *args, **kwargs):
13
15
  file_container = fh
14
16
 
@@ -12,6 +12,8 @@ from dissect.target.container import Container
12
12
  class EwfContainer(Container):
13
13
  """Expert Witness Disk Image Format"""
14
14
 
15
+ __type__ = "ewf"
16
+
15
17
  def __init__(self, fh: Union[list, BinaryIO, Path], *args, **kwargs):
16
18
  fhs = [fh] if not isinstance(fh, list) else fh
17
19
  if hasattr(fhs[0], "read"):
@@ -8,6 +8,8 @@ from dissect.target.container import Container
8
8
 
9
9
 
10
10
  class HddContainer(Container):
11
+ __type__ = "hdd"
12
+
11
13
  def __init__(self, fh: Path, *args, **kwargs):
12
14
  if hasattr(fh, "read"):
13
15
  raise TypeError("HddContainer can only be opened by path")
@@ -9,6 +9,8 @@ from dissect.target.container import Container
9
9
 
10
10
 
11
11
  class HdsContainer(Container):
12
+ __type__ = "hds"
13
+
12
14
  def __init__(self, fh: Union[BinaryIO, Path], *args, **kwargs):
13
15
  f = fh
14
16
  if not hasattr(fh, "read"):
@@ -8,6 +8,8 @@ from dissect.target.container import Container
8
8
 
9
9
 
10
10
  class QCow2Container(Container):
11
+ __type__ = "qcow2"
12
+
11
13
  def __init__(self, fh: Union[BinaryIO, Path], data_file=None, backing_file=None, *args, **kwargs):
12
14
  f = fh
13
15
  if not hasattr(fh, "read"):
@@ -8,6 +8,8 @@ from dissect.target.container import Container
8
8
 
9
9
 
10
10
  class RawContainer(Container):
11
+ __type__ = "raw"
12
+
11
13
  def __init__(self, fh: Union[BinaryIO, Path], *args, **kwargs):
12
14
  if not hasattr(fh, "read"):
13
15
  fh = fh.open("rb")
@@ -12,6 +12,8 @@ def find_files(path: Path) -> List[Path]:
12
12
 
13
13
 
14
14
  class SplitContainer(Container):
15
+ __type__ = "split"
16
+
15
17
  def __init__(self, fh: Union[list, BinaryIO, Path], *args, **kwargs):
16
18
  self._fhs = []
17
19
  self.offsets = [0]
@@ -10,6 +10,8 @@ from dissect.target.container import Container
10
10
  class VdiContainer(Container):
11
11
  """VirtualBox hard disks"""
12
12
 
13
+ __type__ = "vdi"
14
+
13
15
  def __init__(self, fh: Union[BinaryIO, Path], *args, **kwargs):
14
16
  f = fh
15
17
  if not hasattr(fh, "read"):
@@ -8,6 +8,8 @@ from dissect.target.container import Container
8
8
 
9
9
 
10
10
  class VhdContainer(Container):
11
+ __type__ = "vhd"
12
+
11
13
  def __init__(self, fh: Union[BinaryIO, Path], *args, **kwargs):
12
14
  f = fh
13
15
  if not hasattr(fh, "read"):
@@ -8,6 +8,8 @@ from dissect.target.container import Container
8
8
 
9
9
 
10
10
  class VhdxContainer(Container):
11
+ __type__ = "vhdx"
12
+
11
13
  def __init__(self, fh: Union[BinaryIO, Path], *args, **kwargs):
12
14
  self.vhdx = vhdx.VHDX(fh)
13
15
  super().__init__(fh, self.vhdx.size, *args, **kwargs)
@@ -10,6 +10,8 @@ from dissect.target.container import Container
10
10
  class VmdkContainer(Container):
11
11
  """VMWare hard disks"""
12
12
 
13
+ __type__ = "vmdk"
14
+
13
15
  def __init__(self, fh: Union[BinaryIO, Path], *args, **kwargs):
14
16
  self.vmdk = vmdk.VMDK(fh)
15
17
  super().__init__(fh, self.vmdk.size, *args, **kwargs)
@@ -5,6 +5,7 @@ import io
5
5
  import logging
6
6
  import os
7
7
  import stat
8
+ import warnings
8
9
  from collections import defaultdict
9
10
  from typing import (
10
11
  TYPE_CHECKING,
@@ -40,12 +41,16 @@ log = logging.getLogger(__name__)
40
41
  class Filesystem:
41
42
  """Base class for filesystems."""
42
43
 
43
- __fstype__: str = None
44
- """Defines the type of filesystem it is."""
44
+ # Due to lazy importing we generally can't use isinstance(), so we add a short identifying string to each class
45
+ # This has the added benefit of having a readily available "pretty name" for each implementation
46
+ __type__: str = None
47
+ """A short string identifying the type of filesystem."""
48
+ __multi_volume__: bool = False
49
+ """Whether this filesystem supports multiple volumes (disks)."""
45
50
 
46
51
  def __init__(
47
52
  self,
48
- volume: Optional[BinaryIO] = None,
53
+ volume: Optional[Union[BinaryIO, list[BinaryIO]]] = None,
49
54
  alt_separator: str = "",
50
55
  case_sensitive: bool = True,
51
56
  ) -> None:
@@ -53,23 +58,33 @@ class Filesystem:
53
58
 
54
59
  Args:
55
60
  volume: A volume or other file-like object associated with the filesystem.
56
- 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.
57
62
  alt_separator: The alternative separator used to distingish between directories in a path.
58
63
 
59
64
  Raises:
60
- NotImplementedError: When the internal ``__fstype__`` of the class is not defined.
65
+ NotImplementedError: When the internal ``__type__`` of the class is not defined.
61
66
  """
62
67
  self.volume = volume
63
68
  self.case_sensitive = case_sensitive
64
69
  self.alt_separator = alt_separator
65
- if self.__fstype__ is None:
66
- raise NotImplementedError(f"{self.__class__.__name__} must define __fstype__")
70
+
71
+ if self.__type__ is None:
72
+ raise NotImplementedError(f"{self.__class__.__name__} must define __type__")
67
73
 
68
74
  def __repr__(self) -> str:
69
75
  return f"<{self.__class__.__name__}>"
70
76
 
77
+ @classmethod
78
+ @property
79
+ def __fstype__(cls) -> str:
80
+ warnings.warn(
81
+ "The __fstype__ attribute is deprecated and will be removed in dissect.target 3.15. Use __type__ instead",
82
+ category=DeprecationWarning,
83
+ )
84
+ return cls.__type__
85
+
71
86
  def path(self, *args) -> fsutil.TargetPath:
72
- """Get a specific path from the filesystem."""
87
+ """Instantiate a new path-like object on this filesystem."""
73
88
  return fsutil.TargetPath(self, *args)
74
89
 
75
90
  @classmethod
@@ -91,7 +106,7 @@ class Filesystem:
91
106
  except NotImplementedError:
92
107
  raise
93
108
  except Exception as e:
94
- log.warning("Failed to detect %s filesystem", cls.__fstype__)
109
+ log.warning("Failed to detect %s filesystem", cls.__type__)
95
110
  log.debug("", exc_info=e)
96
111
  finally:
97
112
  fh.seek(offset)
@@ -112,6 +127,52 @@ class Filesystem:
112
127
  """
113
128
  raise NotImplementedError()
114
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.__type__)
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
+
115
176
  def get(self, path: str) -> FilesystemEntry:
116
177
  """Retrieve a :class:`FilesystemEntry` from the filesystem.
117
178
 
@@ -1028,7 +1089,7 @@ class VirtualSymlink(FilesystemEntry):
1028
1089
 
1029
1090
 
1030
1091
  class VirtualFilesystem(Filesystem):
1031
- __fstype__ = "virtual"
1092
+ __type__ = "virtual"
1032
1093
 
1033
1094
  def __init__(self, **kwargs):
1034
1095
  super().__init__(None, **kwargs)
@@ -1184,7 +1245,7 @@ class VirtualFilesystem(Filesystem):
1184
1245
 
1185
1246
 
1186
1247
  class RootFilesystem(Filesystem):
1187
- __fstype__ = "root"
1248
+ __type__ = "root"
1188
1249
 
1189
1250
  def __init__(self, target: Target):
1190
1251
  self.target = target
@@ -1448,6 +1509,18 @@ def register(module: str, class_name: str, internal: bool = True) -> None:
1448
1509
  FILESYSTEMS.append(getattr(import_lazy(module), class_name))
1449
1510
 
1450
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
+
1451
1524
  def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1452
1525
  offset = fh.tell()
1453
1526
  fh.seek(0)
@@ -1456,10 +1529,7 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1456
1529
  for filesystem in FILESYSTEMS:
1457
1530
  try:
1458
1531
  if filesystem.detect(fh):
1459
- instance = filesystem(fh, *args, **kwargs)
1460
- instance.volume = fh
1461
-
1462
- return instance
1532
+ return filesystem(fh, *args, **kwargs)
1463
1533
  except ImportError as e:
1464
1534
  log.info("Failed to import %s", filesystem)
1465
1535
  log.debug("", exc_info=e)
@@ -1469,12 +1539,35 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1469
1539
  raise FilesystemError(f"Failed to open filesystem for {fh}")
1470
1540
 
1471
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
+
1472
1564
  register("ntfs", "NtfsFilesystem")
1473
1565
  register("extfs", "ExtFilesystem")
1474
1566
  register("xfs", "XfsFilesystem")
1475
1567
  register("fat", "FatFilesystem")
1476
1568
  register("ffs", "FfsFilesystem")
1477
1569
  register("vmfs", "VmfsFilesystem")
1570
+ register("btrfs", "BtrfsFilesystem")
1478
1571
  register("exfat", "ExfatFilesystem")
1479
1572
  register("squashfs", "SquashFSFilesystem")
1480
1573
  register("zip", "ZipFilesystem")
@@ -13,7 +13,7 @@ from dissect.target.helpers import fsutil
13
13
 
14
14
 
15
15
  class AD1Filesystem(Filesystem):
16
- __fstype__ = "ad1"
16
+ __type__ = "ad1"
17
17
 
18
18
  def __init__(self, fh, *args, **kwargs):
19
19
  super().__init__(fh, *args, **kwargs)
@@ -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
+ __type__ = "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
+ __type__ = "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
@@ -23,7 +23,7 @@ class OS(IntEnum):
23
23
 
24
24
 
25
25
  class CbFilesystem(Filesystem):
26
- __fstype__ = "cb"
26
+ __type__ = "cb"
27
27
 
28
28
  def __init__(self, session: LiveResponseSession, prefix: str, *args, **kwargs):
29
29
  self.session = session
@@ -97,14 +97,14 @@ class CbFilesystemEntry(FilesystemEntry):
97
97
  if not self.is_dir():
98
98
  raise NotADirectoryError(f"'{self.path}' is not a directory")
99
99
 
100
- seperator = self.fs.alt_separator or "/"
101
- for entry in self.fs.session.list_directory(self.cbpath + seperator):
100
+ separator = self.fs.alt_separator or "/"
101
+ for entry in self.fs.session.list_directory(self.cbpath + separator):
102
102
  filename = entry["filename"]
103
103
  if filename in (".", ".."):
104
104
  continue
105
105
 
106
106
  path = fsutil.join(self.path, filename, alt_separator=self.fs.alt_separator)
107
- cbpath = seperator.join([self.cbpath, filename])
107
+ cbpath = separator.join([self.cbpath, filename])
108
108
  yield CbFilesystemEntry(self.fs, path, entry, cbpath)
109
109
 
110
110
  def is_dir(self, follow_symlinks: bool = True) -> bool: