dissect.hypervisor 3.19.dev2__tar.gz → 3.20.dev2__tar.gz

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.
Files changed (102) hide show
  1. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/PKG-INFO +3 -4
  2. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/__init__.py +2 -1
  3. dissect_hypervisor-3.20.dev2/dissect/hypervisor/disk/asif.py +262 -0
  4. dissect_hypervisor-3.20.dev2/dissect/hypervisor/disk/c_asif.py +37 -0
  5. dissect_hypervisor-3.20.dev2/dissect/hypervisor/disk/c_asif.pyi +68 -0
  6. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/c_qcow2.pyi +1 -2
  7. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/vhdx.py +2 -2
  8. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/util/vmtar.py +2 -2
  9. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect.hypervisor.egg-info/PKG-INFO +3 -4
  10. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect.hypervisor.egg-info/SOURCES.txt +5 -0
  11. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/pyproject.toml +25 -8
  12. dissect_hypervisor-3.20.dev2/tests/_data/disk/asif/basic.asif.gz +0 -0
  13. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/conftest.py +5 -0
  14. dissect_hypervisor-3.20.dev2/tests/disk/test_asif.py +17 -0
  15. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/disk/test_hdd.py +1 -1
  16. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/disk/test_qcow2.py +5 -5
  17. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tox.ini +8 -12
  18. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/.git-blame-ignore-revs +0 -0
  19. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/COPYRIGHT +0 -0
  20. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/LICENSE +0 -0
  21. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/MANIFEST.in +0 -0
  22. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/README.md +0 -0
  23. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/descriptor/__init__.py +0 -0
  24. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/descriptor/c_hyperv.py +0 -0
  25. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/descriptor/hyperv.py +0 -0
  26. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/descriptor/ovf.py +0 -0
  27. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/descriptor/pvs.py +0 -0
  28. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/descriptor/vbox.py +0 -0
  29. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/descriptor/vmx.py +0 -0
  30. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/__init__.py +0 -0
  31. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/c_hdd.py +0 -0
  32. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/c_qcow2.py +0 -0
  33. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/c_vdi.py +0 -0
  34. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/c_vhd.py +0 -0
  35. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/c_vhdx.py +0 -0
  36. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/c_vmdk.py +0 -0
  37. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/hdd.py +0 -0
  38. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/qcow2.py +0 -0
  39. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/vdi.py +0 -0
  40. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/vhd.py +0 -0
  41. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/disk/vmdk.py +0 -0
  42. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/exceptions.py +0 -0
  43. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/tools/__init__.py +0 -0
  44. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/tools/envelope.py +0 -0
  45. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/tools/vmtar.py +0 -0
  46. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/util/__init__.py +0 -0
  47. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect/hypervisor/util/envelope.py +0 -0
  48. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect.hypervisor.egg-info/dependency_links.txt +0 -0
  49. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect.hypervisor.egg-info/entry_points.txt +0 -0
  50. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect.hypervisor.egg-info/requires.txt +0 -0
  51. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/dissect.hypervisor.egg-info/top_level.txt +0 -0
  52. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/setup.cfg +0 -0
  53. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/__init__.py +0 -0
  54. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/descriptor/hyperv/test.VMRS +0 -0
  55. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/descriptor/hyperv/test.vmcx +0 -0
  56. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/descriptor/vmx/encrypted.vmx +0 -0
  57. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/expanding.hdd/DiskDescriptor.xml +0 -0
  58. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/expanding.hdd/expanding.hdd +0 -0
  59. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/expanding.hdd/expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  60. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/plain.hdd/DiskDescriptor.xml +0 -0
  61. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/plain.hdd/plain.hdd +0 -0
  62. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/plain.hdd/plain.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  63. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/DiskDescriptor.xml +0 -0
  64. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/split.hdd +0 -0
  65. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/split.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  66. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/split.hdd.1.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  67. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/split.hdd.2.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  68. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/split.hdd.3.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  69. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/split.hdd.4.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  70. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/hdd/split.hdd/split.hdd.5.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz +0 -0
  71. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/qcow2/backing-chain-1.qcow2.gz +0 -0
  72. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/qcow2/backing-chain-2.qcow2.gz +0 -0
  73. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/qcow2/backing-chain-3.qcow2.gz +0 -0
  74. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/qcow2/basic.qcow2.gz +0 -0
  75. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/qcow2/data-file.bin.gz +0 -0
  76. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/qcow2/data-file.qcow2.gz +0 -0
  77. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/qcow2/snapshot.qcow2.gz +0 -0
  78. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/vhd/dynamic.vhd.gz +0 -0
  79. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/vhd/fixed.vhd.gz +0 -0
  80. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/vhdx/differencing.avhdx.gz +0 -0
  81. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/vhdx/dynamic.vhdx.gz +0 -0
  82. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/vhdx/fixed.vhdx.gz +0 -0
  83. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/disk/vmdk/sesparse.vmdk.gz +0 -0
  84. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/util/envelope/encryption.info +0 -0
  85. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/util/envelope/local.tgz.ve +0 -0
  86. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_data/util/vmtar/test.vgz +0 -0
  87. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_docs/Makefile +0 -0
  88. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_docs/conf.py +0 -0
  89. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/_docs/index.rst +0 -0
  90. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/descriptor/__init__.py +0 -0
  91. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/descriptor/test_hyperv.py +0 -0
  92. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/descriptor/test_ovf.py +0 -0
  93. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/descriptor/test_pvs.py +0 -0
  94. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/descriptor/test_vbox.py +0 -0
  95. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/descriptor/test_vmx.py +0 -0
  96. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/disk/__init__.py +0 -0
  97. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/disk/test_vhd.py +0 -0
  98. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/disk/test_vhdx.py +0 -0
  99. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/disk/test_vmdk.py +0 -0
  100. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/util/__init__.py +0 -0
  101. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/util/test_envelope.py +0 -0
  102. {dissect_hypervisor-3.19.dev2 → dissect_hypervisor-3.20.dev2}/tests/util/test_vmtar.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.hypervisor
