Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32bf09887a | |||
| 4559aa6c70 | |||
| 5932d22801 | |||
| 70c90c62ae | |||
| 5166894992 | |||
| 1e9806d2dc | |||
| 82db7a7d72 | |||
| 098b9c3ac6 | |||
| 5bf247c485 |
13 changed files with 1812 additions and 488 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,3 +1,18 @@
|
||||||
|
# 0.8.2
|
||||||
|
|
||||||
|
* Bump dependencies
|
||||||
|
* Make remote harvest zipapp stdlib-only
|
||||||
|
|
||||||
|
# 0.8.1
|
||||||
|
|
||||||
|
* Security: fix a TOCTOU in remote harvest.
|
||||||
|
|
||||||
|
# 0.8.0
|
||||||
|
|
||||||
|
* Security: keep sudo-created remote harvest bundles root-owned while root packages and hashes them, expose only the archive to the authenticated SSH uid, and verify the root-computed digest after download. This removes the post-harvest tampering window created by recursively chowning the bundle before packaging without making the plaintext archive world-readable.
|
||||||
|
* Security: enforce tar member limits while lazily parsing untrusted archives rather than after `TarFile.getmembers()` has already indexed the entire archive; count repeated `.` entries and cap remote compressed downloads as well.
|
||||||
|
* Security: apply aggregate byte and total filesystem-entry limits when freezing directory harvest bundles, reject symlinked bundle roots, and abort when files or discovered directories change during the copy, so direct directory inputs remain bounded and fail closed under mutation.
|
||||||
|
|
||||||
# 0.7.0
|
# 0.7.0
|
||||||
|
|
||||||
* BREAKING CHANGE: Remove the `enroll diff --enforce` option. Enroll no longer applies the old harvest state locally to repair drift; this avoids the risk of enforcing a potentially malicious or tampered harvest. To restore baseline state, regenerate a manifest from the trusted harvest and apply it yourself, or compare two `enroll diff` runs and act on the result.
|
* BREAKING CHANGE: Remove the `enroll diff --enforce` option. Enroll no longer applies the old harvest state locally to repair drift; this avoids the risk of enforcing a potentially malicious or tampered harvest. To restore baseline state, regenerate a manifest from the trusted harvest and apply it yourself, or compare two `enroll diff` runs and act on the result.
|
||||||
|
|
|
||||||
|
|
@ -364,9 +364,7 @@ poetry run enroll --help
|
||||||
|
|
||||||
My Forgejo doesn't currently support federation, so I haven't opened registration/login for issues.
|
My Forgejo doesn't currently support federation, so I haven't opened registration/login for issues.
|
||||||
|
|
||||||
Instead, email me (see `pyproject.toml`) or contact me on the Fediverse:
|
Instead, email me (see `pyproject.toml`).
|
||||||
|
|
||||||
https://goto.mig5.net/@mig5
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
20
debian/changelog
vendored
20
debian/changelog
vendored
|
|
@ -1,3 +1,23 @@
|
||||||
|
enroll (0.8.2) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Make remote harvest zipapp stdlib-only
|
||||||
|
|
||||||
|
-- Miguel Jacq <mig@mig5.net> Mon, 03 Aug 2026 16:00:00 +1000
|
||||||
|
|
||||||
|
enroll (0.8.1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Security: fix a TOCTOU in remote harvest.
|
||||||
|
|
||||||
|
-- Miguel Jacq <mig@mig5.net> Mon, 03 Aug 2026 15:20:00 +1000
|
||||||
|
|
||||||
|
enroll (0.8.0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Security: keep sudo-created remote harvest bundles root-owned while root packages and hashes them, expose only the archive to the authenticated SSH uid, and verify the root-computed digest after download. This removes the post-harvest tampering window created by recursively chowning the bundle before packaging without making the plaintext archive world-readable.
|
||||||
|
* Security: enforce tar member limits while lazily parsing untrusted archives rather than after `TarFile.getmembers()` has already indexed the entire archive; count repeated `.` entries and cap remote compressed downloads as well.
|
||||||
|
* Security: apply aggregate byte and total filesystem-entry limits when freezing directory harvest bundles, reject symlinked bundle roots, and abort when files or discovered directories change during the copy, so direct directory inputs remain bounded and fail closed under mutation.
|
||||||
|
|
||||||
|
-- Miguel Jacq <mig@mig5.net> Mon, 13 Jul 2026 10:00:00 +1000
|
||||||
|
|
||||||
enroll (0.7.0) unstable; urgency=medium
|
enroll (0.7.0) unstable; urgency=medium
|
||||||
|
|
||||||
* BREAKING CHANGE: Remove the `enroll diff --enforce` option. Enroll no longer applies the old harvest state locally to repair drift; this avoids the risk of enforcing a potentially malicious or tampered harvest. To restore baseline state, regenerate a manifest from the trusted harvest and apply it yourself, or compare two `enroll diff` runs and act on the result.
|
* BREAKING CHANGE: Remove the `enroll diff --enforce` option. Enroll no longer applies the old harvest state locally to repair drift; this avoids the risk of enforcing a potentially malicious or tampered harvest. To restore baseline state, regenerate a manifest from the trusted harvest and apply it yourself, or compare two `enroll diff` runs and act on the result.
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,38 @@ from .diff import (
|
||||||
post_webhook,
|
post_webhook,
|
||||||
send_email,
|
send_email,
|
||||||
)
|
)
|
||||||
from .explain import explain_state
|
|
||||||
from .harvest import harvest
|
from .harvest import harvest
|
||||||
from .harvest_safety import ensure_safe_output_parent, write_text_output_file
|
from .harvest_safety import ensure_safe_output_parent, write_text_output_file
|
||||||
from .manifest import manifest
|
|
||||||
from .remote import (
|
from .remote import (
|
||||||
remote_harvest,
|
remote_harvest,
|
||||||
RemoteSudoPasswordRequired,
|
RemoteSudoPasswordRequired,
|
||||||
RemoteSSHKeyPassphraseRequired,
|
RemoteSSHKeyPassphraseRequired,
|
||||||
)
|
)
|
||||||
from .sopsutil import SopsError, encrypt_file_binary
|
from .sopsutil import SopsError, encrypt_file_binary
|
||||||
from .validate import validate_harvest
|
|
||||||
from .version import get_enroll_version
|
from .version import get_enroll_version
|
||||||
|
|
||||||
|
|
||||||
|
def explain_state(*args, **kwargs):
|
||||||
|
"""Load the explain implementation only when that command is used."""
|
||||||
|
from .explain import explain_state as _explain_state
|
||||||
|
|
||||||
|
return _explain_state(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def manifest(*args, **kwargs):
|
||||||
|
"""Load manifest dependencies only when rendering a manifest."""
|
||||||
|
from .manifest import manifest as _manifest
|
||||||
|
|
||||||
|
return _manifest(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_harvest(*args, **kwargs):
|
||||||
|
"""Load jsonschema only when validation is requested."""
|
||||||
|
from .validate import validate_harvest as _validate_harvest
|
||||||
|
|
||||||
|
return _validate_harvest(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def _discover_config_path(argv: list[str]) -> Optional[Path]:
|
def _discover_config_path(argv: list[str]) -> Optional[Path]:
|
||||||
"""Return the config path to use, if any.
|
"""Return the config path to use, if any.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -314,9 +314,13 @@ def copy_safe_artifact_file(src: str | Path, dst: str | Path) -> None:
|
||||||
|
|
||||||
_FREEZE_MAX_FILE_BYTES = 64 * 1024 * 1024
|
_FREEZE_MAX_FILE_BYTES = 64 * 1024 * 1024
|
||||||
_FREEZE_MAX_FILES = 200_000
|
_FREEZE_MAX_FILES = 200_000
|
||||||
|
_FREEZE_MAX_ENTRIES = 200_000
|
||||||
|
_FREEZE_MAX_TOTAL_BYTES = 10 * 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
def _read_all_no_follow(abs_path: str) -> bytes:
|
def _read_all_no_follow(
|
||||||
|
abs_path: str, *, max_total_remaining: int | None = None
|
||||||
|
) -> tuple[bytes, int]:
|
||||||
"""Read a regular file's bytes via a no-follow, non-hardlinked open.
|
"""Read a regular file's bytes via a no-follow, non-hardlinked open.
|
||||||
|
|
||||||
Mirrors the harvest-side capture discipline: open every path component
|
Mirrors the harvest-side capture discipline: open every path component
|
||||||
|
|
@ -348,6 +352,11 @@ def _read_all_no_follow(abs_path: str) -> bytes:
|
||||||
raise ArtifactSafetyError(f"bundle file is hardlinked: {abs_path}")
|
raise ArtifactSafetyError(f"bundle file is hardlinked: {abs_path}")
|
||||||
if st.st_size > _FREEZE_MAX_FILE_BYTES:
|
if st.st_size > _FREEZE_MAX_FILE_BYTES:
|
||||||
raise ArtifactSafetyError(f"bundle file is too large to freeze: {abs_path}")
|
raise ArtifactSafetyError(f"bundle file is too large to freeze: {abs_path}")
|
||||||
|
if max_total_remaining is not None and st.st_size > max_total_remaining:
|
||||||
|
raise ArtifactSafetyError(
|
||||||
|
"bundle total file size exceeds the safe freeze limit "
|
||||||
|
f"({_FREEZE_MAX_TOTAL_BYTES} bytes)"
|
||||||
|
)
|
||||||
|
|
||||||
chunks: list[bytes] = []
|
chunks: list[bytes] = []
|
||||||
remaining = int(st.st_size)
|
remaining = int(st.st_size)
|
||||||
|
|
@ -357,7 +366,35 @@ def _read_all_no_follow(abs_path: str) -> bytes:
|
||||||
break
|
break
|
||||||
chunks.append(chunk)
|
chunks.append(chunk)
|
||||||
remaining -= len(chunk)
|
remaining -= len(chunk)
|
||||||
return b"".join(chunks)
|
|
||||||
|
# A no-follow descriptor prevents path substitution, but an owner of
|
||||||
|
# the source file can still modify the same inode while it is being
|
||||||
|
# copied. Fail closed if the file was truncated, extended, relinked, or
|
||||||
|
# written during the read instead of returning a mixed/partial snapshot.
|
||||||
|
after = os.fstat(fd)
|
||||||
|
before_identity = (
|
||||||
|
st.st_dev,
|
||||||
|
st.st_ino,
|
||||||
|
st.st_mode,
|
||||||
|
st.st_nlink,
|
||||||
|
st.st_size,
|
||||||
|
st.st_mtime_ns,
|
||||||
|
st.st_ctime_ns,
|
||||||
|
)
|
||||||
|
after_identity = (
|
||||||
|
after.st_dev,
|
||||||
|
after.st_ino,
|
||||||
|
after.st_mode,
|
||||||
|
after.st_nlink,
|
||||||
|
after.st_size,
|
||||||
|
after.st_mtime_ns,
|
||||||
|
after.st_ctime_ns,
|
||||||
|
)
|
||||||
|
if remaining != 0 or before_identity != after_identity:
|
||||||
|
raise ArtifactSafetyError(
|
||||||
|
f"bundle file changed while being frozen: {abs_path}"
|
||||||
|
)
|
||||||
|
return b"".join(chunks), int(st.st_size)
|
||||||
finally:
|
finally:
|
||||||
if fd is not None:
|
if fd is not None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -392,7 +429,13 @@ def freeze_directory_bundle(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
src_root = Path(bundle_dir).expanduser()
|
src_root = Path(bundle_dir).expanduser()
|
||||||
if not src_root.is_dir():
|
try:
|
||||||
|
root_st = src_root.lstat()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
raise ArtifactSafetyError(f"{label} is not a directory: {src_root}") from e
|
||||||
|
if stat.S_ISLNK(root_st.st_mode):
|
||||||
|
raise ArtifactSafetyError(f"{label} root is a symlink: {src_root}")
|
||||||
|
if not stat.S_ISDIR(root_st.st_mode):
|
||||||
raise ArtifactSafetyError(f"{label} is not a directory: {src_root}")
|
raise ArtifactSafetyError(f"{label} is not a directory: {src_root}")
|
||||||
|
|
||||||
td = tempfile.TemporaryDirectory(prefix="enroll-frozen-bundle-")
|
td = tempfile.TemporaryDirectory(prefix="enroll-frozen-bundle-")
|
||||||
|
|
@ -405,6 +448,8 @@ def freeze_directory_bundle(
|
||||||
pass
|
pass
|
||||||
|
|
||||||
file_count = 0
|
file_count = 0
|
||||||
|
entry_count = 0
|
||||||
|
total_bytes = 0
|
||||||
|
|
||||||
def _on_walk_error(exc: OSError) -> None:
|
def _on_walk_error(exc: OSError) -> None:
|
||||||
# os.walk() defaults to *silently swallowing* directory-listing
|
# os.walk() defaults to *silently swallowing* directory-listing
|
||||||
|
|
@ -440,26 +485,47 @@ def freeze_directory_bundle(
|
||||||
dp = cur_p / dname
|
dp = cur_p / dname
|
||||||
try:
|
try:
|
||||||
dst = dp.lstat()
|
dst = dp.lstat()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError as e:
|
||||||
dirs.remove(dname)
|
raise ArtifactSafetyError(
|
||||||
continue
|
f"{label} changed while being frozen; discovered "
|
||||||
|
f"directory disappeared: {dp}"
|
||||||
|
) from e
|
||||||
if stat.S_ISLNK(dst.st_mode):
|
if stat.S_ISLNK(dst.st_mode):
|
||||||
raise ArtifactSafetyError(
|
raise ArtifactSafetyError(
|
||||||
f"{label} contains a symlinked directory: {dp}"
|
f"{label} contains a symlinked directory: {dp}"
|
||||||
)
|
)
|
||||||
|
if not stat.S_ISDIR(dst.st_mode):
|
||||||
|
raise ArtifactSafetyError(
|
||||||
|
f"{label} changed while being frozen; discovered "
|
||||||
|
f"directory is no longer a directory: {dp}"
|
||||||
|
)
|
||||||
|
entry_count += 1
|
||||||
|
if entry_count > _FREEZE_MAX_ENTRIES:
|
||||||
|
raise ArtifactSafetyError(
|
||||||
|
f"{label} has too many filesystem entries to freeze safely"
|
||||||
|
)
|
||||||
|
|
||||||
rel_dir = cur_p.relative_to(src_root)
|
rel_dir = cur_p.relative_to(src_root)
|
||||||
target_dir = dst_root / rel_dir
|
target_dir = dst_root / rel_dir
|
||||||
target_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
target_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
|
||||||
for fname in files:
|
for fname in files:
|
||||||
|
entry_count += 1
|
||||||
|
if entry_count > _FREEZE_MAX_ENTRIES:
|
||||||
|
raise ArtifactSafetyError(
|
||||||
|
f"{label} has too many filesystem entries to freeze safely"
|
||||||
|
)
|
||||||
file_count += 1
|
file_count += 1
|
||||||
if file_count > _FREEZE_MAX_FILES:
|
if file_count > _FREEZE_MAX_FILES:
|
||||||
raise ArtifactSafetyError(
|
raise ArtifactSafetyError(
|
||||||
f"{label} has too many files to freeze safely"
|
f"{label} has too many files to freeze safely"
|
||||||
)
|
)
|
||||||
src_file = cur_p / fname
|
src_file = cur_p / fname
|
||||||
data = _read_all_no_follow(str(src_file))
|
data, source_size = _read_all_no_follow(
|
||||||
|
str(src_file),
|
||||||
|
max_total_remaining=_FREEZE_MAX_TOTAL_BYTES - total_bytes,
|
||||||
|
)
|
||||||
|
total_bytes += source_size
|
||||||
dst_file = target_dir / fname
|
dst_file = target_dir / fname
|
||||||
fd = open_no_follow_path(str(dst_file), write=True, mode=0o600)
|
fd = open_no_follow_path(str(dst_file), write=True, mode=0o600)
|
||||||
with os.fdopen(fd, "wb") as fh:
|
with os.fdopen(fd, "wb") as fh:
|
||||||
|
|
|
||||||
526
enroll/remote.py
526
enroll/remote.py
|
|
@ -190,16 +190,27 @@ def remote_harvest(
|
||||||
|
|
||||||
|
|
||||||
# Resource caps for untrusted tar extraction. These mirror the directory-bundle
|
# Resource caps for untrusted tar extraction. These mirror the directory-bundle
|
||||||
# freeze limits (see manifest_safety._FREEZE_MAX_FILES / _FREEZE_MAX_FILE_BYTES)
|
# freeze limits (see manifest_safety._FREEZE_MAX_ENTRIES /
|
||||||
|
# _FREEZE_MAX_FILE_BYTES)
|
||||||
# so a harvest delivered as a tarball is bounded the same way as one delivered as
|
# so a harvest delivered as a tarball is bounded the same way as one delivered as
|
||||||
# a directory. The total-size cap additionally guards against a decompression
|
# a directory. The total-size cap additionally guards against a decompression
|
||||||
# bomb whose members are each individually under the per-file cap.
|
# bomb whose members are each individually under the per-file cap.
|
||||||
_TAR_MAX_MEMBERS = 200_000
|
_TAR_MAX_MEMBERS = 200_000
|
||||||
_TAR_MAX_FILE_BYTES = 64 * 1024 * 1024
|
_TAR_MAX_FILE_BYTES = 64 * 1024 * 1024
|
||||||
_TAR_MAX_TOTAL_BYTES = 10 * 1024 * 1024 * 1024
|
_TAR_MAX_TOTAL_BYTES = 10 * 1024 * 1024 * 1024
|
||||||
|
_TAR_MAX_COMPRESSED_BYTES = 12 * 1024 * 1024 * 1024
|
||||||
_TAR_MAX_PATH_DEPTH = 64
|
_TAR_MAX_PATH_DEPTH = 64
|
||||||
|
|
||||||
|
|
||||||
|
def _check_tar_download_size(size: int) -> None:
|
||||||
|
"""Reject a remote tar stream before it can exhaust local disk."""
|
||||||
|
if size > _TAR_MAX_COMPRESSED_BYTES:
|
||||||
|
raise RuntimeError(
|
||||||
|
"remote harvest archive exceeds compressed download "
|
||||||
|
f"limit ({_TAR_MAX_COMPRESSED_BYTES} bytes)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _safe_extract_tar(tar: tarfile.TarFile, dest: Path) -> None:
|
def _safe_extract_tar(tar: tarfile.TarFile, dest: Path) -> None:
|
||||||
"""Safely extract a tar archive into dest.
|
"""Safely extract a tar archive into dest.
|
||||||
|
|
||||||
|
|
@ -217,21 +228,29 @@ def _safe_extract_tar(tar: tarfile.TarFile, dest: Path) -> None:
|
||||||
|
|
||||||
member_count = 0
|
member_count = 0
|
||||||
total_size = 0
|
total_size = 0
|
||||||
|
safe_members: list[tarfile.TarInfo] = []
|
||||||
|
|
||||||
for m in tar.getmembers():
|
# Iterate lazily. TarFile.getmembers() first scans and materialises the
|
||||||
name = m.name
|
# *entire* archive, which lets an abusive archive consume memory/CPU before
|
||||||
|
# our member-count or size limits are checked. Keeping only the already
|
||||||
# Some tar implementations include a top-level '.' entry when created
|
# validated, bounded prefix means the limits take effect while the archive
|
||||||
# with `tar -C <dir> .`. That's harmless and should be allowed.
|
# is being parsed rather than after it has all been indexed.
|
||||||
if name in {".", "./"}:
|
for m in tar:
|
||||||
continue
|
|
||||||
|
|
||||||
member_count += 1
|
member_count += 1
|
||||||
if member_count > _TAR_MAX_MEMBERS:
|
if member_count > _TAR_MAX_MEMBERS:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"tar archive has too many members (> {_TAR_MAX_MEMBERS})"
|
f"tar archive has too many members (> {_TAR_MAX_MEMBERS})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
name = m.name
|
||||||
|
|
||||||
|
# Some tar implementations include a top-level '.' entry when created
|
||||||
|
# with `tar -C <dir> .`. That's harmless and should be allowed, but it
|
||||||
|
# still counts against the member cap so repeated '.' entries cannot be
|
||||||
|
# used to bypass the archive-work limit.
|
||||||
|
if name in {".", "./"}:
|
||||||
|
continue
|
||||||
|
|
||||||
# Reject absolute paths and any '..' components up front.
|
# Reject absolute paths and any '..' components up front.
|
||||||
p = PurePosixPath(name)
|
p = PurePosixPath(name)
|
||||||
if p.is_absolute() or ".." in p.parts:
|
if p.is_absolute() or ".." in p.parts:
|
||||||
|
|
@ -259,14 +278,14 @@ def _safe_extract_tar(tar: tarfile.TarFile, dest: Path) -> None:
|
||||||
if member_path != dest and not str(member_path).startswith(str(dest) + os.sep):
|
if member_path != dest and not str(member_path).startswith(str(dest) + os.sep):
|
||||||
raise RuntimeError(f"Unsafe tar member path: {name}")
|
raise RuntimeError(f"Unsafe tar member path: {name}")
|
||||||
|
|
||||||
|
safe_members.append(m)
|
||||||
|
|
||||||
# Extract members one-by-one after validation. Pass an explicit tarfile
|
# Extract members one-by-one after validation. Pass an explicit tarfile
|
||||||
# extraction filter on Python versions that support it so Python 3.12/3.13
|
# extraction filter on Python versions that support it so Python 3.12/3.13
|
||||||
# do not warn about the Python 3.14 default changing. Keep the older call
|
# do not warn about the Python 3.14 default changing. Keep the older call
|
||||||
# path for Python 3.10/3.11, where the filter argument is unavailable.
|
# path for Python 3.10/3.11, where the filter argument is unavailable.
|
||||||
supports_filter = hasattr(tarfile, "data_filter")
|
supports_filter = hasattr(tarfile, "data_filter")
|
||||||
for m in tar.getmembers():
|
for m in safe_members:
|
||||||
if m.name in {".", "./"}:
|
|
||||||
continue
|
|
||||||
if supports_filter:
|
if supports_filter:
|
||||||
tar.extract(m, path=dest, filter="data")
|
tar.extract(m, path=dest, filter="data")
|
||||||
else:
|
else:
|
||||||
|
|
@ -281,8 +300,9 @@ def _build_enroll_pyz(tmpdir: Path) -> tuple[Path, str]:
|
||||||
|
|
||||||
Returns ``(pyz_path, sha256_hex)``. The digest is computed on the exact
|
Returns ``(pyz_path, sha256_hex)``. The digest is computed on the exact
|
||||||
bytes written locally so the caller can verify, on the remote side, that the
|
bytes written locally so the caller can verify, on the remote side, that the
|
||||||
file that is about to be executed as root is byte-for-byte the one we built
|
file that is executed is byte-for-byte the one we built (see
|
||||||
(see ``_remote_verify_pyz_sha256``). This is transport/staging integrity
|
``_remote_promote_verified_pyz`` and ``_remote_verify_pyz_sha256``). This is
|
||||||
|
transport/staging integrity
|
||||||
defence-in-depth: it detects a swap of the staged file between upload and
|
defence-in-depth: it detects a swap of the staged file between upload and
|
||||||
execution by anyone who gained write access to the staging directory. It is
|
execution by anyone who gained write access to the staging directory. It is
|
||||||
NOT a defence against a remote host that is already root-compromised -- such
|
NOT a defence against a remote host that is already root-compromised -- such
|
||||||
|
|
@ -368,6 +388,317 @@ def _sha256_file(path: Path) -> str:
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _remote_current_uid(ssh, *, remote_python: str) -> str:
|
||||||
|
"""Return the authenticated SSH account's numeric uid.
|
||||||
|
|
||||||
|
Use the same explicitly selected Python interpreter as the remote zipapp
|
||||||
|
instead of relying on a shell ``id`` binary resolved through the remote
|
||||||
|
account's PATH. The uid is used only to grant that exact account temporary
|
||||||
|
read access to a root-created harvest archive.
|
||||||
|
"""
|
||||||
|
|
||||||
|
script = "import os,sys;sys.stdout.write(str(os.getuid()))"
|
||||||
|
cmd = " ".join(shlex.quote(tok) for tok in (remote_python, "-I", "-c", script))
|
||||||
|
rc, out, err = _ssh_run(ssh, cmd, get_pty=False)
|
||||||
|
uid = out.strip()
|
||||||
|
if rc != 0 or not uid.isascii() or not uid.isdigit():
|
||||||
|
raise RuntimeError(
|
||||||
|
"Unable to determine the numeric uid of the authenticated SSH "
|
||||||
|
"account before exposing the remote harvest archive.\n"
|
||||||
|
f"Command: {cmd}\nExit code: {rc}\nStderr: {err.strip()}"
|
||||||
|
)
|
||||||
|
value = int(uid)
|
||||||
|
if value < 0 or value > 2**32 - 1:
|
||||||
|
raise RuntimeError(f"Remote SSH account returned an invalid uid: {uid}")
|
||||||
|
return uid
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_downloaded_archive_sha256(path: Path, expected_sha256: str) -> None:
|
||||||
|
"""Fail closed if a downloaded remote archive changed after root hashed it."""
|
||||||
|
|
||||||
|
downloaded_sha256 = _sha256_file(path)
|
||||||
|
if downloaded_sha256 != expected_sha256:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Remote harvest archive integrity check failed after download: "
|
||||||
|
"the archive changed after root packaged it. Refusing to extract "
|
||||||
|
"potentially tampered state.\n"
|
||||||
|
f" expected: {expected_sha256}\n"
|
||||||
|
f" received: {downloaded_sha256}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _remote_file_sha256_sudo(
|
||||||
|
ssh,
|
||||||
|
remote_path: str,
|
||||||
|
*,
|
||||||
|
remote_python: str,
|
||||||
|
sudo_password: Optional[str],
|
||||||
|
) -> str:
|
||||||
|
"""Hash a root-owned remote file before granting the SSH user access."""
|
||||||
|
|
||||||
|
hash_script = (
|
||||||
|
"import hashlib,sys;"
|
||||||
|
"h=hashlib.sha256();"
|
||||||
|
"f=open(sys.argv[1],'rb');"
|
||||||
|
"[h.update(c) for c in iter(lambda:f.read(1048576),b'')];"
|
||||||
|
"sys.stdout.write(h.hexdigest())"
|
||||||
|
)
|
||||||
|
cmd = " ".join(
|
||||||
|
shlex.quote(tok)
|
||||||
|
for tok in (remote_python, "-I", "-c", hash_script, remote_path)
|
||||||
|
)
|
||||||
|
rc, out, err = _ssh_run_sudo(ssh, cmd, sudo_password=sudo_password, get_pty=True)
|
||||||
|
digest = out.strip().lower()
|
||||||
|
if rc != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Failed to hash the root-created remote harvest archive.\n"
|
||||||
|
f"Command: sudo {cmd}\nExit code: {rc}\nStderr: {err.strip()}"
|
||||||
|
)
|
||||||
|
if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Remote harvest archive integrity check returned an invalid "
|
||||||
|
f"SHA-256 digest: {digest!r}"
|
||||||
|
)
|
||||||
|
return digest
|
||||||
|
|
||||||
|
|
||||||
|
_REMOTE_PROMOTE_PYZ_SCRIPT = r"""import hashlib
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def _promote(argv):
|
||||||
|
(
|
||||||
|
stage_dir,
|
||||||
|
source_name,
|
||||||
|
root_dir,
|
||||||
|
destination_name,
|
||||||
|
expected,
|
||||||
|
size_text,
|
||||||
|
owner_uid_text,
|
||||||
|
) = argv
|
||||||
|
expected_size = int(size_text)
|
||||||
|
expected_owner_uid = int(owner_uid_text)
|
||||||
|
|
||||||
|
for required_flag in ("O_DIRECTORY", "O_NOFOLLOW", "O_NONBLOCK"):
|
||||||
|
if not hasattr(os, required_flag):
|
||||||
|
raise RuntimeError(
|
||||||
|
"remote platform lacks required safe-open flag " + required_flag
|
||||||
|
)
|
||||||
|
|
||||||
|
if os.geteuid() != expected_owner_uid:
|
||||||
|
raise RuntimeError("promotion helper is not running as the expected user")
|
||||||
|
if not source_name or source_name in {".", ".."} or "/" in source_name:
|
||||||
|
raise RuntimeError("uploaded enroll.pyz name is not a single path component")
|
||||||
|
|
||||||
|
no_follow = os.O_NOFOLLOW
|
||||||
|
cloexec = getattr(os, "O_CLOEXEC", 0)
|
||||||
|
dir_flags = os.O_RDONLY | os.O_DIRECTORY | no_follow | cloexec
|
||||||
|
stage_fd = None
|
||||||
|
root_fd = None
|
||||||
|
source_fd = None
|
||||||
|
destination_fd = None
|
||||||
|
temporary_name = ".enroll.pyz.tmp"
|
||||||
|
temporary_created = False
|
||||||
|
destination_published = False
|
||||||
|
promotion_complete = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
stage_fd = os.open(stage_dir, dir_flags)
|
||||||
|
root_fd = os.open(root_dir, dir_flags)
|
||||||
|
|
||||||
|
root_stat = os.fstat(root_fd)
|
||||||
|
if not stat.S_ISDIR(root_stat.st_mode):
|
||||||
|
raise RuntimeError("destination path is not a directory")
|
||||||
|
if root_stat.st_uid != expected_owner_uid:
|
||||||
|
raise RuntimeError("destination directory has an unexpected owner")
|
||||||
|
if stat.S_IMODE(root_stat.st_mode) != 0o700:
|
||||||
|
raise RuntimeError("destination directory is not mode 0700")
|
||||||
|
|
||||||
|
source_fd = os.open(
|
||||||
|
source_name,
|
||||||
|
os.O_RDONLY | no_follow | os.O_NONBLOCK | cloexec,
|
||||||
|
dir_fd=stage_fd,
|
||||||
|
)
|
||||||
|
source_stat = os.fstat(source_fd)
|
||||||
|
if not stat.S_ISREG(source_stat.st_mode):
|
||||||
|
raise RuntimeError("uploaded enroll.pyz is not a regular file")
|
||||||
|
if source_stat.st_nlink != 1:
|
||||||
|
raise RuntimeError("uploaded enroll.pyz must not be hard-linked")
|
||||||
|
if source_stat.st_size != expected_size:
|
||||||
|
raise RuntimeError("uploaded enroll.pyz has an unexpected size")
|
||||||
|
|
||||||
|
# The directory was freshly created by root, so neither name should exist.
|
||||||
|
# O_EXCL protects the temporary name. Publishing with link() instead of
|
||||||
|
# rename() also fails rather than replacing an unexpected final path.
|
||||||
|
destination_fd = os.open(
|
||||||
|
temporary_name,
|
||||||
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL | no_follow | cloexec,
|
||||||
|
0o500,
|
||||||
|
dir_fd=root_fd,
|
||||||
|
)
|
||||||
|
temporary_created = True
|
||||||
|
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
copied = 0
|
||||||
|
while True:
|
||||||
|
# Read one byte beyond the expected length so concurrent growth cannot
|
||||||
|
# be hidden by stopping exactly at expected_size.
|
||||||
|
chunk = os.read(source_fd, min(1048576, expected_size - copied + 1))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
copied += len(chunk)
|
||||||
|
if copied > expected_size:
|
||||||
|
raise RuntimeError("uploaded enroll.pyz grew while being copied")
|
||||||
|
digest.update(chunk)
|
||||||
|
|
||||||
|
view = memoryview(chunk)
|
||||||
|
while view:
|
||||||
|
written = os.write(destination_fd, view)
|
||||||
|
if written <= 0:
|
||||||
|
raise RuntimeError("short write while promoting enroll.pyz")
|
||||||
|
view = view[written:]
|
||||||
|
|
||||||
|
if copied != expected_size:
|
||||||
|
raise RuntimeError("uploaded enroll.pyz changed size while being copied")
|
||||||
|
|
||||||
|
actual = digest.hexdigest()
|
||||||
|
if actual != expected:
|
||||||
|
raise RuntimeError(
|
||||||
|
"uploaded enroll.pyz SHA-256 mismatch: expected "
|
||||||
|
+ expected
|
||||||
|
+ ", received "
|
||||||
|
+ actual
|
||||||
|
)
|
||||||
|
|
||||||
|
os.fchmod(destination_fd, 0o500)
|
||||||
|
os.fsync(destination_fd)
|
||||||
|
os.close(destination_fd)
|
||||||
|
destination_fd = None
|
||||||
|
|
||||||
|
os.link(
|
||||||
|
temporary_name,
|
||||||
|
destination_name,
|
||||||
|
src_dir_fd=root_fd,
|
||||||
|
dst_dir_fd=root_fd,
|
||||||
|
follow_symlinks=False,
|
||||||
|
)
|
||||||
|
destination_published = True
|
||||||
|
os.unlink(temporary_name, dir_fd=root_fd)
|
||||||
|
temporary_created = False
|
||||||
|
os.fsync(root_fd)
|
||||||
|
promotion_complete = True
|
||||||
|
finally:
|
||||||
|
for fd in (destination_fd, source_fd, stage_fd):
|
||||||
|
if fd is not None:
|
||||||
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# On every failed path, remove anything this invocation created before the
|
||||||
|
# directory descriptor is closed. This leaves no executable partial copy.
|
||||||
|
if root_fd is not None and not promotion_complete:
|
||||||
|
if destination_published:
|
||||||
|
try:
|
||||||
|
os.unlink(destination_name, dir_fd=root_fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if temporary_created:
|
||||||
|
try:
|
||||||
|
os.unlink(temporary_name, dir_fd=root_fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if root_fd is not None:
|
||||||
|
try:
|
||||||
|
os.close(root_fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_promote(sys.argv[1:])
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _remote_promote_verified_pyz(
|
||||||
|
ssh,
|
||||||
|
uploaded_pyz_path: str,
|
||||||
|
root_tmp_dir: str,
|
||||||
|
expected_sha256: str,
|
||||||
|
expected_size: int,
|
||||||
|
*,
|
||||||
|
remote_python: str,
|
||||||
|
sudo_password: Optional[str],
|
||||||
|
) -> str:
|
||||||
|
"""Copy an uploaded zipapp into root-private storage and verify it there.
|
||||||
|
|
||||||
|
The SFTP upload initially lives in a directory controlled by the
|
||||||
|
authenticated SSH account. Hashing that path and later executing it with
|
||||||
|
sudo leaves a verify/use race: the account can replace the path after the
|
||||||
|
hash check. Instead, one privileged process opens the source safely, copies
|
||||||
|
and hashes the bytes into a private root-owned directory, and atomically
|
||||||
|
publishes the destination only after the digest and size match.
|
||||||
|
|
||||||
|
The returned root-owned path is the only path the caller may execute with
|
||||||
|
sudo. Racing or replacing the source can therefore cause only a verified
|
||||||
|
copy of the locally built payload to be published, or a closed failure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected = expected_sha256.strip().lower()
|
||||||
|
if len(expected) != 64 or any(c not in "0123456789abcdef" for c in expected):
|
||||||
|
raise ValueError("expected_sha256 must be a valid SHA-256 digest")
|
||||||
|
if expected_size < 0:
|
||||||
|
raise ValueError("expected_size must not be negative")
|
||||||
|
|
||||||
|
uploaded = PurePosixPath(uploaded_pyz_path)
|
||||||
|
root_dir = PurePosixPath(root_tmp_dir)
|
||||||
|
if not uploaded.is_absolute() or not root_dir.is_absolute():
|
||||||
|
raise ValueError("remote promotion paths must be absolute")
|
||||||
|
|
||||||
|
stage_dir = str(uploaded.parent)
|
||||||
|
source_name = uploaded.name
|
||||||
|
if stage_dir in {"", ".", "/"} or source_name in {"", ".", ".."}:
|
||||||
|
raise ValueError("uploaded_pyz_path must name a file inside a directory")
|
||||||
|
|
||||||
|
destination_name = "enroll.pyz"
|
||||||
|
destination_path = str(root_dir / destination_name)
|
||||||
|
|
||||||
|
cmd = " ".join(
|
||||||
|
shlex.quote(tok)
|
||||||
|
for tok in (
|
||||||
|
remote_python,
|
||||||
|
"-I",
|
||||||
|
"-c",
|
||||||
|
_REMOTE_PROMOTE_PYZ_SCRIPT,
|
||||||
|
stage_dir,
|
||||||
|
source_name,
|
||||||
|
str(root_dir),
|
||||||
|
destination_name,
|
||||||
|
expected,
|
||||||
|
str(expected_size),
|
||||||
|
"0",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rc, out, err = _ssh_run_sudo(
|
||||||
|
ssh,
|
||||||
|
cmd,
|
||||||
|
sudo_password=sudo_password,
|
||||||
|
get_pty=True,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Unable to promote and verify the uploaded enroll.pyz into "
|
||||||
|
"root-private storage. Refusing to execute it.\n"
|
||||||
|
f"Command: sudo {cmd}\n"
|
||||||
|
f"Exit code: {rc}\n"
|
||||||
|
f"Stdout: {out.strip()}\n"
|
||||||
|
f"Stderr: {err.strip()}"
|
||||||
|
)
|
||||||
|
return destination_path
|
||||||
|
|
||||||
|
|
||||||
def _remote_verify_pyz_sha256(
|
def _remote_verify_pyz_sha256(
|
||||||
ssh,
|
ssh,
|
||||||
remote_pyz_path: str,
|
remote_pyz_path: str,
|
||||||
|
|
@ -375,23 +706,13 @@ def _remote_verify_pyz_sha256(
|
||||||
*,
|
*,
|
||||||
remote_python: str,
|
remote_python: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Verify the uploaded zipapp's SHA-256 on the remote before executing it.
|
"""Verify the uploaded zipapp before same-user ``--no-sudo`` execution.
|
||||||
|
|
||||||
This is transport/staging integrity defence-in-depth. The check runs on the
|
The privileged path uses :func:`_remote_promote_verified_pyz` instead,
|
||||||
remote, immediately before the (root) execution of the zipapp, and fails
|
because hashing a user-writable pathname and later executing that pathname
|
||||||
closed if the digest does not match the bytes we built locally. It shrinks
|
as root would leave a verify/use race. This simpler check remains useful in
|
||||||
the window in which a *non-root* tamperer who somehow gained write access to
|
``--no-sudo`` mode for detecting transfer corruption; there is no privilege
|
||||||
the staging directory could swap the file between upload and execution.
|
transition in that mode.
|
||||||
|
|
||||||
It deliberately does NOT establish trust in a root-compromised remote: a
|
|
||||||
host that is already root can forge any check it runs about itself. Per
|
|
||||||
SECURITY.md, such a host is outside Enroll's threat model. The value here is
|
|
||||||
catching accidental corruption and unprivileged-local-user staging races,
|
|
||||||
not defeating a compromised root.
|
|
||||||
|
|
||||||
The hashing is done with Python's hashlib (already required to run the
|
|
||||||
zipapp) rather than a ``sha256sum`` binary, so it does not depend on
|
|
||||||
coreutils being present or on PATH resolution of a hashing tool.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Hash the staged file using the same interpreter that will execute it.
|
# Hash the staged file using the same interpreter that will execute it.
|
||||||
|
|
@ -738,14 +1059,6 @@ def _remote_harvest(
|
||||||
"SSH private key is encrypted and no passphrase was provided."
|
"SSH private key is encrypted and no passphrase was provided."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
# If no username was explicitly provided, SSH may have selected a default.
|
|
||||||
# We need a concrete username for the (sudo) chown step below.
|
|
||||||
resolved_user = remote_user
|
|
||||||
if not resolved_user:
|
|
||||||
rc, out, err = _ssh_run(ssh, "id -un")
|
|
||||||
if rc == 0 and out.strip():
|
|
||||||
resolved_user = out.strip()
|
|
||||||
|
|
||||||
sftp = ssh.open_sftp()
|
sftp = ssh.open_sftp()
|
||||||
rtmp: Optional[str] = None
|
rtmp: Optional[str] = None
|
||||||
remote_root_tmp: Optional[str] = None
|
remote_root_tmp: Optional[str] = None
|
||||||
|
|
@ -765,16 +1078,6 @@ def _remote_harvest(
|
||||||
rapp = f"{rtmp}/enroll.pyz"
|
rapp = f"{rtmp}/enroll.pyz"
|
||||||
sftp.put(str(pyz), rapp)
|
sftp.put(str(pyz), rapp)
|
||||||
|
|
||||||
# Before executing the uploaded zipapp (as root, under sudo), verify
|
|
||||||
# on the remote that the staged bytes match what we built locally.
|
|
||||||
# This is staging/transport integrity defence-in-depth: it fails
|
|
||||||
# closed if the file was swapped or corrupted between upload and
|
|
||||||
# execution. It does not (and cannot) defend against a remote that
|
|
||||||
# is already root-compromised; see _remote_verify_pyz_sha256.
|
|
||||||
_remote_verify_pyz_sha256(
|
|
||||||
ssh, rapp, pyz_sha256, remote_python=remote_python
|
|
||||||
)
|
|
||||||
|
|
||||||
if not no_sudo:
|
if not no_sudo:
|
||||||
# The remote zipapp is staged as the SSH user, but the harvest
|
# The remote zipapp is staged as the SSH user, but the harvest
|
||||||
# itself runs as root. Root must not write its bundle under the
|
# itself runs as root. Root must not write its bundle under the
|
||||||
|
|
@ -798,13 +1101,33 @@ def _remote_harvest(
|
||||||
)
|
)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise RuntimeError(f"Remote sudo chmod failed: {err.strip()}")
|
raise RuntimeError(f"Remote sudo chmod failed: {err.strip()}")
|
||||||
|
# Promote the uploaded payload into the root-private directory
|
||||||
|
# and verify the bytes during that privileged copy. From this point
|
||||||
|
# onward, execute only the immutable root-owned copy; never return
|
||||||
|
# to the SSH-user-controlled staging path.
|
||||||
|
rapp = _remote_promote_verified_pyz(
|
||||||
|
ssh,
|
||||||
|
rapp,
|
||||||
|
remote_root_tmp,
|
||||||
|
pyz_sha256,
|
||||||
|
pyz.stat().st_size,
|
||||||
|
remote_python=remote_python,
|
||||||
|
sudo_password=sudo_password,
|
||||||
|
)
|
||||||
rbundle = f"{remote_root_tmp}/bundle"
|
rbundle = f"{remote_root_tmp}/bundle"
|
||||||
else:
|
else:
|
||||||
|
# There is no privilege transition in --no-sudo mode, but retain
|
||||||
|
# the transport/corruption check before executing as the SSH user.
|
||||||
|
_remote_verify_pyz_sha256(
|
||||||
|
ssh, rapp, pyz_sha256, remote_python=remote_python
|
||||||
|
)
|
||||||
rbundle = f"{rtmp}/bundle"
|
rbundle = f"{rtmp}/bundle"
|
||||||
|
|
||||||
# Run remote harvest.
|
# Run remote harvest.
|
||||||
argv: list[str] = [
|
argv: list[str] = [
|
||||||
remote_python,
|
remote_python,
|
||||||
|
"-I",
|
||||||
|
"-S",
|
||||||
rapp,
|
rapp,
|
||||||
"harvest",
|
"harvest",
|
||||||
"--out",
|
"--out",
|
||||||
|
|
@ -837,40 +1160,121 @@ def _remote_harvest(
|
||||||
)
|
)
|
||||||
|
|
||||||
if not no_sudo:
|
if not no_sudo:
|
||||||
# Ensure user can read the files, before we tar it.
|
# Keep the root-created bundle root-owned until after it has
|
||||||
if not resolved_user:
|
# been packaged. The old flow recursively chowned the bundle to
|
||||||
|
# the SSH user and then ran tar as that user, creating a window
|
||||||
|
# in which the just-harvested state/artifacts could be modified
|
||||||
|
# before Enroll downloaded them. Instead, root creates and
|
||||||
|
# hashes the archive while it is still private. Only that one
|
||||||
|
# archive is then made readable by the authenticated SSH uid.
|
||||||
|
# The SSH user owns the temporary archive and could chmod/edit
|
||||||
|
# it, so the locally downloaded bytes are required to match the
|
||||||
|
# root-computed digest before extraction. The root-owned parent
|
||||||
|
# remains non-writable, preventing path replacement.
|
||||||
|
if remote_root_tmp is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Unable to determine remote username for chown. "
|
"Internal error: remote root staging directory was not initialised"
|
||||||
"Pass --remote-user explicitly or use --no-sudo."
|
|
||||||
)
|
)
|
||||||
chown_target = remote_root_tmp or rbundle
|
|
||||||
chown_cmd = (
|
remote_tgz = f"{remote_root_tmp}/bundle.tgz"
|
||||||
"chown -R -- "
|
remote_uid = _remote_current_uid(ssh, remote_python=remote_python)
|
||||||
f"{shlex.quote(resolved_user)} {shlex.quote(chown_target)}"
|
tar_cmd = (
|
||||||
|
f"tar -czf {shlex.quote(remote_tgz)} "
|
||||||
|
f"-C {shlex.quote(rbundle)} ."
|
||||||
)
|
)
|
||||||
rc, out, err = _ssh_run_sudo(
|
rc, out, err = _ssh_run_sudo(
|
||||||
ssh,
|
ssh,
|
||||||
chown_cmd,
|
tar_cmd,
|
||||||
sudo_password=sudo_password,
|
sudo_password=sudo_password,
|
||||||
get_pty=True,
|
get_pty=True,
|
||||||
)
|
)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"chown of harvest failed.\n"
|
"Remote root tar creation failed.\n"
|
||||||
f"Command: sudo {chown_cmd}\n"
|
f"Command: sudo {tar_cmd}\n"
|
||||||
f"Exit code: {rc}\n"
|
f"Exit code: {rc}\n"
|
||||||
f"Stdout: {out.strip()}\n"
|
f"Stdout: {out.strip()}\n"
|
||||||
f"Stderr: {err.strip()}"
|
f"Stderr: {err.strip()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stream a tarball back to the local machine (avoid creating a tar file on the remote).
|
# Set a private mode while the archive is still root-owned,
|
||||||
|
# then hash it. Only after the trusted digest has been captured
|
||||||
|
# do we transfer ownership of this one file to the SSH uid and
|
||||||
|
# make the root-owned parent traversable. Unlike mode 0444, this
|
||||||
|
# does not expose the harvest to every local account.
|
||||||
|
secure_cmd = f"chmod 0400 -- {shlex.quote(remote_tgz)}"
|
||||||
|
rc, out, err = _ssh_run_sudo(
|
||||||
|
ssh,
|
||||||
|
secure_cmd,
|
||||||
|
sudo_password=sudo_password,
|
||||||
|
get_pty=True,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Unable to secure the root-created harvest archive.\n"
|
||||||
|
f"Command: sudo {secure_cmd}\n"
|
||||||
|
f"Exit code: {rc}\n"
|
||||||
|
f"Stdout: {out.strip()}\n"
|
||||||
|
f"Stderr: {err.strip()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_archive_sha256 = _remote_file_sha256_sudo(
|
||||||
|
ssh,
|
||||||
|
remote_tgz,
|
||||||
|
remote_python=remote_python,
|
||||||
|
sudo_password=sudo_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
for expose_cmd in (
|
||||||
|
f"chown -- {shlex.quote(remote_uid)} {shlex.quote(remote_tgz)}",
|
||||||
|
f"chmod 0711 -- {shlex.quote(remote_root_tmp)}",
|
||||||
|
):
|
||||||
|
rc, out, err = _ssh_run_sudo(
|
||||||
|
ssh,
|
||||||
|
expose_cmd,
|
||||||
|
sudo_password=sudo_password,
|
||||||
|
get_pty=True,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Unable to expose the integrity-protected harvest "
|
||||||
|
"archive to the authenticated SSH account.\n"
|
||||||
|
f"Command: sudo {expose_cmd}\n"
|
||||||
|
f"Exit code: {rc}\n"
|
||||||
|
f"Stdout: {out.strip()}\n"
|
||||||
|
f"Stderr: {err.strip()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _download_progress(transferred: int, _total: int) -> None:
|
||||||
|
_check_tar_download_size(transferred)
|
||||||
|
|
||||||
|
sftp.get(
|
||||||
|
remote_tgz,
|
||||||
|
str(local_tgz),
|
||||||
|
callback=_download_progress,
|
||||||
|
)
|
||||||
|
_verify_downloaded_archive_sha256(local_tgz, expected_archive_sha256)
|
||||||
|
else:
|
||||||
|
# Without sudo there is no privilege boundary between the SSH
|
||||||
|
# user and the harvested bundle, so stream it directly as
|
||||||
|
# before.
|
||||||
cmd = f"tar -cz -C {shlex.quote(rbundle)} ."
|
cmd = f"tar -cz -C {shlex.quote(rbundle)} ."
|
||||||
_stdin, stdout, stderr = ssh.exec_command(cmd) # nosec
|
_stdin, stdout, stderr = ssh.exec_command(cmd) # nosec
|
||||||
|
downloaded = 0
|
||||||
with open(local_tgz, "wb") as f:
|
with open(local_tgz, "wb") as f:
|
||||||
while True:
|
while True:
|
||||||
chunk = stdout.read(1024 * 128)
|
chunk = stdout.read(1024 * 128)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
|
downloaded += len(chunk)
|
||||||
|
try:
|
||||||
|
_check_tar_download_size(downloaded)
|
||||||
|
except RuntimeError:
|
||||||
|
try:
|
||||||
|
stdout.channel.close()
|
||||||
|
except Exception:
|
||||||
|
pass # nosec - best-effort remote stream abort
|
||||||
|
raise
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
rc = stdout.channel.recv_exit_status()
|
rc = stdout.channel.recv_exit_status()
|
||||||
err_text = stderr.read().decode("utf-8", errors="replace")
|
err_text = stderr.read().decode("utf-8", errors="replace")
|
||||||
|
|
@ -888,7 +1292,7 @@ def _remote_harvest(
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Cleanup remote tmpdirs even on failure. The sudo-owned harvest
|
# Cleanup remote tmpdirs even on failure. The sudo-owned harvest
|
||||||
# tempdir may still be root-owned if harvest/chown failed, so remove
|
# tempdir remains root-owned throughout the sudo flow, so remove
|
||||||
# it via sudo and avoid masking the original error if cleanup fails.
|
# it via sudo and avoid masking the original error if cleanup fails.
|
||||||
if remote_root_tmp:
|
if remote_root_tmp:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
708
poetry.lock
generated
708
poetry.lock
generated
|
|
@ -91,109 +91,125 @@ typecheck = ["mypy"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.6.17"
|
version = "2026.7.22"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"},
|
{file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"},
|
||||||
{file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"},
|
{file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "2.0.0"
|
version = "2.1.0"
|
||||||
description = "Foreign Function Interface for Python calling C code."
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "platform_python_implementation != \"PyPy\""
|
markers = "platform_python_implementation != \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
{file = "cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
{file = "cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
|
{file = "cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
|
{file = "cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
|
{file = "cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
|
{file = "cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
|
{file = "cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
|
{file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
|
{file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
|
{file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
|
{file = "cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
|
{file = "cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
|
{file = "cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
|
{file = "cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
|
{file = "cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
|
{file = "cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
|
{file = "cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
|
{file = "cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
|
{file = "cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
|
{file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
|
{file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
|
{file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
|
{file = "cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
|
{file = "cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458"},
|
||||||
{file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
|
{file = "cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
|
{file = "cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
|
{file = "cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
|
{file = "cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
|
{file = "cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
|
{file = "cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
|
{file = "cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
|
{file = "cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
|
{file = "cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
|
{file = "cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
|
{file = "cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
|
{file = "cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384"},
|
||||||
{file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
|
{file = "cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
|
{file = "cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
|
{file = "cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
|
{file = "cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
|
{file = "cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
|
{file = "cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
|
{file = "cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
|
{file = "cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
|
{file = "cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
|
{file = "cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
|
{file = "cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
|
{file = "cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a"},
|
||||||
{file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
|
{file = "cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
|
{file = "cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
|
{file = "cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
|
{file = "cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
|
{file = "cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
|
{file = "cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
|
{file = "cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
|
{file = "cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
|
{file = "cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
|
{file = "cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
|
{file = "cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
|
{file = "cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
|
{file = "cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
|
{file = "cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
|
{file = "cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
|
{file = "cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
|
{file = "cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
|
{file = "cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
|
{file = "cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
|
{file = "cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
|
{file = "cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
|
{file = "cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056"},
|
||||||
{file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
|
{file = "cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
|
{file = "cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
|
{file = "cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
|
{file = "cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
|
{file = "cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
|
{file = "cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
|
{file = "cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
|
{file = "cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
|
{file = "cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
|
{file = "cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
|
{file = "cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
|
{file = "cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0"},
|
||||||
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
|
{file = "cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1"},
|
||||||
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
|
{file = "cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5"},
|
||||||
|
{file = "cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210"},
|
||||||
|
{file = "cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -201,141 +217,105 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.4.7"
|
version = "3.4.9"
|
||||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"},
|
||||||
{file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"},
|
{file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"},
|
||||||
{file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"},
|
{file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"},
|
||||||
{file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"},
|
{file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"},
|
||||||
{file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"},
|
{file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"},
|
{file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"},
|
{file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"},
|
{file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"},
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"},
|
|
||||||
{file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"},
|
|
||||||
{file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"},
|
|
||||||
{file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -353,103 +333,103 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "7.14.2"
|
version = "7.15.3"
|
||||||
description = "Code coverage measurement for Python"
|
description = "Code coverage measurement for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "coverage-7.14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b75818e3046e9319143157f3dc4b43679a550c2060a17cbf3e39cc0b552925"},
|
{file = "coverage-7.15.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3a82b2ceee91ba353e59fe2436d8a9eae799ff9825e5385423ea205d693e2949"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66b08ba4c5cbf0eaa2e9692b203073f198d5d469d8b15d1c7a4854ce7032b2e2"},
|
{file = "coverage-7.15.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3088cce65e54c2eefc08e7e1ca0b0acec1e95e8cf084ac848599103ed0367f74"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:70f266b536c590060b707dddfb6cf9f17e24fd30b992242e774543d256265c43"},
|
{file = "coverage-7.15.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a65e09efb0b5ab21fc54a8a65c5b2e533c0a4c0d064af0259a005dc656dc1b13"},
|
||||||
{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.15.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b51f279a2477b0e1f288b98f141fd227acfdd1d3f0370400e473788879b47871"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c301fe9990cb5c081bf4881cb498743807c8e0e93fad7b85c02788456492ef8"},
|
{file = "coverage-7.15.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7835176988cbcf1f014db683bc33aa15e0558e412bf08deaa99757335b88df15"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d67b0462c8a3c3d93033e7c79cacdfc57d08e5220d9115bcb24a23edf5a5900d"},
|
{file = "coverage-7.15.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f24896dc8863167f6732f4142f5d37e6195eccc8fe5fe528d35d49597d29fdb3"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e763087828ee9644f0c89c57f9b75f0a50fdf3e8f5d8fac5cfc351337e89a99"},
|
{file = "coverage-7.15.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9490d43e5d041fdf376770a886a29722adb05f6b9c21a65c48c81fc8f1c33fd7"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6d4da2baab6d96ceedd9176b3c142e1198b0310bc8dc04e18a3caab65c3a322c"},
|
{file = "coverage-7.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:225e359bd5dedaff6d68e36091af20555866c557d968167308b677379bf575c3"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab565a405bfdea61260145d8cc987aa66d1998fd0e0ccd4348008f4e6a39ee33"},
|
{file = "coverage-7.15.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:22119e2e3b2ac5ac024d50131fdd4b22ab4c6cf8aa2fc792cce73c0d94c5812d"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c13230b688fbb9122251b74daa092175811eb64cb7bd1c98e2c8193dfa2b0bd5"},
|
{file = "coverage-7.15.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:12d555badc462b0f6037ce8bec8b4af8d71f90eb55b57d0a358731f7ee7883e2"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:014c83ba1ec97993cfe94e77fe6b56daa76bc0c218b86938971574c28942d044"},
|
{file = "coverage-7.15.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c4e2cf9cf774939b3dc581c6e31dfe7e8d7608b24f0f17524d6161f8235c3d2c"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6caf54ffbf84b30470a8118f275afee9234e616572e4e41bae1dc19198c37294"},
|
{file = "coverage-7.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cea1b3e19d710f67e2ba9ce0b0b51032c2a9b4808a65ced48ddf336ef7e58058"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-win32.whl", hash = "sha256:4bf9d8a35f77df5638c61b5012ba5225109ec1cc15bc5eb097036b3c3cc939f3"},
|
{file = "coverage-7.15.3-cp310-cp310-win32.whl", hash = "sha256:25c77560309f157e7b7ee8fe0bf78d047ba900b7ae42f0e50e559305b366fea2"},
|
||||||
{file = "coverage-7.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:c1f17a8caebe0facd4556b1e0adfe0987c17feebed88e7bb6b5365c45c84c5d6"},
|
{file = "coverage-7.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:179fbf847e6c3d90ea71bfd570fe57f1ddb1c51474754894871c1e11099efaa0"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909f265c8c41f04c824bf741b2601fdcb56cab4bf56e018996b6494192ba0f58"},
|
{file = "coverage-7.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5f3f854ab4599d98f7799ac9b91e34e8ec9ebc9a6372ee8c1f3413a68cc8b5e9"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8102deaf911938233f760426e6a5e287388521de95111d5c8de26c8a1028924"},
|
{file = "coverage-7.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75268348fee1f199653b8a846262aec5581c6bb008c4f58824959fb708cc688f"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:851f49e7bd7d1cdaf328f3133942b252d5e3d3380690131f423cba8e435b87f5"},
|
{file = "coverage-7.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21081739f6264cc594cad2d42b62befbd17633824022866c68720eb0c4b8d6b4"},
|
||||||
{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.15.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:718d366251b060c10731c7dd359de6caea72250036eb94576aa56dacbf830a11"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7471bc920d97c51c37ea8127f13b2adca43c3d78c53313b26a1f428e99d2c254"},
|
{file = "coverage-7.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa1bbaa502a6e877f3ee67cbac3eba2bb637f623e454e6c37b81b38896dbd48f"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da5057e1bb257c967feee8ba67f3ebf379e801c7717f238b3d8c9caf00fc8f93"},
|
{file = "coverage-7.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:494880c9e60782610683f4eb9b65cce4f886673596b8f3cb2dfa079fc551c743"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33c0da852e8a40246cd8e20cf3b2fc17ca52a45e9b5f7983c93db26f5d24b87b"},
|
{file = "coverage-7.15.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3db264ea689f9e8f9fa4fb9005fee4048c3bff4a547f4cfa27f5086cb0804ec0"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f48a85bb437fab7782021c40bfee6b15146928b96960d008ace41b6901a0f21d"},
|
{file = "coverage-7.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4e869d4799674d67778e76ddbe2e26cf1673369262e231a8ec259421b1015fea"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f44e7579a769a21d5b5e3166916bfe30ee175aaffff750324cbb11be2dbec5ad"},
|
{file = "coverage-7.15.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:696fc7a28bbf717aba8d2c6963d26702945c7832cb313ba3b323aa5b1afb3156"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:78853ca3c6ca2f012daa2b07dbabbb8db0f09d4dbe8ee828d294b3445d3f4cd8"},
|
{file = "coverage-7.15.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3fe9be1c527497d047f770d88a0110189714c36383bb88384508f750c302bffa"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c9c2795ee3692097ff226ab806005d36bb9691fca9b35353542b57ea749cc830"},
|
{file = "coverage-7.15.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2400591f4b2e33746c70846388f8bb4c7e33b820e31cb8c6cb2f25305310438b"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2f5cc48a845d755b6db236f8c29c2b54773eb4c7e4ee2ead43812d73718784b0"},
|
{file = "coverage-7.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e557178799282269412a672e5753f2179edfe1b3f0f19b0c98f8e72d482326a"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-win32.whl", hash = "sha256:9c61cb7eaabcfa609c5bc0f5ff5869d72a2f02f17994e5fba5f971de516f3c82"},
|
{file = "coverage-7.15.3-cp311-cp311-win32.whl", hash = "sha256:68ea6c947375982ae907e19e9d2ef156bd6e68e11f3566dd568d7f4ec974e715"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e715909b0966d1774d8a26e14e2f4a3ae75909dca526901c6306286b2dcbfbdc"},
|
{file = "coverage-7.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:28743dad31622e8c474b17446118037361f5b1f4f2ecdf72d4f6fde246d64446"},
|
||||||
{file = "coverage-7.14.2-cp311-cp311-win_arm64.whl", hash = "sha256:9193f7150937a4fd836b10eaa123e15d98e961d1fabac07e60adf2d4785f888a"},
|
{file = "coverage-7.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:c4398918c4fda32718191239e451fd86ac5ad1e8979b592f1921ee2d1f038965"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:37c94712e533ea06f0b1e4d934811c520b1914ce0e4da3916220717aa7a86bc6"},
|
{file = "coverage-7.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:79a3e32e83227d83d9684459ed579769b56c369ac2d7313099b2d9e031d2e10f"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c050bbc7bba94c77e4ed7438f4fda1babe98ab145691d80aa6f60df934a1468b"},
|
{file = "coverage-7.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:767feb87c5886d781d0a69fafd450a20826ddab7b79bce1665deb64d21441b60"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a7af571767a2ee342a171c16fc1b1a07a0bf511606d381703fb7cf397fe49d46"},
|
{file = "coverage-7.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50951e37033c40548d777b8a8454a2cd622dba1136780065678dccaec307c47f"},
|
||||||
{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.15.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:63a4ff67364afb2cac826b8bbd78a5c50ce656a7b7137436b44d7b96a9271088"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c33e9e4878972f430b0cc06de3bf2a28d054a9efb4f8426d27de0d9cb81396ff"},
|
{file = "coverage-7.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95e42856509675fe26560310313a6117640e96f9a1e19bb3d220116a27c94c"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7967ea55c6dea6becba4d5870e2fa0aa4915a8be7ebff1bb79e6207aa75ce8d"},
|
{file = "coverage-7.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abad631cba27094b4631993f4c72e89ac0ca1b3a0236c7abaf8ca79aea619851"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1322f237c2979b84096f4239c17828ff17fea6b3bbe96c44381c5f587c44c26"},
|
{file = "coverage-7.15.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b0807f1f051dd82a234ad6acdb6f1425baede60be1e84e862496c8cc9262ab9"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77849525340c99f516d793dddbcee16b18d50af892ac43c8de1a6f343d41e3b5"},
|
{file = "coverage-7.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8d6df7aeb5bc464040bbc9ae173d875785d3677ebc4307817997d622d74225e"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef11695493ec3f06f7b2678ca274bcabb4ca04057317df268ddbfd8b05f661a8"},
|
{file = "coverage-7.15.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:974471c506c9f5758808b47c1ebf7949ecd0848f5c1020e78675fefe5ff46866"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8134f0e0723e080d1c27bbe8fc149f0162e429fa1852482150015d0fce83eaf1"},
|
{file = "coverage-7.15.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5cba0c9c13e35c86df7998f1afaf6b1da224a3a39e4da59bdabf60c148046dcb"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:914eead2b843fc357f733b3fe39cc94f1b53d466e8cfe03080b1ed9d24ccfc73"},
|
{file = "coverage-7.15.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4d608dc36a364dce33acbf4fc3a50f9d2054c945f233bb0a2cdb4b90bfa17646"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4b2d5e847fb7958583b74910cc19e5ec4ece514487385677b26433b2546116e"},
|
{file = "coverage-7.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2395869280554a1941da904423c12660c39f721315e1c02d076a7fe0971382f0"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-win32.whl", hash = "sha256:e753db9e40dda7302e0ac3e1e6e1325fb7f7b4694f87a7314ab15dd5d57911a7"},
|
{file = "coverage-7.15.3-cp312-cp312-win32.whl", hash = "sha256:24f3b21840c3eb76cef3cc70b2bf6649010c64471a84a446538a39306e1ba04d"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-win_amd64.whl", hash = "sha256:d32e5ca5f16dafb269ee50b60d32b00c704b3f6f78e238105f1d94a3a5f24bf5"},
|
{file = "coverage-7.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:fa7b17902c3c1dd8a7adb52679b7f6340bba08443d710c8838e04db8cf62be2a"},
|
||||||
{file = "coverage-7.14.2-cp312-cp312-win_arm64.whl", hash = "sha256:dc366f158e2fb2add9d4e57338ca48f12611024278688ee657eb0b853fcb5de5"},
|
{file = "coverage-7.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:fcbe83fb7258eacd293bf5322d88807acb35ed12a5cfa99dd8215c083e3b0235"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e5f077641a6713ce9d38df9e85d4fb9e008677fc0775cbaeb32ddfc3b319d4ca"},
|
{file = "coverage-7.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1182eed05674c63d40951fae27c43e822749f04d25f75df64c2e4fa3168678de"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0907f39b49ae818fe8af50aaa0f19afbc8ca164aea0865181ca7af17a3ac690b"},
|
{file = "coverage-7.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c0c4b0d7c4cd56e470d0c9d8441f42e8a96cdfd95050fec027f1d4dd9f11006c"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734d47669118d75c28981e562d4530ceb77342d31ffef6def5edd5ad4f05d7b"},
|
{file = "coverage-7.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5c9fce9f4998b0d50a753da765b9215a14decc7863822c89d72da7a89ca625b3"},
|
||||||
{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.15.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a47e2a0a0ace9241e70ee00e44520f88b843094603dd54303f1bafecd929c30"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f0a80f4c8ac3f774210b1cc1bc0e31e75502f2818dda9a144ff90e702c4d91d"},
|
{file = "coverage-7.15.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95bad94f83807ae60ed76f3ac012f69b2605ac9ea81bee959a5a483f7fa09c10"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e66f3f22d6c1515ce70f2e7c3e9c6f3ff0ff33480125c9f9c53e8f6508e30f"},
|
{file = "coverage-7.15.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:228e172a76c428bb17d1ab78a2ff188990b0597e5dbd291f52a4edf7412de049"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6a2c37c3114f87ca7f10113756026eecb49656514debad600dcbec21f355ccea"},
|
{file = "coverage-7.15.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cea9fb33887c99349996266f1fd60abe5af3577a90633392001d27ef46b4b66e"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b16a7959d04b1497281c062c180413565c3f3469211d78799ad5b9a75f67796"},
|
{file = "coverage-7.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81760de3155d7f52c21860c4046628dc6bed182f72e3c028e2b4fd46f65aa040"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6466c6999545cf00c4c142dfcbbf2db396dc735f005dcf8f91d57e351a79472b"},
|
{file = "coverage-7.15.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b47ea0a1d3a3d089826c6cbfad8429d7d8872e28e86baa95ddef330f6875da21"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c60915ebb8f562317ba5ff6b8c32e25c0882289b201a9f2fb2987f91efd95d8"},
|
{file = "coverage-7.15.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5459ba486b2a5d58a6c05254779ecdf525e7f20174d0210ceda75ba40fdb8f2c"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:33b830850488acbcd358c78a4fecfafe7031667b4da8ddff5546295dc962cdeb"},
|
{file = "coverage-7.15.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c59209f80a08dbfcdd5109a80dc623cd3b9d22895c85757d34f57a6e6e95570f"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d0f845539230b8269aec902bc978b0cc403f52f002d18a04492efc943404d0bc"},
|
{file = "coverage-7.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f863856c1779d4a5bb6a94698a2f9073e09c6706501f76f3e7780e72df97d21c"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-win32.whl", hash = "sha256:a8ac51a2e441e9119b9395f4d893fbc4934c64c8ba58be9b9eaa85591249e548"},
|
{file = "coverage-7.15.3-cp313-cp313-win32.whl", hash = "sha256:00cbdc5e322927dc30c5e42b863819b1bb867cc66f26ab5372c585850876ab93"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-win_amd64.whl", hash = "sha256:039b264cdb31c44b48f9821e2afbf8f37df49e0fb837e24a942918b36c567e31"},
|
{file = "coverage-7.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:835528518a1d823cf336740324b2f335f7c01e609e74abcb5d5163b3e66661e3"},
|
||||||
{file = "coverage-7.14.2-cp313-cp313-win_arm64.whl", hash = "sha256:7f2ef591e381cc36b8e53334e1b842c760c520c8a52d01e8626209400e93fe6a"},
|
{file = "coverage-7.15.3-cp313-cp313-win_arm64.whl", hash = "sha256:0d2e1f2cbbf36b842f3e2aff8d118c60d677adb498bc6c7fa9c6838738f82767"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7a0d1f026b72d627fa5c8a57cbc86ad209b64aa2a65833c83b290ace5cbee126"},
|
{file = "coverage-7.15.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1e3bb08ad574bd9fb6a991f645728f70d333c1c1958dd5fcde65e24cb862813d"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4d2b86f81c1c9310a7e774e3cc9e927a3d0bf583ecbfa01498dd626930025428"},
|
{file = "coverage-7.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e5860eaff02a0b7f1b73304bdf846596ee62ab3a78d25c68044ebf684cb1fef"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d76bdc1f9396ae70a55d050cf9743d88141c62ce0a22a3f627fab1d11c2f8bc6"},
|
{file = "coverage-7.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:60874e5bd67f0b1bdbe42ab42c7bafa66a6fb8de88721af6df3f7a02713960cd"},
|
||||||
{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.15.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9147be876e9d83765e0b82176674dc248a6b9283e25e01e7462611b97e9b731"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0904f3b79d7b845bef0715afe1900da634d12b97f05b9479cb472880ca07cb9c"},
|
{file = "coverage-7.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61a01f8c3804760fcc5a3d31c4f3cab792d660d44e17bf7adeaf0ea51e07821e"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6795ca4198d6cb7fc2c6163214f6555a6bc5f0ae1e268e76139dec4b37c4499"},
|
{file = "coverage-7.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95bf3e7f26f792e25eb185f85a5a659d48479265176dcfe22b6f334fd0081b5c"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c41e9b60fc0fa57f5d73306417d2f9d668202cca6944f9435878c55a5e7ae213"},
|
{file = "coverage-7.15.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44c41eff9e413fed8740eca75d5438ebeb9d3e45e7cd37c67329213e7a72c764"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419d2aadd5746efc2e9df0f33c05570d8192e6f6a6098ab05acce586f44ce8a5"},
|
{file = "coverage-7.15.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54146bafb61f3ba9895b43af0dd17eba01561d586d44ce84ea221b0cbbee5a9e"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c5d273c5f1411c0d26c4f066c398d4a434b1f97bb5fa409189bedce86d4add4"},
|
{file = "coverage-7.15.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:af000dd1bb859ff8066fda4c79512ff938c798116540307226b373099c7b151f"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fe465bc691264adce601527a972990c1174075d86bcbe9968fd20c95e0b1948"},
|
{file = "coverage-7.15.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a1b82490577f3889950b5a04f18712aef0207243e0749d60fe28c3c73ebfd5fd"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6fbb61617af1c56f95d53170ae9fa6c9aef6de1abd02fcc50064bfc672efb18d"},
|
{file = "coverage-7.15.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c4fc90a60154c3e4b8a2dc206d6dbe852f1c235c249e0dc0cef909d032c9591a"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e1eff22b831dfd5694989cc1f0789980f18391f614ac67c851af9a8e6d25e9ba"},
|
{file = "coverage-7.15.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f25bb884814a892948b4c20394db3f2364dd452d9492736479e7a493e63b0eb6"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-win32.whl", hash = "sha256:58e91be0a233adef698d3e6be54f10401bb91fd7854c0d4c4d50e0d3711e72f1"},
|
{file = "coverage-7.15.3-cp314-cp314-win32.whl", hash = "sha256:722dbf8e7828fbcfe0dc8586167dc0a5ce85ad6ea171dbb21ed3f8d6581d3cb8"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:d8429bf97906bfe6c61f9dbfb3342e0d88120da61939da8bd04f830cc3eab3b8"},
|
{file = "coverage-7.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:64d0845f9c3ed47302bed265c15ab4dbb64aa4ec1490839b8e328f4e7fa914d2"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314-win_arm64.whl", hash = "sha256:13609d9d77249447aa73357b14831b0f3b95f275026c9ff20dd105f981f53a0c"},
|
{file = "coverage-7.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:69bc14684f8fbbee9f9dbaa4fe79719b0da9725fc37956785c06ec365acf6926"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9818486c2bac88ae931df7e04905ee29bef49fd218c00f5f02bed4855254a101"},
|
{file = "coverage-7.15.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f92df943c24b96cb215ca26b4f6a2283e63c5db80f1635aceea7fff11311917b"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58055adffabfa243516a197aa9f85f0dd56d905b0fba1a10193269759c29ccb0"},
|
{file = "coverage-7.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:66591c46bdd2971d3ae2bc503a5f0459c2edcaf6b7e045b292000cc95bc6cb95"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:535747dbc200349d7fb434cffcb28e770f0290f69b225f56dc3803aa7210cdea"},
|
{file = "coverage-7.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa64458b81b18bfc67cdf1f6dc02b23e3edc672f2f8e11771fad75865415a43"},
|
||||||
{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.15.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:447f5421ccf5475956cf516d4ca1d575f487947b6f4e11f9d80c6aefe24b3dc8"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2cf17b33773be446a588551ea6a746b2d70dd0bc90dc31f1dd7648975a63c6b"},
|
{file = "coverage-7.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0c77ef8cd483a4987a5d12d1d9d5f7ee598dfdc6c0844417d847e5768dc779"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adb4a5fef041f7179bb264203add873c147d169cf2f8d0adae89ff2e51271bac"},
|
{file = "coverage-7.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b273f4ff657446a06c2d85bf80e134fa869a92852ba5f87854a70e1fb44da77"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c012ec357dec9408a83dad5541172a63c5cfa1421709f2e5811480d31ae1b28"},
|
{file = "coverage-7.15.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daea8c4fafa22488600405be2c2be525a9406fba3fc0a83acc726db3e14e2005"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dacd0ecd08fda3cb2f85b60cabea7da326dcb2fc15fbb23a88830a80144cc9f2"},
|
{file = "coverage-7.15.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93ff57c530f3fa7aa69f92fb9b8892b8aa82712aa970842f4abf28657f42fb57"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f27e980f2feba5dfe7a32b22b125470de69c0bd113c75e16165de909a777f512"},
|
{file = "coverage-7.15.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4df21bef8b800eebda9018f53d49c9ace3aeb0090c850139b27923aafcb83e91"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:105c00efb65c863630b2b63cbf7b8267e4da2d44b62284efbb19a03b04c337d4"},
|
{file = "coverage-7.15.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:db567b02685f26034adcbd85055f80d12cdf02111b8ed00886093d98b2874ce2"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:571173fa04c8e8d6235ab32ae67fecca97777e2e1b4a1a30f3022c34e397c1c1"},
|
{file = "coverage-7.15.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5318dd51b8600b947e058cf5a4fe54d183d9d13c49b97b64ca7be05a34df9bef"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e532f34d42d1a421fa00ed6b7735d14ac2e340256c1bad26a5e1dc1252b0bed7"},
|
{file = "coverage-7.15.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c995bfa383c54704839b6c4c2627a1c00895597ada0e5e8190c81d8bd620555c"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-win32.whl", hash = "sha256:243971550fb46c3039257f75e65610002d84304c505f609bbd9779e20a653a0a"},
|
{file = "coverage-7.15.3-cp314-cp314t-win32.whl", hash = "sha256:6433fafb8da0e1d02eb53411e0ecdadb6b88f0224fdc23317e703c0e88937d42"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-win_amd64.whl", hash = "sha256:60fb0ca084a92da96474b8b405a7ea76dfecac3c68db54383e7934b6f3871169"},
|
{file = "coverage-7.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fe578952b1b29fe8c777f43f241d49efac4b56724a3434f5d22ebe3c208df429"},
|
||||||
{file = "coverage-7.14.2-cp314-cp314t-win_arm64.whl", hash = "sha256:36a0a3f42ed7dfdbca2a69a541519ffd5064a5692152fc0018109e74370d7345"},
|
{file = "coverage-7.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d2e1acb7aee29dfa8f3e48c23f36670898baca1209d9bdd3985a50c7f982165e"},
|
||||||
{file = "coverage-7.14.2-py3-none-any.whl", hash = "sha256:04d92589e481a8b68a005a5a1e0646a91c76f322c397c4635298c57cf63699b5"},
|
{file = "coverage-7.15.3-py3-none-any.whl", hash = "sha256:da78fa6fc7dafe4212839173133ee85afcf42c5cd5f3e47fa7c1c210453b445e"},
|
||||||
{file = "coverage-7.14.2.tar.gz", hash = "sha256:7a2da3d81cfe17c18038c6d98e6592aa9147d596d056119b0ee612c3c8bd5230"},
|
{file = "coverage-7.15.3.tar.gz", hash = "sha256:ae7ea5a4614acf399ef0483c4cb34f8f8f01df848d8fcbe7d3ce0865733f1c4d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -460,58 +440,58 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "49.0.0"
|
version = "50.0.0"
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.9.0,!=3.9.1,>=3.9"
|
python-versions = "!=3.9.0,!=3.9.1,>=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"},
|
{file = "cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9"},
|
{file = "cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f"},
|
{file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459"},
|
{file = "cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef"},
|
||||||
{file = "cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e"},
|
{file = "cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7"},
|
{file = "cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8"},
|
{file = "cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3"},
|
{file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27"},
|
{file = "cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95"},
|
||||||
{file = "cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61"},
|
{file = "cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a"},
|
{file = "cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36"},
|
{file = "cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e"},
|
{file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b"},
|
{file = "cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7"},
|
||||||
{file = "cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001"},
|
{file = "cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba"},
|
||||||
{file = "cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b"},
|
{file = "cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c"},
|
||||||
{file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838"},
|
{file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a"},
|
||||||
{file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5"},
|
{file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e"},
|
||||||
{file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615"},
|
{file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d"},
|
||||||
{file = "cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6"},
|
{file = "cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437"},
|
||||||
{file = "cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6"},
|
{file = "cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9"},
|
||||||
{file = "cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493"},
|
{file = "cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -749,14 +729,14 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyproject-appimage"
|
name = "pyproject-appimage"
|
||||||
version = "4.2"
|
version = "4.3"
|
||||||
description = "Generate AppImages from your Python projects"
|
description = "Generate AppImages from your Python projects"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pyproject_appimage-4.2-py3-none-any.whl", hash = "sha256:d6892643db5759dc06531a4546bdab404a519c63814c060f8749979a8625d9cc"},
|
{file = "pyproject_appimage-4.3-py3-none-any.whl", hash = "sha256:b9fdc6d1829ead1ca3021fe6d8c9e7c2830b43426c427d91b3d07c5480fd07b6"},
|
||||||
{file = "pyproject_appimage-4.2.tar.gz", hash = "sha256:6b6387250cb1e6ecbb08a13f5810749396ebe8637f2f35bf2296bfdd5e65cd6e"},
|
{file = "pyproject_appimage-4.3.tar.gz", hash = "sha256:03a6afd07672f406f9df18ae3f424f6a370139bbe8f463f729a76fad4a42da3a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -1114,14 +1094,14 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.16.0"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.9+"
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main", "dev"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
{file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"},
|
||||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
{file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"},
|
||||||
]
|
]
|
||||||
markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\""}
|
markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\""}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "enroll"
|
name = "enroll"
|
||||||
version = "0.7.0"
|
version = "0.8.2"
|
||||||
description = "Enroll a server's running state retrospectively into Ansible"
|
description = "Enroll a server's running state retrospectively into Ansible"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ sudo apt-get -y install createrepo-c rpm
|
||||||
BUILD_OUTPUT="${HOME}/git/enroll/dist"
|
BUILD_OUTPUT="${HOME}/git/enroll/dist"
|
||||||
KEYID="54A91143AE0AB4F7743B01FE888ED1B423A3BC99"
|
KEYID="54A91143AE0AB4F7743B01FE888ED1B423A3BC99"
|
||||||
REPO_ROOT="${HOME}/git/repo_rpm"
|
REPO_ROOT="${HOME}/git/repo_rpm"
|
||||||
REMOTE="letessier.mig5.net:/opt/repo_rpm"
|
REMOTE="ashpool.mig5.net:/opt/repo_rpm"
|
||||||
|
|
||||||
DISTS=(
|
DISTS=(
|
||||||
fedora:43
|
fedora:43
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
%global upstream_version 0.7.0
|
%global upstream_version 0.8.2
|
||||||
|
|
||||||
Name: enroll
|
Name: enroll
|
||||||
Version: %{upstream_version}
|
Version: %{upstream_version}
|
||||||
|
|
@ -43,6 +43,13 @@ Enroll a server's running state retrospectively into Ansible.
|
||||||
%{_bindir}/enroll
|
%{_bindir}/enroll
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Aug 03 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||||
|
- Make remote harvest zipapp stdlib-only
|
||||||
|
- Security: fix a TOCTOU in remote harvest.
|
||||||
|
* Mon Jul 13 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||||
|
- Security: keep sudo-created remote harvest bundles root-owned while root packages and hashes them, expose only the archive to the authenticated SSH uid, and verify the root-computed digest after download. This removes the post-harvest tampering window created by recursively chowning the bundle before packaging without making the plaintext archive world-readable.
|
||||||
|
- Security: enforce tar member limits while lazily parsing untrusted archives rather than after `TarFile.getmembers()` has already indexed the entire archive; count repeated `.` entries and cap remote compressed downloads as well.
|
||||||
|
- Security: apply aggregate byte and total filesystem-entry limits when freezing directory harvest bundles, reject symlinked bundle roots, and abort when files or discovered directories change during the copy, so direct directory inputs remain bounded and fail closed under mutation.
|
||||||
* Sun Jul 05 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
* Sun Jul 05 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||||
- BREAKING CHANGE: Remove the `enroll diff --enforce` option. Enroll no longer applies the old harvest state locally to repair drift; this avoids the risk of enforcing a potentially malicious or tampered harvest. To restore baseline state, regenerate a manifest from the trusted harvest and apply it yourself, or compare two `enroll diff` runs and act on the result.
|
- BREAKING CHANGE: Remove the `enroll diff --enforce` option. Enroll no longer applies the old harvest state locally to repair drift; this avoids the risk of enforcing a potentially malicious or tampered harvest. To restore baseline state, regenerate a manifest from the trusted harvest and apply it yourself, or compare two `enroll diff` runs and act on the result.
|
||||||
- BREAKING CHANGE: Group all package and systemd-unit roles into Debian Section/RPM Group roles by default, including managed config files and unit state. This mode is not used if `--fqdn` or `--no-common-roles` is set, in which case, the traditional behaviour of preserving one role per package/unit is used instead.
|
- BREAKING CHANGE: Group all package and systemd-unit roles into Debian Section/RPM Group roles by default, including managed config files and unit state. This mode is not used if `--fqdn` or `--no-common-roles` is set, in which case, the traditional behaviour of preserving one role per package/unit is used instead.
|
||||||
|
|
@ -63,7 +70,7 @@ Enroll a server's running state retrospectively into Ansible.
|
||||||
- Add support for generating ipset and iptables configuration files from runtime, if the former weren't present ('firewall_runtime' role)
|
- Add support for generating ipset and iptables configuration files from runtime, if the former weren't present ('firewall_runtime' role)
|
||||||
* Tue May 12 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
* Tue May 12 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||||
- Add ssh config support where JinjaTurtle is used
|
- Add ssh config support where JinjaTurtle is used
|
||||||
* Tue Feb 16 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
* Mon Feb 16 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||||
- Add capability to handle passphrases on encrypted SSH private keys. Prompting can be forced with `--ask-key-passphrase` or automated (e.g for CI) with `--ssh-key-passphrase env SOMEVAR`
|
- Add capability to handle passphrases on encrypted SSH private keys. Prompting can be forced with `--ask-key-passphrase` or automated (e.g for CI) with `--ssh-key-passphrase env SOMEVAR`
|
||||||
* Fri Jan 16 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
* Fri Jan 16 2026 Miguel Jacq <mig@mig5.net> - %{version}-%{release}
|
||||||
- Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`.
|
- Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`.
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,59 @@ def test_freeze_directory_bundle_rejects_symlinked_subdir(tmp_path: Path):
|
||||||
freeze_directory_bundle(bundle)
|
freeze_directory_bundle(bundle)
|
||||||
|
|
||||||
|
|
||||||
|
def test_freeze_directory_bundle_rejects_symlinked_root(tmp_path: Path):
|
||||||
|
bundle = _write_bundle(tmp_path)
|
||||||
|
link = tmp_path / "bundle-link"
|
||||||
|
link.symlink_to(bundle, target_is_directory=True)
|
||||||
|
|
||||||
|
with pytest.raises(ArtifactSafetyError, match="root is a symlink"):
|
||||||
|
freeze_directory_bundle(link)
|
||||||
|
|
||||||
|
|
||||||
|
def test_freeze_directory_bundle_fails_if_discovered_dir_disappears(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
import enroll.manifest_safety as ms
|
||||||
|
|
||||||
|
bundle = _write_bundle(tmp_path)
|
||||||
|
unstable = bundle / "unstable"
|
||||||
|
unstable.mkdir()
|
||||||
|
real_lstat = ms.Path.lstat
|
||||||
|
|
||||||
|
def fake_lstat(self):
|
||||||
|
if self == unstable:
|
||||||
|
raise FileNotFoundError(str(self))
|
||||||
|
return real_lstat(self)
|
||||||
|
|
||||||
|
monkeypatch.setattr(ms.Path, "lstat", fake_lstat)
|
||||||
|
with pytest.raises(ArtifactSafetyError, match="changed while being frozen"):
|
||||||
|
freeze_directory_bundle(bundle)
|
||||||
|
|
||||||
|
|
||||||
|
def test_freeze_directory_bundle_fails_if_file_changes_during_read(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
import enroll.manifest_safety as ms
|
||||||
|
|
||||||
|
bundle = _write_bundle(tmp_path)
|
||||||
|
changing = bundle / "state.json"
|
||||||
|
real_read = ms.os.read
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
def mutating_read(fd: int, size: int) -> bytes:
|
||||||
|
nonlocal changed
|
||||||
|
data = real_read(fd, size)
|
||||||
|
if not changed and data:
|
||||||
|
changed = True
|
||||||
|
# Same length: size-only checking would miss this in-place rewrite.
|
||||||
|
changing.write_bytes(b'{"tampered":1}')
|
||||||
|
return data
|
||||||
|
|
||||||
|
monkeypatch.setattr(ms.os, "read", mutating_read)
|
||||||
|
with pytest.raises(ArtifactSafetyError, match="changed while being frozen"):
|
||||||
|
freeze_directory_bundle(bundle)
|
||||||
|
|
||||||
|
|
||||||
def test_freeze_directory_bundle_rejects_hardlinked_file(tmp_path: Path):
|
def test_freeze_directory_bundle_rejects_hardlinked_file(tmp_path: Path):
|
||||||
bundle = _write_bundle(tmp_path)
|
bundle = _write_bundle(tmp_path)
|
||||||
secret = tmp_path / "secret"
|
secret = tmp_path / "secret"
|
||||||
|
|
@ -222,6 +275,37 @@ def test_freeze_directory_bundle_rejects_hardlinked_file(tmp_path: Path):
|
||||||
freeze_directory_bundle(bundle)
|
freeze_directory_bundle(bundle)
|
||||||
|
|
||||||
|
|
||||||
|
def test_freeze_directory_bundle_rejects_aggregate_size_bomb(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
import enroll.manifest_safety as ms
|
||||||
|
|
||||||
|
bundle = _write_bundle(tmp_path)
|
||||||
|
extra = bundle / "artifacts" / "app" / "etc" / "app" / "extra.conf"
|
||||||
|
extra.write_bytes(b"x" * 32)
|
||||||
|
|
||||||
|
# Each file is individually small, but the aggregate must still be bounded.
|
||||||
|
monkeypatch.setattr(ms, "_FREEZE_MAX_TOTAL_BYTES", 32)
|
||||||
|
with pytest.raises(ArtifactSafetyError, match="total file size exceeds"):
|
||||||
|
freeze_directory_bundle(bundle)
|
||||||
|
|
||||||
|
|
||||||
|
def test_freeze_directory_bundle_counts_directories_against_entry_cap(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
import enroll.manifest_safety as ms
|
||||||
|
|
||||||
|
bundle = _write_bundle(tmp_path)
|
||||||
|
(bundle / "empty-a").mkdir()
|
||||||
|
(bundle / "empty-b").mkdir()
|
||||||
|
|
||||||
|
# Directory-only trees consume inodes and traversal time too. The old
|
||||||
|
# file-only limit allowed an unbounded number of empty directories.
|
||||||
|
monkeypatch.setattr(ms, "_FREEZE_MAX_ENTRIES", 2)
|
||||||
|
with pytest.raises(ArtifactSafetyError, match="too many filesystem entries"):
|
||||||
|
freeze_directory_bundle(bundle)
|
||||||
|
|
||||||
|
|
||||||
def test_freeze_directory_bundle_fails_loudly_on_unreadable_subdir(tmp_path: Path):
|
def test_freeze_directory_bundle_fails_loudly_on_unreadable_subdir(tmp_path: Path):
|
||||||
"""An unreadable subtree must abort the freeze, not silently truncate it.
|
"""An unreadable subtree must abort the freeze, not silently truncate it.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import io
|
import io
|
||||||
|
import shlex
|
||||||
import tarfile
|
import tarfile
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -8,10 +10,9 @@ from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
# The remote harvest now SHA-256-verifies the uploaded zipapp on the remote
|
# Sudo harvests promote and verify the upload into a root-private directory;
|
||||||
# before executing it. Tests mock _build_enroll_pyz to write these fixed bytes
|
# no-sudo harvests retain the direct remote digest check. Tests mock the local
|
||||||
# and return (path, _FAKE_PYZ_SHA256); the fake SSH routers below answer the
|
# builder with these fixed bytes and digest so both paths can be exercised.
|
||||||
# verification command with the same digest so the happy paths proceed.
|
|
||||||
_FAKE_PYZ_BYTES = b"PYZ"
|
_FAKE_PYZ_BYTES = b"PYZ"
|
||||||
_FAKE_PYZ_SHA256 = "d6f4e1dbf7ba69af6c798c6f6f67383c978e68f4201bf31902275ef37e6263e1"
|
_FAKE_PYZ_SHA256 = "d6f4e1dbf7ba69af6c798c6f6f67383c978e68f4201bf31902275ef37e6263e1"
|
||||||
|
|
||||||
|
|
@ -27,9 +28,28 @@ def _fake_build_enroll_pyz(td) -> tuple[Path, str]:
|
||||||
return p, _FAKE_PYZ_SHA256
|
return p, _FAKE_PYZ_SHA256
|
||||||
|
|
||||||
|
|
||||||
|
def _is_pyz_promote_cmd(cmd: str) -> bool:
|
||||||
|
"""True if *cmd* is the privileged copy-and-verify operation."""
|
||||||
|
return ".enroll.pyz.tmp" in cmd and "os.link" in cmd
|
||||||
|
|
||||||
|
|
||||||
def _is_pyz_verify_cmd(cmd: str) -> bool:
|
def _is_pyz_verify_cmd(cmd: str) -> bool:
|
||||||
"""True if *cmd* is the remote pyz SHA-256 integrity check."""
|
"""True if *cmd* is the no-sudo remote pyz SHA-256 check."""
|
||||||
return "hashlib.sha256" in cmd and "enroll.pyz" in cmd
|
return (
|
||||||
|
"hashlib.sha256" in cmd and "enroll.pyz" in cmd and not _is_pyz_promote_cmd(cmd)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_remote_uid_cmd(cmd: str) -> bool:
|
||||||
|
return "os.getuid" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
def _is_archive_hash_cmd(cmd: str) -> bool:
|
||||||
|
return "hashlib.sha256" in cmd and "bundle.tgz" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
def _archive_sha256(payload: bytes) -> bytes:
|
||||||
|
return hashlib.sha256(payload).hexdigest().encode()
|
||||||
|
|
||||||
|
|
||||||
def _make_tgz_bytes(files: dict[str, bytes]) -> bytes:
|
def _make_tgz_bytes(files: dict[str, bytes]) -> bytes:
|
||||||
|
|
@ -154,6 +174,11 @@ def test_remote_harvest_happy_path(tmp_path: Path, monkeypatch):
|
||||||
def put(self, local: str, remote: str) -> None:
|
def put(self, local: str, remote: str) -> None:
|
||||||
self.put_calls.append((local, remote))
|
self.put_calls.append((local, remote))
|
||||||
|
|
||||||
|
def get(self, _remote: str, local: str, callback=None) -> None:
|
||||||
|
Path(local).write_bytes(tgz)
|
||||||
|
if callback is not None:
|
||||||
|
callback(len(tgz), len(tgz))
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -175,17 +200,22 @@ def test_remote_harvest_happy_path(tmp_path: Path, monkeypatch):
|
||||||
return self._sftp
|
return self._sftp
|
||||||
|
|
||||||
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
||||||
# Integrity check of the uploaded pyz (added with SHA-256 verify).
|
if _is_pyz_promote_cmd(cmd):
|
||||||
|
calls.append((cmd, bool(get_pty)))
|
||||||
|
return (None, _Stdout(b""), _Stderr())
|
||||||
if _is_pyz_verify_cmd(cmd):
|
if _is_pyz_verify_cmd(cmd):
|
||||||
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
||||||
calls.append((cmd, bool(get_pty)))
|
calls.append((cmd, bool(get_pty)))
|
||||||
|
if _is_remote_uid_cmd(cmd):
|
||||||
|
return (None, _Stdout(b"1000"), _Stderr())
|
||||||
|
if _is_archive_hash_cmd(cmd):
|
||||||
|
return (None, _Stdout(_archive_sha256(tgz)), _Stderr())
|
||||||
# The tar stream uses exec_command directly.
|
# The tar stream uses exec_command directly.
|
||||||
if cmd.startswith("tar -cz -C"):
|
if cmd.startswith("tar -cz -C"):
|
||||||
return (None, _Stdout(tgz, rc=0), _Stderr(b""))
|
return (None, _Stdout(tgz, rc=0), _Stderr(b""))
|
||||||
|
|
||||||
# _ssh_run path: id -un, mktemp -d, chmod, sudo harvest, sudo chown, rm -rf
|
# _ssh_run path: mktemp -d, chmod, sudo harvest,
|
||||||
if cmd == "id -un":
|
# root tar creation/permissions, rm -rf
|
||||||
return (None, _Stdout(b"alice\n"), _Stderr())
|
|
||||||
if cmd == "mktemp -d":
|
if cmd == "mktemp -d":
|
||||||
return (None, _Stdout(b"/tmp/enroll-remote-123\n"), _Stderr())
|
return (None, _Stdout(b"/tmp/enroll-remote-123\n"), _Stderr())
|
||||||
if cmd.startswith("sudo -n") and " mktemp -d" in cmd:
|
if cmd.startswith("sudo -n") and " mktemp -d" in cmd:
|
||||||
|
|
@ -206,7 +236,7 @@ def test_remote_harvest_happy_path(tmp_path: Path, monkeypatch):
|
||||||
return (None, _Stdout(b""), _Stderr())
|
return (None, _Stdout(b""), _Stderr())
|
||||||
if " harvest " in cmd:
|
if " harvest " in cmd:
|
||||||
return (None, _Stdout(b""), _Stderr())
|
return (None, _Stdout(b""), _Stderr())
|
||||||
if cmd.startswith("sudo -n") and " chown -R" in cmd:
|
if cmd.startswith("sudo -n") and " tar -czf " in cmd:
|
||||||
if not get_pty:
|
if not get_pty:
|
||||||
msg = b"sudo: sorry, you must have a tty to run sudo\n"
|
msg = b"sudo: sorry, you must have a tty to run sudo\n"
|
||||||
return (None, _Stdout(b"", rc=1, err=msg), _Stderr(msg))
|
return (None, _Stdout(b"", rc=1, err=msg), _Stderr(msg))
|
||||||
|
|
@ -248,7 +278,7 @@ def test_remote_harvest_happy_path(tmp_path: Path, monkeypatch):
|
||||||
assert state_path.exists()
|
assert state_path.exists()
|
||||||
assert b"ok" in state_path.read_bytes()
|
assert b"ok" in state_path.read_bytes()
|
||||||
|
|
||||||
# Ensure we attempted remote harvest with sudo and passed include/exclude and dangerous.
|
# Ensure remote harvest used sudo and passed include/exclude/dangerous.
|
||||||
joined = "\n".join([c for c, _pty in calls])
|
joined = "\n".join([c for c, _pty in calls])
|
||||||
assert "sudo" in joined
|
assert "sudo" in joined
|
||||||
assert "--dangerous" in joined
|
assert "--dangerous" in joined
|
||||||
|
|
@ -257,25 +287,58 @@ def test_remote_harvest_happy_path(tmp_path: Path, monkeypatch):
|
||||||
assert "sudo -n -p '' -- mktemp -d" in joined
|
assert "sudo -n -p '' -- mktemp -d" in joined
|
||||||
assert "--out /tmp/enroll-root-123/bundle" in joined
|
assert "--out /tmp/enroll-root-123/bundle" in joined
|
||||||
assert "--out /tmp/enroll-remote-123/bundle" not in joined
|
assert "--out /tmp/enroll-remote-123/bundle" not in joined
|
||||||
assert "chown -R -- alice /tmp/enroll-root-123" in joined
|
assert "chown -R" not in joined
|
||||||
assert "tar -cz -C /tmp/enroll-root-123/bundle ." in joined
|
assert (
|
||||||
|
"tar -czf /tmp/enroll-root-123/bundle.tgz " "-C /tmp/enroll-root-123/bundle ."
|
||||||
|
) in joined
|
||||||
|
assert "chmod 0400 -- /tmp/enroll-root-123/bundle.tgz" in joined
|
||||||
|
assert "chown -- 1000 /tmp/enroll-root-123/bundle.tgz" in joined
|
||||||
|
assert "chmod 0711 -- /tmp/enroll-root-123" in joined
|
||||||
|
|
||||||
|
# The upload is promoted and verified before harvest, and sudo executes
|
||||||
|
# only the root-owned copy. A later replacement of the SSH-user path can no
|
||||||
|
# longer alter the bytes that Python opens as root.
|
||||||
|
promote_i, promote_cmd = next(
|
||||||
|
(i, c) for i, (c, _pty) in enumerate(calls) if _is_pyz_promote_cmd(c)
|
||||||
|
)
|
||||||
|
harvest_i, harvest_cmd = next(
|
||||||
|
(i, c)
|
||||||
|
for i, (c, _pty) in enumerate(calls)
|
||||||
|
if c.startswith("sudo -n") and " harvest " in c
|
||||||
|
)
|
||||||
|
assert promote_i < harvest_i
|
||||||
|
promote_argv = shlex.split(promote_cmd)
|
||||||
|
assert "/tmp/enroll-remote-123" in promote_argv
|
||||||
|
assert "/tmp/enroll-root-123" in promote_argv
|
||||||
|
assert promote_argv.count("enroll.pyz") >= 2
|
||||||
|
assert "/tmp/enroll-root-123/enroll.pyz" in harvest_cmd
|
||||||
|
assert "/tmp/enroll-remote-123/enroll.pyz" not in harvest_cmd
|
||||||
|
harvest_argv = shlex.split(harvest_cmd)
|
||||||
|
pyz_i = harvest_argv.index("/tmp/enroll-root-123/enroll.pyz")
|
||||||
|
assert harvest_argv[pyz_i - 2 : pyz_i] == ["-I", "-S"]
|
||||||
|
assert not any(_is_pyz_verify_cmd(c) for c, _pty in calls)
|
||||||
|
|
||||||
|
# The trusted digest must be obtained while the archive is still private,
|
||||||
|
# before ownership/read access is handed to the SSH account.
|
||||||
|
archive_hash_i = next(
|
||||||
|
i for i, (c, _pty) in enumerate(calls) if _is_archive_hash_cmd(c)
|
||||||
|
)
|
||||||
|
chown_i = next(i for i, (c, _pty) in enumerate(calls) if "chown -- 1000" in c)
|
||||||
|
expose_i = next(i for i, (c, _pty) in enumerate(calls) if "chmod 0711" in c)
|
||||||
|
assert archive_hash_i < chown_i < expose_i
|
||||||
|
|
||||||
# Ensure we fall back to PTY only when sudo reports it is required.
|
# Ensure we fall back to PTY only when sudo reports it is required.
|
||||||
assert any(c == "id -un" and pty is False for c, pty in calls)
|
|
||||||
|
|
||||||
sudo_harvest = [
|
sudo_harvest = [
|
||||||
(c, pty) for c, pty in calls if c.startswith("sudo -n") and " harvest " in c
|
(c, pty) for c, pty in calls if c.startswith("sudo -n") and " harvest " in c
|
||||||
]
|
]
|
||||||
assert any(pty is False for _c, pty in sudo_harvest)
|
assert any(pty is False for _c, pty in sudo_harvest)
|
||||||
assert any(pty is True for _c, pty in sudo_harvest)
|
assert any(pty is True for _c, pty in sudo_harvest)
|
||||||
|
|
||||||
sudo_chown = [
|
sudo_tar = [
|
||||||
(c, pty) for c, pty in calls if c.startswith("sudo -n") and " chown -R" in c
|
(c, pty) for c, pty in calls if c.startswith("sudo -n") and " tar -czf " in c
|
||||||
]
|
]
|
||||||
assert any(pty is False for _c, pty in sudo_chown)
|
assert any(pty is False for _c, pty in sudo_tar)
|
||||||
assert any(pty is True for _c, pty in sudo_chown)
|
assert any(pty is True for _c, pty in sudo_tar)
|
||||||
|
|
||||||
assert any(c.startswith("tar -cz -C") and pty is False for c, pty in calls)
|
|
||||||
|
|
||||||
|
|
||||||
def test_remote_harvest_no_sudo_does_not_request_pty_or_chown(
|
def test_remote_harvest_no_sudo_does_not_request_pty_or_chown(
|
||||||
|
|
@ -354,6 +417,11 @@ def test_remote_harvest_no_sudo_does_not_request_pty_or_chown(
|
||||||
def put(self, _local: str, _remote: str) -> None:
|
def put(self, _local: str, _remote: str) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get(self, _remote: str, local: str, callback=None) -> None:
|
||||||
|
Path(local).write_bytes(tgz)
|
||||||
|
if callback is not None:
|
||||||
|
callback(len(tgz), len(tgz))
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -374,7 +442,9 @@ def test_remote_harvest_no_sudo_does_not_request_pty_or_chown(
|
||||||
return self._sftp
|
return self._sftp
|
||||||
|
|
||||||
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
||||||
# Integrity check of the uploaded pyz (added with SHA-256 verify).
|
if _is_pyz_promote_cmd(cmd):
|
||||||
|
calls.append((cmd, bool(get_pty)))
|
||||||
|
return (None, _Stdout(b""), _Stderr())
|
||||||
if _is_pyz_verify_cmd(cmd):
|
if _is_pyz_verify_cmd(cmd):
|
||||||
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
||||||
calls.append((cmd, bool(get_pty)))
|
calls.append((cmd, bool(get_pty)))
|
||||||
|
|
@ -509,6 +579,11 @@ def test_remote_harvest_sudo_password_retry_uses_sudo_s_and_writes_password(
|
||||||
def put(self, _local: str, _remote: str) -> None:
|
def put(self, _local: str, _remote: str) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get(self, _remote: str, local: str, callback=None) -> None:
|
||||||
|
Path(local).write_bytes(tgz)
|
||||||
|
if callback is not None:
|
||||||
|
callback(len(tgz), len(tgz))
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -529,10 +604,16 @@ def test_remote_harvest_sudo_password_retry_uses_sudo_s_and_writes_password(
|
||||||
return self._sftp
|
return self._sftp
|
||||||
|
|
||||||
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
||||||
# Integrity check of the uploaded pyz (added with SHA-256 verify).
|
if _is_pyz_promote_cmd(cmd):
|
||||||
|
calls.append((cmd, bool(get_pty)))
|
||||||
|
return (None, _Stdout(b""), _Stderr())
|
||||||
if _is_pyz_verify_cmd(cmd):
|
if _is_pyz_verify_cmd(cmd):
|
||||||
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
||||||
calls.append((cmd, bool(get_pty)))
|
calls.append((cmd, bool(get_pty)))
|
||||||
|
if _is_remote_uid_cmd(cmd):
|
||||||
|
return (None, _Stdout(b"1000"), _Stderr())
|
||||||
|
if _is_archive_hash_cmd(cmd):
|
||||||
|
return (None, _Stdout(_archive_sha256(tgz)), _Stderr())
|
||||||
|
|
||||||
# Tar stream
|
# Tar stream
|
||||||
if cmd.startswith("tar -cz -C"):
|
if cmd.startswith("tar -cz -C"):
|
||||||
|
|
@ -562,10 +643,6 @@ def test_remote_harvest_sudo_password_retry_uses_sudo_s_and_writes_password(
|
||||||
if cmd.startswith("sudo -S") and " harvest " in cmd:
|
if cmd.startswith("sudo -S") and " harvest " in cmd:
|
||||||
return (_Stdin(cmd), _Stdout(b"", rc=0), _Stderr(b""))
|
return (_Stdin(cmd), _Stdout(b"", rc=0), _Stderr(b""))
|
||||||
|
|
||||||
# chown succeeds passwordlessly (e.g., sudo timestamp is warm).
|
|
||||||
if cmd.startswith("sudo -n") and " chown -R" in cmd:
|
|
||||||
return (_Stdin(cmd), _Stdout(b"", rc=0), _Stderr(b""))
|
|
||||||
|
|
||||||
if cmd.startswith("sudo -n") and " rm -rf -- /tmp/enroll-root-789" in cmd:
|
if cmd.startswith("sudo -n") and " rm -rf -- /tmp/enroll-root-789" in cmd:
|
||||||
return (_Stdin(cmd), _Stdout(b"", rc=0), _Stderr(b""))
|
return (_Stdin(cmd), _Stdout(b"", rc=0), _Stderr(b""))
|
||||||
if cmd.startswith("rm -rf"):
|
if cmd.startswith("rm -rf"):
|
||||||
|
|
@ -894,6 +971,11 @@ def test_remote_harvest_ssh_key_passphrase_retry(monkeypatch, tmp_path: Path):
|
||||||
def put(self, _local: str, _remote: str) -> None:
|
def put(self, _local: str, _remote: str) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get(self, _remote: str, local: str, callback=None) -> None:
|
||||||
|
Path(local).write_bytes(tgz)
|
||||||
|
if callback is not None:
|
||||||
|
callback(len(tgz), len(tgz))
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -914,7 +996,8 @@ def test_remote_harvest_ssh_key_passphrase_retry(monkeypatch, tmp_path: Path):
|
||||||
return self._sftp
|
return self._sftp
|
||||||
|
|
||||||
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
def exec_command(self, cmd: str, *, get_pty: bool = False, **_kwargs):
|
||||||
# Integrity check of the uploaded pyz (added with SHA-256 verify).
|
if _is_pyz_promote_cmd(cmd):
|
||||||
|
return (None, _Stdout(b""), _Stderr())
|
||||||
if _is_pyz_verify_cmd(cmd):
|
if _is_pyz_verify_cmd(cmd):
|
||||||
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
||||||
if cmd.startswith("tar -cz -C"):
|
if cmd.startswith("tar -cz -C"):
|
||||||
|
|
@ -1013,6 +1096,11 @@ def test_remote_harvest_ssh_key_passphrase_raises_when_not_interactive(
|
||||||
def put(self, _local: str, _remote: str) -> None:
|
def put(self, _local: str, _remote: str) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get(self, _remote: str, local: str, callback=None) -> None:
|
||||||
|
Path(local).write_bytes(b"")
|
||||||
|
if callback is not None:
|
||||||
|
callback(0, 0)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -1033,7 +1121,8 @@ def test_remote_harvest_ssh_key_passphrase_raises_when_not_interactive(
|
||||||
return self._sftp
|
return self._sftp
|
||||||
|
|
||||||
def exec_command(self, cmd: str, **_kwargs):
|
def exec_command(self, cmd: str, **_kwargs):
|
||||||
# Integrity check of the uploaded pyz (added with SHA-256 verify).
|
if _is_pyz_promote_cmd(cmd):
|
||||||
|
return (None, _Stdout(b""), _Stderr())
|
||||||
if _is_pyz_verify_cmd(cmd):
|
if _is_pyz_verify_cmd(cmd):
|
||||||
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
return (None, _Stdout(_FAKE_PYZ_SHA256.encode()), _Stderr())
|
||||||
return (_Stdout(), _Stdout(), _Stderr())
|
return (_Stdout(), _Stdout(), _Stderr())
|
||||||
|
|
@ -1165,6 +1254,74 @@ def test_remote_verify_pyz_sha256_rejects_nonzero_rc():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_enroll_pyz_harvest_starts_without_site_packages(tmp_path: Path):
|
||||||
|
"""The remote payload must not depend on packages installed on the target.
|
||||||
|
|
||||||
|
``-S`` disables site-packages, reproducing a target that has Python but no
|
||||||
|
jsonschema/PyYAML. ``-I`` also ignores PYTHON* environment variables and
|
||||||
|
the current directory. The old eager CLI imports failed here before
|
||||||
|
argparse could dispatch to ``harvest``.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
pyz, _sha = r._build_enroll_pyz(tmp_path)
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, "-I", "-S", str(pyz), "harvest", "--help"],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
assert "usage: enroll harvest" in proc.stdout
|
||||||
|
assert "ModuleNotFoundError" not in proc.stderr
|
||||||
|
assert "jsonschema" not in proc.stderr
|
||||||
|
assert "yaml" not in proc.stderr.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_enroll_pyz_runs_harvest_without_site_packages(tmp_path: Path):
|
||||||
|
"""Exercise command dispatch and the actual harvest implementation under -S."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
build_dir = tmp_path / "build"
|
||||||
|
build_dir.mkdir()
|
||||||
|
pyz, _sha = r._build_enroll_pyz(build_dir)
|
||||||
|
out_dir = tmp_path / "bundle"
|
||||||
|
proc = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-I",
|
||||||
|
"-S",
|
||||||
|
str(pyz),
|
||||||
|
"harvest",
|
||||||
|
"--out",
|
||||||
|
str(out_dir),
|
||||||
|
"--exclude-path",
|
||||||
|
"/**",
|
||||||
|
"--assume-safe-path",
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
assert (out_dir / "state.json").is_file()
|
||||||
|
assert "ModuleNotFoundError" not in proc.stderr
|
||||||
|
assert "jsonschema" not in proc.stderr
|
||||||
|
assert "yaml" not in proc.stderr.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_build_enroll_pyz_excludes_tests_and_caches_and_returns_sha(tmp_path: Path):
|
def test_build_enroll_pyz_excludes_tests_and_caches_and_returns_sha(tmp_path: Path):
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
@ -1203,6 +1360,73 @@ def test_safe_extract_tar_rejects_too_many_members(tmp_path: Path, monkeypatch):
|
||||||
r._safe_extract_tar(tf, tmp_path)
|
r._safe_extract_tar(tf, tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_downloaded_remote_archive_digest_mismatch_is_rejected(
|
||||||
|
tmp_path: Path,
|
||||||
|
):
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
archive = tmp_path / "bundle.tgz"
|
||||||
|
archive.write_bytes(b"tampered")
|
||||||
|
with pytest.raises(RuntimeError, match="integrity check failed"):
|
||||||
|
r._verify_downloaded_archive_sha256(archive, "0" * 64)
|
||||||
|
|
||||||
|
|
||||||
|
def test_downloaded_remote_archive_digest_match_is_accepted(tmp_path: Path):
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
archive = tmp_path / "bundle.tgz"
|
||||||
|
archive.write_bytes(b"original")
|
||||||
|
expected = hashlib.sha256(b"original").hexdigest()
|
||||||
|
r._verify_downloaded_archive_sha256(archive, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_archive_download_size_is_bounded(monkeypatch):
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
monkeypatch.setattr(r, "_TAR_MAX_COMPRESSED_BYTES", 10)
|
||||||
|
r._check_tar_download_size(10)
|
||||||
|
with pytest.raises(RuntimeError, match="compressed download limit"):
|
||||||
|
r._check_tar_download_size(11)
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_extract_tar_does_not_eagerly_index_archive(tmp_path: Path):
|
||||||
|
"""The safety caps must apply while parsing, before getmembers() can
|
||||||
|
materialise an attacker-sized member list in memory."""
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
bio = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=bio, mode="w:gz") as tf:
|
||||||
|
ti = tarfile.TarInfo(name="state.json")
|
||||||
|
ti.size = 2
|
||||||
|
tf.addfile(ti, io.BytesIO(b"{}"))
|
||||||
|
|
||||||
|
bio.seek(0)
|
||||||
|
with tarfile.open(fileobj=bio, mode="r:gz") as tf:
|
||||||
|
tf.getmembers = lambda: (_ for _ in ()).throw( # type: ignore[method-assign]
|
||||||
|
AssertionError("getmembers() must not be used for untrusted archives")
|
||||||
|
)
|
||||||
|
r._safe_extract_tar(tf, tmp_path)
|
||||||
|
|
||||||
|
assert (tmp_path / "state.json").read_bytes() == b"{}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_extract_tar_counts_dot_entries_against_member_cap(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
import enroll.remote as r
|
||||||
|
|
||||||
|
monkeypatch.setattr(r, "_TAR_MAX_MEMBERS", 2)
|
||||||
|
bio = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=bio, mode="w:gz") as tf:
|
||||||
|
for _ in range(3):
|
||||||
|
tf.addfile(tarfile.TarInfo(name="."))
|
||||||
|
|
||||||
|
bio.seek(0)
|
||||||
|
with tarfile.open(fileobj=bio, mode="r:gz") as tf:
|
||||||
|
with pytest.raises(RuntimeError, match="too many members"):
|
||||||
|
r._safe_extract_tar(tf, tmp_path)
|
||||||
|
|
||||||
|
|
||||||
def test_safe_extract_tar_rejects_oversized_member(tmp_path: Path, monkeypatch):
|
def test_safe_extract_tar_rejects_oversized_member(tmp_path: Path, monkeypatch):
|
||||||
import enroll.remote as r
|
import enroll.remote as r
|
||||||
|
|
||||||
|
|
|
||||||
508
tests/test_remote_pyz_promotion.py
Normal file
508
tests/test_remote_pyz_promotion.py
Normal file
|
|
@ -0,0 +1,508 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import enroll.remote as remote
|
||||||
|
|
||||||
|
|
||||||
|
_TRUSTED_PYZ = b"trusted enroll zipapp bytes\n"
|
||||||
|
_TRUSTED_SHA256 = hashlib.sha256(_TRUSTED_PYZ).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_dirs(tmp_path: Path) -> tuple[Path, Path]:
|
||||||
|
stage_dir = tmp_path / "user-stage"
|
||||||
|
root_dir = tmp_path / "root-stage"
|
||||||
|
stage_dir.mkdir(mode=0o700)
|
||||||
|
root_dir.mkdir(mode=0o700)
|
||||||
|
# mkdir is affected by umask; the production flow explicitly chmods 0700.
|
||||||
|
root_dir.chmod(0o700)
|
||||||
|
return stage_dir, root_dir
|
||||||
|
|
||||||
|
|
||||||
|
def _load_promotion_function():
|
||||||
|
namespace = {"__name__": "enroll_promotion_test"}
|
||||||
|
exec(remote._REMOTE_PROMOTE_PYZ_SCRIPT, namespace)
|
||||||
|
return namespace["_promote"]
|
||||||
|
|
||||||
|
|
||||||
|
def _promotion_args(
|
||||||
|
stage_dir: Path,
|
||||||
|
root_dir: Path,
|
||||||
|
*,
|
||||||
|
expected_sha256: str = _TRUSTED_SHA256,
|
||||||
|
expected_size: int = len(_TRUSTED_PYZ),
|
||||||
|
source_name: str = "enroll.pyz",
|
||||||
|
destination_name: str = "enroll.pyz",
|
||||||
|
expected_owner_uid: int | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
if expected_owner_uid is None:
|
||||||
|
expected_owner_uid = os.geteuid()
|
||||||
|
return [
|
||||||
|
str(stage_dir),
|
||||||
|
source_name,
|
||||||
|
str(root_dir),
|
||||||
|
destination_name,
|
||||||
|
expected_sha256,
|
||||||
|
str(expected_size),
|
||||||
|
str(expected_owner_uid),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _promote_direct(
|
||||||
|
stage_dir: Path,
|
||||||
|
root_dir: Path,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
_load_promotion_function()(_promotion_args(stage_dir, root_dir, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def _run_promotion_subprocess(
|
||||||
|
stage_dir: Path,
|
||||||
|
root_dir: Path,
|
||||||
|
*,
|
||||||
|
timeout: float = 5,
|
||||||
|
**kwargs,
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
|
return subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-I",
|
||||||
|
"-c",
|
||||||
|
remote._REMOTE_PROMOTE_PYZ_SCRIPT,
|
||||||
|
*_promotion_args(stage_dir, root_dir, **kwargs),
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_no_promoted_copy(root_dir: Path) -> None:
|
||||||
|
assert not (root_dir / "enroll.pyz").exists()
|
||||||
|
assert not (root_dir / ".enroll.pyz.tmp").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_promotion_script_entrypoint_publishes_exact_private_copy(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(_TRUSTED_PYZ)
|
||||||
|
|
||||||
|
result = _run_promotion_subprocess(stage_dir, root_dir)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
promoted = root_dir / "enroll.pyz"
|
||||||
|
assert promoted.read_bytes() == _TRUSTED_PYZ
|
||||||
|
promoted_stat = promoted.stat()
|
||||||
|
assert stat.S_IMODE(promoted_stat.st_mode) == 0o500
|
||||||
|
assert promoted_stat.st_nlink == 1
|
||||||
|
assert not (root_dir / ".enroll.pyz.tmp").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_replacing_user_path_after_promotion_cannot_change_root_copy(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
source = stage_dir / "enroll.pyz"
|
||||||
|
source.write_bytes(_TRUSTED_PYZ)
|
||||||
|
|
||||||
|
_promote_direct(stage_dir, root_dir)
|
||||||
|
|
||||||
|
replacement = stage_dir / "replacement.pyz"
|
||||||
|
replacement.write_bytes(b"print('attacker code')\n")
|
||||||
|
os.replace(replacement, source)
|
||||||
|
|
||||||
|
assert source.read_bytes() != _TRUSTED_PYZ
|
||||||
|
assert (root_dir / "enroll.pyz").read_bytes() == _TRUSTED_PYZ
|
||||||
|
|
||||||
|
|
||||||
|
def test_digest_mismatch_fails_closed_and_removes_partial_copy(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
payload = b"attacker-controlled bytes"
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(payload)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="SHA-256 mismatch"):
|
||||||
|
_promote_direct(stage_dir, root_dir, expected_size=len(payload))
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"reported_size", [0, len(_TRUSTED_PYZ) - 1, len(_TRUSTED_PYZ) + 1]
|
||||||
|
)
|
||||||
|
def test_unexpected_source_size_fails_before_publication(
|
||||||
|
tmp_path: Path, reported_size: int
|
||||||
|
):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(_TRUSTED_PYZ)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="unexpected size"):
|
||||||
|
_promote_direct(stage_dir, root_dir, expected_size=reported_size)
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_symlink_is_rejected_without_following_it(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
target = stage_dir / "payload"
|
||||||
|
target.write_bytes(_TRUSTED_PYZ)
|
||||||
|
(stage_dir / "enroll.pyz").symlink_to(target.name)
|
||||||
|
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
_promote_direct(stage_dir, root_dir)
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hardlinked_source_is_rejected(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
source = stage_dir / "enroll.pyz"
|
||||||
|
source.write_bytes(_TRUSTED_PYZ)
|
||||||
|
os.link(source, stage_dir / "second-name")
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="must not be hard-linked"):
|
||||||
|
_promote_direct(stage_dir, root_dir)
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="FIFO test requires POSIX")
|
||||||
|
def test_fifo_source_is_rejected_without_blocking(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
os.mkfifo(stage_dir / "enroll.pyz", mode=0o600)
|
||||||
|
|
||||||
|
result = _run_promotion_subprocess(
|
||||||
|
stage_dir,
|
||||||
|
root_dir,
|
||||||
|
expected_sha256=hashlib.sha256(b"").hexdigest(),
|
||||||
|
expected_size=0,
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode != 0
|
||||||
|
assert "not a regular file" in result.stderr
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_directory_source_is_rejected(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
(stage_dir / "enroll.pyz").mkdir()
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="not a regular file"):
|
||||||
|
_promote_direct(stage_dir, root_dir)
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_symlink_destination_directory_is_rejected(tmp_path: Path):
|
||||||
|
stage_dir = tmp_path / "user-stage"
|
||||||
|
stage_dir.mkdir()
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(_TRUSTED_PYZ)
|
||||||
|
real_root = tmp_path / "real-root"
|
||||||
|
real_root.mkdir(mode=0o700)
|
||||||
|
real_root.chmod(0o700)
|
||||||
|
root_link = tmp_path / "root-link"
|
||||||
|
root_link.symlink_to(real_root, target_is_directory=True)
|
||||||
|
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
_promote_direct(stage_dir, root_link)
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(real_root)
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_private_destination_directory_is_rejected(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(_TRUSTED_PYZ)
|
||||||
|
root_dir.chmod(0o755)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="not mode 0700"):
|
||||||
|
_promote_direct(stage_dir, root_dir)
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_promotion_requires_expected_effective_uid(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(_TRUSTED_PYZ)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="not running as the expected user"):
|
||||||
|
_promote_direct(
|
||||||
|
stage_dir,
|
||||||
|
root_dir,
|
||||||
|
expected_owner_uid=os.geteuid() + 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_temporary_name_is_not_overwritten(tmp_path: Path):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(_TRUSTED_PYZ)
|
||||||
|
temporary = root_dir / ".enroll.pyz.tmp"
|
||||||
|
temporary.write_bytes(b"pre-existing")
|
||||||
|
|
||||||
|
with pytest.raises(FileExistsError):
|
||||||
|
_promote_direct(stage_dir, root_dir)
|
||||||
|
|
||||||
|
assert temporary.read_bytes() == b"pre-existing"
|
||||||
|
assert not (root_dir / "enroll.pyz").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_existing_final_name_is_not_replaced_and_partial_copy_is_removed(
|
||||||
|
tmp_path: Path,
|
||||||
|
):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
(stage_dir / "enroll.pyz").write_bytes(_TRUSTED_PYZ)
|
||||||
|
final = root_dir / "enroll.pyz"
|
||||||
|
final.write_bytes(b"pre-existing")
|
||||||
|
|
||||||
|
with pytest.raises(FileExistsError):
|
||||||
|
_promote_direct(stage_dir, root_dir)
|
||||||
|
|
||||||
|
assert final.read_bytes() == b"pre-existing"
|
||||||
|
assert not (root_dir / ".enroll.pyz.tmp").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_replacement_after_open_cannot_change_promoted_inode(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
source = stage_dir / "enroll.pyz"
|
||||||
|
source.write_bytes(_TRUSTED_PYZ)
|
||||||
|
replacement = stage_dir / "attacker.pyz"
|
||||||
|
replacement.write_bytes(b"print('attacker code')\n")
|
||||||
|
|
||||||
|
promote = _load_promotion_function()
|
||||||
|
real_read = os.read
|
||||||
|
replaced = False
|
||||||
|
|
||||||
|
def replace_path_before_first_read(fd: int, count: int) -> bytes:
|
||||||
|
nonlocal replaced
|
||||||
|
if not replaced:
|
||||||
|
replaced = True
|
||||||
|
os.replace(replacement, source)
|
||||||
|
return real_read(fd, count)
|
||||||
|
|
||||||
|
monkeypatch.setattr(os, "read", replace_path_before_first_read)
|
||||||
|
promote(_promotion_args(stage_dir, root_dir))
|
||||||
|
|
||||||
|
assert replaced is True
|
||||||
|
assert source.read_bytes() != _TRUSTED_PYZ
|
||||||
|
assert (root_dir / "enroll.pyz").read_bytes() == _TRUSTED_PYZ
|
||||||
|
|
||||||
|
|
||||||
|
def test_in_place_mutation_after_fstat_fails_digest_and_cleans_output(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
stage_dir, root_dir = _prepare_dirs(tmp_path)
|
||||||
|
source = stage_dir / "enroll.pyz"
|
||||||
|
source.write_bytes(_TRUSTED_PYZ)
|
||||||
|
|
||||||
|
promote = _load_promotion_function()
|
||||||
|
real_read = os.read
|
||||||
|
attacker_fd = os.open(source, os.O_WRONLY)
|
||||||
|
mutated = False
|
||||||
|
|
||||||
|
def mutate_inode_before_first_read(fd: int, count: int) -> bytes:
|
||||||
|
nonlocal mutated
|
||||||
|
if not mutated:
|
||||||
|
mutated = True
|
||||||
|
os.lseek(attacker_fd, 0, os.SEEK_SET)
|
||||||
|
os.write(attacker_fd, b"X" * len(_TRUSTED_PYZ))
|
||||||
|
os.fsync(attacker_fd)
|
||||||
|
return real_read(fd, count)
|
||||||
|
|
||||||
|
monkeypatch.setattr(os, "read", mutate_inode_before_first_read)
|
||||||
|
try:
|
||||||
|
with pytest.raises(RuntimeError, match="SHA-256 mismatch"):
|
||||||
|
promote(_promotion_args(stage_dir, root_dir))
|
||||||
|
finally:
|
||||||
|
os.close(attacker_fd)
|
||||||
|
|
||||||
|
assert mutated is True
|
||||||
|
_assert_no_promoted_copy(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_promote_builds_one_privileged_atomic_command(monkeypatch):
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
def fake_ssh_run_sudo(ssh, cmd, *, sudo_password, get_pty):
|
||||||
|
captured.update(
|
||||||
|
ssh=ssh,
|
||||||
|
cmd=cmd,
|
||||||
|
sudo_password=sudo_password,
|
||||||
|
get_pty=get_pty,
|
||||||
|
)
|
||||||
|
return 0, "", ""
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote, "_ssh_run_sudo", fake_ssh_run_sudo)
|
||||||
|
marker = object()
|
||||||
|
|
||||||
|
result = remote._remote_promote_verified_pyz(
|
||||||
|
marker,
|
||||||
|
"/tmp/user-stage/enroll.pyz",
|
||||||
|
"/tmp/root-stage",
|
||||||
|
_TRUSTED_SHA256,
|
||||||
|
len(_TRUSTED_PYZ),
|
||||||
|
remote_python="/usr/bin/python3",
|
||||||
|
sudo_password="secret",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == "/tmp/root-stage/enroll.pyz"
|
||||||
|
assert captured["ssh"] is marker
|
||||||
|
assert captured["sudo_password"] == "secret"
|
||||||
|
assert captured["get_pty"] is True
|
||||||
|
argv = shlex.split(str(captured["cmd"]))
|
||||||
|
assert argv == [
|
||||||
|
"/usr/bin/python3",
|
||||||
|
"-I",
|
||||||
|
"-c",
|
||||||
|
remote._REMOTE_PROMOTE_PYZ_SCRIPT,
|
||||||
|
"/tmp/user-stage",
|
||||||
|
"enroll.pyz",
|
||||||
|
"/tmp/root-stage",
|
||||||
|
"enroll.pyz",
|
||||||
|
_TRUSTED_SHA256,
|
||||||
|
str(len(_TRUSTED_PYZ)),
|
||||||
|
"0",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("uploaded", "root_dir", "digest", "size"),
|
||||||
|
[
|
||||||
|
("relative/enroll.pyz", "/tmp/root", _TRUSTED_SHA256, len(_TRUSTED_PYZ)),
|
||||||
|
("/tmp/stage/enroll.pyz", "relative/root", _TRUSTED_SHA256, len(_TRUSTED_PYZ)),
|
||||||
|
("/tmp/stage/enroll.pyz", "/tmp/root", "not-a-digest", len(_TRUSTED_PYZ)),
|
||||||
|
("/tmp/stage/enroll.pyz", "/tmp/root", _TRUSTED_SHA256, -1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_remote_promote_rejects_invalid_local_parameters_before_sudo(
|
||||||
|
monkeypatch, uploaded: str, root_dir: str, digest: str, size: int
|
||||||
|
):
|
||||||
|
called = False
|
||||||
|
|
||||||
|
def unexpected_sudo(*_args, **_kwargs):
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
raise AssertionError("sudo must not run")
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote, "_ssh_run_sudo", unexpected_sudo)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
remote._remote_promote_verified_pyz(
|
||||||
|
object(),
|
||||||
|
uploaded,
|
||||||
|
root_dir,
|
||||||
|
digest,
|
||||||
|
size,
|
||||||
|
remote_python="python3",
|
||||||
|
sudo_password=None,
|
||||||
|
)
|
||||||
|
assert called is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_remote_promote_reports_privileged_failure(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
remote,
|
||||||
|
"_ssh_run_sudo",
|
||||||
|
lambda *_args, **_kwargs: (1, "", "digest mismatch"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Refusing to execute"):
|
||||||
|
remote._remote_promote_verified_pyz(
|
||||||
|
object(),
|
||||||
|
"/tmp/stage/enroll.pyz",
|
||||||
|
"/tmp/root",
|
||||||
|
_TRUSTED_SHA256,
|
||||||
|
len(_TRUSTED_PYZ),
|
||||||
|
remote_python="python3",
|
||||||
|
sudo_password=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sudo_harvest_stops_before_execution_when_promotion_fails(
|
||||||
|
tmp_path: Path, monkeypatch
|
||||||
|
):
|
||||||
|
import types
|
||||||
|
|
||||||
|
# The fake builder must create the file because _remote_harvest reads its size.
|
||||||
|
def build_pyz(td):
|
||||||
|
pyz = Path(td) / "enroll.pyz"
|
||||||
|
pyz.write_bytes(_TRUSTED_PYZ)
|
||||||
|
return pyz, _TRUSTED_SHA256
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote, "_build_enroll_pyz", build_pyz)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
remote,
|
||||||
|
"_remote_promote_verified_pyz",
|
||||||
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(
|
||||||
|
RuntimeError("promotion rejected attacker bytes")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
user_commands: list[str] = []
|
||||||
|
sudo_commands: list[str] = []
|
||||||
|
|
||||||
|
def fake_run(_ssh, cmd, **_kwargs):
|
||||||
|
user_commands.append(cmd)
|
||||||
|
if cmd == "mktemp -d":
|
||||||
|
return 0, "/tmp/user-stage\n", ""
|
||||||
|
return 0, "", ""
|
||||||
|
|
||||||
|
def fake_sudo(_ssh, cmd, **_kwargs):
|
||||||
|
sudo_commands.append(cmd)
|
||||||
|
if cmd == "mktemp -d":
|
||||||
|
return 0, "/tmp/root-stage\n", ""
|
||||||
|
return 0, "", ""
|
||||||
|
|
||||||
|
monkeypatch.setattr(remote, "_ssh_run", fake_run)
|
||||||
|
monkeypatch.setattr(remote, "_ssh_run_sudo", fake_sudo)
|
||||||
|
|
||||||
|
class FakeSFTP:
|
||||||
|
def put(self, _local, _remote):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
class FakeSSH:
|
||||||
|
def load_system_host_keys(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_missing_host_key_policy(self, _policy):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def connect(self, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def open_sftp(self):
|
||||||
|
return FakeSFTP()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setitem(
|
||||||
|
sys.modules,
|
||||||
|
"paramiko",
|
||||||
|
types.SimpleNamespace(
|
||||||
|
SSHClient=FakeSSH,
|
||||||
|
RejectPolicy=type("RejectPolicy", (), {}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="promotion rejected attacker bytes"):
|
||||||
|
remote._remote_harvest(
|
||||||
|
local_out_dir=tmp_path / "out",
|
||||||
|
remote_host="example.com",
|
||||||
|
no_sudo=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not any(" harvest " in command for command in sudo_commands)
|
||||||
|
assert any(command == "rm -rf -- /tmp/root-stage" for command in sudo_commands)
|
||||||
|
assert any(command == "rm -rf -- /tmp/user-stage" for command in user_commands)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue