From 9edced30e17d7159cf00dae05a32395a534ba3e9 Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 4 Mar 2026 16:41:34 +0100 Subject: [PATCH 1/4] fox-it/dissect/issues/99 ruff fix --- acquire/collector.py | 5 +---- acquire/dynamic/windows/collect.py | 1 - acquire/dynamic/windows/exceptions.py | 3 +++ acquire/dynamic/windows/handles.py | 2 -- acquire/dynamic/windows/ntdll.py | 4 +--- acquire/gui/__init__.py | 2 ++ acquire/hashes.py | 14 +++----------- acquire/outputs/__init__.py | 2 ++ acquire/outputs/base.py | 1 - acquire/outputs/zip.py | 1 - acquire/uploaders/minio.py | 1 - pyproject.toml | 16 +++++++++++++++- tests/_docs/conf.py | 2 ++ tests/test_acquire_profiles.py | 2 ++ 14 files changed, 31 insertions(+), 25 deletions(-) diff --git a/acquire/collector.py b/acquire/collector.py index ae6bb727..c4be7e78 100644 --- a/acquire/collector.py +++ b/acquire/collector.py @@ -682,7 +682,6 @@ def write_bytes(self, destination_path: str, data: bytes) -> None: def get_report_summary(report: CollectionReport) -> str: """Create a table-view report summary with success/failure/missing/empty counters per module""" - record_counts = report.get_counts_per_module_per_outcome() if not record_counts: @@ -748,11 +747,9 @@ def get_report_summary(report: CollectionReport) -> str: def get_full_formatted_report(report: CollectionReport, record_indent: int = 4) -> str: - """ - Create a full list of successful / failed / missing / empty artifacts collected, + """Create a full list of successful / failed / missing / empty artifacts collected, broken down by module. """ - record_line_template = "{record.outcome}: {record.artifact_type}:{record.artifact_value}" blocks = [] diff --git a/acquire/dynamic/windows/collect.py b/acquire/dynamic/windows/collect.py index 64f7f92a..01dbb0d6 100644 --- a/acquire/dynamic/windows/collect.py +++ b/acquire/dynamic/windows/collect.py @@ -24,7 +24,6 @@ def collect_named_objects(path: str = "\\") -> list[NamedObject]: Parameters: path: point to start searching from """ - try: dir_handle = open_directory_object(dir_name=path, root_handle=None) except AccessDeniedError: diff --git a/acquire/dynamic/windows/exceptions.py b/acquire/dynamic/windows/exceptions.py index 90fa2244..fef737de 100644 --- a/acquire/dynamic/windows/exceptions.py +++ b/acquire/dynamic/windows/exceptions.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + class WindowsDynamicError(Exception): pass diff --git a/acquire/dynamic/windows/handles.py b/acquire/dynamic/windows/handles.py index 0ba15456..f739846f 100644 --- a/acquire/dynamic/windows/handles.py +++ b/acquire/dynamic/windows/handles.py @@ -164,7 +164,6 @@ def _get_file_name_thread(h_file: HANDLE, queue: Queue) -> None: def get_handle_name(pid: int, handle: SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX) -> str | None: """Return handle name.""" - remote = pid != GetCurrentProcessId() if remote: @@ -268,7 +267,6 @@ def serialize_handles_into_csv(rows: Iterator[Handle], compress: bool = True) -> Serialize provided rows into normal or gzip-compressed CSV, and return a tuple containing the result bytes. """ - raw_buffer = io.BytesIO() buffer = gzip.GzipFile(fileobj=raw_buffer, mode="wb") if compress else raw_buffer diff --git a/acquire/dynamic/windows/ntdll.py b/acquire/dynamic/windows/ntdll.py index 2f645f65..f5ec7406 100644 --- a/acquire/dynamic/windows/ntdll.py +++ b/acquire/dynamic/windows/ntdll.py @@ -93,8 +93,7 @@ class ACCESS_MASK(IntFlag): class OBJ_ATTR(IntFlag): - """ - https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/shared/ntdef.h + """https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/shared/ntdef.h https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/object-handles """ @@ -189,7 +188,6 @@ def open_directory_object(dir_name: str, root_handle: HANDLE = None) -> HANDLE: dir_name: Specific directory we want to try and open. root_handle: From which point we want to start querying the object. """ - object_name = UNICODE_STRING.from_str(dir_name) p_name = ctypes.pointer(object_name) diff --git a/acquire/gui/__init__.py b/acquire/gui/__init__.py index 1c80ba49..8570b28d 100644 --- a/acquire/gui/__init__.py +++ b/acquire/gui/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from acquire.gui.base import GUI, GUIError __all__ = ["GUI", "GUIError"] diff --git a/acquire/hashes.py b/acquire/hashes.py index 127d87fd..e5b8e3f3 100644 --- a/acquire/hashes.py +++ b/acquire/hashes.py @@ -47,7 +47,6 @@ def get_paths_from_dir( extensions: set[str] | None = None, ) -> Iterator[Path]: """Yield paths that match provided `glob` pattern and `extensions` values""" - extension_suffixes = {f".{ext}" for ext in extensions} if extensions else None for path in target.fs.path("/").glob(glob): @@ -57,13 +56,11 @@ def get_paths_from_dir( def get_path_details(path: TargetPath, hash_funcs: Iterator[HashFunc] | None = None) -> tuple: - """ - Calculate and return the details for specified path. + """Calculate and return the details for specified path. The details include file size and hashes, calculated for hash functions provided in `hash_funcs`. """ - hash_funcs = hash_funcs or [] if hash_funcs: provided_hash_funcs_sorted = sorted(set(hash_funcs)) @@ -120,7 +117,6 @@ def filter_out_by_value_match( offsets: Iterator[int] = (0,), ) -> Iterator[Path]: """Filter out paths where file data matches the provided `value` at the specified offsets""" - if not offsets: raise ValueError("No offsets provided") @@ -158,8 +154,7 @@ def collect_hashes( specs: Iterator[Iterator[tuple]], path_filters: Iterator[Callable[[Iterator[Path]], Iterator[Path]]] | None = None, ) -> Iterator[tuple]: - """ - Walk through the paths, calculate hashes and return details per path. + """Walk through the paths, calculate hashes and return details per path. Spec contains a path selector and a list of hash functions to compute against the paths. For example:: @@ -169,7 +164,6 @@ def collect_hashes( (HashFunc.MD5, HashFunc.SHA1), ] """ - log.info("Starting to collect hashes for spec: %s", specs) stream_hash_func_pairs = [] @@ -225,11 +219,9 @@ def collect_hashes( def serialize_into_csv(rows: Iterator[list], compress: bool = True) -> tuple[int, bytes]: - """ - Serialize provided rows into normal or gzip-compressed CSV, and return a tuple + """Serialize provided rows into normal or gzip-compressed CSV, and return a tuple containing the number of rows processed and the result bytes. """ - raw_buffer = io.BytesIO() counter = 0 diff --git a/acquire/outputs/__init__.py b/acquire/outputs/__init__.py index 41b7e088..65f564b1 100644 --- a/acquire/outputs/__init__.py +++ b/acquire/outputs/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from acquire.outputs.dir import DirectoryOutput from acquire.outputs.tar import TAR_COMPRESSION_METHODS, TarOutput from acquire.outputs.zip import ZIP_COMPRESSION_METHODS, ZipOutput diff --git a/acquire/outputs/base.py b/acquire/outputs/base.py index 0d4c8910..3a01a6d0 100644 --- a/acquire/outputs/base.py +++ b/acquire/outputs/base.py @@ -70,7 +70,6 @@ def write_bytes( entry: The optional filesystem entry to write. size: The optional file size in bytes of the entry to write. """ - stream = io.BytesIO(data) self.write(output_path, stream, entry=entry, size=size) diff --git a/acquire/outputs/zip.py b/acquire/outputs/zip.py index 4ad16142..2709c3ed 100644 --- a/acquire/outputs/zip.py +++ b/acquire/outputs/zip.py @@ -111,7 +111,6 @@ def close(self) -> None: def _get_external_attr(self, entry: FilesystemEntry) -> int: """Return the appropriate external attributes of the entry.""" - # The Python zipfile module accepts the 16-bit "Mode" field (that stores st_mode field from # struct stat, containing user/group/other permissions, setuid/setgid and symlink info, etc) of the # ASi extra block for Unix as bits 16-31 of the external_attr diff --git a/acquire/uploaders/minio.py b/acquire/uploaders/minio.py index 839dee99..a7efd4a0 100644 --- a/acquire/uploaders/minio.py +++ b/acquire/uploaders/minio.py @@ -18,7 +18,6 @@ def __init__(self, upload: dict[str, str], **kwargs: dict[str, Any]) -> None: Raises: ValueError: When the configuration is invalid. """ - self.endpoint = upload.get("endpoint") self.access_id = upload.get("access_id") self.access_key = upload.get("access_key") diff --git a/pyproject.toml b/pyproject.toml index eb188d6b..25acb014 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,14 +115,28 @@ select = [ "PERF", "FURB", "RUF", + "D" ] -ignore = ["E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM105", "TRY003"] +ignore = [ + "E203", "B904", "UP024", "ANN002", "ANN003", "ANN204", "ANN401", "SIM105", "TRY003", + # Ignore some pydocstyle rules for now as they require a larger cleanup + "D1", + "D205", + "D301", + "D417", + # Seems bugged: https://github.com/astral-sh/ruff/issues/16824 + "D402", +] + +[tool.ruff.lint.pydocstyle] +convention = "google" [tool.ruff.lint.per-file-ignores] "tests/_docs/**" = ["INP001"] [tool.ruff.lint.isort] known-first-party = ["acquire"] +required-imports = ["from __future__ import annotations"] [tool.setuptools.packages.find] include = ["acquire", "acquire.*"] diff --git a/tests/_docs/conf.py b/tests/_docs/conf.py index ccf0b0bb..894634a8 100644 --- a/tests/_docs/conf.py +++ b/tests/_docs/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + project = "acquire" extensions = [ diff --git a/tests/test_acquire_profiles.py b/tests/test_acquire_profiles.py index bb4ad0ee..d35b7bef 100644 --- a/tests/test_acquire_profiles.py +++ b/tests/test_acquire_profiles.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from dissect.target.plugin import OSPlugin from dissect.target.plugins.os.default._os import DefaultOSPlugin From f93c5fb6f81808fa8c379495dfd7bf4365e2651e Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 4 Mar 2026 16:43:10 +0100 Subject: [PATCH 2/4] ruff fix unsafe --- acquire/acquire.py | 2 +- acquire/collector.py | 2 +- acquire/dynamic/windows/collect.py | 2 +- acquire/dynamic/windows/ntdll.py | 2 +- acquire/dynamic/windows/types.py | 2 +- acquire/hashes.py | 10 +++++----- acquire/uploaders/plugin_registry.py | 2 +- acquire/utils.py | 2 +- tests/test_acquire_profiles.py | 6 +++++- 9 files changed, 17 insertions(+), 13 deletions(-) diff --git a/acquire/acquire.py b/acquire/acquire.py index 2d18c35f..ce028137 100644 --- a/acquire/acquire.py +++ b/acquire/acquire.py @@ -245,7 +245,7 @@ def wrapper(module_cls: type[Module]) -> type[Module]: def local_module(cls: type[object]) -> object: - """A decorator that sets property `__local__` on a module class to mark it for local target only""" + """A decorator that sets property `__local__` on a module class to mark it for local target only.""" cls.__local__ = True return cls diff --git a/acquire/collector.py b/acquire/collector.py index c4be7e78..eb6bde04 100644 --- a/acquire/collector.py +++ b/acquire/collector.py @@ -681,7 +681,7 @@ def write_bytes(self, destination_path: str, data: bytes) -> None: def get_report_summary(report: CollectionReport) -> str: - """Create a table-view report summary with success/failure/missing/empty counters per module""" + """Create a table-view report summary with success/failure/missing/empty counters per module.""" record_counts = report.get_counts_per_module_per_outcome() if not record_counts: diff --git a/acquire/dynamic/windows/collect.py b/acquire/dynamic/windows/collect.py index 01dbb0d6..46aac640 100644 --- a/acquire/dynamic/windows/collect.py +++ b/acquire/dynamic/windows/collect.py @@ -44,7 +44,7 @@ def collect_named_objects(path: str = "\\") -> list[NamedObject]: def collect_open_handles(handle_types: list[NamedObject] | None = None) -> Iterator[Handle]: - """Collect open handles + """Collect open handles. Collect open handles and optionally provide a list to explicitly collect specific types of handles. diff --git a/acquire/dynamic/windows/ntdll.py b/acquire/dynamic/windows/ntdll.py index f5ec7406..f64b069b 100644 --- a/acquire/dynamic/windows/ntdll.py +++ b/acquire/dynamic/windows/ntdll.py @@ -168,7 +168,7 @@ def close_handle(handle: HANDLE) -> None: def validate_ntstatus(status: NTSTATUS) -> None: - """Validates the result status of a Nt call + """Validates the result status of a Nt call. Parameters: status: the return value of a ntcall diff --git a/acquire/dynamic/windows/types.py b/acquire/dynamic/windows/types.py index 737f76ab..56c4646c 100644 --- a/acquire/dynamic/windows/types.py +++ b/acquire/dynamic/windows/types.py @@ -167,7 +167,7 @@ class TOKEN_PRIVILEGES(ctypes.Structure): class Handle: - """Handle object""" + """Handle object.""" def __init__(self, handle: SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, handle_type: str, handle_name: str) -> None: self.name = handle_name diff --git a/acquire/hashes.py b/acquire/hashes.py index e5b8e3f3..62535b7a 100644 --- a/acquire/hashes.py +++ b/acquire/hashes.py @@ -46,7 +46,7 @@ def get_paths_from_dir( glob: str, extensions: set[str] | None = None, ) -> Iterator[Path]: - """Yield paths that match provided `glob` pattern and `extensions` values""" + """Yield paths that match provided `glob` pattern and `extensions` values.""" extension_suffixes = {f".{ext}" for ext in extensions} if extensions else None for path in target.fs.path("/").glob(glob): @@ -81,7 +81,7 @@ def get_path_details(path: TargetPath, hash_funcs: Iterator[HashFunc] | None = N def filter_out_nonfiles(paths: Iterator[Path]) -> Iterator[Path]: - """Filter out paths that are not files""" + """Filter out paths that are not files.""" for path in paths: try: is_file = path.is_file() @@ -96,7 +96,7 @@ def filter_out_nonfiles(paths: Iterator[Path]) -> Iterator[Path]: def filter_out_huge_files(paths: Iterator[Path], *, max_size_bytes: int) -> Iterator[Path]: - """Filter out paths that are larger than `max_size_bytes` value""" + """Filter out paths that are larger than `max_size_bytes` value.""" for path in paths: try: file_size = path.stat().st_size @@ -116,7 +116,7 @@ def filter_out_by_value_match( value: bytes, offsets: Iterator[int] = (0,), ) -> Iterator[Path]: - """Filter out paths where file data matches the provided `value` at the specified offsets""" + """Filter out paths where file data matches the provided `value` at the specified offsets.""" if not offsets: raise ValueError("No offsets provided") @@ -144,7 +144,7 @@ def filter_out_by_path_match( re_pattern: str, re_flags: re.RegexFlag = re.IGNORECASE, ) -> Iterator[Path]: - """Filter out paths that match provided regex pattern""" + """Filter out paths that match provided regex pattern.""" pattern = re.compile(re_pattern, flags=re_flags) return filter(lambda p: not pattern.match(str(p)), paths) diff --git a/acquire/uploaders/plugin_registry.py b/acquire/uploaders/plugin_registry.py index 6de78145..5475063e 100644 --- a/acquire/uploaders/plugin_registry.py +++ b/acquire/uploaders/plugin_registry.py @@ -53,7 +53,7 @@ def remove(self, name: str) -> None: self.plugins.pop(name) def items(self) -> ItemsView[str, T]: - """Returns all the items inside the ``plugins`` dictionary""" + """Returns all the items inside the ``plugins`` dictionary.""" return self.plugins.items() def get(self, name: str) -> T: diff --git a/acquire/utils.py b/acquire/utils.py index db49b11d..26c3a01e 100644 --- a/acquire/utils.py +++ b/acquire/utils.py @@ -31,7 +31,7 @@ class StrEnum(str, Enum): - """Sortable and serializible string-based enum""" + """Sortable and serializible string-based enum.""" def _create_profile_information(profiles: dict) -> str: diff --git a/tests/test_acquire_profiles.py b/tests/test_acquire_profiles.py index d35b7bef..0c6de2d0 100644 --- a/tests/test_acquire_profiles.py +++ b/tests/test_acquire_profiles.py @@ -1,7 +1,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest -from dissect.target.plugin import OSPlugin from dissect.target.plugins.os.default._os import DefaultOSPlugin from dissect.target.plugins.os.unix.linux._os import LinuxPlugin from dissect.target.plugins.os.unix.linux.fortios._os import FortiOSPlugin @@ -9,6 +10,9 @@ from acquire.acquire import PROFILES, _get_modules_for_profile +if TYPE_CHECKING: + from dissect.target.plugin import OSPlugin + @pytest.mark.parametrize( argnames=("os_plugin", "expected_value"), From ddcc7c17bd104947773d755688af0c9aa3facd58 Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 4 Mar 2026 16:47:01 +0100 Subject: [PATCH 3/4] Fix docstyle --- acquire/dynamic/windows/ntdll.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/acquire/dynamic/windows/ntdll.py b/acquire/dynamic/windows/ntdll.py index f64b069b..e3349db4 100644 --- a/acquire/dynamic/windows/ntdll.py +++ b/acquire/dynamic/windows/ntdll.py @@ -93,8 +93,11 @@ class ACCESS_MASK(IntFlag): class OBJ_ATTR(IntFlag): - """https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/shared/ntdef.h - https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/object-handles + """Drivers object handles values. + + References: + - https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/shared/ntdef.h + - https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/object-handles """ OBJ_INHERIT = 0x00000002 From 202af0247215037b4a1933e53f23b3a6e35b96ec Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 6 Mar 2026 16:06:25 +0100 Subject: [PATCH 4/4] Add pydocstyle rules --- acquire/acquire.py | 4 +++- acquire/dynamic/windows/collect.py | 7 +++++-- acquire/dynamic/windows/handles.py | 3 ++- acquire/tools/decrypter.py | 2 +- pyproject.toml | 9 ++++++++- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/acquire/acquire.py b/acquire/acquire.py index ce028137..3f0392b7 100644 --- a/acquire/acquire.py +++ b/acquire/acquire.py @@ -49,7 +49,7 @@ from acquire.log import get_file_handler, reconfigure_log_file, setup_logging from acquire.outputs import OUTPUTS from acquire.uploaders.minio import MinIO -from acquire.uploaders.plugin import UploaderPlugin, upload_files_using_uploader +from acquire.uploaders.plugin import upload_files_using_uploader from acquire.uploaders.plugin_registry import UploaderRegistry from acquire.utils import ( check_and_set_acquire_args, @@ -71,6 +71,8 @@ from dissect.target.filesystem import Filesystem + from acquire.uploaders.plugin import UploaderPlugin + try: from acquire.version import version except ImportError: diff --git a/acquire/dynamic/windows/collect.py b/acquire/dynamic/windows/collect.py index 46aac640..c557df7a 100644 --- a/acquire/dynamic/windows/collect.py +++ b/acquire/dynamic/windows/collect.py @@ -4,8 +4,8 @@ from typing import TYPE_CHECKING from acquire.dynamic.windows.exceptions import AccessDeniedError -from acquire.dynamic.windows.handles import Handle, get_handles -from acquire.dynamic.windows.named_objects import NamedObject, NamedObjectType +from acquire.dynamic.windows.handles import get_handles +from acquire.dynamic.windows.named_objects import NamedObjectType from acquire.dynamic.windows.ntdll import ( close_handle, open_directory_object, @@ -15,6 +15,9 @@ if TYPE_CHECKING: from collections.abc import Iterator + from acquire.dynamic.windows.handles import Handle + from acquire.dynamic.windows.named_objects import NamedObject + log = getLogger(__name__) diff --git a/acquire/dynamic/windows/handles.py b/acquire/dynamic/windows/handles.py index f739846f..6f6f63b6 100644 --- a/acquire/dynamic/windows/handles.py +++ b/acquire/dynamic/windows/handles.py @@ -5,7 +5,7 @@ import gzip import io import threading -from logging import Filter, LogRecord, getLogger +from logging import Filter, getLogger from queue import Empty, Queue from typing import TYPE_CHECKING @@ -39,6 +39,7 @@ if TYPE_CHECKING: from collections.abc import Iterator + from logging import LogRecord log = getLogger(__name__) diff --git a/acquire/tools/decrypter.py b/acquire/tools/decrypter.py index d4aec221..488d4419 100644 --- a/acquire/tools/decrypter.py +++ b/acquire/tools/decrypter.py @@ -16,7 +16,6 @@ from datetime import datetime, timezone from pathlib import Path from queue import Empty as QueueEmptyError -from queue import Queue from typing import TYPE_CHECKING, BinaryIO from urllib import request from urllib.error import HTTPError @@ -62,6 +61,7 @@ if TYPE_CHECKING: from collections.abc import Iterator + from queue import Queue from threading import Event log = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index 25acb014..7ef605d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,9 @@ acquire-decrypt = "acquire.tools.decrypter:main" line-length = 120 required-version = ">=0.13.1" +# Exclude setuptools_scm autogenerated file from linting +extend-exclude = ["acquire/version.py"] + [tool.ruff.format] docstring-code-format = true @@ -107,7 +110,7 @@ select = [ "SLOT", "SIM", "TID", - "TCH", + "TC", "PTH", "PLC", "TRY", @@ -127,10 +130,14 @@ ignore = [ # Seems bugged: https://github.com/astral-sh/ruff/issues/16824 "D402", ] +future-annotations = true [tool.ruff.lint.pydocstyle] convention = "google" +[tool.ruff.lint.flake8-type-checking] +strict = true + [tool.ruff.lint.per-file-ignores] "tests/_docs/**" = ["INP001"]