diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index efe6f99..f8ed89d 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -71,7 +71,6 @@ jobs: - name: Install Poetry env: PYTHON_BIN: ${{ matrix.python }} - POETRY_VERSION: "2.4.1" run: | set -eux if ! command -v pipx >/dev/null 2>&1; then @@ -81,11 +80,9 @@ jobs: if [ -z "${PIPX_BIN}" ]; then PIPX_BIN="${HOME}/.local/bin/pipx" fi - "${PIPX_BIN}" install --python "${PYTHON_BIN}" "poetry==${POETRY_VERSION}" + "${PIPX_BIN}" install --python "${PYTHON_BIN}" poetry==1.8.3 + /root/.local/bin/poetry --version echo "$HOME/.local/bin" >> "$GITHUB_PATH" - export PATH="$HOME/.local/bin:$PATH" - poetry --version - poetry --version | grep -E "Poetry \(version 2\." - name: Install project deps (including test extras) env: diff --git a/enroll/accounts.py b/enroll/accounts.py index 16890d6..b4f774b 100644 --- a/enroll/accounts.py +++ b/enroll/accounts.py @@ -146,12 +146,7 @@ def is_human_user(uid: int, shell: str, uid_min: int) -> bool: def find_user_ssh_files(home: str) -> List[str]: sshdir = os.path.join(home, ".ssh") out: List[str] = [] - # ``os.path.isdir`` follows symlinks, so a user who replaces ``~/.ssh`` - # with a link to a sensitive directory (e.g. /etc/ssl/private) could - # otherwise have a regular file inside it harvested through the symlinked - # parent. Refuse a symlinked .ssh outright; capture_file() applies the - # same parent-symlink protection at copy time as defense in depth. - if os.path.islink(sshdir) or not os.path.isdir(sshdir): + if not os.path.isdir(sshdir): return out ak = os.path.join(sshdir, "authorized_keys") diff --git a/enroll/capture.py b/enroll/capture.py index 66065e6..1edf405 100644 --- a/enroll/capture.py +++ b/enroll/capture.py @@ -5,7 +5,7 @@ import errno import stat from typing import List, Optional, Set -from .fsutil import open_no_follow_path, stat_triplet, stat_triplet_from_stat +from .fsutil import stat_triplet, stat_triplet_from_stat from .harvest_types import ExcludedFile, ManagedFile, ManagedLink from .ignore import IgnorePolicy from .pathfilter import PathFilter @@ -55,7 +55,10 @@ def files_differ(a: str, b: str, *, max_bytes: int = 2_000_000) -> bool: def _open_no_follow_write(path: str, mode: int = 0o600) -> int: - return open_no_follow_path(path, write=True, mode=mode) + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + return os.open(path, flags, mode) def write_bytes_into_bundle( @@ -89,10 +92,14 @@ def copy_into_bundle( symlinks at copy time and refuses destination symlink overwrites. """ + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + fd = -1 try: try: - fd = open_no_follow_path(abs_path) + fd = os.open(abs_path, flags) except OSError as e: if e.errno in {errno.ELOOP, errno.ENOTDIR}: raise OSError("refusing to copy symlink source") from e diff --git a/enroll/explain.py b/enroll/explain.py index 17f1b4b..84d5de0 100644 --- a/enroll/explain.py +++ b/enroll/explain.py @@ -189,12 +189,6 @@ _EXCLUDED_REASONS: Dict[str, ReasonInfo] = { "Not a regular file", "Excluded because it was not a regular file (device, socket, etc.).", ), - "symlink_component": ReasonInfo( - "Unsafe symlinked path", - "Excluded because a directory in the path was a symlink, which could " - "redirect capture into a sensitive location; Enroll refuses to follow " - "symlinked parents when harvesting files.", - ), "binary_like": ReasonInfo( "Binary-like", "Excluded because it looked like binary content (not useful for config management).", diff --git a/enroll/fsutil.py b/enroll/fsutil.py index b4f6c3d..6360058 100644 --- a/enroll/fsutil.py +++ b/enroll/fsutil.py @@ -1,131 +1,10 @@ from __future__ import annotations -import errno import hashlib import os -import stat from typing import Tuple -def open_no_follow_path(path: str, *, write: bool = False, mode: int = 0o600) -> int: - """Open ``path`` without following a symlink in *any* path component. - - ``O_NOFOLLOW`` only protects the final component of a path. A regular - file reached through a symlinked *parent* directory (for example a user - replacing ``~/.ssh`` with a link to a sensitive directory) would still be - opened by a plain ``os.open(path, O_NOFOLLOW)``. - - This helper resolves the path one component at a time with ``openat`` - semantics: - - - each intermediate component is opened relative to its parent's - descriptor without following symlinks; - - the final component is opened with ``O_NOFOLLOW`` (read, or - ``O_WRONLY | O_CREAT | O_EXCL`` when ``write`` is True). - - The important detail is that intermediate components are opened with - ``O_PATH | O_NOFOLLOW`` when ``O_PATH`` is available, and then verified - with ``fstat()``. On Linux, ``O_RDONLY | O_DIRECTORY | O_NOFOLLOW`` is not - sufficient for this job: a symlink whose target is a directory can still be - opened as the target directory on some kernels. Opening with ``O_PATH`` and - checking the resulting descriptor reliably exposes such a component as a - symlink instead. - - A symlink (or a ``..`` component) anywhere in the path raises - ``OSError(ELOOP)``. On platforms without ``openat``/``O_DIRECTORY`` - support, this falls back to a single ``O_NOFOLLOW`` open of the whole path, - which is no worse than the historical behaviour. - """ - - cloexec = getattr(os, "O_CLOEXEC", 0) - nofollow = getattr(os, "O_NOFOLLOW", 0) - o_directory = getattr(os, "O_DIRECTORY", 0) - o_path = getattr(os, "O_PATH", 0) - - if write: - final_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | cloexec | nofollow - else: - final_flags = os.O_RDONLY | cloexec | nofollow - - supports_openat = bool( - o_directory and nofollow and os.open in getattr(os, "supports_dir_fd", set()) - ) - if not supports_openat: - return os.open(path, final_flags, mode) - - absolute = path.startswith("/") - parts = [p for p in path.split("/") if p not in ("", ".")] - if not parts: - return os.open(path, final_flags, mode) - - *parent_parts, leaf = parts - - # Use O_PATH for directory descriptors when available. O_PATH descriptors - # can be used as dir_fd anchors for later openat-style calls, and with - # O_NOFOLLOW they let us fstat() a symlink component instead of silently - # following it. If O_PATH is unavailable, use O_RDONLY and an lstat() - # pre-check for intermediate components as a best-effort fallback. - dir_base_flags = (o_path if o_path else os.O_RDONLY) | cloexec | o_directory - component_flags = ( - (o_path if o_path else os.O_RDONLY) | cloexec | o_directory | nofollow - ) - - dir_fd = os.open("/" if absolute else ".", dir_base_flags) - try: - for component in parent_parts: - if component == "..": - raise OSError(errno.ELOOP, "unsafe '..' path component", path) - - if not o_path: - # Best-effort fallback for platforms without O_PATH. This is not - # as race-resistant as the descriptor-only path, but it avoids - # known symlink parents where we cannot open the component itself - # as a non-followed O_PATH descriptor. - try: - st = os.lstat(component, dir_fd=dir_fd) - except OSError: - raise - if stat.S_ISLNK(st.st_mode): - raise OSError(errno.ELOOP, "symlinked path component", path) - if not stat.S_ISDIR(st.st_mode): - raise OSError(errno.ENOTDIR, "non-directory path component", path) - - try: - next_fd = os.open(component, component_flags, dir_fd=dir_fd) - except OSError as e: - if e.errno in {errno.ELOOP, errno.ENOTDIR}: - try: - st = os.lstat(component, dir_fd=dir_fd) - except OSError: - raise - if stat.S_ISLNK(st.st_mode): - raise OSError( - errno.ELOOP, - "symlinked path component", - path, - ) from e - raise - - try: - st = os.fstat(next_fd) - if stat.S_ISLNK(st.st_mode): - raise OSError(errno.ELOOP, "symlinked path component", path) - if not stat.S_ISDIR(st.st_mode): - raise OSError(errno.ENOTDIR, "non-directory path component", path) - except Exception: - os.close(next_fd) - raise - - os.close(dir_fd) - dir_fd = next_fd - - if leaf == "..": - raise OSError(errno.ELOOP, "unsafe '..' path component", path) - return os.open(leaf, final_flags, mode, dir_fd=dir_fd) - finally: - os.close(dir_fd) - - def stat_triplet_from_stat(st: os.stat_result) -> Tuple[str, str, str]: """Return (owner, group, mode) for an existing stat result.""" diff --git a/enroll/harvest_collectors/paths.py b/enroll/harvest_collectors/paths.py index eda2d41..f11896a 100644 --- a/enroll/harvest_collectors/paths.py +++ b/enroll/harvest_collectors/paths.py @@ -5,13 +5,12 @@ import os from typing import Dict, List, Optional, Set from .. import harvest as h -from ..capture import capture_file, capture_link +from ..capture import capture_file from ..harvest_types import ( ExcludedFile, ExtraPathsSnapshot, ManagedDir, ManagedFile, - ManagedLink, UsrLocalCustomSnapshot, ) from ..system_paths import MAX_FILES_CAP @@ -133,7 +132,6 @@ class ExtraPathsCollector(HarvestCollector): self.notes: List[str] = [] self.excluded: List[ExcludedFile] = [] self.managed: List[ManagedFile] = [] - self.managed_links: List[ManagedLink] = [] self.managed_dirs: List[ManagedDir] = [] self.dir_seen: Set[str] = set() @@ -180,53 +178,28 @@ class ExtraPathsCollector(HarvestCollector): exclude_patterns=self.exclude_specs, managed_dirs=self.managed_dirs, managed_files=self.managed, - managed_links=self.managed_links, excluded=self.excluded, notes=self.notes, ) def _collect_included_dirs(self) -> None: - role_seen = self.seen_by_role.setdefault(self.role_name, set()) for pat in self.context.path_filter.iter_include_patterns(): if pat.kind == "prefix": path = pat.value - if os.path.islink(path): - self._capture_included_link(path, role_seen) - elif os.path.isdir(path): - self._walk_and_capture_dirs(path, role_seen) + if os.path.isdir(path) and not os.path.islink(path): + self._walk_and_capture_dirs(path) elif pat.kind == "glob": for hit in glob.glob(pat.value, recursive=True): - if os.path.islink(hit): - self._capture_included_link(hit, role_seen) - elif os.path.isdir(hit): - self._walk_and_capture_dirs(hit, role_seen) + if os.path.isdir(hit) and not os.path.islink(hit): + self._walk_and_capture_dirs(hit) - def _capture_included_link(self, path: str, role_seen: Set[str]) -> None: - path = os.path.normpath(path) - if not path.startswith("/"): - path = "/" + path - if path in self.already_all: - return - if capture_link( - role_name=self.role_name, - abs_path=path, - reason="user_include_link", - policy=self.context.policy, - path_filter=self.context.path_filter, - managed_out=self.managed_links, - excluded_out=self.excluded, - seen_role=role_seen, - seen_global=self.context.captured_global, - ): - self.already_all.add(path) - - def _walk_and_capture_dirs(self, root: str, role_seen: Set[str]) -> None: + def _walk_and_capture_dirs(self, root: str) -> None: root = os.path.normpath(root) if not root.startswith("/"): root = "/" + root if not os.path.isdir(root) or os.path.islink(root): return - for dirpath, dirnames, filenames in os.walk(root, followlinks=False): + for dirpath, dirnames, _ in os.walk(root, followlinks=False): if len(self.managed_dirs) >= MAX_FILES_CAP: self.notes.append( f"Reached directory cap ({MAX_FILES_CAP}) while scanning {root}." @@ -270,17 +243,7 @@ class ExtraPathsCollector(HarvestCollector): pruned: List[str] = [] for dirname in dirnames: path = os.path.join(dirpath, dirname) - if self.context.path_filter.is_excluded(path): - continue - if os.path.islink(path): - self._capture_included_link(path, role_seen) + if os.path.islink(path) or self.context.path_filter.is_excluded(path): continue pruned.append(dirname) dirnames[:] = pruned - - for filename in filenames: - path = os.path.join(dirpath, filename) - if self.context.path_filter.is_excluded(path): - continue - if os.path.islink(path): - self._capture_included_link(path, role_seen) diff --git a/enroll/ignore.py b/enroll/ignore.py index eed4035..4e64984 100644 --- a/enroll/ignore.py +++ b/enroll/ignore.py @@ -8,8 +8,6 @@ import stat from dataclasses import dataclass from typing import Optional -from .fsutil import open_no_follow_path - DEFAULT_DENY_GLOBS = [ # Common backup copies created by passwd tools (can contain sensitive data) @@ -99,34 +97,6 @@ BLOCK_START = b"/*" BLOCK_END = b"*/" -def normalize_for_match(path: str) -> str: - """Lexically normalize a path string for deny/allow glob matching. - - This collapses redundant separators ("//"), resolves "." and ".." - segments, and strips trailing slashes using ``os.path.normpath`` -- a - pure string operation that never touches the filesystem. - - It is deliberately NOT ``os.path.realpath``/``Path.resolve``: resolving - symlinks would stat the filesystem and reintroduce a time-of-check / - time-of-use window before the later ``O_NOFOLLOW`` open in - ``inspect_file``. The goal here is only to stop a non-canonical *string* - (e.g. "/etc//shadow" or "/etc/foo/../shadow") from slipping past a deny - glob like "/etc/shadow". It is defense-in-depth on top of the no-follow - open, not a load-bearing control by itself. - - ``normpath`` preserves a leading "//" because POSIX treats it as - implementation-defined; for glob matching we collapse it to a single - leading slash so patterns anchored at "/" still match. - """ - - if not path: - return path - normalized = os.path.normpath(path) - if normalized.startswith("//") and not normalized.startswith("///"): - normalized = normalized[1:] - return normalized - - @dataclass(frozen=True) class FileInspection: """Bytes and metadata captured from one safely-opened source file.""" @@ -175,31 +145,26 @@ class IgnorePolicy: yield raw def _path_deny_reason(self, path: str) -> Optional[str]: - # Match against a lexically-normalized path so non-canonical spellings - # (e.g. "/etc//shadow", "/etc/foo/../shadow") cannot slip past a deny - # glob. The original path is still what gets opened/recorded. - match_path = normalize_for_match(path) # Always ignore plain *.log files (rarely useful as config, often noisy). - if match_path.endswith(".log"): + if path.endswith(".log"): return "log_file" # Ignore editor/backup files that end with a trailing tilde. - if match_path.endswith("~"): + if path.endswith("~"): return "backup_file" # Ignore backup shadow files - if match_path.startswith("/etc/") and match_path.endswith("-"): + if path.startswith("/etc/") and path.endswith("-"): return "backup_file" if not self.dangerous: for g in self.deny_globs or []: - if fnmatch.fnmatch(match_path, g): + if fnmatch.fnmatch(path, g): return "denied_path" return None def _content_deny_reason(self, path: str, data: bytes) -> Optional[str]: if b"\x00" in data: - match_path = normalize_for_match(path) for g in self.allow_binary_globs or []: - if fnmatch.fnmatch(match_path, g): + if fnmatch.fnmatch(path, g): # Binary is acceptable for explicitly-allowed paths. return None return "binary_like" @@ -215,33 +180,26 @@ class IgnorePolicy: def inspect_file(self, path: str) -> tuple[Optional[str], Optional[FileInspection]]: """Safely inspect a regular file and return the exact bytes to copy. - The source is opened with O_NOFOLLOW on every path component (see - ``fsutil.open_no_follow_path``), fstat() is taken from that file - descriptor, and the whole file is read only after the size cap passes. - With the default 256 KiB cap this avoids a memory DoS while ensuring - secret scanning covers every byte that may be copied. - - Opening every component without following symlinks means a regular - file reached through a symlinked *parent* directory is refused with - ``symlink_component`` rather than silently captured -- its logical - path would not have matched the deny globs. + The source is opened with O_NOFOLLOW where available, fstat() is taken + from that file descriptor, and the whole file is read only after the + size cap passes. With the default 256 KiB cap this avoids a memory DoS + while ensuring secret scanning covers every byte that may be copied. """ deny = self._path_deny_reason(path) if deny: return deny, None + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) + if hasattr(os, "O_NOFOLLOW"): + flags |= os.O_NOFOLLOW + fd: Optional[int] = None try: try: - fd = open_no_follow_path(path) + fd = os.open(path, flags) except OSError as e: - if e.errno == errno.ELOOP: - # A symlink (or unsafe '..') somewhere in the path. This is - # distinct from "not a regular file" so operators can see - # why a path under a symlinked parent was skipped. - return "symlink_component", None - if e.errno == errno.ENOTDIR: + if e.errno in {errno.ELOOP, errno.ENOTDIR}: return "not_regular_file", None return "unreadable", None @@ -293,9 +251,8 @@ class IgnorePolicy: No size checks or content scanning are performed for directories. """ if not self.dangerous: - match_path = normalize_for_match(path) for g in self.deny_globs or []: - if fnmatch.fnmatch(match_path, g): + if fnmatch.fnmatch(path, g): return "denied_path" try: @@ -326,17 +283,16 @@ class IgnorePolicy: """ # Keep the same fast-path filename ignores as deny_reason(). - match_path = normalize_for_match(path) - if match_path.endswith(".log"): + if path.endswith(".log"): return "log_file" - if match_path.endswith("~"): + if path.endswith("~"): return "backup_file" - if match_path.startswith("/etc/") and match_path.endswith("-"): + if path.startswith("/etc/") and path.endswith("-"): return "backup_file" if not self.dangerous: for g in self.deny_globs or []: - if fnmatch.fnmatch(match_path, g): + if fnmatch.fnmatch(path, g): return "denied_path" try: diff --git a/poetry.lock b/poetry.lock index 135fa3e..95944e8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "attrs" @@ -6,7 +6,6 @@ version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, @@ -18,7 +17,6 @@ version = "5.0.0" description = "Modern password hashing for your software and your servers" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be"}, {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2"}, @@ -95,7 +93,6 @@ version = "2026.6.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" -groups = ["appimage"] files = [ {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, @@ -107,8 +104,6 @@ version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -205,7 +200,6 @@ version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["appimage"] files = [ {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, @@ -344,8 +338,6 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -353,110 +345,124 @@ files = [ [[package]] name = "coverage" -version = "7.14.2" +version = "7.14.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" -groups = ["dev"] files = [ - {file = "coverage-7.14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b75818e3046e9319143157f3dc4b43679a550c2060a17cbf3e39cc0b552925"}, - {file = "coverage-7.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66b08ba4c5cbf0eaa2e9692b203073f198d5d469d8b15d1c7a4854ce7032b2e2"}, - {file = "coverage-7.14.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:70f266b536c590060b707dddfb6cf9f17e24fd30b992242e774543d256265c43"}, - {file = "coverage-7.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb40cac5b1a6378fdccc99268f1033112ee4636e4fd9aaf240f6930d1fcea12c"}, - {file = "coverage-7.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c301fe9990cb5c081bf4881cb498743807c8e0e93fad7b85c02788456492ef8"}, - {file = "coverage-7.14.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d67b0462c8a3c3d93033e7c79cacdfc57d08e5220d9115bcb24a23edf5a5900d"}, - {file = "coverage-7.14.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e763087828ee9644f0c89c57f9b75f0a50fdf3e8f5d8fac5cfc351337e89a99"}, - {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6d4da2baab6d96ceedd9176b3c142e1198b0310bc8dc04e18a3caab65c3a322c"}, - {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab565a405bfdea61260145d8cc987aa66d1998fd0e0ccd4348008f4e6a39ee33"}, - {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c13230b688fbb9122251b74daa092175811eb64cb7bd1c98e2c8193dfa2b0bd5"}, - {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:014c83ba1ec97993cfe94e77fe6b56daa76bc0c218b86938971574c28942d044"}, - {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6caf54ffbf84b30470a8118f275afee9234e616572e4e41bae1dc19198c37294"}, - {file = "coverage-7.14.2-cp310-cp310-win32.whl", hash = "sha256:4bf9d8a35f77df5638c61b5012ba5225109ec1cc15bc5eb097036b3c3cc939f3"}, - {file = "coverage-7.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:c1f17a8caebe0facd4556b1e0adfe0987c17feebed88e7bb6b5365c45c84c5d6"}, - {file = "coverage-7.14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909f265c8c41f04c824bf741b2601fdcb56cab4bf56e018996b6494192ba0f58"}, - {file = "coverage-7.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8102deaf911938233f760426e6a5e287388521de95111d5c8de26c8a1028924"}, - {file = "coverage-7.14.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:851f49e7bd7d1cdaf328f3133942b252d5e3d3380690131f423cba8e435b87f5"}, - {file = "coverage-7.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04cb445bed86aaf00aaa97d41a8b6e30f100f21e81c34caaec4efc684cb57768"}, - {file = "coverage-7.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7471bc920d97c51c37ea8127f13b2adca43c3d78c53313b26a1f428e99d2c254"}, - {file = "coverage-7.14.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da5057e1bb257c967feee8ba67f3ebf379e801c7717f238b3d8c9caf00fc8f93"}, - {file = "coverage-7.14.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33c0da852e8a40246cd8e20cf3b2fc17ca52a45e9b5f7983c93db26f5d24b87b"}, - {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f48a85bb437fab7782021c40bfee6b15146928b96960d008ace41b6901a0f21d"}, - {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f44e7579a769a21d5b5e3166916bfe30ee175aaffff750324cbb11be2dbec5ad"}, - {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:78853ca3c6ca2f012daa2b07dbabbb8db0f09d4dbe8ee828d294b3445d3f4cd8"}, - {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c9c2795ee3692097ff226ab806005d36bb9691fca9b35353542b57ea749cc830"}, - {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2f5cc48a845d755b6db236f8c29c2b54773eb4c7e4ee2ead43812d73718784b0"}, - {file = "coverage-7.14.2-cp311-cp311-win32.whl", hash = "sha256:9c61cb7eaabcfa609c5bc0f5ff5869d72a2f02f17994e5fba5f971de516f3c82"}, - {file = "coverage-7.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e715909b0966d1774d8a26e14e2f4a3ae75909dca526901c6306286b2dcbfbdc"}, - {file = "coverage-7.14.2-cp311-cp311-win_arm64.whl", hash = "sha256:9193f7150937a4fd836b10eaa123e15d98e961d1fabac07e60adf2d4785f888a"}, - {file = "coverage-7.14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:37c94712e533ea06f0b1e4d934811c520b1914ce0e4da3916220717aa7a86bc6"}, - {file = "coverage-7.14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c050bbc7bba94c77e4ed7438f4fda1babe98ab145691d80aa6f60df934a1468b"}, - {file = "coverage-7.14.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a7af571767a2ee342a171c16fc1b1a07a0bf511606d381703fb7cf397fe49d46"}, - {file = "coverage-7.14.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240"}, - {file = "coverage-7.14.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c33e9e4878972f430b0cc06de3bf2a28d054a9efb4f8426d27de0d9cb81396ff"}, - {file = "coverage-7.14.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7967ea55c6dea6becba4d5870e2fa0aa4915a8be7ebff1bb79e6207aa75ce8d"}, - {file = "coverage-7.14.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1322f237c2979b84096f4239c17828ff17fea6b3bbe96c44381c5f587c44c26"}, - {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77849525340c99f516d793dddbcee16b18d50af892ac43c8de1a6f343d41e3b5"}, - {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef11695493ec3f06f7b2678ca274bcabb4ca04057317df268ddbfd8b05f661a8"}, - {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8134f0e0723e080d1c27bbe8fc149f0162e429fa1852482150015d0fce83eaf1"}, - {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:914eead2b843fc357f733b3fe39cc94f1b53d466e8cfe03080b1ed9d24ccfc73"}, - {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4b2d5e847fb7958583b74910cc19e5ec4ece514487385677b26433b2546116e"}, - {file = "coverage-7.14.2-cp312-cp312-win32.whl", hash = "sha256:e753db9e40dda7302e0ac3e1e6e1325fb7f7b4694f87a7314ab15dd5d57911a7"}, - {file = "coverage-7.14.2-cp312-cp312-win_amd64.whl", hash = "sha256:d32e5ca5f16dafb269ee50b60d32b00c704b3f6f78e238105f1d94a3a5f24bf5"}, - {file = "coverage-7.14.2-cp312-cp312-win_arm64.whl", hash = "sha256:dc366f158e2fb2add9d4e57338ca48f12611024278688ee657eb0b853fcb5de5"}, - {file = "coverage-7.14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e5f077641a6713ce9d38df9e85d4fb9e008677fc0775cbaeb32ddfc3b319d4ca"}, - {file = "coverage-7.14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0907f39b49ae818fe8af50aaa0f19afbc8ca164aea0865181ca7af17a3ac690b"}, - {file = "coverage-7.14.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734d47669118d75c28981e562d4530ceb77342d31ffef6def5edd5ad4f05d7b"}, - {file = "coverage-7.14.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e"}, - {file = "coverage-7.14.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f0a80f4c8ac3f774210b1cc1bc0e31e75502f2818dda9a144ff90e702c4d91d"}, - {file = "coverage-7.14.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e66f3f22d6c1515ce70f2e7c3e9c6f3ff0ff33480125c9f9c53e8f6508e30f"}, - {file = "coverage-7.14.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6a2c37c3114f87ca7f10113756026eecb49656514debad600dcbec21f355ccea"}, - {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b16a7959d04b1497281c062c180413565c3f3469211d78799ad5b9a75f67796"}, - {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6466c6999545cf00c4c142dfcbbf2db396dc735f005dcf8f91d57e351a79472b"}, - {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c60915ebb8f562317ba5ff6b8c32e25c0882289b201a9f2fb2987f91efd95d8"}, - {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:33b830850488acbcd358c78a4fecfafe7031667b4da8ddff5546295dc962cdeb"}, - {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d0f845539230b8269aec902bc978b0cc403f52f002d18a04492efc943404d0bc"}, - {file = "coverage-7.14.2-cp313-cp313-win32.whl", hash = "sha256:a8ac51a2e441e9119b9395f4d893fbc4934c64c8ba58be9b9eaa85591249e548"}, - {file = "coverage-7.14.2-cp313-cp313-win_amd64.whl", hash = "sha256:039b264cdb31c44b48f9821e2afbf8f37df49e0fb837e24a942918b36c567e31"}, - {file = "coverage-7.14.2-cp313-cp313-win_arm64.whl", hash = "sha256:7f2ef591e381cc36b8e53334e1b842c760c520c8a52d01e8626209400e93fe6a"}, - {file = "coverage-7.14.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7a0d1f026b72d627fa5c8a57cbc86ad209b64aa2a65833c83b290ace5cbee126"}, - {file = "coverage-7.14.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4d2b86f81c1c9310a7e774e3cc9e927a3d0bf583ecbfa01498dd626930025428"}, - {file = "coverage-7.14.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d76bdc1f9396ae70a55d050cf9743d88141c62ce0a22a3f627fab1d11c2f8bc6"}, - {file = "coverage-7.14.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64"}, - {file = "coverage-7.14.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0904f3b79d7b845bef0715afe1900da634d12b97f05b9479cb472880ca07cb9c"}, - {file = "coverage-7.14.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6795ca4198d6cb7fc2c6163214f6555a6bc5f0ae1e268e76139dec4b37c4499"}, - {file = "coverage-7.14.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c41e9b60fc0fa57f5d73306417d2f9d668202cca6944f9435878c55a5e7ae213"}, - {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419d2aadd5746efc2e9df0f33c05570d8192e6f6a6098ab05acce586f44ce8a5"}, - {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c5d273c5f1411c0d26c4f066c398d4a434b1f97bb5fa409189bedce86d4add4"}, - {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fe465bc691264adce601527a972990c1174075d86bcbe9968fd20c95e0b1948"}, - {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6fbb61617af1c56f95d53170ae9fa6c9aef6de1abd02fcc50064bfc672efb18d"}, - {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e1eff22b831dfd5694989cc1f0789980f18391f614ac67c851af9a8e6d25e9ba"}, - {file = "coverage-7.14.2-cp314-cp314-win32.whl", hash = "sha256:58e91be0a233adef698d3e6be54f10401bb91fd7854c0d4c4d50e0d3711e72f1"}, - {file = "coverage-7.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:d8429bf97906bfe6c61f9dbfb3342e0d88120da61939da8bd04f830cc3eab3b8"}, - {file = "coverage-7.14.2-cp314-cp314-win_arm64.whl", hash = "sha256:13609d9d77249447aa73357b14831b0f3b95f275026c9ff20dd105f981f53a0c"}, - {file = "coverage-7.14.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9818486c2bac88ae931df7e04905ee29bef49fd218c00f5f02bed4855254a101"}, - {file = "coverage-7.14.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58055adffabfa243516a197aa9f85f0dd56d905b0fba1a10193269759c29ccb0"}, - {file = "coverage-7.14.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:535747dbc200349d7fb434cffcb28e770f0290f69b225f56dc3803aa7210cdea"}, - {file = "coverage-7.14.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:420c66e35d85c0ca5dc6a38147d83ef239762542900e5921ebbdb89333c540ea"}, - {file = "coverage-7.14.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2cf17b33773be446a588551ea6a746b2d70dd0bc90dc31f1dd7648975a63c6b"}, - {file = "coverage-7.14.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adb4a5fef041f7179bb264203add873c147d169cf2f8d0adae89ff2e51271bac"}, - {file = "coverage-7.14.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c012ec357dec9408a83dad5541172a63c5cfa1421709f2e5811480d31ae1b28"}, - {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dacd0ecd08fda3cb2f85b60cabea7da326dcb2fc15fbb23a88830a80144cc9f2"}, - {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f27e980f2feba5dfe7a32b22b125470de69c0bd113c75e16165de909a777f512"}, - {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:105c00efb65c863630b2b63cbf7b8267e4da2d44b62284efbb19a03b04c337d4"}, - {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:571173fa04c8e8d6235ab32ae67fecca97777e2e1b4a1a30f3022c34e397c1c1"}, - {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e532f34d42d1a421fa00ed6b7735d14ac2e340256c1bad26a5e1dc1252b0bed7"}, - {file = "coverage-7.14.2-cp314-cp314t-win32.whl", hash = "sha256:243971550fb46c3039257f75e65610002d84304c505f609bbd9779e20a653a0a"}, - {file = "coverage-7.14.2-cp314-cp314t-win_amd64.whl", hash = "sha256:60fb0ca084a92da96474b8b405a7ea76dfecac3c68db54383e7934b6f3871169"}, - {file = "coverage-7.14.2-cp314-cp314t-win_arm64.whl", hash = "sha256:36a0a3f42ed7dfdbca2a69a541519ffd5064a5692152fc0018109e74370d7345"}, - {file = "coverage-7.14.2-py3-none-any.whl", hash = "sha256:04d92589e481a8b68a005a5a1e0646a91c76f322c397c4635298c57cf63699b5"}, - {file = "coverage-7.14.2.tar.gz", hash = "sha256:7a2da3d81cfe17c18038c6d98e6592aa9147d596d056119b0ee612c3c8bd5230"}, + {file = "coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf"}, + {file = "coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550"}, + {file = "coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b"}, + {file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332"}, + {file = "coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59"}, + {file = "coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253"}, + {file = "coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f"}, + {file = "coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860"}, + {file = "coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df"}, + {file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9"}, + {file = "coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548"}, + {file = "coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e"}, + {file = "coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3"}, + {file = "coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c"}, + {file = "coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad"}, + {file = "coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02"}, + {file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a"}, + {file = "coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1"}, + {file = "coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e"}, + {file = "coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a"}, + {file = "coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793"}, + {file = "coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be"}, + {file = "coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d"}, + {file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33"}, + {file = "coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c"}, + {file = "coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416"}, + {file = "coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42"}, + {file = "coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d"}, + {file = "coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2"}, + {file = "coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69"}, + {file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54"}, + {file = "coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1"}, + {file = "coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce"}, + {file = "coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1"}, + {file = "coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee"}, + {file = "coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851"}, + {file = "coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4"}, + {file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d"}, + {file = "coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee"}, + {file = "coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7"}, + {file = "coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343"}, + {file = "coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1"}, + {file = "coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65"}, + {file = "coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890"}, + {file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd"}, + {file = "coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e"}, + {file = "coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c"}, + {file = "coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af"}, + {file = "coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2"}, + {file = "coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +toml = ["tomli"] [[package]] name = "cryptography" @@ -464,7 +470,6 @@ version = "49.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.9" -groups = ["main"] files = [ {file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"}, {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"}, @@ -516,7 +521,7 @@ files = [ [package.dependencies] cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} -typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11\""} [package.extras] ssh = ["bcrypt (>=3.1.5)"] @@ -527,7 +532,6 @@ version = "5.0" description = "A library for working with .desktop files" optional = false python-versions = ">=3.10" -groups = ["appimage"] files = [ {file = "desktop_entry_lib-5.0-py3-none-any.whl", hash = "sha256:e60a0c2c5e42492dbe5378e596b1de87d1b1c4dc74d1f41998a164ee27a1226f"}, {file = "desktop_entry_lib-5.0.tar.gz", hash = "sha256:9a621bac1819fe21021356e41fec0ac096ed56e6eb5dcfe0639cd8654914b864"}, @@ -542,8 +546,6 @@ version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, @@ -561,7 +563,6 @@ version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" -groups = ["appimage"] files = [ {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, @@ -576,7 +577,6 @@ version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" -groups = ["dev"] files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -588,7 +588,6 @@ version = "3.0.3" description = "Pythonic task execution" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"}, {file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"}, @@ -600,7 +599,6 @@ version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, @@ -608,7 +606,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.3.6" +jsonschema-specifications = ">=2023.03.6" referencing = ">=0.28.4" rpds-py = ">=0.25.0" @@ -622,7 +620,6 @@ version = "2025.9.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, @@ -637,7 +634,6 @@ version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, @@ -649,7 +645,6 @@ version = "5.0.0" description = "SSH2 protocol library" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c"}, {file = "paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79"}, @@ -667,7 +662,6 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -683,8 +677,6 @@ version = "3.0" description = "C parser in Python" optional = false python-versions = ">=3.10" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, @@ -696,7 +688,6 @@ version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, @@ -711,7 +702,6 @@ version = "1.6.2" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"}, {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"}, @@ -753,7 +743,6 @@ version = "4.2" description = "Generate AppImages from your Python projects" optional = false python-versions = ">=3.9" -groups = ["appimage"] files = [ {file = "pyproject_appimage-4.2-py3-none-any.whl", hash = "sha256:d6892643db5759dc06531a4546bdab404a519c63814c060f8749979a8625d9cc"}, {file = "pyproject_appimage-4.2.tar.gz", hash = "sha256:6b6387250cb1e6ecbb08a13f5810749396ebe8637f2f35bf2296bfdd5e65cd6e"}, @@ -770,7 +759,6 @@ version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, @@ -794,7 +782,6 @@ version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, @@ -813,7 +800,6 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -896,7 +882,6 @@ version = "0.37.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, @@ -913,7 +898,6 @@ version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" -groups = ["appimage"] files = [ {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, @@ -935,7 +919,6 @@ version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, @@ -1060,7 +1043,6 @@ version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["appimage", "dev"] files = [ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, @@ -1110,7 +1092,6 @@ files = [ {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] -markers = {appimage = "python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\""} [[package]] name = "typing-extensions" @@ -1118,12 +1099,10 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\""} [[package]] name = "urllib3" @@ -1131,19 +1110,18 @@ version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" -groups = ["appimage"] files = [ {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +zstd = ["backports-zstd (>=1.0.0)"] [metadata] -lock-version = "2.1" -python-versions = ">=3.10" -content-hash = "84ca97970dccb13020927ea329bab59d43e8e8c954befe06c8048caa3cc8020b" +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "30e16396439f2cdd69005a5b7bdf8144aac33422a77a63accbc9eaa74151d851" diff --git a/pyproject.toml b/pyproject.toml index 7bc45d3..69d360f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,51 +1,34 @@ -[build-system] -requires = ["poetry-core>=2.0.0,<3.0.0"] -build-backend = "poetry.core.masonry.api" - -[project] +[tool.poetry] name = "enroll" version = "0.7.0b7" description = "Enroll a server's running state retrospectively into Ansible, Puppet or Salt" -readme = "README.md" -requires-python = ">=3.10" +authors = ["Miguel Jacq "] license = "GPL-3.0-or-later" -authors = [ - { name = "Miguel Jacq", email = "mig@mig5.net" }, -] -dependencies = [ - "PyYAML>=6,<7", - "paramiko>=3.5", - "jsonschema>=4.23,<5", +readme = "README.md" +packages = [{ include = "enroll" }] +repository = "https://git.mig5.net/mig5/enroll" +include = [ + { path = "enroll/schema/state.schema.json", format = ["sdist", "wheel"] } ] -[project.urls] -Repository = "https://git.mig5.net/mig5/enroll" +[tool.poetry.dependencies] +python = "^3.10" +pyyaml = "^6" +paramiko = ">=3.5" +jsonschema = "^4.23.0" -[project.scripts] +[tool.poetry.scripts] enroll = "enroll.cli:main" -[dependency-groups] -dev = [ - "pytest>=8,<9", - "pytest-cov>=5,<6", -] -appimage = [ - "pyproject-appimage>=4.2,<5", -] - -[tool.poetry] -requires-poetry = ">=2.0" -packages = [{ include = "enroll" }] -include = [ - { path = "enroll/schema/state.schema.json", format = ["sdist", "wheel"] }, -] - -[tool.poetry.group.dev] -optional = true - -[tool.poetry.group.appimage] -optional = true +[build-system] +requires = ["poetry-core>=1.8.0"] +build-backend = "poetry.core.masonry.api" [tool.pyproject-appimage] script = "enroll" output = "Enroll.AppImage" + +[tool.poetry.dev-dependencies] +pytest = "^8" +pytest-cov = "^5" +pyproject-appimage = "^4.2" diff --git a/pytests.sh b/pytests.sh index 8d2e42e..30bddb5 100755 --- a/pytests.sh +++ b/pytests.sh @@ -2,4 +2,4 @@ set -eou pipefail -poetry run python -m pytest -q tests -vvv --cov=enroll --cov-report=term-missing +poetry run pytest -q tests -vvv --cov=enroll --cov-report=term-missing diff --git a/tests.sh b/tests.sh index af27e9b..4d4a410 100755 --- a/tests.sh +++ b/tests.sh @@ -330,7 +330,7 @@ ensure_salt() { run_pytests() { section "Python unit tests" cd "${PROJECT_ROOT}" - run poetry run python -m pytest -vvvv --cov=enroll --cov-report=term-missing --disable-warnings + run poetry run pytest -vvvv --cov=enroll --cov-report=term-missing --disable-warnings } prepare_harvest_fixture() { diff --git a/tests/test_accounts.py b/tests/test_accounts.py index ac12774..a78c5f6 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -221,25 +221,6 @@ def test_find_user_ssh_files_ignores_symlink(tmp_path: Path): assert result == [] -def test_find_user_ssh_files_ignores_symlinked_ssh_dir(tmp_path: Path): - """A user who replaces ~/.ssh with a symlink to a sensitive directory must - not have files inside it harvested through the symlinked parent. os.path.isdir - follows symlinks, so the directory itself must be checked with islink(). - """ - - from enroll.accounts import find_user_ssh_files - - sensitive = tmp_path / "sensitive" - sensitive.mkdir() - (sensitive / "authorized_keys").write_text("ssh-rsa AAAA...\n", encoding="utf-8") - - home = tmp_path / "home" / "mallory" - home.mkdir(parents=True) - os.symlink(str(sensitive), str(home / ".ssh")) - - assert find_user_ssh_files(str(home)) == [] - - def test_find_user_ssh_files_handles_home_not_starting_with_slash(): from enroll.accounts import find_user_ssh_files diff --git a/tests/test_fsutil.py b/tests/test_fsutil.py index 7b5771b..ebe2224 100644 --- a/tests/test_fsutil.py +++ b/tests/test_fsutil.py @@ -23,54 +23,3 @@ def test_stat_triplet_reports_mode(tmp_path: Path): assert mode == "0600" assert owner # non-empty string assert group # non-empty string - - -def test_open_no_follow_path_reads_regular_file(tmp_path: Path): - from enroll.fsutil import open_no_follow_path - - nested = tmp_path / "a" / "b" - nested.mkdir(parents=True) - f = nested / "file.txt" - f.write_text("hello\n", encoding="utf-8") - - fd = open_no_follow_path(str(f)) - try: - assert os.read(fd, 100) == b"hello\n" - finally: - os.close(fd) - - -def test_open_no_follow_path_refuses_symlinked_parent(tmp_path: Path): - import errno - - from enroll.fsutil import open_no_follow_path - - real = tmp_path / "real" - real.mkdir() - (real / "file.txt").write_text("x\n", encoding="utf-8") - (tmp_path / "link").symlink_to(real) - - try: - fd = open_no_follow_path(str(tmp_path / "link" / "file.txt")) - os.close(fd) - raise AssertionError("expected OSError for symlinked parent") - except OSError as e: - assert e.errno == errno.ELOOP - - -def test_open_no_follow_path_refuses_symlinked_leaf(tmp_path: Path): - import errno - - from enroll.fsutil import open_no_follow_path - - target = tmp_path / "target.txt" - target.write_text("x\n", encoding="utf-8") - link = tmp_path / "link.txt" - link.symlink_to(target) - - try: - fd = open_no_follow_path(str(link)) - os.close(fd) - raise AssertionError("expected OSError for symlinked leaf") - except OSError as e: - assert e.errno == errno.ELOOP diff --git a/tests/test_harvest_collectors.py b/tests/test_harvest_collectors.py index f4de696..94b5259 100644 --- a/tests/test_harvest_collectors.py +++ b/tests/test_harvest_collectors.py @@ -394,53 +394,3 @@ def test_usr_local_custom_collector_scans_executable_bin_and_notes_cap( "usr_local_etc_custom", "usr_local_bin_script", ] - - -def test_extra_paths_collector_records_symlinks_without_following(tmp_path): - root = tmp_path / "include" - root.mkdir() - real_file = root / "real.conf" - real_file.write_text("ok", encoding="utf-8") - (root / "link.conf").symlink_to("real.conf") - - outside = tmp_path / "outside" - outside.mkdir() - (outside / "outside.conf").write_text("do-not-follow", encoding="utf-8") - (root / "shared").symlink_to(outside, target_is_directory=True) - - ctx = _context(tmp_path, include=[str(root)]) - result = ExtraPathsCollector( - ctx, - seen_by_role={}, - already_all=set(), - include_paths=[str(root)], - ).collect() - - links = {(link.path, link.target, link.reason) for link in result.managed_links} - assert (str(root / "link.conf"), "real.conf", "user_include_link") in links - assert (str(root / "shared"), str(outside), "user_include_link") in links - - managed_files = {mf.path for mf in result.managed_files} - assert str(real_file) in managed_files - assert str(outside / "outside.conf") not in managed_files - - -def test_extra_paths_collector_records_include_path_that_is_symlink(tmp_path): - real_root = tmp_path / "real" - real_root.mkdir() - (real_root / "inside.conf").write_text("do-not-follow", encoding="utf-8") - link_root = tmp_path / "linked-root" - link_root.symlink_to(real_root, target_is_directory=True) - - ctx = _context(tmp_path, include=[str(link_root)]) - result = ExtraPathsCollector( - ctx, - seen_by_role={}, - already_all=set(), - include_paths=[str(link_root)], - ).collect() - - assert [(link.path, link.target, link.reason) for link in result.managed_links] == [ - (str(link_root), str(real_root), "user_include_link") - ] - assert result.managed_files == [] diff --git a/tests/test_harvest_safety.py b/tests/test_harvest_safety.py index ef01cc5..6c5b338 100644 --- a/tests/test_harvest_safety.py +++ b/tests/test_harvest_safety.py @@ -102,45 +102,7 @@ def test_capture_file_rejects_symlink_source_with_ignore_policy(tmp_path: Path): assert ok is False assert managed == [] - # Symlinked sources are now reported with the dedicated symlink_component - # reason (covers both symlinked leaves and symlinked parent directories), - # which is more precise than the old generic not_regular_file. - assert excluded and excluded[0].reason == "symlink_component" - - -def test_capture_file_rejects_symlinked_parent_with_ignore_policy(tmp_path: Path): - """O_NOFOLLOW only guards the final component. A regular file reached - through a symlinked *parent* directory must still be refused, otherwise a - file whose real location is deny-globbed could be captured while its - logical (recorded) path looks safe. - """ - - secret = tmp_path / "secretroot" - secret.mkdir() - (secret / "config").write_text("listen_port=8080\n", encoding="utf-8") - (tmp_path / "allowed").symlink_to(secret, target_is_directory=True) - bundle = tmp_path / "bundle" - bundle.mkdir() - - managed: list[ManagedFile] = [] - excluded: list[ExcludedFile] = [] - ok = capture_file( - bundle_dir=str(bundle), - role_name="role", - abs_path=str(tmp_path / "allowed" / "config"), - reason="test", - policy=IgnorePolicy(), - path_filter=PathFilter(), - managed_out=managed, - excluded_out=excluded, - ) - - assert ok is False - assert managed == [] - assert excluded and excluded[0].reason == "symlink_component" - # Nothing should have been written into the bundle. - artifact = bundle / "artifacts" / "role" / "allowed" / "config" - assert not artifact.exists() + assert excluded and excluded[0].reason == "not_regular_file" def test_prepare_new_private_dir_rejects_symlink_parent(tmp_path: Path): diff --git a/tests/test_ignore.py b/tests/test_ignore.py index 2f138ef..9c3fac8 100644 --- a/tests/test_ignore.py +++ b/tests/test_ignore.py @@ -236,10 +236,7 @@ def test_deny_reason_symlink_file(tmp_path: Path): link = tmp_path / "link" os.symlink(str(real_file), str(link)) reason = pol.deny_reason(str(link)) - # A symlinked path (final component or parent) is refused with the - # dedicated symlink_component reason so operators can tell symlink - # redirection apart from genuine non-regular files (sockets, devices). - assert reason == "symlink_component" + assert reason == "not_regular_file" def test_deny_reason_logs(tmp_path: Path): @@ -309,110 +306,3 @@ def test_secret_scan_reads_whole_file_under_size_cap(tmp_path): p = tmp_path / "large.conf" p.write_bytes(b"A" * 70_000 + b"\nlate_token = abc123\n") assert IgnorePolicy().deny_reason(str(p)) == "sensitive_content" - - -def test_normalize_for_match_collapses_noncanonical_paths(): - from enroll.ignore import normalize_for_match - - assert normalize_for_match("/etc/shadow") == "/etc/shadow" - assert normalize_for_match("/etc//shadow") == "/etc/shadow" - assert normalize_for_match("/etc/foo/../shadow") == "/etc/shadow" - assert normalize_for_match("/etc/./shadow") == "/etc/shadow" - assert normalize_for_match("/etc/shadow/") == "/etc/shadow" - # A leading "//" is POSIX-significant to normpath but must collapse for - # glob matching anchored at "/". - assert normalize_for_match("//etc/shadow") == "/etc/shadow" - # "///" collapses to "/" via normpath already; ensure we don't mangle it. - assert normalize_for_match("///etc/shadow") == "/etc/shadow" - # Empty stays empty (no crash). - assert normalize_for_match("") == "" - - -def test_deny_reason_denies_noncanonical_sensitive_paths(): - # Regression: non-canonical spellings of a denied path must still be denied - # rather than slipping past the deny glob. Defense-in-depth on top of the - # O_NOFOLLOW open in inspect_file(); see normalize_for_match(). - pol = IgnorePolicy() - assert pol._path_deny_reason("/etc//shadow") == "denied_path" - assert pol._path_deny_reason("/etc/foo/../shadow") == "denied_path" - assert pol._path_deny_reason("/etc/./shadow") == "denied_path" - assert pol._path_deny_reason("/etc/ssl/private/../private/key") == "denied_path" - assert pol._path_deny_reason("//etc/shadow") == "denied_path" - # A normal config path is unaffected. - assert pol._path_deny_reason("/etc/nginx/nginx.conf") is None - - -def test_deny_reason_dir_denies_noncanonical_sensitive_paths(): - pol = IgnorePolicy() - # normpath("/etc/ssl/private/../private") -> "/etc/ssl/private" which is the - # glob root itself, so use paths that still resolve to a child of it. - assert pol.deny_reason_dir("/etc/ssl/private/sub/../child") == "denied_path" - assert pol.deny_reason_dir("/etc//ssl/private/sub") == "denied_path" - - -def test_deny_reason_link_denies_noncanonical_sensitive_paths(): - pol = IgnorePolicy() - assert pol.deny_reason_link("/etc/ssh/../ssh/ssh_host_rsa_key") == "denied_path" - assert pol.deny_reason_link("/etc//ssh/ssh_host_ed25519_key") == "denied_path" - - -def test_noncanonical_backup_and_log_fastpaths(): - pol = IgnorePolicy() - assert pol._path_deny_reason("/var/log/foo/../bar.log") == "log_file" - assert pol._path_deny_reason("/etc/foo/../something~") == "backup_file" - assert pol._path_deny_reason("/etc//passwd-") == "backup_file" - - -def test_inspect_file_refuses_symlinked_parent_directory(tmp_path: Path): - """A regular file reached through a symlinked *parent* directory must be - refused, even though O_NOFOLLOW alone would only guard the final - component. Otherwise a file whose real location is deny-globbed (or whose - content is benign) could be captured while its logical path looks safe. - """ - - pol = IgnorePolicy() - secret = tmp_path / "secretroot" - secret.mkdir() - (secret / "config").write_text("listen_port=8080\n", encoding="utf-8") - (tmp_path / "allowed").symlink_to(secret) - - reason, inspection = pol.inspect_file(str(tmp_path / "allowed" / "config")) - assert reason == "symlink_component" - assert inspection is None - - -def test_inspect_file_refuses_denyglob_evasion_via_symlinked_parent(tmp_path: Path): - """The strongest variant: the real file lives under a deny-globbed dir, - but is reached via a symlinked parent so the *logical* path does not match - the deny glob. Content is non-secret-looking (DH params), so only the - parent-symlink check stands between the operator and disclosure. - """ - - pol = IgnorePolicy() - realdir = tmp_path / "ssl_private" - realdir.mkdir() - (realdir / "dhparam.pem").write_text( - "-----BEGIN DH PARAMETERS-----\nMII...\n-----END DH PARAMETERS-----\n", - encoding="utf-8", - ) - (tmp_path / "innocent").symlink_to(realdir) - - reason, inspection = pol.inspect_file(str(tmp_path / "innocent" / "dhparam.pem")) - assert reason == "symlink_component" - assert inspection is None - - -def test_inspect_file_still_captures_normal_nested_file(tmp_path: Path): - """Regression guard: ordinary files in real (non-symlinked) directories - must still be inspected and returned. - """ - - pol = IgnorePolicy() - nested = tmp_path / "etc" / "myapp" - nested.mkdir(parents=True) - (nested / "app.conf").write_text("workers=4\n", encoding="utf-8") - - reason, inspection = pol.inspect_file(str(nested / "app.conf")) - assert reason is None - assert inspection is not None - assert inspection.data == b"workers=4\n"