3
- Version: 3.19.dev2
3
+ Version: 3.20.dev2
4
4
  Summary: A Dissect module implementing parsers for various hypervisor disk, backup and configuration files
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
- License: Affero General Public License v3
6
+ License-Expression: AGPL-3.0-or-later
7
7
  Project-URL: homepage, https://dissect.tools
8
8
  Project-URL: documentation, https://docs.dissect.tools/en/latest/projects/dissect.hypervisor
9
9
  Project-URL: repository, https://github.com/fox-it/dissect.hypervisor
@@ -11,14 +11,13 @@ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Intended Audience :: Information Technology
14
- Classifier: License :: OSI Approved
15
14
  Classifier: Operating System :: OS Independent
16
15
  Classifier: Programming Language :: Python :: 3
17
16
  Classifier: Topic :: Internet :: Log Analysis
18
17
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
18
  Classifier: Topic :: Security
20
19
  Classifier: Topic :: Utilities
21
- Requires-Python: ~=3.9
20
+ Requires-Python: >=3.10
22
21
  Description-Content-Type: text/markdown
23
22
  License-File: LICENSE
24
23
  License-File: COPYRIGHT
@@ -1,8 +1,9 @@
1
1
  from dissect.hypervisor.descriptor import hyperv, ovf, pvs, vbox, vmx
2
- from dissect.hypervisor.disk import hdd, qcow2, vdi, vhd, vhdx, vmdk
2
+ from dissect.hypervisor.disk import asif, hdd, qcow2, vdi, vhd, vhdx, vmdk
3
3
  from dissect.hypervisor.util import envelope, vmtar
4
4
 
