diff --git a/dissect/target/loaders/itunes.py b/dissect/target/loaders/itunes.py index 4b0831d053..1635ebb1cb 100644 --- a/dissect/target/loaders/itunes.py +++ b/dissect/target/loaders/itunes.py @@ -146,7 +146,7 @@ def open(self, password: str | None = None, kek: bytes | None = None) -> None: def _open_manifest_db(self) -> SQLite3: path = self.root.joinpath("Manifest.db") if not self.encrypted or self.manifest["Lockdown"]["ProductVersion"] < "10.2": - fh = path.open("rb") + fh = path else: key = self.key_bag.unwrap(self.manifest["ManifestKey"]) fh = BytesIO(aes_decrypt(path.read_bytes(), key)) diff --git a/dissect/target/plugins/apps/av/mcafee.py b/dissect/target/plugins/apps/av/mcafee.py index 8ac9b8b47c..2ede58e7e5 100644 --- a/dissect/target/plugins/apps/av/mcafee.py +++ b/dissect/target/plugins/apps/av/mcafee.py @@ -96,8 +96,7 @@ def msc(self) -> Iterator[McAfeeMscLogRecord]: len_marker = len(self.MARKER_SUSPICIOUS_UDP_CONNECTION) for log_file in self.get_log_files(): - with log_file.open() as open_log: - database = SQLite3(open_log) + with SQLite3(log_file) as database: fields = defaultdict(dict) fields_table = database.table(self.TABLE_FIELD) diff --git a/dissect/target/plugins/apps/av/sophos.py b/dissect/target/plugins/apps/av/sophos.py index 32cf893fc2..2cb6377484 100644 --- a/dissect/target/plugins/apps/av/sophos.py +++ b/dissect/target/plugins/apps/av/sophos.py @@ -78,8 +78,7 @@ def hitmanlogs(self) -> Iterator[HitmanAlertRecord]: """ if self.target.fs.path(self.LOG_SOPHOS_HITMAN).exists(): try: - fh = self.target.fs.path(self.LOG_SOPHOS_HITMAN).open("rb") - db = SQLite3(fh) + db = SQLite3(self.target.fs.path(self.LOG_SOPHOS_HITMAN)) alerts = next(filter(lambda t: t.name == "Alerts", db.tables())) for alert in alerts.rows(): yield HitmanAlertRecord( diff --git a/dissect/target/plugins/apps/browser/chromium.py b/dissect/target/plugins/apps/browser/chromium.py index 24641e0a1f..7fe228b18d 100644 --- a/dissect/target/plugins/apps/browser/chromium.py +++ b/dissect/target/plugins/apps/browser/chromium.py @@ -158,7 +158,7 @@ def _iter_db(self, filename: str, subdirs: list[str] | None = None) -> Iterator[ seen.add(db_file) try: - yield user, db_file, SQLite3(db_file.open()) + yield user, db_file, SQLite3(db_file) except FileNotFoundError: self.target.log.warning("Could not find %s file: %s", filename, db_file) except DBError as e: diff --git a/dissect/target/plugins/apps/browser/firefox.py b/dissect/target/plugins/apps/browser/firefox.py index a1d33cb1d2..f40fd6dd27 100644 --- a/dissect/target/plugins/apps/browser/firefox.py +++ b/dissect/target/plugins/apps/browser/firefox.py @@ -170,7 +170,7 @@ def _iter_db(self, filename: str) -> Iterator[tuple[UserDetails | None, Path, SQ db_file = profile_dir.joinpath(filename) try: - yield user_details, db_file, SQLite3(db_file.open()) + yield user_details, db_file, SQLite3(db_file) except FileNotFoundError: self.target.log.info("Could not find %s file: %s", filename, db_file) except DBError as e: @@ -673,9 +673,7 @@ def decrypt_master_key(key4_file: Path, primary_password: bytes) -> bytes: # Extract neccesary information from the key4.db file. Multiple values might exist for the # values we are interested in. Generally the last entry will be the currently active value, # which is why we need to iterate every row in the table to get the last entry. - with key4_file.open("rb") as fh: - db = SQLite3(fh) - + with SQLite3(key4_file) as db: # Get the last ``item`` (global salt) and ``item2`` (password check) values. if table := db.table("metadata"): for row in table.rows(): @@ -693,40 +691,42 @@ def decrypt_master_key(key4_file: Path, primary_password: bytes) -> bytes: else: raise ValueError(f"Missing table 'nssPrivate' in key4.db {key4_file}") - if not master_key: - raise ValueError(f"Password master key is not defined in key4.db {key4_file}") + if not master_key: + raise ValueError(f"Password master key is not defined in key4.db {key4_file}") - if master_key_cka != CKA_ID: - raise ValueError( - f"Password master key CKA_ID '{master_key_cka}' is not equal to expected value '{CKA_ID}' in {key4_file}" - ) + if master_key_cka != CKA_ID: + raise ValueError( + f"Password master key CKA_ID '{master_key_cka}' is not equal to expected value '{CKA_ID}' in {key4_file}" # noqa: E501 + ) - decoded_password_check: core.Sequence = core.load(password_check) - decoded_master_key: core.Sequence = core.load(master_key) + decoded_password_check: core.Sequence = core.load(password_check) + decoded_master_key: core.Sequence = core.load(master_key) - try: - decrypted_password_check, algorithm = _decrypt_master_key(decoded_password_check, primary_password, global_salt) + try: + decrypted_password_check, algorithm = _decrypt_master_key( + decoded_password_check, primary_password, global_salt + ) - except EOFError: - raise ValueError("No primary password provided") + except EOFError: + raise ValueError("No primary password provided") - except ValueError as e: - raise ValueError(f"Unable to decrypt Firefox password check: {e!s}") from e + except ValueError as e: + raise ValueError(f"Unable to decrypt Firefox password check: {e!s}") from e - if not decrypted_password_check: - raise ValueError(f"Encountered unknown algorithm {algorithm} while decrypting Firefox master key") + if not decrypted_password_check: + raise ValueError(f"Encountered unknown algorithm {algorithm} while decrypting Firefox master key") - expected_password_check = b"password-check\x02\x02" + expected_password_check = b"password-check\x02\x02" - if decrypted_password_check != expected_password_check: - log.debug("Expected %s but got %s", expected_password_check, decrypted_password_check) - raise ValueError("Master key decryption failed. Provided password could be missing or incorrect") + if decrypted_password_check != expected_password_check: + log.debug("Expected %s but got %s", expected_password_check, decrypted_password_check) + raise ValueError("Master key decryption failed. Provided password could be missing or incorrect") - decrypted, algorithm = _decrypt_master_key(decoded_master_key, primary_password, global_salt) + decrypted, algorithm = _decrypt_master_key(decoded_master_key, primary_password, global_salt) - block_size = 16 if algos.EncryptionAlgorithmId.map(algorithm) == "pbes2" else 8 + block_size = 16 if algos.EncryptionAlgorithmId.map(algorithm) == "pbes2" else 8 - return unpad(decrypted, block_size) + return unpad(decrypted, block_size) def decrypt_value(b64_ciphertext: str, key: bytes) -> bytes | None: diff --git a/dissect/target/plugins/apps/container/podman.py b/dissect/target/plugins/apps/container/podman.py index 4b493a6b99..a5cd11bdf0 100755 --- a/dissect/target/plugins/apps/container/podman.py +++ b/dissect/target/plugins/apps/container/podman.py @@ -175,7 +175,7 @@ def _find_containers_sqlite(self, path: Path) -> Iterator[PodmanContainerRecord] """ try: - db = SQLite3(path.open("rb")) + db = SQLite3(path) except (ValueError, DBError) as e: self.target.log.warning("Unable to read Podman database %s: %s", path, e) self.target.log.debug("", exc_info=e) diff --git a/dissect/target/plugins/os/unix/esxi/configstore.py b/dissect/target/plugins/os/unix/esxi/configstore.py index 12b0328188..3f3210529a 100644 --- a/dissect/target/plugins/os/unix/esxi/configstore.py +++ b/dissect/target/plugins/os/unix/esxi/configstore.py @@ -1,7 +1,7 @@ from __future__ import annotations import json as jsonlib -from typing import TYPE_CHECKING, Any, BinaryIO +from typing import TYPE_CHECKING, Any from dissect.database.sqlite3 import SQLite3 @@ -9,6 +9,8 @@ from dissect.target.plugin import Plugin, internal if TYPE_CHECKING: + from pathlib import Path + from dissect.target.target import Target @@ -25,8 +27,7 @@ def __init__(self, target: Target): # It's made available at /etc/vmware/configstore/current-store-1 during boot, but stored at # the path used below in local.tgz if (path := target.fs.path("/var/lib/vmware/configstore/backup/current-store-1")).exists(): - with path.open("rb") as fh: - self._configstore = parse_config_store(fh) + self._configstore = parse_config_store(path) def check_compatible(self) -> None: if self.target.os != "esxi": @@ -41,43 +42,42 @@ def get(self, key: str, default: Any = None) -> dict[str, Any]: return self._configstore.get(key, default) -def parse_config_store(fh: BinaryIO) -> dict[str, Any]: - db = SQLite3(fh) - - store = {} - - if table := db.table("Config"): - for row in table.rows(): - component_name = row.Component - config_group_name = row.ConfigGroup - value_group_name = row.Name - identifier_name = row.Identifier - - if component_name not in store: - store[component_name] = {} - component = store[component_name] - - if config_group_name not in component: - component[config_group_name] = {} - config_group = component[config_group_name] - - if value_group_name not in config_group: - config_group[value_group_name] = {} - value_group = config_group[value_group_name] - - if identifier_name not in value_group: - value_group[identifier_name] = {} - identifier = value_group[identifier_name] - - identifier["modified_time"] = row.ModifiedTime - identifier["creation_time"] = row.CreationTime - identifier["version"] = row.Version - identifier["success"] = row.Success - identifier["auto_conf_value"] = jsonlib.loads(row.AutoConfValue) if row.AutoConfValue else None - identifier["user_value"] = jsonlib.loads(row.UserValue) if row.UserValue else None - identifier["vital_value"] = jsonlib.loads(row.VitalValue) if row.VitalValue else None - identifier["cached_value"] = jsonlib.loads(row.CachedValue) if row.CachedValue else None - identifier["desired_value"] = jsonlib.loads(row.DesiredValue) if row.DesiredValue else None - identifier["revision"] = row.Revision - - return store +def parse_config_store(path: Path) -> dict[str, Any]: + with SQLite3(path) as db: + store = {} + + if table := db.table("Config"): + for row in table.rows(): + component_name = row.Component + config_group_name = row.ConfigGroup + value_group_name = row.Name + identifier_name = row.Identifier + + if component_name not in store: + store[component_name] = {} + component = store[component_name] + + if config_group_name not in component: + component[config_group_name] = {} + config_group = component[config_group_name] + + if value_group_name not in config_group: + config_group[value_group_name] = {} + value_group = config_group[value_group_name] + + if identifier_name not in value_group: + value_group[identifier_name] = {} + identifier = value_group[identifier_name] + + identifier["modified_time"] = row.ModifiedTime + identifier["creation_time"] = row.CreationTime + identifier["version"] = row.Version + identifier["success"] = row.Success + identifier["auto_conf_value"] = jsonlib.loads(row.AutoConfValue) if row.AutoConfValue else None + identifier["user_value"] = jsonlib.loads(row.UserValue) if row.UserValue else None + identifier["vital_value"] = jsonlib.loads(row.VitalValue) if row.VitalValue else None + identifier["cached_value"] = jsonlib.loads(row.CachedValue) if row.CachedValue else None + identifier["desired_value"] = jsonlib.loads(row.DesiredValue) if row.DesiredValue else None + identifier["revision"] = row.Revision + + return store diff --git a/dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py b/dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py index e559254fec..915d335d81 100644 --- a/dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py +++ b/dissect/target/plugins/os/unix/linux/debian/proxmox/_os.py @@ -18,6 +18,8 @@ from dissect.target.plugins.os.unix.linux.debian._os import DebianPlugin if TYPE_CHECKING: + from pathlib import Path + from typing_extensions import Self from dissect.target.target import Target @@ -37,8 +39,7 @@ def create(cls, target: Target, sysvol: Filesystem) -> Self: obj = super().create(target, sysvol) if (config_db := target.fs.path("/var/lib/pve-cluster/config.db")).exists(): - with config_db.open("rb") as fh: - vfs = _create_pmxcfs(fh, obj.hostname) + vfs = _create_pmxcfs(config_db, obj.hostname) target.fs.mount("/etc/pve", vfs) @@ -63,41 +64,40 @@ def os(self) -> str: DT_REG = 8 -def _create_pmxcfs(fh: BinaryIO, hostname: str | None = None) -> VirtualFilesystem: +def _create_pmxcfs(fh: Path, hostname: str | None = None) -> VirtualFilesystem: # https://pve.proxmox.com/wiki/Proxmox_Cluster_File_System_(pmxcfs) - db = SQLite3(fh) - - entries = {row.inode: row for row in db.table("tree")} - - vfs = VirtualFilesystem() - for entry in entries.values(): - if entry.type == DT_DIR: - cls = ProxmoxConfigDirectoryEntry - elif entry.type == DT_REG: - cls = ProxmoxConfigFileEntry - else: - raise ValueError(f"Unknown pmxcfs file type: {entry.type}") - - parts = [] - current = entry - while current.parent != 0: + with SQLite3(fh) as db: + entries = {row.inode: row for row in db.table("tree")} + + vfs = VirtualFilesystem() + for entry in entries.values(): + if entry.type == DT_DIR: + cls = ProxmoxConfigDirectoryEntry + elif entry.type == DT_REG: + cls = ProxmoxConfigFileEntry + else: + raise ValueError(f"Unknown pmxcfs file type: {entry.type}") + + parts = [] + current = entry + while current.parent != 0: + parts.append(current.name) + current = entries[current.parent] parts.append(current.name) - current = entries[current.parent] - parts.append(current.name) - path = "/".join(parts[::-1]) - vfs.map_file_entry(path, cls(vfs, path, entry)) + path = "/".join(parts[::-1]) + vfs.map_file_entry(path, cls(vfs, path, entry)) - if hostname: - node_root = vfs.path(f"nodes/{hostname}") - vfs.symlink(str(node_root), "local") - vfs.symlink(str(node_root / "lxc"), "lxc") - vfs.symlink(str(node_root / "openvz"), "openvz") - vfs.symlink(str(node_root / "qemu-server"), "qemu-server") + if hostname: + node_root = vfs.path(f"nodes/{hostname}") + vfs.symlink(str(node_root), "local") + vfs.symlink(str(node_root / "lxc"), "lxc") + vfs.symlink(str(node_root / "openvz"), "openvz") + vfs.symlink(str(node_root / "qemu-server"), "qemu-server") - # TODO: .version, .members, .vmlist, maybe .clusterlog and .rrd? + # TODO: .version, .members, .vmlist, maybe .clusterlog and .rrd? - return vfs + return vfs class ProxmoxConfigFileEntry(VirtualFile): diff --git a/dissect/target/plugins/os/windows/activitiescache.py b/dissect/target/plugins/os/windows/activitiescache.py index 237c20fa99..ebdbad3319 100644 --- a/dissect/target/plugins/os/windows/activitiescache.py +++ b/dissect/target/plugins/os/windows/activitiescache.py @@ -120,41 +120,39 @@ def activitiescache(self) -> Iterator[ActivitiesCacheRecord]: clipboard_payload (string): ClipboardPayload field. """ for user, cache_file in self.cachefiles: - fh = cache_file.open() - db = SQLite3(fh) - - if table := db.table("Activity"): - for r in table.rows(): - yield ActivitiesCacheRecord( - start_time=mkts(r["[StartTime]"]), - end_time=mkts(r["[EndTime]"]), - last_modified_time=mkts(r["[LastModifiedTime]"]), - last_modified_on_client=mkts(r["[LastModifiedOnClient]"]), - original_last_modified_on_client=mkts(r["[OriginalLastModifiedOnClient]"]), - expiration_time=mkts(r["[ExpirationTime]"]), - activity_id=r["[Id]"].hex(), - app_id=r["[AppId]"], - enterprise_id=r["[EnterpriseId]"] or None, - app_activity_id=r["[AppActivityId]"], - group_app_activity_id=r["[GroupAppActivityId]"] or None, - group=r["[Group]"], - activity_type=r["[ActivityType]"], - activity_status=r["[ActivityStatus]"], - activity_priority=r["[Priority]"], - match_id=r["[MatchId]"], - etag=r["[ETag]"], - tag=r["[Tag]"], - is_local_only=r["[IsLocalOnly]"], - created_in_cloud=r["[CreatedInCloud]"], - platform_device_id=r["[PlatformDeviceId]"], - package_id_hash=r["[PackageIdHash]"], - payload=r["[Payload]"], - original_payload=r["[OriginalPayload]"], - clipboard_payload=r["[ClipboardPayload]"], - source=cache_file, - _target=self.target, - _user=user, - ) + with SQLite3(cache_file) as db: + if table := db.table("Activity"): + for r in table.rows(): + yield ActivitiesCacheRecord( + start_time=mkts(r["[StartTime]"]), + end_time=mkts(r["[EndTime]"]), + last_modified_time=mkts(r["[LastModifiedTime]"]), + last_modified_on_client=mkts(r["[LastModifiedOnClient]"]), + original_last_modified_on_client=mkts(r["[OriginalLastModifiedOnClient]"]), + expiration_time=mkts(r["[ExpirationTime]"]), + activity_id=r["[Id]"].hex(), + app_id=r["[AppId]"], + enterprise_id=r["[EnterpriseId]"] or None, + app_activity_id=r["[AppActivityId]"], + group_app_activity_id=r["[GroupAppActivityId]"] or None, + group=r["[Group]"], + activity_type=r["[ActivityType]"], + activity_status=r["[ActivityStatus]"], + activity_priority=r["[Priority]"], + match_id=r["[MatchId]"], + etag=r["[ETag]"], + tag=r["[Tag]"], + is_local_only=r["[IsLocalOnly]"], + created_in_cloud=r["[CreatedInCloud]"], + platform_device_id=r["[PlatformDeviceId]"], + package_id_hash=r["[PackageIdHash]"], + payload=r["[Payload]"], + original_payload=r["[OriginalPayload]"], + clipboard_payload=r["[ClipboardPayload]"], + source=cache_file, + _target=self.target, + _user=user, + ) def mkts(ts: int) -> datetime | None: diff --git a/dissect/target/plugins/os/windows/cam.py b/dissect/target/plugins/os/windows/cam.py index 7a8222ad51..11f7fab752 100644 --- a/dissect/target/plugins/os/windows/cam.py +++ b/dissect/target/plugins/os/windows/cam.py @@ -159,7 +159,7 @@ def _find_db(self) -> TargetPath | None: ) def _open_db(self) -> SQLite3 | None: - return SQLite3(self.camdb_path.open("rb")) if self.camdb_path else None + return SQLite3(self.camdb_path) if self.camdb_path else None def _build_context_dict(self) -> defaultdict[str, dict] | None: MAPDB = defaultdict(dict) diff --git a/dissect/target/plugins/os/windows/notifications.py b/dissect/target/plugins/os/windows/notifications.py index f079eed39c..6ccc35ce37 100644 --- a/dissect/target/plugins/os/windows/notifications.py +++ b/dissect/target/plugins/os/windows/notifications.py @@ -444,50 +444,50 @@ def wpndatabase(self) -> Iterator[WpnDatabaseNotificationRecord | WpnDatabaseNot target_tz = self.target.datetime.tzinfo for user, wpndatabase in self.wpndb_files: - db = SQLite3(wpndatabase.open()) - handlers = {} - - if table := db.table("NotificationHandler"): - for row in table.rows(): - handlers[row["[RecordId]"]] = WpnDatabaseNotificationHandlerRecord( - created_time=datetime.datetime.strptime(row["[CreatedTime]"], "%Y-%m-%d %H:%M:%S").replace( - tzinfo=target_tz - ), - modified_time=datetime.datetime.strptime(row["[ModifiedTime]"], "%Y-%m-%d %H:%M:%S").replace( - tzinfo=target_tz - ), - id=row["[RecordId]"], - primary_id=row["[PrimaryId]"], - wns_id=row["[WNSId]"], - handler_type=row["[HandlerType]"], - wnf_event_name=row["[WNFEventName]"], - system_data_property_set=row["[SystemDataPropertySet]"], - _target=self.target, - _user=user, - ) - - if table := db.table("Notification"): - for row in table.rows(): - record = WpnDatabaseNotificationRecord( - arrival_time=wintimestamp(row["[ArrivalTime]"]), - expiry_time=wintimestamp(row["[ExpiryTime]"]), - order=row["[Order]"], - id=row["[Id]"], - handler_id=row["[HandlerId]"], - activity_id=UUID(bytes=row["[ActivityId]"]), - type=row["[Type]"], - payload_data=row["[Payload]"], - payload_type=row["[PayloadType]"], - tag=row["[Tag]"], - group=row["[Group]"], - boot_id=row["[BootId]"], - expires_on_reboot=row["[ExpiresOnReboot]"] != "FALSE", - _target=self.target, - _user=user, - ) - handler = handlers.get(row["[HandlerId]"]) - - if handler: - yield GroupedRecord("windows/notification/wpndatabase/grouped", [record, handler]) - else: - yield record + with SQLite3(wpndatabase) as db: + handlers = {} + + if table := db.table("NotificationHandler"): + for row in table.rows(): + handlers[row["[RecordId]"]] = WpnDatabaseNotificationHandlerRecord( + created_time=datetime.datetime.strptime(row["[CreatedTime]"], "%Y-%m-%d %H:%M:%S").replace( + tzinfo=target_tz + ), + modified_time=datetime.datetime.strptime( + row["[ModifiedTime]"], "%Y-%m-%d %H:%M:%S" + ).replace(tzinfo=target_tz), + id=row["[RecordId]"], + primary_id=row["[PrimaryId]"], + wns_id=row["[WNSId]"], + handler_type=row["[HandlerType]"], + wnf_event_name=row["[WNFEventName]"], + system_data_property_set=row["[SystemDataPropertySet]"], + _target=self.target, + _user=user, + ) + + if table := db.table("Notification"): + for row in table.rows(): + record = WpnDatabaseNotificationRecord( + arrival_time=wintimestamp(row["[ArrivalTime]"]), + expiry_time=wintimestamp(row["[ExpiryTime]"]), + order=row["[Order]"], + id=row["[Id]"], + handler_id=row["[HandlerId]"], + activity_id=UUID(bytes=row["[ActivityId]"]), + type=row["[Type]"], + payload_data=row["[Payload]"], + payload_type=row["[PayloadType]"], + tag=row["[Tag]"], + group=row["[Group]"], + boot_id=row["[BootId]"], + expires_on_reboot=row["[ExpiresOnReboot]"] != "FALSE", + _target=self.target, + _user=user, + ) + handler = handlers.get(row["[HandlerId]"]) + + if handler: + yield GroupedRecord("windows/notification/wpndatabase/grouped", [record, handler]) + else: + yield record diff --git a/dissect/target/plugins/os/windows/search.py b/dissect/target/plugins/os/windows/search.py index dd1bf8c100..1f76e94509 100644 --- a/dissect/target/plugins/os/windows/search.py +++ b/dissect/target/plugins/os/windows/search.py @@ -166,9 +166,7 @@ def parse_esedb(self, path: Path, user_details: UserDetails | None) -> Iterator[ def parse_sqlite(self, path: Path, user_details: UserDetails | None) -> Iterator[SearchIndexRecords]: """Parse the SQLite3 ``SystemIndex_1_PropertyStore`` table.""" - with path.open("rb") as fh: - db = SQLite3(fh) - + with SQLite3(path) as db: # ``ColumnId`` is translated using the ``SystemIndex_1_PropertyStore_Metadata`` table. columns = { row.get("Id"): row.get("UniqueKey", "").split("-", maxsplit=1)[-1] diff --git a/pyproject.toml b/pyproject.toml index 17e86337c3..fe9f4f41cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ dependencies = [ "defusedxml", "dissect.cstruct>=4,<5", - "dissect.database>=1,<2", + "dissect.database>=1.1.dev3,<2", # TODO: update on release! "dissect.eventlog>=3,<4", "dissect.evidence>=3.13.dev2,<4", # TODO: update on release! "dissect.hypervisor>=3.20,<4", @@ -84,7 +84,7 @@ dev = [ "dissect.clfs[dev]>=1.0.dev,<2.0.dev", "dissect.cramfs[dev]>=1.0.dev,<2.0.dev", "dissect.cstruct>=4.0.dev,<5.0.dev", - "dissect.database[dev]>=1.0.dev,<2.0.dev", + "dissect.database[dev]>=1.1.dev3,<2.0.dev", # TODO: update on release! "dissect.etl[dev]>=3.0.dev,<4.0.dev", "dissect.eventlog[dev]>=3.0.dev,<4.0.dev", "dissect.evidence[dev]>=3.13.dev2,<4.0.dev",