5
5
  __all__ = [
6
+ "asif",
6
7
  "envelope",
7
8
  "hdd",
8
9
  "hyperv",
@@ -0,0 +1,262 @@
1
+ from __future__ import annotations
2
+
3
+ import plistlib
4
+ from functools import cached_property, lru_cache
5
+ from typing import Any, BinaryIO
6
+ from uuid import UUID
7
+
8
+ from dissect.util.stream import AlignedStream
9
+
10
+ from dissect.hypervisor.disk.c_asif import c_asif
11
+ from dissect.hypervisor.exceptions import InvalidSignature
12
+
13
+
14
+ class ASIF:
15
+ """Apple Sparse Image Format (ASIF) disk image.
16
+
17
+ ASIF disk images are a virtual disk format introduced in macOS Tahoe. They can be used in Apple's Virtualization
18
+ framework, as well as through Disk Utility.
19
+
20
+ An ASIF file is pretty straight forward. There's a small header which, among some other details, contains two
21
+ directory offsets. Each directory contains a list of tables, which in turn contain a list of data entries. Each data
22
+ entry points to a chunk of data in the ASIF file. The chunk size is defined in the header and is typically 1 MiB.
23
+ The chunk size is always a multiple of the block size, which is also defined in the header (typically 512 bytes).
24
+ Each directory has a version number, and the directory with the highest version number is the active directory. This
25
+ allows for atomic updates of the directory/table data.
26
+
27
+ The maximum virtual disk size seems to be just under 4 PiB, with a small portion at the end reserved for metadata.
28
+ The actual size of the virtual disk is defined in the header, as well as the maximum size the disk can grow to.
29
+
30
+ The offset to the metadata block is typically ``(4 PiB - 1 chunk)``, meaning it's within the reserved area.
31
+ The metadata block contains a small header and a plist. The plist should contain an ``internal metadata`` and
32
+ ``user metadata`` dictionary. Besides a "stable uuid", it's unclear what the metadata is used for or how to set it.
33
+
34
+ Args:
35
+ fh: File-like object containing the ASIF image.
36
+
37
+ Resources:
38
+ - Reversing ``diskimagescontroller``
39
+ - https://developer.apple.com/documentation/virtualization/vzdiskimagestoragedeviceattachment/
40
+ """
41
+
42
+ def __init__(self, fh: BinaryIO):
43
+ self.fh = fh
44
+
45
+ self.header = c_asif.asif_header(fh)
46
+ if self.header.header_signature != c_asif.ASIF_HEADER_SIGNATURE:
47
+ raise InvalidSignature(
48
+ f"Not a valid ASIF image (expected {c_asif.ASIF_HEADER_SIGNATURE:#x}, "
49
+ f"got {self.header.header_signature:#x})"
50
+ )
51
+
52
+ self.guid = UUID(bytes=self.header.guid)
53
+ self.block_size = self.header.block_size
54
+ self.chunk_size = self.header.chunk_size
55
+ self.size = self.header.sector_count * self.block_size
56
+ self.max_size = self.header.max_sector_count * self.block_size
57
+
58
+ # The following math is taken from the assembly with some creative variable naming
59
+ # It's possible that some of this can be simplified or the names improved
60
+ self._blocks_per_chunk = self.chunk_size // self.block_size
61
+
62
+ # This check doesn't really make sense, but keep it in for now
63
+ reserved_size = 4 * self.chunk_size
64
+ self._num_reserved_table_entries = (
65
+ 1 if reserved_size < self._blocks_per_chunk else reserved_size // self._blocks_per_chunk
66
+ )
67
+
68
+ self._max_table_entries = self.chunk_size >> 3
69
+ self._num_table_entries = self._max_table_entries - (
70
+ self._max_table_entries % (self._num_reserved_table_entries + 1)
71
+ )
72
+ self._num_reserved_directory_entries = (self._num_reserved_table_entries + self._num_table_entries) // (
73
+ self._num_reserved_table_entries + 1
74
+ )
75
+ self._num_usable_entries = self._num_table_entries - self._num_reserved_directory_entries
76
+ # This is the size in bytes of data covered by a single table
77
+ self._size_per_table = self._num_usable_entries * self.chunk_size
78
+
79
+ max_size = self.block_size * self.header.max_sector_count
80
+ self._num_directory_entries = (self._size_per_table + max_size - 1) // self._size_per_table
81
+
82
+ self._aligned_table_size = (
83
+ (self.block_size + 8 * self._num_table_entries - 1) // self.block_size * self.block_size
84
+ )
85
+
86
+ self.directories = sorted(
87
+ (Directory(self, offset) for offset in self.header.directory_offsets),
88
+ key=lambda d: d.version,
89
+ reverse=True,
90
+ )
91
+ self.active_directory = self.directories[0]
92
+
93
+ self.metadata_header = None
94
+ self.metadata: dict[str, Any] = {}
95
+ if self.header.metadata_chunk:
96
+ # Open the file in reserved mode to read from the reserved area
97
+ with self.open(reserved=True) as disk:
98
+ metadata_offset = self.header.metadata_chunk * self.chunk_size
99
+ disk.seek(metadata_offset)
100
+ self.metadata_header = c_asif.asif_meta_header(disk)
101
+
102
+ if self.metadata_header.header_signature != c_asif.ASIF_META_HEADER_SIGNATURE:
103
+ raise InvalidSignature(
104
+ f"Invalid a ASIF metadata header (expected {c_asif.ASIF_META_HEADER_SIGNATURE:#x}, "
105
+ f"got {self.metadata_header.header_signature:#x})"
106
+ )
107
+
108
+ disk.seek(metadata_offset + self.metadata_header.header_size)
109
+ self.metadata = plistlib.loads(disk.read(self.metadata_header.data_size).strip(b"\x00"))
110
+
111
+ @property
112
+ def internal_metadata(self) -> dict[str, Any]:
113
+ """Get internal metadata from the ASIF image.
114
+
115
+ Returns:
116
+ A dictionary containing the internal metadata.
117
+ """
118
+ return self.metadata.get("internal metadata", {})
119
+
120
+ @property
121
+ def user_metadata(self) -> dict[str, Any]:
122
+ """Get user metadata from the ASIF image.
123
+
124
+ Returns:
125
+ A dictionary containing the user metadata.
126
+ """
127
+ return self.metadata.get("user metadata", {})
128
+
129
+ def open(self, reserved: bool = False) -> DataStream:
130
+ """Open a stream to read the ASIF image data.
131
+
132
+ Args:
133
+ reserved: Whether to allow reading into the reserved area of the ASIF image.
134
+
135
+ Returns:
136
+ A stream-like object that can be used to read the image data.
137
+ """
138
+ return DataStream(self, reserved)
139
+
140
+
141
+ class Directory:
142
+ """ASIF Directory.
143
+
144
+ A directory has a version (``uint64``) followed by a list of table entries (``uint64[]``).
145
+ The version number is used to determine the active directory, with the highest version being the active one.
146
+ Each table entry is a chunk number and points to a table in the ASIF image.
147
+
148
+ Args:
149
+ asif: The ASIF image this directory belongs to.
150
+ offset: Offset of the directory in the ASIF image.
151
+ """
152
+
153
+ def __init__(self, asif: ASIF, offset: int):
154
+ self.asif = asif
155
+ self.offset = offset
156
+
157
+ self.asif.fh.seek(offset)
158
+ self.version = c_asif.uint64(self.asif.fh)
159
+
160
+ self.table = lru_cache(128)(self.table)
161
+
162
+ def __repr__(self) -> str:
163
+ return f"<Directory offset={self.offset:#x} version={self.version}>"
164
+
165
+ @cached_property
166
+ def entries(self) -> list[int]:
167
+ """List of table entries in the directory."""
168
+ # Seek over the version
169
+ self.asif.fh.seek(self.offset + 8)
170
+ return c_asif.uint64[self.asif._num_directory_entries](self.asif.fh)
171
+
172
+ def table(self, index: int) -> Table:
173
+ """Get a table from the directory.
174
+
175
+ Args:
176
+ index: Index of the table in the directory.
177
+ """
178
+ if index >= self.asif._num_directory_entries:
179
+ raise IndexError("Table index out of range")
180
+ return Table(self, index)
181
+
182
+
183
+ class Table:
184
+ """ASIF Table.
185
+
186
+ A table contains a list of data entries (``uint64[]``). Each data entry is a chunk number and points to a chunk of
187
+ data in the ASIF image. Each table covers a fixed amount of data in the virtual disk.
188
+
189
+ Data entries have 55 bits usable for the chunk number and 9 bits reserved for flags.
190
+
191
+ .. rubric :: Encoding
192
+ .. code-block:: c
193
+
194
+ 0b00000000 01111111 11111111 11111111 11111111 11111111 11111111 11111111 (chunk number)
195
+ 0b00111111 10000000 00000000 00000000 00000000 00000000 00000000 00000000 (reserved)
196
+ 0b01000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 (entry dirty)
197
+ 0b10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 (content dirty)
198
+
199
+ Args:
200
+ directory: The directory this table belongs to.
201
+ index: Index of the table in the directory.
202
+ """
203
+
204
+ def __init__(self, directory: Directory, index: int):
205
+ self.asif = directory.asif
206
+ self.directory = directory
207
+ self.index = index
208
+
209
+ self.offset = self.directory.entries[index] * self.asif.chunk_size
210
+ self.virtual_offset = index * self.asif._size_per_table
211
+
212
+ def __repr__(self) -> str:
213
+ return f"<Table index={self.index} offset={self.offset:#x} virtual_offset={self.virtual_offset:#x}>"
214
+
215
+ @cached_property
216
+ def entries(self) -> list[int]:
217
+ """List of data entries in the table."""
218
+ self.asif.fh.seek(self.offset)
219
+ return c_asif.uint64[self.asif._num_table_entries](self.asif.fh)
220
+
221
+
222
+ class DataStream(AlignedStream):
223
+ """Stream to read data from an ASIF image.
224
+
225
+ Args:
226
+ asif: The ASIF image to read from.
227
+ reserved: Whether to allow reading into the reserved area of the ASIF image.
228
+ """
229
+
230
+ def __init__(self, asif: ASIF, reserved: bool = False):
231
+ super().__init__(asif.max_size if reserved else asif.size, align=asif.chunk_size)
232
+ self.asif = asif
233
+ self.reserved = reserved
234
+ self.directory = asif.active_directory
235
+
236
+ def _read(self, offset: int, length: int) -> bytes:
237
+ result = []
238
+ while length:
239
+ table = self.directory.table(offset // self.asif._size_per_table)
240
+ relative_block_index = (offset // self.asif.block_size) - (table.virtual_offset // self.asif.block_size)
241
+ data_idx = (
242
+ relative_block_index // self.asif._blocks_per_chunk
243
+ + relative_block_index // self.asif._blocks_per_chunk * self.asif._num_reserved_table_entries
244
+ ) // self.asif._num_reserved_table_entries
245
+
246
+ # 0x8000000000000000 = content dirty bit
247
+ # 0x4000000000000000 = entry dirty bit
248
+ # 0x3F80000000000000 = reserved bits
249
+ chunk = table.entries[data_idx] & 0x7FFFFFFFFFFFFF
250
+ raw_offset = chunk * self.asif.chunk_size
251
+
252
+ read_length = min(length, self.asif.chunk_size)
253
+ if chunk == 0:
254
+ result.append(b"\x00" * read_length)
255
+ else:
256
+ self.asif.fh.seek(raw_offset)
257
+ result.append(self.asif.fh.read(read_length))
258
+
259
+ offset += read_length
260
+ length -= read_length
261
+
262
+ return b"".join(result)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from dissect.cstruct import cstruct
4
+
5
+ asif_def = """
6
+ #define ASIF_HEADER_SIGNATURE 0x73686477 // 'shdw'
7
+ #define ASIF_META_HEADER_SIGNATURE 0x6D657461 // 'meta'
8
+
9
+ struct asif_header {
10
+ uint32 header_signature;
11
+ uint32 header_version;
12
+ uint32 header_size;
13
+ uint32 header_flags;
14
+ uint64 directory_offsets[2];
15
+ char guid[16];
16
+ uint64 sector_count;
17
+ uint64 max_sector_count;
18
+ uint32 chunk_size;
19
+ uint16 block_size;
20
+ uint16 total_segments;
21
+ uint64 metadata_chunk;
22
+ char unk_50[16];
23
+ uint32 read_only_flags;
24
+ uint32 metadata_flags;
25
+ uint32 metadata_read_only_flags;
26
+ };
27
+
28
+ struct asif_meta_header {
29
+ uint32 header_signature;
30
+ uint32 header_version;
31
+ uint32 header_size;
32
+ uint64 data_size;
33
+ uint64 unk_14;
34
+ };
35
+ """
36
+
37
+ c_asif = cstruct(endian=">").load(asif_def)
@@ -0,0 +1,68 @@
1
+ # Generated by cstruct-stubgen
2
+ from typing import BinaryIO, Literal, TypeAlias, overload
3
+
4
+ import dissect.cstruct as __cs__
5
+
6
+ class _c_asif(__cs__.cstruct):
7
+ ASIF_HEADER_SIGNATURE: Literal[1936221303] = ...
8
+ ASIF_META_HEADER_SIGNATURE: Literal[1835365473] = ...
9
+ class asif_header(__cs__.Structure):
10
+ header_signature: _c_asif.uint32
11
+ header_version: _c_asif.uint32
12
+ header_size: _c_asif.uint32
13
+ header_flags: _c_asif.uint32
14
+ directory_offsets: __cs__.Array[_c_asif.uint64]
15
+ guid: __cs__.CharArray
16
+ sector_count: _c_asif.uint64
17
+ max_sector_count: _c_asif.uint64
18
+ chunk_size: _c_asif.uint32
19
+ block_size: _c_asif.uint16
20
+ total_segments: _c_asif.uint16
21
+ metadata_chunk: _c_asif.uint64
22
+ unk_50: __cs__.CharArray
23
+ read_only_flags: _c_asif.uint32
24
+ metadata_flags: _c_asif.uint32
25
+ metadata_read_only_flags: _c_asif.uint32
26
+ @overload
27
+ def __init__(
28
+ self,
29
+ header_signature: _c_asif.uint32 | None = ...,
30
+ header_version: _c_asif.uint32 | None = ...,
31
+ header_size: _c_asif.uint32 | None = ...,
32
+ header_flags: _c_asif.uint32 | None = ...,
33
+ directory_offsets: __cs__.Array[_c_asif.uint64] | None = ...,
34
+ guid: __cs__.CharArray | None = ...,
35
+ sector_count: _c_asif.uint64 | None = ...,
36
+ max_sector_count: _c_asif.uint64 | None = ...,
37
+ chunk_size: _c_asif.uint32 | None = ...,
38
+ block_size: _c_asif.uint16 | None = ...,
39
+ total_segments: _c_asif.uint16 | None = ...,
40
+ metadata_chunk: _c_asif.uint64 | None = ...,
41
+ unk_50: __cs__.CharArray | None = ...,
42
+ read_only_flags: _c_asif.uint32 | None = ...,
43
+ metadata_flags: _c_asif.uint32 | None = ...,
44
+ metadata_read_only_flags: _c_asif.uint32 | None = ...,
45
+ ): ...
46
+ @overload
47
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
48
+
49
+ class asif_meta_header(__cs__.Structure):
50
+ header_signature: _c_asif.uint32
51
+ header_version: _c_asif.uint32
52
+ header_size: _c_asif.uint32
53
+ data_size: _c_asif.uint64
54
+ unk_14: _c_asif.uint64
55
+ @overload
56
+ def __init__(
57
+ self,
58
+ header_signature: _c_asif.uint32 | None = ...,
59
+ header_version: _c_asif.uint32 | None = ...,
60
+ header_size: _c_asif.uint32 | None = ...,
61
+ data_size: _c_asif.uint64 | None = ...,
62
+ unk_14: _c_asif.uint64 | None = ...,
63
+ ): ...
64
+ @overload
65
+ def __init__(self, fh: bytes | memoryview | bytearray | BinaryIO, /): ...
66
+
67
+ # Technically `c_asif` is an instance of `_c_asif`, but then we can't use it in type hints
68
+ c_asif: TypeAlias = _c_asif
@@ -1,8 +1,7 @@
1
1
  # Generated by cstruct-stubgen
2
- from typing import BinaryIO, Literal, overload
2
+ from typing import BinaryIO, Literal, TypeAlias, overload
3
3
 
4
4
  import dissect.cstruct as __cs__
5
- from typing_extensions import TypeAlias
6
5
 
7
6
  class _c_qcow2(__cs__.cstruct):
8
7
  MIN_CLUSTER_BITS: Literal[9] = ...
@@ -7,7 +7,7 @@ import logging
7
7
  import os
8
8
  from functools import lru_cache
9
9
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Final
10
+ from typing import TYPE_CHECKING, Any, BinaryIO, Final
11
11
  from uuid import UUID
12
12
 
13
13
  from dissect.util.stream import AlignedStream
@@ -29,7 +29,7 @@ from dissect.hypervisor.disk.c_vhdx import (
29
29
  from dissect.hypervisor.exceptions import InvalidSignature, InvalidVirtualDisk
30
30
 
31
31
  if TYPE_CHECKING:
32
- from collections.abc import Iterator
32
+ from collections.abc import Callable, Iterator
33
33
 
34
34
  log = logging.getLogger(__name__)
35
35
  log.setLevel(os.getenv("DISSECT_LOG_VHDX", "CRITICAL"))
@@ -62,12 +62,12 @@ class VisorTarFile(tarfile.TarFile):
62
62
  raise tarfile.TarError("visor currently only supports read mode")
63
63
 
64
64
  try:
65
- from gzip import GzipFile
65
+ from gzip import GzipFile # noqa: PLC0415
66
66
  except ImportError:
67
67
  raise tarfile.CompressionError("gzip module is not available") from None
68
68
 
69
69
  try:
70
- from lzma import LZMAError, LZMAFile
70
+ from lzma import LZMAError, LZMAFile # noqa: PLC0415
71
71
  except ImportError:
72
72
  raise tarfile.CompressionError("lzma module is not available") from None
73
73
 
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.hypervisor
3
- Version: 3.19.dev2
3
+ Version: 3.20.dev2
4
4
  Summary: A Dissect module implementing parsers for various hypervisor disk, backup and configuration files
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
- License: Affero General Public License v3
6
+ License-Expression: AGPL-3.0-or-later
7
7
  Project-URL: homepage, https://dissect.tools
8
8
  Project-URL: documentation, https://docs.dissect.tools/en/latest/projects/dissect.hypervisor
9
9
  Project-URL: repository, https://github.com/fox-it/dissect.hypervisor
@@ -11,14 +11,13 @@ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Intended Audience :: Information Technology
14
- Classifier: License :: OSI Approved
15
14
  Classifier: Operating System :: OS Independent
16
15
  Classifier: Programming Language :: Python :: 3
17
16
  Classifier: Topic :: Internet :: Log Analysis
18
17
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
18
  Classifier: Topic :: Security
20
19
  Classifier: Topic :: Utilities
21
- Requires-Python: ~=3.9
20
+ Requires-Python: >=3.10
22
21
  Description-Content-Type: text/markdown
23
22
  License-File: LICENSE
24
23
  License-File: COPYRIGHT
@@ -21,6 +21,9 @@ dissect/hypervisor/descriptor/pvs.py
21
21
  dissect/hypervisor/descriptor/vbox.py
22
22
  dissect/hypervisor/descriptor/vmx.py
23
23
  dissect/hypervisor/disk/__init__.py
24
+ dissect/hypervisor/disk/asif.py
25
+ dissect/hypervisor/disk/c_asif.py
26
+ dissect/hypervisor/disk/c_asif.pyi
24
27
  dissect/hypervisor/disk/c_hdd.py
25
28
  dissect/hypervisor/disk/c_qcow2.py
26
29
  dissect/hypervisor/disk/c_qcow2.pyi
@@ -45,6 +48,7 @@ tests/conftest.py
45
48
  tests/_data/descriptor/hyperv/test.VMRS
46
49
  tests/_data/descriptor/hyperv/test.vmcx
47
50
  tests/_data/descriptor/vmx/encrypted.vmx
51
+ tests/_data/disk/asif/basic.asif.gz
48
52
  tests/_data/disk/hdd/expanding.hdd/DiskDescriptor.xml
49
53
  tests/_data/disk/hdd/expanding.hdd/expanding.hdd
50
54
  tests/_data/disk/hdd/expanding.hdd/expanding.hdd.0.{5fbaabe3-6958-40ff-92a7-860e329aab41}.hds.gz
@@ -85,6 +89,7 @@ tests/descriptor/test_pvs.py
85
89
  tests/descriptor/test_vbox.py
86
90
  tests/descriptor/test_vmx.py
87
91
  tests/disk/__init__.py
92
+ tests/disk/test_asif.py
88
93
  tests/disk/test_hdd.py
89
94
  tests/disk/test_qcow2.py
90
95
  tests/disk/test_vhd.py
@@ -1,13 +1,14 @@
1
1
  [build-system]
2
- requires = ["setuptools>=65.5.0", "setuptools_scm[toml]>=6.4.0"]
2
+ requires = ["setuptools>=77.0.0", "setuptools_scm[toml]>=6.4.0"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dissect.hypervisor"
7
7
  description = "A Dissect module implementing parsers for various hypervisor disk, backup and configuration files"
8
8
  readme = "README.md"
9
- requires-python = "~=3.9"
10
- license.text = "Affero General Public License v3"
9
+ requires-python = ">=3.10"
10
+ license = "AGPL-3.0-or-later"
11
+ license-files = ["LICENSE", "COPYRIGHT"]
11
12
  authors = [
12
13
  {name = "Dissect Team", email = "dissect@fox-it.com"}
13
14
  ]
@@ -16,7 +17,6 @@ classifiers = [
16
17
  "Environment :: Console",
17
18
  "Intended Audience :: Developers",
18
19
  "Intended Audience :: Information Technology",
19
- "License :: OSI Approved",
20
20
  "Operating System :: OS Independent",
21
21
  "Programming Language :: Python :: 3",
22
22
  "Topic :: Internet :: Log Analysis",
@@ -46,13 +46,33 @@ dev = [
46
46
  "dissect.util>=3.0.dev,<4.0.dev",
47
47
  ]
48
48
 
49
+ [dependency-groups]
50
+ test = [
51
+ "pytest",
52
+ ]
53
+ lint = [
54
+ "ruff==0.13.1",
55
+ "vermin",
56
+ ]
57
+ build = [
58
+ "build",
59
+ ]
60
+ debug = [
61
+ "ipdb",
62
+ ]
63
+ dev = [
64
+ {include-group = "test"},
65
+ {include-group = "lint"},
66
+ {include-group = "debug"},
67
+ ]
68
+
49
69
  [project.scripts]
50
70
  envelope-decrypt = "dissect.hypervisor.tools.envelope:main"
51
71
  vmtar = "dissect.hypervisor.tools.vmtar:main"
52
72
 
53
73
  [tool.ruff]
54
74
  line-length = 120
55
- required-version = ">=0.11.0"
75
+ required-version = ">=0.13.1"
56
76
 
57
77
  [tool.ruff.format]
58
78
  docstring-code-format = true
@@ -102,9 +122,6 @@ ignore = ["E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM1
102
122
  known-first-party = ["dissect.hypervisor"]
103
123
  known-third-party = ["dissect"]
104
124
 
105
- [tool.setuptools]
106
- license-files = ["LICENSE", "COPYRIGHT"]
107
-
108
125
  [tool.setuptools.packages.find]
109
126
  include = ["dissect.*"]
110
127
 
@@ -108,6 +108,11 @@ def snapshot_qcow2() -> Iterator[BinaryIO]:
108
108
  yield from open_file_gz("_data/disk/qcow2/snapshot.qcow2.gz")
109
109
 
110
110
 
111
+ @pytest.fixture
112
+ def basic_asif() -> Iterator[BinaryIO]:
113
+ yield from open_file_gz("_data/disk/asif/basic.asif.gz")
114
+
115
+
111
116
  @pytest.fixture
112
117
  def envelope() -> Iterator[BinaryIO]:
113
118
  yield from open_file("_data/util/envelope/local.tgz.ve")
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import BinaryIO
4
+
5
+ from dissect.hypervisor.disk.asif import ASIF
6
+
7
+
8
+ def test_asif(basic_asif: BinaryIO) -> None:
9
+ """Test ASIF parsing."""
10
+ asif = ASIF(basic_asif)
11
+
12
+ assert asif.internal_metadata == {"stable uuid": "13db9632-b79f-4e95-aada-835d5ef97bba"}
13
+ assert asif.user_metadata == {}
14
+
15
+ with asif.open() as stream:
16
+ for i in range(100):
17
+ assert stream.read(1024 * 1024).strip(bytes([i])) == b"", f"Mismatch at offset {i * 1024 * 1024:#x}"
@@ -61,7 +61,7 @@ def test_split_hdd(split_hdd: Path) -> None:
61
61
 
62
62
  start = 0
63
63
 
64
- for storage, split_size in zip(storages, split_sizes):
64
+ for storage, split_size in zip(storages, split_sizes, strict=False):
65
65
  assert storage.start == start
66
66
  assert storage.end == start + split_size
67
67
  assert len(storage.images) == 1
@@ -31,7 +31,7 @@ def test_basic(basic_qcow2: BinaryIO) -> None:
31
31
  def test_data_file(data_file_qcow2: Path) -> None:
32
32
  # Test with file handle
33
33
  with gzip.open(data_file_qcow2, "rb") as fh:
34
- with pytest.raises(Error, match="data-file required but not provided \\(image_data_file = 'data-file.bin'\\)"):
34
+ with pytest.raises(Error, match=r"data-file required but not provided \(image_data_file = 'data-file.bin'\)"):
35
35
  QCow2(fh)
36
36
 
37
37
  with gzip.open(data_file_qcow2.with_name("data-file.bin.gz"), "rb") as fh_bin:
@@ -47,7 +47,7 @@ def test_data_file(data_file_qcow2: Path) -> None:
47
47
  # Test with allow_no_data_file
48
48
  qcow2 = QCow2(fh, allow_no_data_file=True)
49
49
  assert qcow2.data_file is None
50
- with pytest.raises(Error, match="data-file required but not provided \\(image_data_file = 'data-file.bin'\\)"):
50
+ with pytest.raises(Error, match=r"data-file required but not provided \(image_data_file = 'data-file.bin'\)"):
51
51
  qcow2.open()
52
52
 
53
53
  # Test with Path
@@ -68,12 +68,12 @@ def test_backing_file(backing_chain_qcow2: tuple[Path, Path, Path]) -> None:
68
68
  # Test with file handle
69
69
  with gzip.open(file1, "rb") as fh1, gzip.open(file2, "rb") as fh2, gzip.open(file3, "rb") as fh3:
70
70
  with pytest.raises(
71
- Error, match="backing-file required but not provided \\(auto_backing_file = 'backing-chain-2.qcow2'\\)"
71
+ Error, match=r"backing-file required but not provided \(auto_backing_file = 'backing-chain-2.qcow2'\)"
72
72
  ):
73
73
  QCow2(fh1)
74
74
 
75
75
  with pytest.raises(
76
- Error, match="backing-file required but not provided \\(auto_backing_file = 'backing-chain-3.qcow2'\\)"
76
+ Error, match=r"backing-file required but not provided \(auto_backing_file = 'backing-chain-3.qcow2'\)"
77
77
  ):
78
78
  QCow2(fh1, backing_file=fh2)
79
79
 
@@ -87,7 +87,7 @@ def test_backing_file(backing_chain_qcow2: tuple[Path, Path, Path]) -> None:
87
87
  qcow2 = QCow2(fh1, allow_no_backing_file=True)
88
88
  assert qcow2.backing_file is None
89
89
  with pytest.raises(
90
- Error, match="backing-file required but not provided \\(auto_backing_file = 'backing-chain-2.qcow2'\\)"
90
+ Error, match=r"backing-file required but not provided \(auto_backing_file = 'backing-chain-2.qcow2'\)"
91
91
  ):
92
92
  qcow2.open()
93
93
 
@@ -4,7 +4,7 @@ envlist = lint, py3, pypy3
4
4
  # requires if they are not available on the host system. This requires the
5
5
  # locally installed tox to have a minimum version 3.3.0. This means the names
6
6
  # of the configuration options are still according to the tox 3.x syntax.
7
- minversion = 4.4.3
7
+ minversion = 4.27.0
8
8
  # This version of virtualenv will install setuptools version 68.2.2 and pip
9
9
  # 23.3.1. These versions fully support python projects defined only through a
10
10
  # pyproject.toml file (PEP-517/PEP-518/PEP-621). This pip version also support
@@ -14,9 +14,9 @@ requires = virtualenv>=20.24.6
14
14
  [testenv]
15
15
  extras = dev
16
16
  deps =
17
- pytest
18
17
  pytest-cov
19
18
  coverage
19
+ dependency_groups = test
20
20
  commands =
21
21
  pytest --basetemp="{envtmpdir}" {posargs:--color=yes --cov=dissect --cov-report=term-missing -v tests}
22
22
  coverage report
@@ -24,28 +24,24 @@ commands =
24
24
 
25
25
  [testenv:build]
26
26
  package = skip
27
- deps =
28
- build
27
+ dependency_groups = build
29
28
  commands =
30
29
  pyproject-build
31
30
 
32
31
  [testenv:fix]
33
32
  package = skip
34
- deps =
35
- ruff==0.11.10
33
+ dependency_groups = lint
36
34
  commands =
37
- ruff format dissect tests
38
35
  ruff check --fix dissect tests
36
+ ruff format dissect tests
39
37
 
40
38
  [testenv:lint]
41
39
  package = skip
42
- deps =
43
- ruff==0.11.10
44
- vermin
40
+ dependency_groups = lint
45
41
  commands =
46
- ruff format --check dissect tests
47
42
  ruff check dissect tests
48
- vermin -t=3.9- --no-tips --lint dissect tests
43
+ ruff format --check dissect tests
44
+ vermin -t=3.10- --no-tips --lint dissect tests
49
45
 
50
46
  [testenv:docs-build]
51
47
  allowlist_externals = make