From f5eaac9f751ee5d6026ee2bea2677134ae10ee4d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 13 Jan 2026 21:56:28 +1100 Subject: [PATCH 01/48] Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config. --- CHANGELOG.md | 4 ++++ debian/changelog | 6 +++++ enroll/cli.py | 48 ++++++++++++++++++++++++++++++++------- enroll/remote.py | 59 ++++++++++++++++++++++++++++++++++++++++++++---- pyproject.toml | 2 +- rpm/enroll.spec | 5 +++- 6 files changed, 110 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29da559..0772cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.4.2 + + * Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config. + # 0.4.1 * Add interactive output when 'enroll diff --enforce' is invoking Ansible. diff --git a/debian/changelog b/debian/changelog index 086b52e..58e80e3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +enroll (0.4.2) unstable; urgency=medium + + * Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config. + + -- Miguel Jacq Tue, 13 Jan 2026 21:55:00 +1100 + enroll (0.4.1) unstable; urgency=medium * Add interactive output when 'enroll diff --enforce' is invoking Ansible. diff --git a/enroll/cli.py b/enroll/cli.py index c1f0870..69e85ed 100644 --- a/enroll/cli.py +++ b/enroll/cli.py @@ -350,16 +350,33 @@ def _add_remote_args(p: argparse.ArgumentParser) -> None: "--remote-host", help="SSH host to run harvesting on (if set, harvest runs remotely and is pulled locally).", ) + p.add_argument( + "--remote-ssh-config", + nargs="?", + const=str(Path.home() / ".ssh" / "config"), + default=None, + help=( + "Use OpenSSH-style ssh_config settings for --remote-host. " + "If provided without a value, defaults to ~/.ssh/config. " + "(Applies HostName/User/Port/IdentityFile/ProxyCommand/HostKeyAlias when supported.)" + ), + ) p.add_argument( "--remote-port", type=int, - default=22, - help="SSH port for --remote-host (default: 22).", + default=None, + help=( + "SSH port for --remote-host. If omitted, defaults to 22, or a value from ssh_config when " + "--remote-ssh-config is set." + ), ) p.add_argument( "--remote-user", - default=os.environ.get("USER") or None, - help="SSH username for --remote-host (default: local $USER).", + default=None, + help=( + "SSH username for --remote-host. If omitted, defaults to local $USER, or a value from ssh_config when " + "--remote-ssh-config is set." + ), ) # Align terminology with Ansible: "become" == sudo. @@ -728,6 +745,17 @@ def main() -> None: ) args = ap.parse_args(argv) + # Preserve historical defaults for remote harvesting unless ssh_config lookup is enabled. + # This lets ssh_config values take effect when the user did not explicitly set + # --remote-user / --remote-port. + if hasattr(args, "remote_host"): + rsc = getattr(args, "remote_ssh_config", None) + if not rsc: + if getattr(args, "remote_port", None) is None: + setattr(args, "remote_port", 22) + if getattr(args, "remote_user", None) is None: + setattr(args, "remote_user", os.environ.get("USER") or None) + try: if args.cmd == "harvest": sops_fps = getattr(args, "sops", None) @@ -745,8 +773,9 @@ def main() -> None: ask_become_pass=args.ask_become_pass, local_out_dir=tmp_bundle, remote_host=args.remote_host, - remote_port=int(args.remote_port), + remote_port=args.remote_port, remote_user=args.remote_user, + remote_ssh_config=args.remote_ssh_config, dangerous=bool(args.dangerous), no_sudo=bool(args.no_sudo), include_paths=list(getattr(args, "include_path", []) or []), @@ -766,8 +795,9 @@ def main() -> None: ask_become_pass=args.ask_become_pass, local_out_dir=out_dir, remote_host=args.remote_host, - remote_port=int(args.remote_port), + remote_port=args.remote_port, remote_user=args.remote_user, + remote_ssh_config=args.remote_ssh_config, dangerous=bool(args.dangerous), no_sudo=bool(args.no_sudo), include_paths=list(getattr(args, "include_path", []) or []), @@ -968,8 +998,9 @@ def main() -> None: ask_become_pass=args.ask_become_pass, local_out_dir=tmp_bundle, remote_host=args.remote_host, - remote_port=int(args.remote_port), + remote_port=args.remote_port, remote_user=args.remote_user, + remote_ssh_config=args.remote_ssh_config, dangerous=bool(args.dangerous), no_sudo=bool(args.no_sudo), include_paths=list(getattr(args, "include_path", []) or []), @@ -998,8 +1029,9 @@ def main() -> None: ask_become_pass=args.ask_become_pass, local_out_dir=harvest_dir, remote_host=args.remote_host, - remote_port=int(args.remote_port), + remote_port=args.remote_port, remote_user=args.remote_user, + remote_ssh_config=args.remote_ssh_config, dangerous=bool(args.dangerous), no_sudo=bool(args.no_sudo), include_paths=list(getattr(args, "include_path", []) or []), diff --git a/enroll/remote.py b/enroll/remote.py index 93cee74..c7b54a6 100644 --- a/enroll/remote.py +++ b/enroll/remote.py @@ -330,8 +330,9 @@ def _remote_harvest( *, local_out_dir: Path, remote_host: str, - remote_port: int = 22, + remote_port: Optional[int] = None, remote_user: Optional[str] = None, + remote_ssh_config: Optional[str] = None, remote_python: str = "python3", dangerous: bool = False, no_sudo: bool = False, @@ -370,10 +371,60 @@ def _remote_harvest( # Users should add the key to known_hosts. ssh.set_missing_host_key_policy(paramiko.RejectPolicy()) + # Resolve SSH connection parameters. + connect_host = remote_host + connect_port = int(remote_port) if remote_port is not None else 22 + connect_user = remote_user + key_filename = None + sock = None + hostkey_name = connect_host + + if remote_ssh_config: + from paramiko.config import SSHConfig # type: ignore + from paramiko.proxy import ProxyCommand # type: ignore + import socket as _socket + + cfg_path = Path(str(remote_ssh_config)).expanduser() + if not cfg_path.exists(): + raise RuntimeError(f"SSH config file not found: {cfg_path}") + + cfg = SSHConfig() + with cfg_path.open("r", encoding="utf-8") as _fp: + cfg.parse(_fp) + hcfg = cfg.lookup(remote_host) + + connect_host = str(hcfg.get("hostname") or remote_host) + hostkey_name = str(hcfg.get("hostkeyalias") or connect_host) + + if remote_port is None and hcfg.get("port"): + try: + connect_port = int(str(hcfg.get("port"))) + except ValueError: + pass + if connect_user is None and hcfg.get("user"): + connect_user = str(hcfg.get("user")) + + ident = hcfg.get("identityfile") + if ident: + if isinstance(ident, (list, tuple)): + key_filename = [str(Path(p).expanduser()) for p in ident] + else: + key_filename = str(Path(str(ident)).expanduser()) + + proxycmd = hcfg.get("proxycommand") + if proxycmd: + sock = ProxyCommand(str(proxycmd)) + elif hostkey_name != connect_host: + # If HostKeyAlias is used, connect to HostName via a socket but + # use HostKeyAlias for known_hosts lookups. + sock = _socket.create_connection((connect_host, connect_port)) + ssh.connect( - hostname=remote_host, - port=int(remote_port), - username=remote_user, + hostname=hostkey_name if sock is not None else connect_host, + port=connect_port, + username=connect_user, + key_filename=key_filename, + sock=sock, allow_agent=True, look_for_keys=True, ) diff --git a/pyproject.toml b/pyproject.toml index 84b7887..92756d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "enroll" -version = "0.4.1" +version = "0.4.2" description = "Enroll a server's running state retrospectively into Ansible" authors = ["Miguel Jacq "] license = "GPL-3.0-or-later" diff --git a/rpm/enroll.spec b/rpm/enroll.spec index 30bac4e..98f3f8f 100644 --- a/rpm/enroll.spec +++ b/rpm/enroll.spec @@ -1,4 +1,4 @@ -%global upstream_version 0.4.1 +%global upstream_version 0.4.2 Name: enroll Version: %{upstream_version} @@ -43,6 +43,9 @@ Enroll a server's running state retrospectively into Ansible. %{_bindir}/enroll %changelog +* Tue Jan 13 2026 Miguel Jacq - %{version}-%{release} +- Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be s +et, but it can be an 'alias' represented by the 'Host' value in the ssh config. * Sun Jan 11 2026 Miguel Jacq - %{version}-%{release} - Add interactive output when 'enroll diff --enforce' is invoking Ansible. * Sat Jan 10 2026 Miguel Jacq - %{version}-%{release} From 478b0e1b9d8adf81bed7642e74857c62d6b9c315 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 13 Jan 2026 22:03:58 +1100 Subject: [PATCH 02/48] Add README example for --remote-ssh-config --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ba536b..c9b448a 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Harvest state about a host and write a harvest bundle. **Common flags** - Remote harvesting: - - `--remote-host`, `--remote-user`, `--remote-port` + - `--remote-host`, `--remote-user`, `--remote-port`, `--remote-ssh-config` - `--no-sudo` (if you don't want/need sudo) - Sensitive-data behaviour: - default: tries to avoid likely secrets @@ -355,6 +355,14 @@ enroll harvest --out /tmp/enroll-harvest enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest ``` +### Remote harvest over SSH, where the SSH configuration is in ~/.ssh/config (e.g a different SSH key) + +Note: you must still pass `--remote-host`, but in this case, its value can be the 'Host' alias of an entry in your `~/.ssh/config`. + +```bash +enroll harvest --remote-host myhostalias --remote-ssh-config ~/.ssh/config --out /tmp/enroll-harvest +``` + ### Include paths (`--include-path`) ```bash # Add a few dotfiles from /home (still secret-safe unless --dangerous) From 1856e3a79d1e6cad2db40b67dce32b413c154660 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 16 Jan 2026 10:58:39 +1100 Subject: [PATCH 03/48] Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. --- CHANGELOG.md | 4 ++++ debian/changelog | 6 ++++++ enroll/remote.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 2 +- rpm/enroll.spec | 4 +++- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0772cc4..99fd7e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.4.3 + + * Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. + # 0.4.2 * Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config. diff --git a/debian/changelog b/debian/changelog index 58e80e3..bd5049d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +enroll (0.4.3) unstable; urgency=medium + + * Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. + + -- Miguel Jacq Fri, 16 Jan 2026 11:00 +1100 + enroll (0.4.2) unstable; urgency=medium * Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be set, but it can be an 'alias' represented by the 'Host' value in the ssh config. diff --git a/enroll/remote.py b/enroll/remote.py index c7b54a6..53e47b5 100644 --- a/enroll/remote.py +++ b/enroll/remote.py @@ -379,6 +379,10 @@ def _remote_harvest( sock = None hostkey_name = connect_host + # Timeouts derived from ssh_config if set (ConnectTimeout). + # Used both for socket connect (when we create one) and Paramiko handshake/auth. + connect_timeout: Optional[float] = None + if remote_ssh_config: from paramiko.config import SSHConfig # type: ignore from paramiko.proxy import ProxyCommand # type: ignore @@ -411,14 +415,58 @@ def _remote_harvest( else: key_filename = str(Path(str(ident)).expanduser()) + # Honour OpenSSH ConnectTimeout (seconds) if present. + if hcfg.get("connecttimeout"): + try: + connect_timeout = float(str(hcfg.get("connecttimeout"))) + except (TypeError, ValueError): + connect_timeout = None + proxycmd = hcfg.get("proxycommand") + + # AddressFamily support: inet (IPv4 only), inet6 (IPv6 only), any (default). + addrfam = str(hcfg.get("addressfamily") or "any").strip().lower() + family: Optional[int] = None + if addrfam == "inet": + family = _socket.AF_INET + elif addrfam == "inet6": + family = _socket.AF_INET6 + if proxycmd: + # ProxyCommand provides the transport; AddressFamily doesn't apply here. sock = ProxyCommand(str(proxycmd)) + elif family is not None: + # Enforce the requested address family by pre-connecting the socket and + # passing it into Paramiko via sock=. + last_err: Optional[OSError] = None + infos = _socket.getaddrinfo( + connect_host, connect_port, family, _socket.SOCK_STREAM + ) + for af, socktype, proto, _, sa in infos: + s = _socket.socket(af, socktype, proto) + if connect_timeout is not None: + s.settimeout(connect_timeout) + try: + s.connect(sa) + sock = s + break + except OSError as e: + last_err = e + try: + s.close() + except Exception: + pass # nosec + if sock is None and last_err is not None: + raise last_err elif hostkey_name != connect_host: # If HostKeyAlias is used, connect to HostName via a socket but # use HostKeyAlias for known_hosts lookups. - sock = _socket.create_connection((connect_host, connect_port)) + sock = _socket.create_connection( + (connect_host, connect_port), timeout=connect_timeout + ) + # If we created a socket (sock!=None), pass hostkey_name as hostname so + # known_hosts lookup uses HostKeyAlias (or whatever hostkey_name resolved to). ssh.connect( hostname=hostkey_name if sock is not None else connect_host, port=connect_port, @@ -427,6 +475,9 @@ def _remote_harvest( sock=sock, allow_agent=True, look_for_keys=True, + timeout=connect_timeout, + banner_timeout=connect_timeout, + auth_timeout=connect_timeout, ) # If no username was explicitly provided, SSH may have selected a default. diff --git a/pyproject.toml b/pyproject.toml index 92756d1..b4e33dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "enroll" -version = "0.4.2" +version = "0.4.3" description = "Enroll a server's running state retrospectively into Ansible" authors = ["Miguel Jacq "] license = "GPL-3.0-or-later" diff --git a/rpm/enroll.spec b/rpm/enroll.spec index 98f3f8f..1217762 100644 --- a/rpm/enroll.spec +++ b/rpm/enroll.spec @@ -1,4 +1,4 @@ -%global upstream_version 0.4.2 +%global upstream_version 0.4.3 Name: enroll Version: %{upstream_version} @@ -43,6 +43,8 @@ Enroll a server's running state retrospectively into Ansible. %{_bindir}/enroll %changelog +* Fri Jan 16 2026 Miguel Jacq - %{version}-%{release} +- Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. * Tue Jan 13 2026 Miguel Jacq - %{version}-%{release} - Support `--remote-ssh-config [path-to-ssh-config]` as an argument in case extra params are required beyond `--remote-port` or `--remote-user`. Note: `--remote-host` must still be s et, but it can be an 'alias' represented by the 'Host' value in the ssh config. From 5f6b0f49d995e124799440a892d689022c51e2df Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 16 Jan 2026 10:59:22 +1100 Subject: [PATCH 04/48] Update dependencies --- CHANGELOG.md | 1 + poetry.lock | 91 +++++++++++++++++++++++++++------------------------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fd7e3..0bbd0db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 0.4.3 * Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. + * Update dependencies # 0.4.2 diff --git a/poetry.lock b/poetry.lock index 3dcf380..aaffe41 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1027,53 +1027,58 @@ files = [ [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, - {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, - {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, - {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, - {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, - {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, - {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, - {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, - {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, - {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, - {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, - {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, - {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, - {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, - {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, - {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, - {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, - {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, - {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, - {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, - {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, - {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, - {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, - {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, - {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, - {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, - {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, - {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] [[package]] From 87ddf52e81343ab5d739c6c8c12360c90cadd282 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 17 Feb 2026 10:00:39 +1100 Subject: [PATCH 05/48] Update cryptography dependency --- CHANGELOG.md | 4 ++ poetry.lock | 107 ++++++++++++++++++++++++--------------------------- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bbd0db..cc8add9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.4.4 (unreleased) + + * Update cryptography dependency + # 0.4.3 * Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. diff --git a/poetry.lock b/poetry.lock index aaffe41..bdb0099 100644 --- a/poetry.lock +++ b/poetry.lock @@ -436,65 +436,60 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" files = [ - {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, - {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, - {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, - {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, - {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, - {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, - {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, - {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, - {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, - {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, - {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, - {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, - {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, - {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, - {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, - {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, - {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, - {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, - {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, - {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, - {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, - {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, - {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, - {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, - {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, + {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, + {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, + {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, + {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, + {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, + {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, + {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, + {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, + {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, + {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, ] [package.dependencies] @@ -508,7 +503,7 @@ nox = ["nox[uv] (>=2024.4.15)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] From 778237740a4474e095be6f11ebf77618371e2990 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 17 Feb 2026 10:35:51 +1100 Subject: [PATCH 06/48] Add ability to gracefully handle an encrypted private key for SSH (can be forced or automated with an env var too) --- CHANGELOG.md | 1 + README.md | 23 ++++++++- enroll/cli.py | 46 +++++++++++++++++- enroll/remote.py | 121 +++++++++++++++++++++++++++++++++++++---------- 4 files changed, 164 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc8add9..5d3c3f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 0.4.4 (unreleased) * Update cryptography dependency + * 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` # 0.4.3 diff --git a/README.md b/README.md index c9b448a..9fdd756 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,27 @@ Harvest state about a host and write a harvest bundle. - glob (default): supports `*` and `**` (prefix with `glob:` to force) - regex: prefix with `re:` or `regex:` - Precedence: excludes win over includes. -* Using remote mode and sudo requires password? - - `--ask-become-pass` (or `-K`) will prompt for the password. If you forget, and remote requires password for sudo, it'll still fall back to prompting for a password, but will be a bit slower to do so. + * Using remote mode and auth requires secrets? + * sudo password: + * `--ask-become-pass` (or `-K`) prompts for the sudo password. + * If you forget, and remote sudo requires a password, Enroll will still fall back to prompting in interactive mode (slightly slower due to retry). + * SSH private-key passphrase: + * `--ask-key-passphrase` prompts for the SSH key passphrase. + * `--ssh-key-passphrase-env ENV_VAR` reads the SSH key passphrase from an environment variable (useful for CI/non-interactive runs). + * If neither is provided, and Enroll detects an encrypted key in an interactive session, it will still fall back to prompting on-demand. + * In non-interactive sessions, pass `--ask-key-passphrase` or `--ssh-key-passphrase-env ENV_VAR` when using encrypted private keys. + * Note: `--ask-key-passphrase` and `--ssh-key-passphrase-env` are mutually exclusive. + +Examples (encrypted SSH key) + +```bash +# Interactive +enroll harvest --remote-host myhost.example.com --remote-user myuser --ask-key-passphrase --out /tmp/enroll-harvest + +# Non-interactive / CI +export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple' +enroll single-shot --remote-host myhost.example.com --remote-user myuser --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn myhost.example.com +``` --- diff --git a/enroll/cli.py b/enroll/cli.py index 69e85ed..44de047 100644 --- a/enroll/cli.py +++ b/enroll/cli.py @@ -22,7 +22,11 @@ from .diff import ( from .explain import explain_state from .harvest import harvest from .manifest import manifest -from .remote import remote_harvest, RemoteSudoPasswordRequired +from .remote import ( + remote_harvest, + RemoteSudoPasswordRequired, + RemoteSSHKeyPassphraseRequired, +) from .sopsutil import SopsError, encrypt_file_binary from .validate import validate_harvest from .version import get_enroll_version @@ -390,6 +394,24 @@ def _add_remote_args(p: argparse.ArgumentParser) -> None: ), ) + keyp = p.add_mutually_exclusive_group() + keyp.add_argument( + "--ask-key-passphrase", + action="store_true", + help=( + "Prompt for the SSH private key passphrase when using --remote-host. " + "If not set, enroll will still prompt on-demand if it detects an encrypted key in an interactive session." + ), + ) + keyp.add_argument( + "--ssh-key-passphrase-env", + metavar="ENV_VAR", + help=( + "Read the SSH private key passphrase from environment variable ENV_VAR " + "(useful for non-interactive runs/CI)." + ), + ) + def main() -> None: ap = argparse.ArgumentParser(prog="enroll") @@ -771,6 +793,10 @@ def main() -> None: pass remote_harvest( ask_become_pass=args.ask_become_pass, + ask_key_passphrase=bool(args.ask_key_passphrase), + ssh_key_passphrase_env=getattr( + args, "ssh_key_passphrase_env", None + ), local_out_dir=tmp_bundle, remote_host=args.remote_host, remote_port=args.remote_port, @@ -793,6 +819,10 @@ def main() -> None: ) state = remote_harvest( ask_become_pass=args.ask_become_pass, + ask_key_passphrase=bool(args.ask_key_passphrase), + ssh_key_passphrase_env=getattr( + args, "ssh_key_passphrase_env", None + ), local_out_dir=out_dir, remote_host=args.remote_host, remote_port=args.remote_port, @@ -996,6 +1026,10 @@ def main() -> None: pass remote_harvest( ask_become_pass=args.ask_become_pass, + ask_key_passphrase=bool(args.ask_key_passphrase), + ssh_key_passphrase_env=getattr( + args, "ssh_key_passphrase_env", None + ), local_out_dir=tmp_bundle, remote_host=args.remote_host, remote_port=args.remote_port, @@ -1027,6 +1061,10 @@ def main() -> None: ) remote_harvest( ask_become_pass=args.ask_become_pass, + ask_key_passphrase=bool(args.ask_key_passphrase), + ssh_key_passphrase_env=getattr( + args, "ssh_key_passphrase_env", None + ), local_out_dir=harvest_dir, remote_host=args.remote_host, remote_port=args.remote_port, @@ -1096,6 +1134,12 @@ def main() -> None: raise SystemExit( "error: remote sudo requires a password. Re-run with --ask-become-pass." ) from None + except RemoteSSHKeyPassphraseRequired as e: + msg = str(e).strip() or ( + "SSH private key passphrase is required. " + "Re-run with --ask-key-passphrase or --ssh-key-passphrase-env VAR." + ) + raise SystemExit(f"error: {msg}") from None except RuntimeError as e: raise SystemExit(f"error: {e}") from None except SopsError as e: diff --git a/enroll/remote.py b/enroll/remote.py index 53e47b5..45e2798 100644 --- a/enroll/remote.py +++ b/enroll/remote.py @@ -18,6 +18,10 @@ class RemoteSudoPasswordRequired(RuntimeError): """Raised when sudo requires a password but none was provided.""" +class RemoteSSHKeyPassphraseRequired(RuntimeError): + """Raised when SSH private key decryption needs a passphrase.""" + + def _sudo_password_required(out: str, err: str) -> bool: """Return True if sudo output indicates it needs a password/TTY.""" blob = (out + "\n" + err).lower() @@ -68,11 +72,42 @@ def _resolve_become_password( return None +def _resolve_ssh_key_passphrase( + ask_key_passphrase: bool, + *, + env_var: Optional[str] = None, + prompt: str = "SSH key passphrase: ", + getpass_fn: Callable[[str], str] = getpass.getpass, +) -> Optional[str]: + """Resolve SSH private-key passphrase from env and/or prompt. + + Precedence: + 1) --ssh-key-passphrase-env style input (env_var) + 2) --ask-key-passphrase style interactive prompt + 3) None + """ + if env_var: + val = os.environ.get(str(env_var)) + if val is None: + raise RuntimeError( + "SSH key passphrase environment variable is not set: " f"{env_var}" + ) + return val + + if ask_key_passphrase: + return getpass_fn(prompt) + + return None + + def remote_harvest( *, ask_become_pass: bool = False, + ask_key_passphrase: bool = False, + ssh_key_passphrase_env: Optional[str] = None, no_sudo: bool = False, prompt: str = "sudo password: ", + key_prompt: str = "SSH key passphrase: ", getpass_fn: Optional[Callable[[str], str]] = None, stdin: Optional[TextIO] = None, **kwargs, @@ -97,21 +132,52 @@ def remote_harvest( prompt=prompt, getpass_fn=getpass_fn, ) + ssh_key_passphrase = _resolve_ssh_key_passphrase( + ask_key_passphrase, + env_var=ssh_key_passphrase_env, + prompt=key_prompt, + getpass_fn=getpass_fn, + ) - try: - return _remote_harvest(sudo_password=sudo_password, no_sudo=no_sudo, **kwargs) - except RemoteSudoPasswordRequired: - if sudo_password is not None: - raise + while True: + try: + return _remote_harvest( + sudo_password=sudo_password, + no_sudo=no_sudo, + ssh_key_passphrase=ssh_key_passphrase, + **kwargs, + ) + except RemoteSSHKeyPassphraseRequired: + # Already tried a passphrase and still failed. + if ssh_key_passphrase is not None: + raise RemoteSSHKeyPassphraseRequired( + "SSH private key could not be decrypted with the supplied " + "passphrase." + ) from None - # Fallback prompt if interactive - if stdin is not None and getattr(stdin, "isatty", lambda: False)(): - pw = getpass_fn(prompt) - return _remote_harvest(sudo_password=pw, no_sudo=no_sudo, **kwargs) + # Fallback prompt if interactive. + if stdin is not None and getattr(stdin, "isatty", lambda: False)(): + ssh_key_passphrase = getpass_fn(key_prompt) + continue - raise RemoteSudoPasswordRequired( - "Remote sudo requires a password. Re-run with --ask-become-pass." - ) + raise RemoteSSHKeyPassphraseRequired( + "SSH private key is encrypted and needs a passphrase. " + "Re-run with --ask-key-passphrase or " + "--ssh-key-passphrase-env VAR." + ) + + except RemoteSudoPasswordRequired: + if sudo_password is not None: + raise + + # Fallback prompt if interactive. + if stdin is not None and getattr(stdin, "isatty", lambda: False)(): + sudo_password = getpass_fn(prompt) + continue + + raise RemoteSudoPasswordRequired( + "Remote sudo requires a password. Re-run with --ask-become-pass." + ) def _safe_extract_tar(tar: tarfile.TarFile, dest: Path) -> None: @@ -337,6 +403,7 @@ def _remote_harvest( dangerous: bool = False, no_sudo: bool = False, sudo_password: Optional[str] = None, + ssh_key_passphrase: Optional[str] = None, include_paths: Optional[list[str]] = None, exclude_paths: Optional[list[str]] = None, ) -> Path: @@ -467,18 +534,24 @@ def _remote_harvest( # If we created a socket (sock!=None), pass hostkey_name as hostname so # known_hosts lookup uses HostKeyAlias (or whatever hostkey_name resolved to). - ssh.connect( - hostname=hostkey_name if sock is not None else connect_host, - port=connect_port, - username=connect_user, - key_filename=key_filename, - sock=sock, - allow_agent=True, - look_for_keys=True, - timeout=connect_timeout, - banner_timeout=connect_timeout, - auth_timeout=connect_timeout, - ) + try: + ssh.connect( + hostname=hostkey_name if sock is not None else connect_host, + port=connect_port, + username=connect_user, + key_filename=key_filename, + sock=sock, + allow_agent=True, + look_for_keys=True, + timeout=connect_timeout, + banner_timeout=connect_timeout, + auth_timeout=connect_timeout, + passphrase=ssh_key_passphrase, + ) + except paramiko.PasswordRequiredException as e: # type: ignore[attr-defined] + raise RemoteSSHKeyPassphraseRequired( + "SSH private key is encrypted and no passphrase was provided." + ) 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. From d403dcb918a812d00ecee24169599ae28439356e Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 17 Feb 2026 10:58:38 +1100 Subject: [PATCH 07/48] 0.4.4 --- CHANGELOG.md | 2 +- debian/changelog | 6 ++++++ pyproject.toml | 2 +- rpm/enroll.spec | 4 +++- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d3c3f8..4b1428c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 0.4.4 (unreleased) +# 0.4.4 * Update cryptography dependency * 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` diff --git a/debian/changelog b/debian/changelog index bd5049d..6fa0d96 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +enroll (0.4.4) unstable; urgency=medium + + * 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` + + -- Miguel Jacq Tue, 17 Feb 2026 11:00 +1100 + enroll (0.4.3) unstable; urgency=medium * Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. diff --git a/pyproject.toml b/pyproject.toml index b4e33dc..e4eb610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "enroll" -version = "0.4.3" +version = "0.4.4" description = "Enroll a server's running state retrospectively into Ansible" authors = ["Miguel Jacq "] license = "GPL-3.0-or-later" diff --git a/rpm/enroll.spec b/rpm/enroll.spec index 1217762..451c168 100644 --- a/rpm/enroll.spec +++ b/rpm/enroll.spec @@ -1,4 +1,4 @@ -%global upstream_version 0.4.3 +%global upstream_version 0.4.4 Name: enroll Version: %{upstream_version} @@ -43,6 +43,8 @@ Enroll a server's running state retrospectively into Ansible. %{_bindir}/enroll %changelog +* Tue Feb 16 2026 Miguel Jacq - %{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` * Fri Jan 16 2026 Miguel Jacq - %{version}-%{release} - Add support for AddressFamily and ConnectTimeout in the .ssh/config when using `--remote-ssh-config`. * Tue Jan 13 2026 Miguel Jacq - %{version}-%{release} From 4ea7267b9243f684301d7f0a34eccae59523e670 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 11 Mar 2026 12:02:39 +1100 Subject: [PATCH 08/48] Update my GPG key --- README.md | 2 +- release.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fdd756..c2843fd 100644 --- a/README.md +++ b/README.md @@ -612,7 +612,7 @@ exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool [manifest] # you can set defaults here too, e.g. no_jinjaturtle = true -sops = 00AE817C24A10C2540461A9C1D7CDE0234DB458D +sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99 [diff] # ignore noisy drift diff --git a/release.sh b/release.sh index 7937741..d8454a2 100755 --- a/release.sh +++ b/release.sh @@ -46,7 +46,7 @@ done # RPM sudo apt-get -y install createrepo-c rpm BUILD_OUTPUT="${HOME}/git/enroll/dist" -KEYID="00AE817C24A10C2540461A9C1D7CDE0234DB458D" +KEYID="54A91143AE0AB4F7743B01FE888ED1B423A3BC99" REPO_ROOT="${HOME}/git/repo_rpm" REMOTE="letessier.mig5.net:/opt/repo_rpm" From 5c686d27cced47c1e79d054623a102a54fefc8f3 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 23 Mar 2026 11:20:56 +1100 Subject: [PATCH 09/48] Remove trivy.. --- .forgejo/workflows/trivy.yml | 40 ------------------------------------ 1 file changed, 40 deletions(-) delete mode 100644 .forgejo/workflows/trivy.yml diff --git a/.forgejo/workflows/trivy.yml b/.forgejo/workflows/trivy.yml deleted file mode 100644 index d5585f4..0000000 --- a/.forgejo/workflows/trivy.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Trivy - -on: - schedule: - - cron: '0 1 * * *' - push: - -jobs: - test: - runs-on: docker - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install system dependencies - run: | - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends wget gnupg - wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | tee /usr/share/keyrings/trivy.gpg > /dev/null - echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | tee -a /etc/apt/sources.list.d/trivy.list - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends trivy - - - name: Run trivy - run: | - trivy fs --no-progress --ignore-unfixed --format table --disable-telemetry --skip-version-check --exit-code 1 . - - # Notify if any previous step in this job failed - - name: Notify on failure - if: ${{ failure() }} - env: - WEBHOOK_URL: ${{ secrets.NODERED_WEBHOOK_URL }} - REPOSITORY: ${{ forgejo.repository }} - RUN_NUMBER: ${{ forgejo.run_number }} - SERVER_URL: ${{ forgejo.server_url }} - run: | - curl -X POST \ - -H "Content-Type: application/json" \ - -d "{\"repository\":\"$REPOSITORY\",\"run_number\":\"$RUN_NUMBER\",\"status\":\"failure\",\"url\":\"$SERVER_URL/$REPOSITORY/actions/runs/$RUN_NUMBER\"}" \ - "$WEBHOOK_URL" From 5695f4258e1fc5d36a13e8d6d478fd3614834653 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 12 May 2026 12:23:41 +1000 Subject: [PATCH 10/48] Add support for ssh configs as templates, via JinjaTurtle --- debian/changelog | 6 ++++++ enroll/jinjaturtle.py | 6 ++++++ tests/test_jinjaturtle.py | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/debian/changelog b/debian/changelog index 6fa0d96..ee732b6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +enroll (0.5.0) unstable; urgency=medium + + * Add ssh config support where JinjaTurtle is used + + -- Miguel Jacq Tue, 12 May 2026 12:00 +1000 + enroll (0.4.4) unstable; urgency=medium * 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` diff --git a/enroll/jinjaturtle.py b/enroll/jinjaturtle.py index 6a13fa1..7a2702e 100644 --- a/enroll/jinjaturtle.py +++ b/enroll/jinjaturtle.py @@ -46,6 +46,12 @@ def infer_other_formats(dest_path: str) -> Optional[str]: # systemd units if suffix in SYSTEMD_SUFFIXES: return "systemd" + # OpenSSH system config files and snippets + parts = {part.lower() for part in p.parts} + if name in {"sshd_config", "ssh_config"}: + return "ssh" + if suffix == ".conf" and {"sshd_config.d", "ssh_config.d"} & parts: + return "ssh" return None diff --git a/tests/test_jinjaturtle.py b/tests/test_jinjaturtle.py index c0447b1..b2c9022 100644 --- a/tests/test_jinjaturtle.py +++ b/tests/test_jinjaturtle.py @@ -131,3 +131,15 @@ def test_manifest_uses_jinjaturtle_templates_and_does_not_copy_raw( encoding="utf-8" ) assert "foo_key: 1" in defaults + + +def test_openssh_paths_are_jinjaturtle_supported_and_forced_to_ssh() -> None: + from enroll.jinjaturtle import can_jinjify_path, infer_other_formats + + assert infer_other_formats("/etc/ssh/sshd_config") == "ssh" + assert infer_other_formats("/etc/ssh/ssh_config") == "ssh" + assert infer_other_formats("/etc/ssh/sshd_config.d/50-hardening.conf") == "ssh" + assert infer_other_formats("/etc/ssh/ssh_config.d/99-proxy.conf") == "ssh" + + assert can_jinjify_path("/etc/ssh/sshd_config") + assert can_jinjify_path("/etc/ssh/ssh_config") From 618dd20e7cfad39707f976c4c36b94fd66800aa9 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 12 May 2026 12:23:52 +1000 Subject: [PATCH 11/48] Update deps --- poetry.lock | 732 +++++++++++++++++++++++++++------------------------- 1 file changed, 376 insertions(+), 356 deletions(-) diff --git a/poetry.lock b/poetry.lock index bdb0099..c94436e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] [[package]] @@ -89,13 +89,13 @@ typecheck = ["mypy"] [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.4.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, + {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, + {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, ] [[package]] @@ -196,124 +196,140 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {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.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {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.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {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.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {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.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {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.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.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {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.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {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.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {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.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.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {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.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {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.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {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.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.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {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.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {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.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {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.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.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {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.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {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.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {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.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.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {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.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {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]] @@ -329,103 +345,117 @@ files = [ [[package]] name = "coverage" -version = "7.13.1" +version = "7.14.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" files = [ - {file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"}, - {file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"}, - {file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"}, - {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"}, - {file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"}, - {file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"}, - {file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"}, - {file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"}, - {file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"}, - {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"}, - {file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"}, - {file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"}, - {file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"}, - {file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"}, - {file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"}, - {file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"}, - {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"}, - {file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"}, - {file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"}, - {file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"}, - {file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"}, - {file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"}, - {file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"}, - {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"}, - {file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"}, - {file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"}, - {file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"}, - {file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"}, - {file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"}, - {file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"}, - {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"}, - {file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"}, - {file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"}, - {file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"}, - {file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"}, - {file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"}, - {file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"}, - {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"}, - {file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"}, - {file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"}, - {file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"}, - {file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"}, - {file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"}, - {file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"}, - {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"}, - {file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"}, - {file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"}, - {file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"}, - {file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"}, - {file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"}, + {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"}, + {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"}, + {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"}, + {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"}, + {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"}, + {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"}, + {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"}, + {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"}, + {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"}, + {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"}, + {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"}, + {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"}, + {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"}, + {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"}, + {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"}, + {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"}, + {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"}, + {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"}, + {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"}, + {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"}, + {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"}, + {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"}, + {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"}, + {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"}, ] [package.dependencies] @@ -436,75 +466,68 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "46.0.5" +version = "48.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.8" +python-versions = "!=3.9.0,!=3.9.1,>=3.9" files = [ - {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, - {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, - {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, - {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, - {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, - {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, - {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, - {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, - {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, - {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, - {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, - {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, - {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, - {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, - {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, - {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, - {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, + {file = "cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c"}, + {file = "cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5"}, + {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321"}, + {file = "cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74"}, + {file = "cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4"}, + {file = "cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7"}, + {file = "cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336"}, + {file = "cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057"}, + {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae"}, + {file = "cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c"}, + {file = "cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f"}, + {file = "cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12"}, + {file = "cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a"}, + {file = "cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239"}, + {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c"}, + {file = "cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4"}, + {file = "cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd"}, + {file = "cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355"}, + {file = "cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a"}, + {file = "cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920"}, ] [package.dependencies] -cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9\" and platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] [[package]] name = "desktop-entry-lib" @@ -539,17 +562,17 @@ test = ["pytest (>=6)"] [[package]] name = "idna" -version = "3.11" +version = "3.14" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69"}, + {file = "idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "iniconfig" @@ -564,13 +587,13 @@ files = [ [[package]] name = "invoke" -version = "2.2.1" +version = "3.0.3" description = "Pythonic task execution" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" files = [ - {file = "invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8"}, - {file = "invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707"}, + {file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"}, + {file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"}, ] [[package]] @@ -610,24 +633,24 @@ referencing = ">=0.31.0" [[package]] name = "packaging" -version = "25.0" +version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, ] [[package]] name = "paramiko" -version = "4.0.0" +version = "5.0.0" description = "SSH2 protocol library" optional = false python-versions = ">=3.9" files = [ - {file = "paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9"}, - {file = "paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f"}, + {file = "paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c"}, + {file = "paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79"}, ] [package.dependencies] @@ -636,9 +659,6 @@ cryptography = ">=3.3" invoke = ">=2.0" pynacl = ">=1.5" -[package.extras] -gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] - [[package]] name = "pluggy" version = "1.6.0" @@ -656,24 +676,24 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pycparser" -version = "2.23" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] @@ -877,24 +897,24 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.5" +version = "2.34.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60"}, + {file = "requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "rpds-py" @@ -1022,58 +1042,58 @@ files = [ [[package]] name = "tomli" -version = "2.4.0" +version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, - {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, - {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, - {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, - {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, - {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, - {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, - {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, - {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, - {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, - {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, - {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, - {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, - {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, - {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, - {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, - {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, - {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, - {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] [[package]] @@ -1089,13 +1109,13 @@ files = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] From 3fcfefe6440beb06b9a0174afc365691ed55125f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 12 May 2026 12:24:00 +1000 Subject: [PATCH 12/48] 0.5.0 --- pyproject.toml | 2 +- rpm/enroll.spec | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e4eb610..4afda15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "enroll" -version = "0.4.4" +version = "0.5.0" description = "Enroll a server's running state retrospectively into Ansible" authors = ["Miguel Jacq "] license = "GPL-3.0-or-later" diff --git a/rpm/enroll.spec b/rpm/enroll.spec index 451c168..2980f32 100644 --- a/rpm/enroll.spec +++ b/rpm/enroll.spec @@ -1,4 +1,4 @@ -%global upstream_version 0.4.4 +%global upstream_version 0.5.0 Name: enroll Version: %{upstream_version} @@ -43,6 +43,8 @@ Enroll a server's running state retrospectively into Ansible. %{_bindir}/enroll %changelog +* Tue May 12 2026 Miguel Jacq - %{version}-%{release} +- Add ssh config support where JinjaTurtle is used * Tue Feb 16 2026 Miguel Jacq - %{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` * Fri Jan 16 2026 Miguel Jacq - %{version}-%{release} From b25dd1e314a85da0085d542ab167f5d38b9e7435 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 14 May 2026 15:16:36 +1000 Subject: [PATCH 13/48] * Add support for capturing ipset and iptables configuration files * Add support for generating ipset and iptables configuration files from runtime, if the former weren't present (`firewall_runtime` role) * Dependency updates --- CHANGELOG.md | 11 ++ README.md | 4 + debian/changelog | 7 + enroll/explain.py | 22 ++- enroll/harvest.py | 288 +++++++++++++++++++++++++++++++- enroll/manifest.py | 192 +++++++++++++++++++++ enroll/schema/state.schema.json | 78 ++++++++- enroll/validate.py | 31 ++++ poetry.lock | 12 +- pyproject.toml | 2 +- rpm/enroll.spec | 5 +- tests/test_harvest_helpers.py | 118 +++++++++++++ tests/test_manifest.py | 97 +++++++++++ 13 files changed, 856 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b1428c..ef94a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# 0.6.0 + + * Add support for capturing ipset and iptables configuration files + * Add support for generating ipset and iptables configuration files from runtime, if the former weren't present (`firewall_runtime` role) + * Dependency updates + +# 0.5.0 + + * Add support for templating `sshd_config`, if a compatible version of JinjaTurtle is also present. + * Dependency updates + # 0.4.4 * Update cryptography dependency diff --git a/README.md b/README.md index c2843fd..d2d51ad 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ - Defensively excludes likely secrets (path denylist + content sniff + size caps). - Captures non-system users and their SSH public keys and any .bashrc or .bash_aliases or .profile files that deviate from the skel defaults. - Captures miscellaneous `/etc` files it can't attribute to a package and installs them in an `etc_custom` role. +- Captures live ipset and iptables runtime state into a fallback `firewall_runtime` role, when active ipsets/iptables rules are present *and* no corresponding persistent ipset/iptables *files* were found. - Captures symlinks in common applications that rely on them, e.g apache2/nginx 'sites-enabled' - Ditto for /usr/local/bin (for non-binary files) and /usr/local/etc - Avoids trying to start systemd services that were detected as inactive during harvest. @@ -70,6 +71,8 @@ Harvest state about a host and write a harvest bundle. - Changed-from-default config (plus related custom/unowned files under service dirs) - Non-system users + SSH public keys - Misc `/etc` that can't be attributed to a package (`etc_custom` role) +- Static firewall config files such as nftables, UFW, firewalld, `/etc/iptables/rules.v4`, `/etc/iptables/rules.v6`, and `/etc/ipset*` +- Live kernel ipset/iptables state via `ipset save`, `iptables-save`, and `ip6tables-save` as a fallback, but only when the corresponding persistent config was not found (`firewall_runtime` role at manifest time) - Optional user-specified extra files/dirs via `--include-path` (emitted as an `extra_paths` role at manifest time) **Common flags** @@ -531,6 +534,7 @@ Roles collected - packages: 232 package snapshot(s), 41 file(s), 0 excluded - apt_config: 26 file(s), 7 dir(s), 10 excluded - dnf_config: 0 file(s), 0 dir(s), 0 excluded +- firewall_runtime: 2 snapshot(s), 1 ipset(s) - etc_custom: 70 file(s), 20 dir(s), 0 excluded - usr_local_custom: 35 file(s), 1 dir(s), 0 excluded - extra_paths: 0 file(s), 0 dir(s), 0 excluded diff --git a/debian/changelog b/debian/changelog index ee732b6..5292e0e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +enroll (0.6.0) unstable; urgency=medium + + * Add support for capturing ipset and iptables configuration files + * Add support for generating ipset and iptables configuration files from runtime, if the former weren't present ('firewall_runtime' role) + + -- Miguel Jacq Thu, 14 May 2026 15:00 +1000 + enroll (0.5.0) unstable; urgency=medium * Add ssh config support where JinjaTurtle is used diff --git a/enroll/explain.py b/enroll/explain.py index 835f207..131f2df 100644 --- a/enroll/explain.py +++ b/enroll/explain.py @@ -72,7 +72,7 @@ _MANAGED_FILE_REASONS: Dict[str, ReasonInfo] = { ), "system_firewall": ReasonInfo( "Firewall configuration", - "Firewall rules/configuration (ufw, nftables, iptables, etc.).", + "Firewall rules/configuration (ufw, nftables, iptables, ipset, etc.).", ), "system_sysctl": ReasonInfo( "sysctl configuration", @@ -211,6 +211,10 @@ _OBSERVED_VIA: Dict[str, ReasonInfo] = { "Referenced by package role", "Package was referenced by an enroll packages snapshot/role.", ), + "firewall_runtime": ReasonInfo( + "Referenced by firewall runtime role", + "Package was referenced by captured live ipset/iptables runtime state.", + ), } @@ -359,6 +363,22 @@ def explain_state( } ) + # Runtime firewall snapshot + firewall_obj = roles.get("firewall_runtime") or {} + if isinstance(firewall_obj, dict) and firewall_obj: + captures = [ + key + for key in ("ipset_save", "iptables_v4_save", "iptables_v6_save") + if firewall_obj.get(key) + ] + role_summaries.append( + { + "role": "firewall_runtime", + "summary": f"{len(captures)} snapshot(s), {len(firewall_obj.get('ipset_sets') or [])} ipset(s)", + "notes": firewall_obj.get("notes") or [], + } + ) + # Single snapshots for rname in [ "apt_config", diff --git a/enroll/harvest.py b/enroll/harvest.py index ff62fb7..b64862e 100644 --- a/enroll/harvest.py +++ b/enroll/harvest.py @@ -5,10 +5,12 @@ import json import os import re import shutil +import shlex import stat +import subprocess # nosec import time from dataclasses import dataclass, asdict, field -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, Tuple from .systemd import ( list_enabled_services, @@ -148,6 +150,17 @@ class ExtraPathsSnapshot: notes: List[str] = field(default_factory=list) +@dataclass +class FirewallRuntimeSnapshot: + role_name: str + packages: List[str] = field(default_factory=list) + ipset_save: Optional[str] = None + ipset_sets: List[str] = field(default_factory=list) + iptables_v4_save: Optional[str] = None + iptables_v6_save: Optional[str] = None + notes: List[str] = field(default_factory=list) + + ALLOWED_UNOWNED_EXTS = { ".cfg", ".cnf", @@ -653,6 +666,13 @@ _SYSTEM_CAPTURE_GLOBS: List[tuple[str, str]] = [ ("/etc/nftables.d/*", "system_firewall"), ("/etc/iptables/rules.v4", "system_firewall"), ("/etc/iptables/rules.v6", "system_firewall"), + ("/etc/sysconfig/iptables", "system_firewall"), + ("/etc/sysconfig/ip6tables", "system_firewall"), + ("/etc/ipset.conf", "system_firewall"), + ("/etc/ipset/*", "system_firewall"), + ("/etc/ipset.d/*", "system_firewall"), + ("/etc/sysconfig/ipset", "system_firewall"), + ("/etc/default/ipset", "system_firewall"), ("/etc/ufw/*", "system_firewall"), ("/etc/default/ufw", "system_firewall"), ("/etc/firewalld/*", "system_firewall"), @@ -664,6 +684,46 @@ _SYSTEM_CAPTURE_GLOBS: List[tuple[str, str]] = [ ] +# Persistent firewall files that are treated as authoritative for their +# respective runtime state. If any matching file exists, the runtime capture +# for that family is retained only as static managed-file harvest output and +# not duplicated through the generated firewall_runtime role. +_PERSISTENT_IPTABLES_V4_GLOBS = [ + "/etc/iptables/rules.v4", + "/etc/sysconfig/iptables", +] + +_PERSISTENT_IPTABLES_V6_GLOBS = [ + "/etc/iptables/rules.v6", + "/etc/sysconfig/ip6tables", +] + +_PERSISTENT_IPSET_GLOBS = [ + "/etc/ipset.conf", + "/etc/ipset/*", + "/etc/ipset.d/*", + "/etc/sysconfig/ipset", +] + + +def _persistent_firewall_files(globs: List[str]) -> List[str]: + """Return persistent firewall files matching ``globs``. + + This intentionally uses the same file walking helper as the static system + capture path so the runtime fallback decision matches what Enroll can + harvest as managed files. + """ + seen: Set[str] = set() + out: List[str] = [] + for spec in globs: + for path in _iter_matching_files(spec): + if path in seen: + continue + seen.add(path) + out.append(path) + return sorted(out) + + def _iter_matching_files(spec: str, *, cap: int = MAX_FILES_CAP) -> List[str]: """Expand a glob spec and also walk directories to collect files.""" out: List[str] = [] @@ -854,6 +914,200 @@ def _iter_system_capture_paths() -> List[tuple[str, str]]: return uniq +_FIREWALL_CAPTURE_COMMANDS: Dict[str, Tuple[str, ...]] = { + "ipset_save": ("ipset", "save"), + "iptables_v4_save": ("iptables-save",), + "iptables_v6_save": ("ip6tables-save",), +} + + +def _run_capture_command( + command_key: str, *, timeout: int = 10 +) -> tuple[Optional[str], Optional[str]]: + """Return (stdout, error_note) for an allowlisted local state command. + + The command key is resolved through ``_FIREWALL_CAPTURE_COMMANDS`` so this + helper never executes caller-supplied argv. Commands are run with + ``shell=False`` explicitly to avoid shell interpretation. + """ + argv = _FIREWALL_CAPTURE_COMMANDS.get(command_key) + if argv is None: + return None, f"Unknown capture command: {command_key}" + + exe = argv[0] + if shutil.which(exe) is None: + return None, f"{exe} not found on PATH." + + try: + proc = subprocess.run( # nosec + argv, + shell=False, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=timeout, + ) + except Exception as e: # noqa: BLE001 + return None, f"{' '.join(argv)} failed: {e!r}" + + if proc.returncode != 0: + stderr = (proc.stderr or "").strip() + if len(stderr) > 300: + stderr = stderr[:297] + "..." + return ( + None, + f"{' '.join(argv)} exited {proc.returncode}: {stderr or '(no stderr)'}", + ) + + return proc.stdout or "", None + + +def _write_generated_artifact( + bundle_dir: str, role_name: str, src_rel: str, content: str +) -> None: + """Write a generated harvest artifact that did not exist as a file on disk.""" + dst = os.path.join(bundle_dir, "artifacts", role_name, src_rel) + os.makedirs(os.path.dirname(dst), exist_ok=True) + with open(dst, "w", encoding="utf-8") as f: + f.write(content) + + +def _ipset_save_has_state(text: str) -> bool: + for raw in (text or "").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith(("create ", "add ")): + return True + return False + + +def _parse_ipset_set_names(text: str) -> List[str]: + names: List[str] = [] + seen: Set[str] = set() + for raw in (text or "").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + try: + toks = shlex.split(line) + except ValueError: + toks = line.split() + if len(toks) >= 2 and toks[0] == "create" and toks[1] not in seen: + seen.add(toks[1]) + names.append(toks[1]) + return names + + +def _iptables_save_has_state(text: str) -> bool: + """Return True when iptables-save output contains non-default state.""" + for raw in (text or "").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if line.startswith("*") or line == "COMMIT": + continue + if line.startswith(":"): + parts = line.split() + chain_name = parts[0][1:] if parts else "" + policy = parts[1] if len(parts) >= 2 else "" + # Built-in empty chains usually look like ':INPUT ACCEPT [0:0]'. + # A changed policy, or any custom chain, is meaningful state. + if policy not in ("ACCEPT", "-"): + return True + if policy == "-" and chain_name: + return True + continue + if line.startswith(("-A ", "-I ", "-N ", "-P ", "-R ")): + return True + return False + + +def _collect_firewall_runtime_snapshot( + bundle_dir: str, + *, + persistent_ipset_files: Optional[List[str]] = None, + persistent_iptables_v4_files: Optional[List[str]] = None, + persistent_iptables_v6_files: Optional[List[str]] = None, +) -> FirewallRuntimeSnapshot: + """Capture live kernel firewall state only when no persistent config exists. + + Enroll also harvests persistent firewall files such as + /etc/iptables/rules.v4, /etc/iptables/rules.v6, and /etc/ipset.conf as + managed files. The generated runtime restore role is therefore a fallback: + it captures each firewall family only when that family has no persistent + file to avoid generating two roles that try to manage the same state. + """ + role_name = "firewall_runtime" + packages: Set[str] = set() + notes: List[str] = [] + ipset_save_rel: Optional[str] = None + ipset_sets: List[str] = [] + iptables_v4_rel: Optional[str] = None + iptables_v6_rel: Optional[str] = None + + persistent_ipset_files = persistent_ipset_files or [] + persistent_iptables_v4_files = persistent_iptables_v4_files or [] + persistent_iptables_v6_files = persistent_iptables_v6_files or [] + + if persistent_ipset_files: + notes.append( + "Live ipset runtime capture skipped because persistent ipset " + f"configuration was found: {', '.join(persistent_ipset_files)}" + ) + else: + ipset_out, ipset_err = _run_capture_command("ipset_save") + if ipset_err: + notes.append(ipset_err) + elif ipset_out is not None and _ipset_save_has_state(ipset_out): + ipset_save_rel = "firewall/ipset.save" + _write_generated_artifact(bundle_dir, role_name, ipset_save_rel, ipset_out) + ipset_sets = _parse_ipset_set_names(ipset_out) + packages.add("ipset") + + if persistent_iptables_v4_files: + notes.append( + "Live IPv4 iptables runtime capture skipped because persistent " + f"IPv4 iptables configuration was found: {', '.join(persistent_iptables_v4_files)}" + ) + else: + ipt4_out, ipt4_err = _run_capture_command("iptables_v4_save") + if ipt4_err: + notes.append(ipt4_err) + elif ipt4_out is not None and _iptables_save_has_state(ipt4_out): + iptables_v4_rel = "firewall/iptables.v4" + _write_generated_artifact(bundle_dir, role_name, iptables_v4_rel, ipt4_out) + packages.add("iptables") + + if persistent_iptables_v6_files: + notes.append( + "Live IPv6 iptables runtime capture skipped because persistent " + f"IPv6 iptables configuration was found: {', '.join(persistent_iptables_v6_files)}" + ) + else: + ipt6_out, ipt6_err = _run_capture_command("iptables_v6_save") + if ipt6_err: + notes.append(ipt6_err) + elif ipt6_out is not None and _iptables_save_has_state(ipt6_out): + iptables_v6_rel = "firewall/iptables.v6" + _write_generated_artifact(bundle_dir, role_name, iptables_v6_rel, ipt6_out) + packages.add("iptables") + + # Package names are intentionally added only when matching live state was + # captured. Merely having iptables/ipset installed should not create a role. + + return FirewallRuntimeSnapshot( + role_name=role_name, + packages=sorted(packages), + ipset_save=ipset_save_rel, + ipset_sets=ipset_sets, + iptables_v4_save=iptables_v4_rel, + iptables_v6_save=iptables_v6_rel, + notes=notes, + ) + + def harvest( bundle_dir: str, policy: Optional[IgnorePolicy] = None, @@ -907,6 +1161,29 @@ def harvest( installed_pkgs = backend.installed_packages() or {} installed_names: Set[str] = set(installed_pkgs.keys()) + persistent_ipset_files = _persistent_firewall_files(_PERSISTENT_IPSET_GLOBS) + persistent_iptables_v4_files = _persistent_firewall_files( + _PERSISTENT_IPTABLES_V4_GLOBS + ) + persistent_iptables_v6_files = _persistent_firewall_files( + _PERSISTENT_IPTABLES_V6_GLOBS + ) + + if hasattr(os, "geteuid") and os.geteuid() != 0: + firewall_runtime_snapshot = FirewallRuntimeSnapshot( + role_name="firewall_runtime", + notes=[ + "Live ipset/iptables runtime capture skipped because harvest is not running as root." + ], + ) + else: + firewall_runtime_snapshot = _collect_firewall_runtime_snapshot( + bundle_dir, + persistent_ipset_files=persistent_ipset_files, + persistent_iptables_v4_files=persistent_iptables_v4_files, + persistent_iptables_v6_files=persistent_iptables_v6_files, + ) + def _pick_installed(cands: List[str]) -> Optional[str]: for c in cands: if c in installed_names: @@ -2121,6 +2398,7 @@ def harvest( pkg_names |= manual_set pkg_names |= set(pkg_units.keys()) pkg_names |= {ps.package for ps in pkg_snaps} + pkg_names |= set(firewall_runtime_snapshot.packages or []) packages_inventory: Dict[str, Dict[str, object]] = {} for pkg in sorted(pkg_names): @@ -2136,6 +2414,13 @@ def harvest( observed.append({"kind": "systemd_unit", "ref": unit}) for rn in sorted(set(pkg_role_names.get(pkg, []))): observed.append({"kind": "package_role", "ref": rn}) + if pkg in set(firewall_runtime_snapshot.packages or []): + observed.append( + {"kind": "firewall_runtime", "ref": firewall_runtime_snapshot.role_name} + ) + pkg_roles_map.setdefault(pkg, set()).add( + firewall_runtime_snapshot.role_name + ) roles = sorted(pkg_roles_map.get(pkg, set())) @@ -2219,6 +2504,7 @@ def harvest( "packages": [asdict(p) for p in pkg_snaps], "apt_config": asdict(apt_config_snapshot), "dnf_config": asdict(dnf_config_snapshot), + "firewall_runtime": asdict(firewall_runtime_snapshot), "etc_custom": asdict(etc_custom_snapshot), "usr_local_custom": asdict(usr_local_custom_snapshot), "extra_paths": asdict(extra_paths_snapshot), diff --git a/enroll/manifest.py b/enroll/manifest.py index 0186621..99adbb7 100644 --- a/enroll/manifest.py +++ b/enroll/manifest.py @@ -582,6 +582,97 @@ def _render_install_packages_tasks(role: str, var_prefix: str) -> str: """ +def _render_firewall_runtime_tasks(var_prefix: str) -> str: + """Render tasks for live ipset/iptables snapshots.""" + return f"""- name: Ensure firewall runtime snapshot directory exists + ansible.builtin.file: + path: /etc/enroll/firewall + state: directory + owner: root + group: root + mode: "0750" + +- name: Deploy captured ipset snapshot + vars: + _enroll_ff: + files: + - "{{{{ inventory_dir }}}}/host_vars/{{{{ inventory_hostname }}}}/{{{{ role_name }}}}/.files/{{{{ {var_prefix}_ipset_save }}}}" + - "{{{{ role_path }}}}/files/{{{{ {var_prefix}_ipset_save }}}}" + ansible.builtin.copy: + src: "{{{{ lookup('ansible.builtin.first_found', _enroll_ff) }}}}" + dest: /etc/enroll/firewall/ipset.save + owner: root + group: root + mode: "0600" + when: ({var_prefix}_ipset_save | default('') | length) > 0 + +- name: Flush captured ipsets before restoring members + ansible.builtin.command: + cmd: "ipset flush {{{{ item }}}}" + loop: "{{{{ {var_prefix}_ipset_sets | default([]) }}}}" + register: _enroll_ipset_flush + failed_when: false + changed_when: false + when: + - ({var_prefix}_ipset_save | default('') | length) > 0 + - {var_prefix}_sync_ipsets_exact | default(true) | bool + +- name: Restore captured ipsets + ansible.builtin.shell: "ipset restore -exist < /etc/enroll/firewall/ipset.save" + args: + executable: /bin/sh + register: _enroll_ipset_restore + changed_when: _enroll_ipset_restore.rc == 0 + when: ({var_prefix}_ipset_save | default('') | length) > 0 + +- name: Deploy captured IPv4 iptables snapshot + vars: + _enroll_ff: + files: + - "{{{{ inventory_dir }}}}/host_vars/{{{{ inventory_hostname }}}}/{{{{ role_name }}}}/.files/{{{{ {var_prefix}_iptables_v4_save }}}}" + - "{{{{ role_path }}}}/files/{{{{ {var_prefix}_iptables_v4_save }}}}" + ansible.builtin.copy: + src: "{{{{ lookup('ansible.builtin.first_found', _enroll_ff) }}}}" + dest: /etc/enroll/firewall/iptables.v4 + owner: root + group: root + mode: "0600" + when: ({var_prefix}_iptables_v4_save | default('') | length) > 0 + +- name: Restore captured IPv4 iptables rules + ansible.builtin.command: + cmd: iptables-restore /etc/enroll/firewall/iptables.v4 + register: _enroll_iptables_v4_restore + changed_when: _enroll_iptables_v4_restore.rc == 0 + when: + - ({var_prefix}_iptables_v4_save | default('') | length) > 0 + - {var_prefix}_restore_iptables | default(true) | bool + +- name: Deploy captured IPv6 iptables snapshot + vars: + _enroll_ff: + files: + - "{{{{ inventory_dir }}}}/host_vars/{{{{ inventory_hostname }}}}/{{{{ role_name }}}}/.files/{{{{ {var_prefix}_iptables_v6_save }}}}" + - "{{{{ role_path }}}}/files/{{{{ {var_prefix}_iptables_v6_save }}}}" + ansible.builtin.copy: + src: "{{{{ lookup('ansible.builtin.first_found', _enroll_ff) }}}}" + dest: /etc/enroll/firewall/iptables.v6 + owner: root + group: root + mode: "0600" + when: ({var_prefix}_iptables_v6_save | default('') | length) > 0 + +- name: Restore captured IPv6 iptables rules + ansible.builtin.command: + cmd: ip6tables-restore /etc/enroll/firewall/iptables.v6 + register: _enroll_iptables_v6_restore + changed_when: _enroll_iptables_v6_restore.rc == 0 + when: + - ({var_prefix}_iptables_v6_save | default('') | length) > 0 + - {var_prefix}_restore_iptables | default(true) | bool +""" + + def _prepare_bundle_dir( bundle: str, *, @@ -746,6 +837,7 @@ def _manifest_from_bundle_dir( users_snapshot: Dict[str, Any] = roles.get("users", {}) apt_config_snapshot: Dict[str, Any] = roles.get("apt_config", {}) dnf_config_snapshot: Dict[str, Any] = roles.get("dnf_config", {}) + firewall_runtime_snapshot: Dict[str, Any] = roles.get("firewall_runtime", {}) etc_custom_snapshot: Dict[str, Any] = roles.get("etc_custom", {}) usr_local_custom_snapshot: Dict[str, Any] = roles.get("usr_local_custom", {}) extra_paths_snapshot: Dict[str, Any] = roles.get("extra_paths", {}) @@ -782,6 +874,7 @@ def _manifest_from_bundle_dir( manifested_users_roles: List[str] = [] manifested_apt_config_roles: List[str] = [] manifested_dnf_config_roles: List[str] = [] + manifested_firewall_runtime_roles: List[str] = [] manifested_etc_custom_roles: List[str] = [] manifested_usr_local_custom_roles: List[str] = [] manifested_extra_paths_roles: List[str] = [] @@ -1332,6 +1425,104 @@ DNF/YUM configuration harvested from the system (repos, config files, and RPM GP manifested_dnf_config_roles.append(role) + # ------------------------- + # firewall_runtime role (live ipset/iptables kernel state) + # ------------------------- + if firewall_runtime_snapshot and ( + firewall_runtime_snapshot.get("ipset_save") + or firewall_runtime_snapshot.get("iptables_v4_save") + or firewall_runtime_snapshot.get("iptables_v6_save") + ): + role = firewall_runtime_snapshot.get("role_name", "firewall_runtime") + role_dir = os.path.join(roles_root, role) + _write_role_scaffold(role_dir) + + var_prefix = role + packages = firewall_runtime_snapshot.get("packages", []) or [] + ipset_save = firewall_runtime_snapshot.get("ipset_save") or "" + ipset_sets = firewall_runtime_snapshot.get("ipset_sets", []) or [] + iptables_v4_save = firewall_runtime_snapshot.get("iptables_v4_save") or "" + iptables_v6_save = firewall_runtime_snapshot.get("iptables_v6_save") or "" + notes = firewall_runtime_snapshot.get("notes", []) or [] + + # Generated firewall snapshots are host-specific in site mode. + if site_mode: + _copy_artifacts( + bundle_dir, + role, + _host_role_files_dir(out_dir, fqdn or "", role), + ) + else: + _copy_artifacts(bundle_dir, role, os.path.join(role_dir, "files")) + + vars_map: Dict[str, Any] = { + f"{var_prefix}_packages": packages, + f"{var_prefix}_ipset_save": ipset_save, + f"{var_prefix}_ipset_sets": ipset_sets, + f"{var_prefix}_iptables_v4_save": iptables_v4_save, + f"{var_prefix}_iptables_v6_save": iptables_v6_save, + f"{var_prefix}_sync_ipsets_exact": True, + f"{var_prefix}_restore_iptables": True, + } + + if site_mode: + _write_role_defaults( + role_dir, + { + f"{var_prefix}_packages": [], + f"{var_prefix}_ipset_save": "", + f"{var_prefix}_ipset_sets": [], + f"{var_prefix}_iptables_v4_save": "", + f"{var_prefix}_iptables_v6_save": "", + f"{var_prefix}_sync_ipsets_exact": True, + f"{var_prefix}_restore_iptables": True, + }, + ) + _write_hostvars(out_dir, fqdn or "", role, vars_map) + else: + _write_role_defaults(role_dir, vars_map) + + tasks = ( + "---\n" + + _render_install_packages_tasks(role, var_prefix) + + _render_firewall_runtime_tasks(var_prefix) + ) + with open( + os.path.join(role_dir, "tasks", "main.yml"), "w", encoding="utf-8" + ) as f: + f.write(tasks.rstrip() + "\n") + + with open( + os.path.join(role_dir, "meta", "main.yml"), "w", encoding="utf-8" + ) as f: + f.write("---\ndependencies: []\n") + + readme = f"""# {role} + +Generated from live firewall runtime state captured during harvest. + +This role restores live ipset and iptables state only for firewall families where Enroll did not find corresponding persistent configuration on the source host. Static firewall configuration files, such as `/etc/iptables/rules.v4`, `/etc/iptables/rules.v6`, UFW, nftables, firewalld, or `/etc/ipset*`, are harvested separately as managed files and treated as authoritative for their respective family. + +## Captured snapshots +- ipset: {ipset_save or "(none)"} +- iptables IPv4: {iptables_v4_save or "(none)"} +- iptables IPv6: {iptables_v6_save or "(none)"} + +## Captured ipsets +{os.linesep.join("- " + x for x in ipset_sets) or "- (none)"} + +## Notes +{os.linesep.join("- " + n for n in notes) or "- (none)"} + +## Safety notes +- `firewall_runtime_sync_ipsets_exact` defaults to `true`; it flushes captured set members before replaying the saved members so stale entries are removed. This applies only when no persistent ipset config was found. +- `firewall_runtime_restore_iptables` defaults to `true`; `iptables-restore`/`ip6tables-restore` replace only captured families. A family is captured only when no corresponding persistent iptables config was found. +""" + with open(os.path.join(role_dir, "README.md"), "w", encoding="utf-8") as f: + f.write(readme) + + manifested_firewall_runtime_roles.append(role) + # ------------------------- # etc_custom role (unowned /etc not already attributed) # ------------------------- @@ -2012,6 +2203,7 @@ Generated for package `{pkg}`. + manifested_extra_paths_roles + manifested_users_roles + tail_roles + + manifested_firewall_runtime_roles ) if site_mode: diff --git a/enroll/schema/state.schema.json b/enroll/schema/state.schema.json index 083f90f..d0bde52 100644 --- a/enroll/schema/state.schema.json +++ b/enroll/schema/state.schema.json @@ -60,7 +60,7 @@ "enum": [ "user_excluded", "unreadable", - "backup_file", + "backup_file", "log_file", "denied_path", "too_large", @@ -315,6 +315,23 @@ "ref" ], "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "kind": { + "const": "firewall_runtime" + }, + "ref": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "kind", + "ref" + ], + "type": "object" } ] }, @@ -579,6 +596,62 @@ } ], "unevaluatedProperties": false + }, + "FirewallRuntimeSnapshot": { + "additionalProperties": false, + "properties": { + "role_name": { + "const": "firewall_runtime" + }, + "packages": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "ipset_save": { + "type": [ + "string", + "null" + ] + }, + "ipset_sets": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array" + }, + "iptables_v4_save": { + "type": [ + "string", + "null" + ] + }, + "iptables_v6_save": { + "type": [ + "string", + "null" + ] + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "role_name", + "packages", + "ipset_save", + "ipset_sets", + "iptables_v4_save", + "iptables_v6_save", + "notes" + ], + "type": "object" } }, "$id": "https://enroll.sh/schema/state.schema.json", @@ -686,6 +759,9 @@ }, "usr_local_custom": { "$ref": "#/$defs/UsrLocalCustomSnapshot" + }, + "firewall_runtime": { + "$ref": "#/$defs/FirewallRuntimeSnapshot" } }, "required": [ diff --git a/enroll/validate.py b/enroll/validate.py index 5a8fa88..f3291e9 100644 --- a/enroll/validate.py +++ b/enroll/validate.py @@ -197,6 +197,37 @@ def validate_harvest( f"artifact is not a file for role {role_name}: artifacts/{role_name}/{src_rel}" ) + # Runtime firewall snapshots are generated artifacts rather than managed files. + fw = (state.get("roles") or {}).get("firewall_runtime") or {} + if isinstance(fw, dict): + for key in ("ipset_save", "iptables_v4_save", "iptables_v6_save"): + src_rel = str(fw.get(key) or "") + if not src_rel: + continue + if src_rel.startswith("/") or ".." in src_rel.split("/"): + errors.append( + f"firewall_runtime {key} has suspicious src_rel: {src_rel!r}" + ) + continue + referenced.add( + (str(fw.get("role_name") or "firewall_runtime"), src_rel) + ) + p = ( + artifacts_dir + / str(fw.get("role_name") or "firewall_runtime") + / src_rel + ) + if not p.exists(): + errors.append( + "missing firewall runtime artifact: " + f"artifacts/{fw.get('role_name') or 'firewall_runtime'}/{src_rel}" + ) + elif not p.is_file(): + errors.append( + "firewall runtime artifact is not a file: " + f"artifacts/{fw.get('role_name') or 'firewall_runtime'}/{src_rel}" + ) + # Warn if there are extra files in artifacts not referenced. if artifacts_dir.exists() and artifacts_dir.is_dir(): for fp in artifacts_dir.rglob("*"): diff --git a/poetry.lock b/poetry.lock index c94436e..b338a10 100644 --- a/poetry.lock +++ b/poetry.lock @@ -562,13 +562,13 @@ test = ["pytest (>=6)"] [[package]] name = "idna" -version = "3.14" +version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" files = [ - {file = "idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69"}, - {file = "idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3"}, + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] [package.extras] @@ -897,13 +897,13 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.34.0" +version = "2.34.1" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" files = [ - {file = "requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60"}, - {file = "requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a"}, + {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"}, + {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 4afda15..a7a83d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "enroll" -version = "0.5.0" +version = "0.6.0" description = "Enroll a server's running state retrospectively into Ansible" authors = ["Miguel Jacq "] license = "GPL-3.0-or-later" diff --git a/rpm/enroll.spec b/rpm/enroll.spec index 2980f32..0e83c84 100644 --- a/rpm/enroll.spec +++ b/rpm/enroll.spec @@ -1,4 +1,4 @@ -%global upstream_version 0.5.0 +%global upstream_version 0.6.0 Name: enroll Version: %{upstream_version} @@ -43,6 +43,9 @@ Enroll a server's running state retrospectively into Ansible. %{_bindir}/enroll %changelog +* Thu May 14 2026 Miguel Jacq - %{version}-%{release} +- Add support for capturing ipset and iptables configuration files +- 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 - %{version}-%{release} - Add ssh config support where JinjaTurtle is used * Tue Feb 16 2026 Miguel Jacq - %{version}-%{release} diff --git a/tests/test_harvest_helpers.py b/tests/test_harvest_helpers.py index 531a62c..a0d2c91 100644 --- a/tests/test_harvest_helpers.py +++ b/tests/test_harvest_helpers.py @@ -168,3 +168,121 @@ def test_iter_system_capture_paths_dedupes_first_reason(monkeypatch): ) out = h._iter_system_capture_paths() assert out == [("/dup", "r1")] + + +def test_ipset_and_iptables_state_helpers(tmp_path: Path): + ipset_save = """create blocklist hash:ip family inet hashsize 1024 maxelem 65536 +add blocklist 203.0.113.10 +create nets hash:net family inet +""" + assert h._ipset_save_has_state(ipset_save) + assert h._parse_ipset_set_names(ipset_save) == ["blocklist", "nets"] + assert not h._ipset_save_has_state("# empty\n") + + empty_iptables = """*filter +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +COMMIT +""" + assert not h._iptables_save_has_state(empty_iptables) + + native_rule = empty_iptables.replace( + "COMMIT", "-A INPUT -p tcp --dport 22 -j ACCEPT\nCOMMIT" + ) + assert h._iptables_save_has_state(native_rule) + + changed_policy = empty_iptables.replace(":INPUT ACCEPT", ":INPUT DROP") + assert h._iptables_save_has_state(changed_policy) + + +def test_collect_firewall_runtime_snapshot_writes_generated_artifacts( + monkeypatch, tmp_path: Path +): + outputs = { + "ipset_save": ( + "create blocklist hash:ip family inet\nadd blocklist 203.0.113.10\n", + None, + ), + "iptables_v4_save": ( + "*filter\n:INPUT DROP [0:0]\n-A INPUT -m set --match-set blocklist src -j DROP\nCOMMIT\n", + None, + ), + "iptables_v6_save": ("*filter\n:INPUT ACCEPT [0:0]\nCOMMIT\n", None), + } + + def fake_run(command_key, *, timeout=10): + return outputs[command_key] + + monkeypatch.setattr(h, "_run_capture_command", fake_run) + + snap = h._collect_firewall_runtime_snapshot(str(tmp_path)) + assert snap.role_name == "firewall_runtime" + assert snap.packages == ["ipset", "iptables"] + assert snap.ipset_save == "firewall/ipset.save" + assert snap.ipset_sets == ["blocklist"] + assert snap.iptables_v4_save == "firewall/iptables.v4" + assert snap.iptables_v6_save is None + + assert ( + (tmp_path / "artifacts" / "firewall_runtime" / "firewall" / "ipset.save") + .read_text(encoding="utf-8") + .startswith("create blocklist") + ) + assert ( + (tmp_path / "artifacts" / "firewall_runtime" / "firewall" / "iptables.v4") + .read_text(encoding="utf-8") + .startswith("*filter") + ) + + +def test_collect_firewall_runtime_snapshot_is_per_family_fallback( + monkeypatch, tmp_path: Path +): + calls = [] + outputs = { + "ipset_save": ( + "create blocklist hash:ip family inet\nadd blocklist 203.0.113.10\n", + None, + ), + "iptables_v4_save": ( + "*filter\n:INPUT DROP [0:0]\n-A INPUT -p tcp --dport 22 -j ACCEPT\nCOMMIT\n", + None, + ), + "iptables_v6_save": ( + "*filter\n:INPUT DROP [0:0]\n-A INPUT -p tcp --dport 22 -j ACCEPT\nCOMMIT\n", + None, + ), + } + + def fake_run(command_key, *, timeout=10): + calls.append(command_key) + return outputs[command_key] + + monkeypatch.setattr(h, "_run_capture_command", fake_run) + + snap = h._collect_firewall_runtime_snapshot( + str(tmp_path), + persistent_ipset_files=["/etc/ipset.conf"], + persistent_iptables_v4_files=["/etc/iptables/rules.v4"], + persistent_iptables_v6_files=[], + ) + + assert "ipset_save" not in calls + assert "iptables_v4_save" not in calls + assert "iptables_v6_save" in calls + assert snap.ipset_save is None + assert snap.iptables_v4_save is None + assert snap.iptables_v6_save == "firewall/iptables.v6" + assert snap.packages == ["iptables"] + assert any("persistent ipset configuration" in note for note in snap.notes) + assert any("persistent IPv4 iptables configuration" in note for note in snap.notes) + assert not ( + tmp_path / "artifacts" / "firewall_runtime" / "firewall" / "ipset.save" + ).exists() + assert not ( + tmp_path / "artifacts" / "firewall_runtime" / "firewall" / "iptables.v4" + ).exists() + assert ( + tmp_path / "artifacts" / "firewall_runtime" / "firewall" / "iptables.v6" + ).exists() diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 073fd6d..658d77f 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -795,3 +795,100 @@ def test_manifest_applies_jinjaturtle_to_jinjifyable_managed_file( assert not ( out_dir / "roles" / "apt_config" / "files" / "etc" / "apt" / "foo.ini" ).exists() + + +def test_manifest_writes_firewall_runtime_role(tmp_path: Path): + bundle = tmp_path / "bundle" + out = tmp_path / "ansible" + (bundle / "artifacts" / "firewall_runtime" / "firewall").mkdir( + parents=True, exist_ok=True + ) + (bundle / "artifacts" / "firewall_runtime" / "firewall" / "ipset.save").write_text( + "create blocklist hash:ip family inet\nadd blocklist 203.0.113.10\n", + encoding="utf-8", + ) + (bundle / "artifacts" / "firewall_runtime" / "firewall" / "iptables.v4").write_text( + "*filter\n:INPUT DROP [0:0]\n-A INPUT -m set --match-set blocklist src -j DROP\nCOMMIT\n", + encoding="utf-8", + ) + + state = { + "schema_version": 3, + "host": {"hostname": "test", "os": "debian", "pkg_backend": "dpkg"}, + "inventory": {"packages": {}}, + "roles": { + "users": { + "role_name": "users", + "users": [], + "managed_files": [], + "excluded": [], + "notes": [], + }, + "services": [], + "packages": [], + "apt_config": { + "role_name": "apt_config", + "managed_files": [], + "excluded": [], + "notes": [], + }, + "dnf_config": { + "role_name": "dnf_config", + "managed_files": [], + "excluded": [], + "notes": [], + }, + "firewall_runtime": { + "role_name": "firewall_runtime", + "packages": ["ipset", "iptables"], + "ipset_save": "firewall/ipset.save", + "ipset_sets": ["blocklist"], + "iptables_v4_save": "firewall/iptables.v4", + "iptables_v6_save": None, + "notes": [], + }, + "etc_custom": { + "role_name": "etc_custom", + "managed_files": [], + "excluded": [], + "notes": [], + }, + "usr_local_custom": { + "role_name": "usr_local_custom", + "managed_files": [], + "excluded": [], + "notes": [], + }, + "extra_paths": { + "role_name": "extra_paths", + "include_patterns": [], + "exclude_patterns": [], + "managed_files": [], + "excluded": [], + "notes": [], + }, + }, + } + (bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8") + + manifest.manifest(str(bundle), str(out)) + + tasks = (out / "roles" / "firewall_runtime" / "tasks" / "main.yml").read_text( + encoding="utf-8" + ) + assert "ipset restore -exist" in tasks + assert "iptables-restore /etc/enroll/firewall/iptables.v4" in tasks + assert "ipset flush {{ item }}" in tasks + + defaults = (out / "roles" / "firewall_runtime" / "defaults" / "main.yml").read_text( + encoding="utf-8" + ) + assert "firewall_runtime_ipset_sets:" in defaults + assert "- blocklist" in defaults + assert "firewall_runtime_restore_iptables: true" in defaults + + pb = (out / "playbook.yml").read_text(encoding="utf-8") + assert "role: firewall_runtime" in pb + assert ( + out / "roles" / "firewall_runtime" / "files" / "firewall" / "ipset.save" + ).exists() From 1544dc0295c19aa870c023daac4b6f0631abdcfe Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 31 May 2026 16:50:57 +1000 Subject: [PATCH 14/48] more test coverage --- .gitignore | 1 + tests/test_accounts.py | 144 ++++++ tests/test_debian.py | 241 +++++++++ tests/test_diff_bundle.py | 992 ++++++++++++++++++++++++++++++++++++ tests/test_harvest.py | 147 ++++++ tests/test_ignore.py | 240 +++++++++ tests/test_manifest.py | 172 +++++++ tests/test_misc_coverage.py | 416 --------------- tests/test_pathfilter.py | 154 ++++++ tests/test_platform.py | 173 +++++++ tests/test_remote.py | 449 ++++++++++++++++ tests/test_rpm.py | 31 ++ tests/test_sopsutil.py | 54 ++ tests/test_systemd.py | 129 ++++- tests/test_validate.py | 231 +++++++++ 15 files changed, 3150 insertions(+), 424 deletions(-) delete mode 100644 tests/test_misc_coverage.py create mode 100644 tests/test_sopsutil.py diff --git a/.gitignore b/.gitignore index 07c956d..4ef962d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist *.pdf *.csv *.html +coverage.xml diff --git a/tests/test_accounts.py b/tests/test_accounts.py index d5cc267..9e60b57 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -141,3 +141,147 @@ def test_collect_non_system_users(monkeypatch, tmp_path: Path): assert u.primary_group == "users" assert u.supplementary_groups == ["admins"] assert u.ssh_files == ["/home/alice/.ssh/authorized_keys"] + + +def test_parse_login_defs_file_not_found(tmp_path: Path): + from enroll.accounts import parse_login_defs + + nonexistent = tmp_path / "nonexistent" / "login.defs" + vals = parse_login_defs(str(nonexistent)) + assert vals == {} + + +def test_parse_login_defs_handles_invalid_numbers(tmp_path: Path): + from enroll.accounts import parse_login_defs + + p = tmp_path / "login.defs" + p.write_text("UID_MIN not_a_number\nUID_MAX 60000\n", encoding="utf-8") + vals = parse_login_defs(str(p)) + assert "UID_MIN" not in vals + assert vals["UID_MAX"] == 60000 + + +def test_parse_group_handles_invalid_gid(tmp_path: Path): + from enroll.accounts import parse_group + + p = tmp_path / "group" + p.write_text( + "valid:x:1000:user1\n" "invalid_gid:x:notanint:user2\n", + encoding="utf-8", + ) + gid_to_name, name_to_gid, members = parse_group(str(p)) + assert 1000 in gid_to_name + assert gid_to_name[1000] == "valid" + assert "invalid_gid" not in name_to_gid + + +def test_parse_group_line_too_short(tmp_path: Path): + from enroll.accounts import parse_group + + p = tmp_path / "group" + p.write_text( + "valid:x:1000:user1\n" "shortline:x:1001\n", + encoding="utf-8", + ) + gid_to_name, name_to_gid, members = parse_group(str(p)) + assert 1000 in gid_to_name + assert 1001 not in gid_to_name + + +def test_is_human_user_filters_by_uid_and_shell(): + from enroll.accounts import is_human_user + + assert is_human_user(1000, "/bin/bash", 1000) is True + assert is_human_user(999, "/bin/bash", 1000) is False + assert is_human_user(1000, "/usr/sbin/nologin", 1000) is False + assert is_human_user(1000, "/usr/bin/nologin", 1000) is False + assert is_human_user(1000, "/bin/false", 1000) is False + assert is_human_user(1000, "", 1000) is True + + +def test_find_user_ssh_files_no_ssh_dir(tmp_path: Path): + from enroll.accounts import find_user_ssh_files + + home = tmp_path / "home" / "user" + home.mkdir(parents=True) + assert find_user_ssh_files(str(home)) == [] + + +def test_find_user_ssh_files_ignores_symlink(tmp_path: Path): + from enroll.accounts import find_user_ssh_files + + home = tmp_path / "home" / "user" + sshdir = home / ".ssh" + sshdir.mkdir(parents=True) + target = sshdir / "real_file" + target.write_text("x", encoding="utf-8") + os.symlink(str(target), str(sshdir / "authorized_keys")) + + result = find_user_ssh_files(str(home)) + assert result == [] + + +def test_find_user_ssh_files_handles_home_not_starting_with_slash(): + from enroll.accounts import find_user_ssh_files + + assert find_user_ssh_files("relative/path") == [] + assert find_user_ssh_files("") == [] + + +def test_collect_non_system_users_skips_nologin_users(tmp_path: Path): + import enroll.accounts as a + + orig_parse_login_defs = a.parse_login_defs + orig_parse_passwd = a.parse_passwd + orig_parse_group = a.parse_group + + passwd = tmp_path / "passwd" + passwd.write_text( + "root:x:0:0:root:/root:/bin/bash\n" + "alice:x:1000:1000:Alice:/home/alice:/bin/bash\n" + "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n" + "sysuser:x:100:100:Sys:/home/sys:/bin/bash\n", + encoding="utf-8", + ) + group = tmp_path / "group" + group.write_text("users:x:1000:alice\n", encoding="utf-8") + defs = tmp_path / "login.defs" + defs.write_text("UID_MIN 1000\n", encoding="utf-8") + + monkeypatch_wrapper = lambda fn, p: lambda path=str(p): fn(path) + + a.parse_login_defs = monkeypatch_wrapper(orig_parse_login_defs, defs) + a.parse_passwd = monkeypatch_wrapper(orig_parse_passwd, passwd) + a.parse_group = monkeypatch_wrapper(orig_parse_group, group) + a.find_user_ssh_files = lambda home: [] + + users = a.collect_non_system_users() + assert [u.name for u in users] == ["alice"] + + +def test_collect_non_system_users_skips_below_uid_min(tmp_path: Path): + import enroll.accounts as a + + orig_parse_login_defs = a.parse_login_defs + orig_parse_passwd = a.parse_passwd + orig_parse_group = a.parse_group + + passwd = tmp_path / "passwd" + passwd.write_text( + "root:x:0:0:root:/root:/bin/bash\n" + "sysuser:x:999:999:Sys:/home/sys:/bin/bash\n" + "alice:x:1000:1000:Alice:/home/alice:/bin/bash\n", + encoding="utf-8", + ) + group = tmp_path / "group" + group.write_text("users:x:1000:alice\n", encoding="utf-8") + defs = tmp_path / "login.defs" + defs.write_text("UID_MIN 1000\n", encoding="utf-8") + + a.parse_login_defs = lambda path=str(defs): orig_parse_login_defs(path) + a.parse_passwd = lambda path=str(passwd): orig_parse_passwd(path) + a.parse_group = lambda path=str(group): orig_parse_group(path) + a.find_user_ssh_files = lambda home: [] + + users = a.collect_non_system_users() + assert [u.name for u in users] == ["alice"] diff --git a/tests/test_debian.py b/tests/test_debian.py index abad361..818ee8a 100644 --- a/tests/test_debian.py +++ b/tests/test_debian.py @@ -96,3 +96,244 @@ def test_parse_status_conffiles_handles_continuations(tmp_path: Path): assert m["nginx"]["/etc/nginx/nginx.conf"] == "abcdef" assert m["nginx"]["/etc/nginx/mime.types"] == "123456" assert "other" not in m + + +def test_dpkg_owner_returns_none_on_diversion_only(monkeypatch): + import enroll.debian as d + + class P: + def __init__(self, rc: int, out: str): + self.returncode = rc + self.stdout = out + self.stderr = "" + + def fake_run(cmd, text, capture_output): + return P(0, "diversion by foo from: /etc/something\n") + + monkeypatch.setattr(d.subprocess, "run", fake_run) + assert d.dpkg_owner("/etc/something") is None + + +def test_dpkg_owner_handles_line_without_colon(monkeypatch): + import enroll.debian as d + + class P: + def __init__(self, rc: int, out: str): + self.returncode = rc + self.stdout = out + self.stderr = "" + + def fake_run(cmd, text, capture_output): + return P(0, "invalid line without colon\n") + + monkeypatch.setattr(d.subprocess, "run", fake_run) + assert d.dpkg_owner("/etc/foo") is None + + +def test_list_manual_packages_returns_empty_on_error(monkeypatch): + import enroll.debian as d + + class P: + def __init__(self, rc: int, out: str): + self.returncode = rc + self.stdout = out + self.stderr = "" + + def fake_run(cmd, text, capture_output): + return P(1, "error") + + monkeypatch.setattr(d.subprocess, "run", fake_run) + assert d.list_manual_packages() == [] + + +def test_list_installed_packages_handles_exception(monkeypatch): + import enroll.debian as d + + def fake_run(*args, **kwargs): + raise Exception("simulated error") + + monkeypatch.setattr(d.subprocess, "run", fake_run) + assert d.list_installed_packages() == {} + + +def test_list_installed_packages_parses_output(): + import enroll.debian as d + + class P: + def __init__(self, rc: int, out: str): + self.returncode = rc + self.stdout = out + self.stderr = "" + + original_run = d.subprocess.run + + def fake_run(cmd, text, capture_output, check): + return P(0, "nginx\t1.18.0\tamd64\nvim\t8.2\tamd64\n") + + d.subprocess.run = fake_run + try: + result = d.list_installed_packages() + assert "nginx" in result + assert result["nginx"][0]["version"] == "1.18.0" + assert result["nginx"][0]["arch"] == "amd64" + assert "vim" in result + finally: + d.subprocess.run = original_run + + +def test_list_installed_packages_skips_invalid_lines(): + import enroll.debian as d + + class P: + def __init__(self, rc: int, out: str): + self.returncode = rc + self.stdout = out + self.stderr = "" + + original_run = d.subprocess.run + + def fake_run(cmd, text, capture_output, check): + return P(0, "nginx\t1.18.0\tamd64\ninvalid_line\n\t1.0\tamd64\n") + + d.subprocess.run = fake_run + try: + result = d.list_installed_packages() + assert "nginx" in result + assert "invalid_line" not in result + finally: + d.subprocess.run = original_run + + +def test_list_installed_packages_handles_empty_name(): + import enroll.debian as d + + class P: + def __init__(self, rc: int, out: str): + self.returncode = rc + self.stdout = out + self.stderr = "" + + original_run = d.subprocess.run + + def fake_run(cmd, text, capture_output, check): + return P(0, "\t1.0\tamd64\nnginx\t1.18.0\tamd64\n") + + d.subprocess.run = fake_run + try: + result = d.list_installed_packages() + assert "" not in result + assert "nginx" in result + finally: + d.subprocess.run = original_run + + +def test_list_installed_packages_sorts_output(): + import enroll.debian as d + + class P: + def __init__(self, rc: int, out: str): + self.returncode = rc + self.stdout = out + self.stderr = "" + + original_run = d.subprocess.run + + def fake_run(cmd, text, capture_output, check): + return P(0, "nginx\t1.18.0\tamd64\nnginx\t1.19.0\tarm64\n") + + d.subprocess.run = fake_run + try: + result = d.list_installed_packages() + assert len(result["nginx"]) == 2 + assert result["nginx"][0]["arch"] == "amd64" + assert result["nginx"][1]["arch"] == "arm64" + finally: + d.subprocess.run = original_run + + +def test_build_dpkg_etc_index_handles_missing_file(tmp_path: Path): + import enroll.debian as d + + info = tmp_path / "info" + info.mkdir() + # Don't create any .list files + + owned, owner_map, topdir_to_pkgs, pkg_to_etc = d.build_dpkg_etc_index(str(info)) + assert owned == set() + assert owner_map == {} + assert topdir_to_pkgs == {} + assert pkg_to_etc == {} + + +def test_build_dpkg_etc_index_skips_non_etc_paths(tmp_path: Path): + import enroll.debian as d + + info = tmp_path / "info" + info.mkdir() + (info / "foo.list").write_text("/usr/bin/foo\n/etc/bar\n", encoding="utf-8") + + owned, owner_map, topdir_to_pkgs, pkg_to_etc = d.build_dpkg_etc_index(str(info)) + assert "/usr/bin/foo" not in owned + assert "/etc/bar" in owned + assert "foo" not in topdir_to_pkgs + + +def test_parse_status_conffiles_handles_empty_status(tmp_path: Path): + import enroll.debian as d + + status = tmp_path / "status" + status.write_text("", encoding="utf-8") + m = d.parse_status_conffiles(str(status)) + assert m == {} + + +def test_parse_status_conffiles_handles_package_without_conffiles(tmp_path: Path): + import enroll.debian as d + + status = tmp_path / "status" + status.write_text( + "Package: nginx\nVersion: 1\nStatus: install ok installed\n", + encoding="utf-8", + ) + m = d.parse_status_conffiles(str(status)) + assert m == {} + + +def test_read_pkg_md5sums_returns_empty_if_file_not_exists(tmp_path: Path): + import enroll.debian as d + + result = d.read_pkg_md5sums("nonexistent_package") + assert result == {} + + +def test_read_pkg_md5sums_parses_md5sums_file(tmp_path: Path, monkeypatch): + import enroll.debian as d + + info_dir = tmp_path / "info" + info_dir.mkdir() + md5_file = info_dir / "nginx.md5sums" + md5_file.write_text( + "abcdef1234567890abcdef1234567890 etc/nginx/nginx.conf\n" + "1234567890abcdef1234567890abcdef etc/nginx/sites-enabled/default\n", + encoding="utf-8", + ) + + def fake_exists(path): + return str(path).endswith("nginx.md5sums") + + monkeypatch.setattr(d.os.path, "exists", fake_exists) + + original_open = open + + def fake_open(path, *args, **kwargs): + if "nginx.md5sums" in str(path): + return original_open(md5_file, *args, **kwargs) + return original_open(path, *args, **kwargs) + + monkeypatch.setattr("builtins.open", fake_open, raising=False) + + result = d.read_pkg_md5sums("nginx") + assert result["etc/nginx/nginx.conf"] == "abcdef1234567890abcdef1234567890" + assert ( + result["etc/nginx/sites-enabled/default"] == "1234567890abcdef1234567890abcdef" + ) diff --git a/tests/test_diff_bundle.py b/tests/test_diff_bundle.py index 66ef094..ae12187 100644 --- a/tests/test_diff_bundle.py +++ b/tests/test_diff_bundle.py @@ -87,3 +87,995 @@ def test_bundle_from_input_missing_path(tmp_path: Path): with pytest.raises(RuntimeError, match="not found"): d._bundle_from_input(str(tmp_path / "nope"), sops_mode=False) + + +import json +import sys + + +from enroll.diff import ( + _bundle_from_input, + _file_index, + _iter_managed_files, + _load_state, + _pkg_version_display, + _pkg_version_key, + _progress_enabled, + _roles, + _service_units, + _sha256, + _users_by_name, + compare_harvests, +) +from enroll.sopsutil import SopsError + + +def test_progress_enabled_when_tty(monkeypatch): + monkeypatch.setattr(sys.stderr, "isatty", lambda: True) + monkeypatch.delenv("ENROLL_NO_PROGRESS", raising=False) + assert _progress_enabled() is True + + +def test_progress_enabled_when_not_tty(monkeypatch): + monkeypatch.setattr(sys.stderr, "isatty", lambda: False) + monkeypatch.delenv("ENROLL_NO_PROGRESS", raising=False) + assert _progress_enabled() is False + + +def test_progress_enabled_with_env_var(monkeypatch): + monkeypatch.setattr(sys.stderr, "isatty", lambda: True) + monkeypatch.setenv("ENROLL_NO_PROGRESS", "1") + assert _progress_enabled() is False + + monkeypatch.setenv("ENROLL_NO_PROGRESS", "true") + assert _progress_enabled() is False + + monkeypatch.setenv("ENROLL_NO_PROGRESS", "yes") + assert _progress_enabled() is False + + +def test_sha256(tmp_path: Path): + test_file = tmp_path / "test.txt" + test_file.write_text("hello world", encoding="utf-8") + hash_result = _sha256(test_file) + assert len(hash_result) == 64 + + +def test_sha256_empty_file(tmp_path: Path): + test_file = tmp_path / "empty.txt" + test_file.write_bytes(b"") + hash_result = _sha256(test_file) + assert ( + hash_result + == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ) + + +def test_bundle_from_input_directory(tmp_path: Path): + result = _bundle_from_input(str(tmp_path), sops_mode=False) + assert result.dir == tmp_path + assert result.tempdir is None + + +def test_bundle_from_input_state_json_path(tmp_path: Path): + state_file = tmp_path / "state.json" + state_file.write_text("{}", encoding="utf-8") + result = _bundle_from_input(str(state_file), sops_mode=False) + assert result.dir == tmp_path + assert result.tempdir is None + + +def test_bundle_from_input_not_found(): + with pytest.raises(RuntimeError) as exc_info: + _bundle_from_input("/nonexistent/path", sops_mode=False) + assert "not found" in str(exc_info.value).lower() + + +def test_bundle_from_input_tarball(tmp_path: Path): + bundle_dir = tmp_path / "bundle" + bundle_dir.mkdir() + state_file = bundle_dir / "state.json" + state_file.write_text("{}", encoding="utf-8") + + tar_path = tmp_path / "bundle.tar.gz" + with tarfile.open(tar_path, "w:gz") as tf: + tf.add(bundle_dir, arcname="bundle") + + result = _bundle_from_input(str(tar_path), sops_mode=False) + assert result.dir.exists() + assert result.tempdir is not None + result.tempdir.cleanup() + + +def test_bundle_from_input_invalid_type(tmp_path: Path): + test_file = tmp_path / "test.txt" + test_file.write_text("not a bundle", encoding="utf-8") + + with pytest.raises(RuntimeError) as exc_info: + _bundle_from_input(str(test_file), sops_mode=False) + assert "not a directory" in str(exc_info.value).lower() + + +def test_load_state(tmp_path: Path): + state_file = tmp_path / "state.json" + state_file.write_text('{"host": {"hostname": "test"}}', encoding="utf-8") + result = _load_state(tmp_path) + assert result["host"]["hostname"] == "test" + + +def test_roles_empty_state(): + assert _roles({}) == {} + + +def test_roles_with_roles(): + state = {"roles": {"users": {}, "services": []}} + result = _roles(state) + assert "users" in result + + +def test_service_units_empty(): + assert _service_units({}) == {} + + +def test_service_units_with_services(): + state = { + "roles": { + "services": [ + {"unit": "nginx.service", "active_state": "active"}, + {"unit": "ssh.service", "active_state": "inactive"}, + ] + } + } + result = _service_units(state) + assert "nginx.service" in result + assert "ssh.service" in result + assert result["nginx.service"]["active_state"] == "active" + + +def test_users_by_name_empty(): + assert _users_by_name({}) == {} + + +def test_users_by_name_with_users(): + state = { + "roles": { + "users": { + "users": [ + {"name": "alice", "uid": 1000}, + {"name": "bob", "uid": 1001}, + ] + } + } + } + result = _users_by_name(state) + assert "alice" in result + assert "bob" in result + assert result["alice"]["uid"] == 1000 + + +def test_pkg_version_key_with_version(): + entry = {"version": "1.2.3"} + assert _pkg_version_key(entry) == "1.2.3" + + +def test_pkg_version_key_with_installations(): + entry = { + "installations": [ + {"arch": "x86_64", "version": "1.2.3"}, + {"arch": "aarch64", "version": "1.2.3"}, + ] + } + result = _pkg_version_key(entry) + assert "x86_64:1.2.3" in result + assert "aarch64:1.2.3" in result + + +def test_pkg_version_key_with_empty_version(): + entry = {"version": None} + assert _pkg_version_key(entry) is None + + +def test_pkg_version_key_with_invalid_installations(): + entry = {"installations": ["not_a_dict", {"arch": "x86_64", "version": "1.0"}]} + result = _pkg_version_key(entry) + assert "x86_64:1.0" in result + + +def test_pkg_version_display_with_version(): + entry = {"version": "1.2.3"} + assert _pkg_version_display(entry) == "1.2.3" + + +def test_pkg_version_display_with_installations(): + entry = { + "installations": [ + {"arch": "x86_64", "version": "1.2.3"}, + ] + } + assert _pkg_version_display(entry) == "1.2.3 (x86_64)" + + +def test_pkg_version_display_empty(): + assert _pkg_version_display({}) is None + + +def test_iter_managed_files_empty(): + state = {"roles": {}} + files = list(_iter_managed_files(state)) + assert files == [] + + +def test_iter_managed_files_services(): + state = { + "roles": { + "services": [ + { + "role_name": "nginx", + "managed_files": [ + {"path": "/etc/nginx/nginx.conf", "src_rel": "nginx.conf"} + ], + } + ] + } + } + files = list(_iter_managed_files(state)) + assert len(files) == 1 + assert files[0] == ( + "nginx", + {"path": "/etc/nginx/nginx.conf", "src_rel": "nginx.conf"}, + ) + + +def test_iter_managed_files_packages(): + state = { + "roles": { + "packages": [ + { + "role_name": "vim", + "managed_files": [{"path": "/usr/bin/vim", "src_rel": "bin/vim"}], + } + ] + } + } + files = list(_iter_managed_files(state)) + assert len(files) == 1 + assert files[0][0] == "vim" + + +def test_iter_managed_files_users(): + state = { + "roles": { + "users": { + "role_name": "users", + "managed_files": [{"path": "/etc/passwd", "src_rel": "passwd"}], + } + } + } + files = list(_iter_managed_files(state)) + assert len(files) == 1 + assert files[0][0] == "users" + + +def test_iter_managed_files_apt_config(): + state = { + "roles": { + "apt_config": { + "role_name": "apt_config", + "managed_files": [ + {"path": "/etc/apt/sources.list", "src_rel": "sources.list"} + ], + } + } + } + files = list(_iter_managed_files(state)) + assert len(files) == 1 + assert files[0][0] == "apt_config" + + +def test_iter_managed_files_etc_custom(): + state = { + "roles": { + "etc_custom": { + "role_name": "etc_custom", + "managed_files": [ + {"path": "/etc/custom.conf", "src_rel": "custom.conf"} + ], + } + } + } + files = list(_iter_managed_files(state)) + assert len(files) == 1 + assert files[0][0] == "etc_custom" + + +def test_iter_managed_files_usr_local_custom(): + state = { + "roles": { + "usr_local_custom": { + "role_name": "usr_local_custom", + "managed_files": [ + {"path": "/usr/local/bin/script", "src_rel": "bin/script"} + ], + } + } + } + files = list(_iter_managed_files(state)) + assert len(files) == 1 + assert files[0][0] == "usr_local_custom" + + +def test_iter_managed_files_extra_paths(): + state = { + "roles": { + "extra_paths": { + "role_name": "extra_paths", + "managed_files": [{"path": "/opt/app/config", "src_rel": "config"}], + } + } + } + files = list(_iter_managed_files(state)) + assert len(files) == 1 + assert files[0][0] == "extra_paths" + + +def test_file_index_empty(): + state = {"roles": {}} + index = _file_index(Path("/tmp"), state) + assert index == {} + + +def test_file_index_with_files(tmp_path: Path): + state = { + "roles": { + "users": { + "managed_files": [ + {"path": "/etc/passwd", "src_rel": "passwd", "owner": "root"}, + ] + } + } + } + index = _file_index(tmp_path, state) + assert "/etc/passwd" in index + assert index["/etc/passwd"].role == "users" + assert index["/etc/passwd"].owner == "root" + + +def test_file_index_duplicates_first_wins(tmp_path: Path): + state = { + "roles": { + "users": { + "managed_files": [ + {"path": "/etc/passwd", "src_rel": "passwd"}, + ] + }, + "etc_custom": { + "managed_files": [ + {"path": "/etc/passwd", "src_rel": "custom_passwd"}, + ] + }, + } + } + index = _file_index(tmp_path, state) + assert "/etc/passwd" in index + assert index["/etc/passwd"].src_rel == "passwd" + + +def test_file_index_skips_missing_path_or_src_rel(tmp_path: Path): + state = { + "roles": { + "users": { + "managed_files": [ + {"path": "/etc/passwd"}, # missing src_rel + {"src_rel": "passwd"}, # missing path + ] + } + } + } + index = _file_index(tmp_path, state) + assert index == {} + + +def test_compare_harvests_no_changes(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + (old_bundle / "state.json").write_text( + json.dumps( + { + "inventory": {"packages": {"vim": {"version": "1.0"}}}, + "roles": {}, + } + ), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + (new_bundle / "state.json").write_text( + json.dumps( + { + "inventory": {"packages": {"vim": {"version": "1.0"}}}, + "roles": {}, + } + ), + encoding="utf-8", + ) + + report, has_changes = compare_harvests(str(old_bundle), str(new_bundle)) + assert has_changes is False + assert report["packages"]["added"] == [] + assert report["packages"]["removed"] == [] + + +def test_compare_harvests_package_added(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + (old_bundle / "state.json").write_text( + json.dumps({"inventory": {"packages": {}}, "roles": {}}), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + (new_bundle / "state.json").write_text( + json.dumps( + {"inventory": {"packages": {"vim": {"version": "1.0"}}}, "roles": {}} + ), + encoding="utf-8", + ) + + report, has_changes = compare_harvests(str(old_bundle), str(new_bundle)) + assert has_changes is True + assert "vim" in report["packages"]["added"] + + +def test_compare_harvests_package_removed(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + (old_bundle / "state.json").write_text( + json.dumps( + {"inventory": {"packages": {"vim": {"version": "1.0"}}}, "roles": {}} + ), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + (new_bundle / "state.json").write_text( + json.dumps({"inventory": {"packages": {}}, "roles": {}}), + encoding="utf-8", + ) + + report, has_changes = compare_harvests(str(old_bundle), str(new_bundle)) + assert has_changes is True + assert "vim" in report["packages"]["removed"] + + +def test_compare_harvests_package_version_changed(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + (old_bundle / "state.json").write_text( + json.dumps( + {"inventory": {"packages": {"vim": {"version": "1.0"}}}, "roles": {}} + ), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + (new_bundle / "state.json").write_text( + json.dumps( + {"inventory": {"packages": {"vim": {"version": "2.0"}}}, "roles": {}} + ), + encoding="utf-8", + ) + + report, has_changes = compare_harvests(str(old_bundle), str(new_bundle)) + assert has_changes is True + assert len(report["packages"]["version_changed"]) == 1 + + +def test_compare_harvests_ignore_package_versions(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + (old_bundle / "state.json").write_text( + json.dumps( + {"inventory": {"packages": {"vim": {"version": "1.0"}}}, "roles": {}} + ), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + (new_bundle / "state.json").write_text( + json.dumps( + {"inventory": {"packages": {"vim": {"version": "2.0"}}}, "roles": {}} + ), + encoding="utf-8", + ) + + report, has_changes = compare_harvests( + str(old_bundle), str(new_bundle), ignore_package_versions=True + ) + assert report["packages"]["version_changed_ignored_count"] == 1 + + +def test_compare_harvests_service_added(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + (old_bundle / "state.json").write_text( + json.dumps({"inventory": {"packages": {}}, "roles": {"services": []}}), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + (new_bundle / "state.json").write_text( + json.dumps( + { + "inventory": {"packages": {}}, + "roles": {"services": [{"unit": "nginx.service"}]}, + } + ), + encoding="utf-8", + ) + + report, has_changes = compare_harvests(str(old_bundle), str(new_bundle)) + assert has_changes is True + assert "nginx.service" in report["services"]["enabled_added"] + + +def test_compare_harvests_user_added(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + (old_bundle / "state.json").write_text( + json.dumps({"inventory": {"packages": {}}, "roles": {"users": {"users": []}}}), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + (new_bundle / "state.json").write_text( + json.dumps( + { + "inventory": {"packages": {}}, + "roles": {"users": {"users": [{"name": "alice", "uid": 1000}]}}, + } + ), + encoding="utf-8", + ) + + report, has_changes = compare_harvests(str(old_bundle), str(new_bundle)) + assert has_changes is True + assert "alice" in report["users"]["added"] + + +def test_compare_harvests_with_exclude_paths(tmp_path: Path): + old_bundle = tmp_path / "old" + old_bundle.mkdir() + old_artifacts = old_bundle / "artifacts" / "users" + old_artifacts.mkdir(parents=True) + (old_artifacts / "passwd").write_text("old", encoding="utf-8") + (old_bundle / "state.json").write_text( + json.dumps( + { + "inventory": {"packages": {}}, + "roles": { + "users": { + "managed_files": [{"path": "/etc/passwd", "src_rel": "passwd"}] + } + }, + } + ), + encoding="utf-8", + ) + + new_bundle = tmp_path / "new" + new_bundle.mkdir() + new_artifacts = new_bundle / "artifacts" / "users" + new_artifacts.mkdir(parents=True) + (new_artifacts / "passwd").write_text("new", encoding="utf-8") + (new_bundle / "state.json").write_text( + json.dumps( + { + "inventory": {"packages": {}}, + "roles": { + "users": { + "managed_files": [{"path": "/etc/passwd", "src_rel": "passwd"}] + } + }, + } + ), + encoding="utf-8", + ) + + report, has_changes = compare_harvests( + str(old_bundle), str(new_bundle), exclude_paths=["/etc/passwd"] + ) + assert "/etc/passwd" not in [f["path"] for f in report["files"]["added"]] + assert "/etc/passwd" not in [f["path"] for f in report["files"]["removed"]] + assert "/etc/passwd" not in [f["path"] for f in report["files"]["changed"]] + + +from enroll.diff import ( + _Spinner, + _enforcement_plan, + has_enforceable_drift, + _role_tag, + _utc_now_iso, + _report_markdown, +) + + +def test_utc_now_iso(): + result = _utc_now_iso() + assert "T" in result + assert "+" in result or "Z" in result + + +def test_spinner_start_stop(monkeypatch): + # Mock sys.stderr to avoid actual writes + class FakeStderr: + def write(self, s): + pass + + def flush(self): + pass + + def isatty(self): + return True + + monkeypatch.setattr(sys, "stderr", FakeStderr()) + + spinner = _Spinner("Test") + spinner.start() + spinner.stop(final_line="Done") + # Should not raise + + +def test_spinner_stop_without_start(): + spinner = _Spinner("Test") + spinner.stop(final_line="Done") + # Should not raise + + +def test_spinner_run_exception(monkeypatch): + class FakeStderr: + def write(self, s): + raise Exception("Write error") + + def flush(self): + pass + + monkeypatch.setattr(sys, "stderr", FakeStderr()) + + spinner = _Spinner("Test") + spinner.start() + spinner.stop() + + +def test_spinner_double_start(): + spinner = _Spinner("Test") + spinner.start() + spinner.start() # Should not raise or spawn another thread + spinner.stop() + + +def test_role_tag_normal(): + assert _role_tag("nginx") == "role_nginx" + assert _role_tag("my-app") == "role_my-app" + + +def test_role_tag_with_special_chars(): + assert _role_tag("my.app") == "role_my_app" + assert _role_tag("my app") == "role_my_app" + + +def test_role_tag_empty(): + assert _role_tag("") == "role_other" + assert _role_tag(" ") == "role_other" + + +def test_has_enforceable_drift_packages_removed(): + report = {"packages": {"removed": ["vim"]}} + assert has_enforceable_drift(report) is True + + +def test_has_enforceable_drift_services_removed(): + report = {"services": {"enabled_removed": ["nginx.service"]}} + assert has_enforceable_drift(report) is True + + +def test_has_enforceable_drift_service_changed(): + report = { + "services": { + "changed": [ + { + "unit": "nginx.service", + "changes": {"active_state": {"old": "active", "new": "inactive"}}, + } + ] + } + } + assert has_enforceable_drift(report) is True + + +def test_has_enforceable_drift_service_package_only_changed(): + # Service changed only in packages - should NOT be enforceable + report = { + "services": { + "changed": [ + { + "unit": "nginx.service", + "changes": {"packages": {"added": ["nginx-extra"]}}, + } + ] + } + } + assert has_enforceable_drift(report) is False + + +def test_has_enforceable_drift_users_removed(): + report = {"users": {"removed": ["alice"]}} + assert has_enforceable_drift(report) is True + + +def test_has_enforceable_drift_users_changed(): + report = { + "users": { + "changed": [ + {"name": "alice", "changes": {"uid": {"old": 1000, "new": 1001}}} + ] + } + } + assert has_enforceable_drift(report) is True + + +def test_has_enforceable_drift_files_removed(): + report = { + "files": { + "removed": [{"path": "/etc/passwd", "role": "users", "reason": "conffile"}] + } + } + assert has_enforceable_drift(report) is True + + +def test_has_enforceable_drift_files_changed(): + report = { + "files": { + "changed": [ + { + "path": "/etc/passwd", + "changes": {"content": {"old": "sha1", "new": "sha2"}}, + } + ] + } + } + assert has_enforceable_drift(report) is True + + +def test_has_enforceable_drift_no_drift(): + report = { + "packages": {"added": ["newpkg"]}, + "services": {"enabled_added": ["new.service"]}, + "users": {"added": ["bob"]}, + "files": {"added": ["/opt/newfile"]}, + } + assert has_enforceable_drift(report) is False + + +def test_enforcement_plan_packages_removed(monkeypatch, tmp_path: Path): + old_state = { + "roles": { + "services": [{"role_name": "nginx", "packages": ["nginx"]}], + "packages": [{"role_name": "vim", "package": "vim"}], + } + } + report = {"packages": {"removed": ["nginx", "vim"]}} + + result = _enforcement_plan(report, old_state, tmp_path) + assert "nginx" in result.get("roles", []) + assert "vim" in result.get("roles", []) + assert "role_nginx" in result.get("tags", []) + + +def test_enforcement_plan_users_changed(): + old_state = { + "roles": {"users": {"role_name": "users", "users": [{"name": "alice"}]}} + } + report = {"users": {"changed": [{"name": "alice", "changes": {"uid": {}}}]}} + + result = _enforcement_plan(report, old_state, Path("/tmp")) + assert "users" in result.get("roles", []) + + +def test_enforcement_plan_files_removed(tmp_path: Path): + # Create the artifacts directory structure that _file_index expects + artifacts_dir = tmp_path / "artifacts" / "etc_custom" + artifacts_dir.mkdir(parents=True) + + old_state = { + "roles": { + "etc_custom": { + "role_name": "etc_custom", + "managed_files": [ + {"path": "/etc/custom.conf", "src_rel": "custom.conf"} + ], + } + } + } + report = { + "files": {"removed": [{"path": "/etc/custom.conf", "role": "etc_custom"}]} + } + + result = _enforcement_plan(report, old_state, tmp_path) + assert "etc_custom" in result.get("roles", []) + + +def test_enforcement_plan_no_drift(): + old_state = {"roles": {}} + report = {"packages": {"added": ["newpkg"]}} + + result = _enforcement_plan(report, old_state, Path("/tmp")) + assert result.get("roles", []) == [] + + +def test_bundle_from_input_tgz(monkeypatch, tmp_path: Path): + bundle_dir = tmp_path / "bundle" + bundle_dir.mkdir() + state_file = bundle_dir / "state.json" + state_file.write_text("{}", encoding="utf-8") + + tar_path = tmp_path / "bundle.tgz" + with tarfile.open(tar_path, "w:gz") as tf: + tf.add(bundle_dir, arcname="bundle") + + result = _bundle_from_input(str(tar_path), sops_mode=False) + assert result.dir.exists() + assert result.tempdir is not None + result.tempdir.cleanup() + + +def test_bundle_from_input_sops_mode_no_sops(monkeypatch, tmp_path: Path): + # Create a fake .sops file + sops_file = tmp_path / "harvest.sops" + sops_file.write_bytes(b"encrypted") + + def fake_require(): + raise SopsError("sops not found") + + import enroll.diff as d + + monkeypatch.setattr(d, "require_sops_cmd", fake_require) + + with pytest.raises(SopsError): + _bundle_from_input(str(sops_file), sops_mode=True) + + +def test_report_markdown_basic(): + report = { + "generated_at": "2024-01-01T00:00:00Z", + "old": {"input": "old.tar.gz", "host": "host1"}, + "new": {"input": "new.tar.gz", "host": "host2"}, + "packages": {"added": ["vim"], "removed": [], "version_changed": []}, + "services": {"enabled_added": [], "enabled_removed": [], "changed": []}, + "users": {"added": [], "removed": [], "changed": []}, + "files": {"added": [], "removed": [], "changed": []}, + } + result = _report_markdown(report) + assert "## Packages" in result + assert "+ vim" in result + + +def test_report_markdown_with_enforcement_applied(): + report = { + "generated_at": "2024-01-01T00:00:00Z", + "old": {"input": "old.tar.gz"}, + "new": {"input": "new.tar.gz"}, + "packages": {"added": [], "removed": [], "version_changed": []}, + "services": {"enabled_added": [], "enabled_removed": [], "changed": []}, + "users": {"added": [], "removed": [], "changed": []}, + "files": {"added": [], "removed": [], "changed": []}, + "enforcement": { + "status": "applied", + "tags": ["role_users"], + "returncode": 0, + "finished_at": "2024-01-01T00:01:00Z", + }, + } + result = _report_markdown(report) + assert "Applied old harvest" in result + assert "role_users" in result + + +def test_report_markdown_with_enforcement_failed(): + report = { + "generated_at": "2024-01-01T00:00:00Z", + "old": {"input": "old.tar.gz"}, + "new": {"input": "new.tar.gz"}, + "packages": {"added": [], "removed": [], "version_changed": []}, + "services": {"enabled_added": [], "enabled_removed": [], "changed": []}, + "users": {"added": [], "removed": [], "changed": []}, + "files": {"added": [], "removed": [], "changed": []}, + "enforcement": { + "status": "failed", + "returncode": 1, + }, + } + result = _report_markdown(report) + assert "ansible-playbook failed" in result + + +def test_report_markdown_with_enforcement_skipped(): + report = { + "generated_at": "2024-01-01T00:00:00Z", + "old": {"input": "old.tar.gz"}, + "new": {"input": "new.tar.gz"}, + "packages": {"added": [], "removed": [], "version_changed": []}, + "services": {"enabled_added": [], "enabled_removed": [], "changed": []}, + "users": {"added": [], "removed": [], "changed": []}, + "files": {"added": [], "removed": [], "changed": []}, + "enforcement": { + "status": "skipped", + "reason": "no drift", + }, + } + result = _report_markdown(report) + assert "Skipped" in result + assert "no drift" in result + + +def test_report_markdown_with_version_ignored(): + report = { + "generated_at": "2024-01-01T00:00:00Z", + "old": {"input": "old.tar.gz"}, + "new": {"input": "new.tar.gz"}, + "packages": { + "added": [], + "removed": [], + "version_changed": [{"package": "vim", "old": "1.0", "new": "2.0"}], + "version_changed_ignored_count": 1, + }, + "services": {"enabled_added": [], "enabled_removed": [], "changed": []}, + "users": {"added": [], "removed": [], "changed": []}, + "files": {"added": [], "removed": [], "changed": []}, + } + result = _report_markdown(report) + assert "ignored 1" in result + + +def test_report_markdown_with_service_package_changes(): + report = { + "generated_at": "2024-01-01T00:00:00Z", + "old": {"input": "old.tar.gz"}, + "new": {"input": "new.tar.gz"}, + "packages": {"added": [], "removed": [], "version_changed": []}, + "services": { + "enabled_added": [], + "enabled_removed": [], + "changed": [ + { + "unit": "nginx.service", + "changes": {"packages": {"added": ["nginx-extra"], "removed": []}}, + } + ], + }, + "users": {"added": [], "removed": [], "changed": []}, + "files": {"added": [], "removed": [], "changed": []}, + } + result = _report_markdown(report) + assert "packages added" in result + + +def test_report_markdown_empty(): + report = { + "generated_at": "2024-01-01T00:00:00Z", + "old": {"input": "old.tar.gz"}, + "new": {"input": "new.tar.gz"}, + "packages": {}, + "services": {}, + "users": {}, + "files": {}, + } + result = _report_markdown(report) + assert "## Packages" in result + assert "## Services" in result diff --git a/tests/test_harvest.py b/tests/test_harvest.py index 1b884aa..33b5302 100644 --- a/tests/test_harvest.py +++ b/tests/test_harvest.py @@ -1,4 +1,5 @@ import json +import enroll.harvest as harvest from pathlib import Path import enroll.harvest as h @@ -367,3 +368,149 @@ def test_shared_cron_snippet_prefers_matching_role_over_lexicographic( assert all( mf["path"] != "/etc/cron.d/ntpsec" for mf in svc_apparmor["managed_files"] ) + + +def test_files_differ_same_content(tmp_path: Path): + file1 = tmp_path / "file1.txt" + file2 = tmp_path / "file2.txt" + file1.write_text("same content", encoding="utf-8") + file2.write_text("same content", encoding="utf-8") + assert harvest._files_differ(str(file1), str(file2)) is False + + +def test_files_differ_different_content(tmp_path: Path): + file1 = tmp_path / "file1.txt" + file2 = tmp_path / "file2.txt" + file1.write_text("content1", encoding="utf-8") + file2.write_text("content2", encoding="utf-8") + assert harvest._files_differ(str(file1), str(file2)) is True + + +def test_files_differ_missing_file(tmp_path: Path): + file1 = tmp_path / "file1.txt" + file1.write_text("content", encoding="utf-8") + file2 = tmp_path / "file2.txt" + assert harvest._files_differ(str(file1), str(file2)) is True + + +def test_files_differ_both_missing(tmp_path: Path): + file1 = tmp_path / "file1.txt" + file2 = tmp_path / "file2.txt" + assert harvest._files_differ(str(file1), str(file2)) is True + + +def test_files_differ_binary(tmp_path: Path): + file1 = tmp_path / "file1.bin" + file2 = tmp_path / "file2.bin" + file1.write_bytes(b"\x00\x01\x02\x03") + file2.write_bytes(b"\x00\x01\x02\x03") + assert harvest._files_differ(str(file1), str(file2)) is False + + +def test_files_differ_binary_different(tmp_path: Path): + file1 = tmp_path / "file1.bin" + file2 = tmp_path / "file2.bin" + file1.write_bytes(b"\x00\x01\x02\x03") + file2.write_bytes(b"\x00\x01\x02\x04") + assert harvest._files_differ(str(file1), str(file2)) is True + + +def test_files_differ_non_regular_a(tmp_path: Path): + directory = tmp_path / "dir" + directory.mkdir() + file1 = tmp_path / "file1.txt" + file1.write_text("content", encoding="utf-8") + assert harvest._files_differ(str(directory), str(file1)) is True + + +def test_files_differ_non_regular_b(tmp_path: Path): + file1 = tmp_path / "file1.txt" + file1.write_text("content", encoding="utf-8") + directory = tmp_path / "dir" + directory.mkdir() + assert harvest._files_differ(str(file1), str(directory)) is True + + +def test_files_differ_size_mismatch(tmp_path: Path): + file1 = tmp_path / "file1.txt" + file1.write_text("short", encoding="utf-8") + file2 = tmp_path / "file2.txt" + file2.write_text("much longer content", encoding="utf-8") + assert harvest._files_differ(str(file1), str(file2)) is True + + +def test_files_differ_large_files(tmp_path: Path): + file1 = tmp_path / "file1.bin" + file2 = tmp_path / "file2.bin" + file1.write_bytes(b"x" * 3_000_000) + file2.write_bytes(b"x" * 3_000_000) + assert harvest._files_differ(str(file1), str(file2)) is True + + +def test_is_confish_with_conf(tmp_path: Path): + file1 = tmp_path / "test.conf" + file1.write_text("content", encoding="utf-8") + assert harvest._is_confish(str(file1)) is True + + +def test_is_confish_with_yaml(tmp_path: Path): + file1 = tmp_path / "test.yaml" + file1.write_text("content", encoding="utf-8") + assert harvest._is_confish(str(file1)) is True + + +def test_is_confish_with_json(tmp_path: Path): + file1 = tmp_path / "test.json" + file1.write_text("{}", encoding="utf-8") + assert harvest._is_confish(str(file1)) is True + + +def test_is_confish_with_service(tmp_path: Path): + file1 = tmp_path / "test.service" + file1.write_text("[Unit]", encoding="utf-8") + assert harvest._is_confish(str(file1)) is True + + +def test_is_confish_with_extensionless(tmp_path: Path): + file1 = tmp_path / "default" + file1.write_text("OPTIONS=", encoding="utf-8") + assert harvest._is_confish(str(file1)) is True + + +def test_is_confish_not_config(tmp_path: Path): + file1 = tmp_path / "test.log" + file1.write_text("log", encoding="utf-8") + assert harvest._is_confish(str(file1)) is False + + +def test_is_confish_nonexistent(): + assert harvest._is_confish("/nonexistent/file.xyz") is False + + +def test_topdirs_for_package_with_multiple_paths(): + pkg_to_etc_paths = { + "nginx": ["/etc/nginx/nginx.conf", "/etc/nginx/sites-enabled/default"], + } + result = harvest._topdirs_for_package("nginx", pkg_to_etc_paths) + assert result == {"nginx"} + + +def test_topdirs_for_package_with_multiple_topdirs(): + pkg_to_etc_paths = { + "multi": ["/etc/nginx/nginx.conf", "/etc/ssh/sshd_config"], + } + result = harvest._topdirs_for_package("multi", pkg_to_etc_paths) + assert result == {"nginx", "ssh"} + + +def test_topdirs_for_package_empty(): + result = harvest._topdirs_for_package("empty", {}) + assert result == set() + + +def test_topdirs_for_package_no_etc(): + pkg_to_etc_paths = { + "other": ["/usr/share/doc/file"], + } + result = harvest._topdirs_for_package("other", pkg_to_etc_paths) + assert result == set() diff --git a/tests/test_ignore.py b/tests/test_ignore.py index 1eaae01..2ba9a90 100644 --- a/tests/test_ignore.py +++ b/tests/test_ignore.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +import os +from pathlib import Path + from enroll.ignore import IgnorePolicy @@ -8,3 +13,238 @@ def test_ignore_policy_denies_common_backup_files(): assert pol.deny_reason("/etc/group-") == "backup_file" assert pol.deny_reason("/etc/something~") == "backup_file" assert pol.deny_reason("/foobar") == "unreadable" + + +def test_deny_reason_dir_with_denied_path(): + pol = IgnorePolicy() + assert pol.deny_reason_dir("/etc/ssl/private/key") == "denied_path" + assert pol.deny_reason_dir("/etc/ssh/ssh_host_key") == "denied_path" + assert pol.deny_reason_dir("/etc/ssh") is None + + +def test_deny_reason_dir_unreadable(tmp_path: Path): + pol = IgnorePolicy() + nonexistent = tmp_path / "nonexistent" + assert pol.deny_reason_dir(str(nonexistent)) == "unreadable" + + +def test_deny_reason_dir_symlink(tmp_path: Path): + pol = IgnorePolicy() + real_dir = tmp_path / "real" + real_dir.mkdir() + link = tmp_path / "link" + os.symlink(str(real_dir), str(link)) + assert pol.deny_reason_dir(str(link)) == "symlink" + + +def test_deny_reason_dir_not_directory(tmp_path: Path): + pol = IgnorePolicy() + regular_file = tmp_path / "file.txt" + regular_file.write_text("content", encoding="utf-8") + assert pol.deny_reason_dir(str(regular_file)) == "not_directory" + + +def test_deny_reason_dir_dangerous_mode(tmp_path: Path): + pol = IgnorePolicy(dangerous=True) + real_dir = tmp_path / "private" + real_dir.mkdir() + assert pol.deny_reason_dir(str(real_dir)) is None + + +def test_deny_reason_link_basic(tmp_path: Path): + pol = IgnorePolicy() + real_file = tmp_path / "real" + real_file.write_text("content", encoding="utf-8") + link = tmp_path / "link" + os.symlink(str(real_file), str(link)) + assert pol.deny_reason_link(str(link)) is None + + +def test_deny_reason_link_denied_path(): + pol = IgnorePolicy() + assert pol.deny_reason_link("/etc/ssh/ssh_host_rsa_key") == "denied_path" + + +def test_deny_reason_link_unreadable(tmp_path: Path): + pol = IgnorePolicy() + # Create a symlink in a directory that doesn't exist + # This simulates an unreadable path + broken_link = tmp_path / "broken_link" + os.symlink("/nonexistent/target", str(broken_link)) + # Broken symlinks are still readable (we can readlink them) + # So they return None (allowed) unless they match deny globs + result = pol.deny_reason_link(str(broken_link)) + # Broken symlinks are allowed - we can still read the link target + assert result is None + + +def test_deny_reason_link_not_symlink(tmp_path: Path): + pol = IgnorePolicy() + regular_file = tmp_path / "file.txt" + regular_file.write_text("content", encoding="utf-8") + assert pol.deny_reason_link(str(regular_file)) == "not_symlink" + + +def test_deny_reason_link_log_file(): + pol = IgnorePolicy() + assert pol.deny_reason_link("/var/log/something.log") == "log_file" + + +def test_deny_reason_link_backup_file(): + pol = IgnorePolicy() + assert pol.deny_reason_link("/etc/passwd-") == "backup_file" + assert pol.deny_reason_link("/etc/something~") == "backup_file" + + +def test_deny_reason_link_dangerous_mode(tmp_path: Path): + pol = IgnorePolicy(dangerous=True) + real_file = tmp_path / "real" + real_file.write_text("content", encoding="utf-8") + link = tmp_path / "link" + os.symlink(str(real_file), str(link)) + assert pol.deny_reason_link(str(link)) is None + + +def test_iter_effective_lines_with_comments(): + pol = IgnorePolicy() + content = b""" +# This is a comment +; This is also a comment +* continuation +def main(): + pass +""" + lines = list(pol.iter_effective_lines(content)) + assert b"def main():" in lines + assert b"# This is a comment" not in lines + + +def test_iter_effective_lines_with_block_comments(): + pol = IgnorePolicy() + content = b""" +/* This is a block comment + spanning multiple lines */ +int x = 5; +""" + lines = list(pol.iter_effective_lines(content)) + assert b"int x = 5;" in lines + assert b"/*" not in lines + + +def test_iter_effective_lines_empty(): + pol = IgnorePolicy() + content = b"" + lines = list(pol.iter_effective_lines(content)) + assert lines == [] + + +def test_deny_reason_binary_not_allowed(tmp_path: Path): + pol = IgnorePolicy() + binary = tmp_path / "random.bin" + binary.write_bytes(b"\x00\x01\x02\x03") + reason = pol.deny_reason(str(binary)) + assert reason == "binary_like" + + +def test_deny_reason_sensitive_content(tmp_path: Path): + pol = IgnorePolicy() + config = tmp_path / "config.txt" + config.write_text("password=secret123", encoding="utf-8") + reason = pol.deny_reason(str(config)) + assert reason == "sensitive_content" + + +def test_deny_reason_sensitive_api_key(tmp_path: Path): + pol = IgnorePolicy() + config = tmp_path / "config.txt" + config.write_text("api_key=abc123", encoding="utf-8") + reason = pol.deny_reason(str(config)) + assert reason == "sensitive_content" + + +def test_deny_reason_private_key(tmp_path: Path): + pol = IgnorePolicy() + key = tmp_path / "key.pem" + key.write_text( + "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA...", encoding="utf-8" + ) + reason = pol.deny_reason(str(key)) + assert reason == "sensitive_content" + + +def test_deny_reason_too_large(tmp_path: Path): + pol = IgnorePolicy(max_file_bytes=100) + large = tmp_path / "large.txt" + large.write_bytes(b"x" * 200) + reason = pol.deny_reason(str(large)) + assert reason == "too_large" + + +def test_deny_reason_unreadable(tmp_path: Path): + pol = IgnorePolicy() + nonexistent = tmp_path / "nonexistent" + reason = pol.deny_reason(str(nonexistent)) + assert reason == "unreadable" + + +def test_deny_reason_not_regular_file(tmp_path: Path): + pol = IgnorePolicy() + directory = tmp_path / "dir" + directory.mkdir() + reason = pol.deny_reason(str(directory)) + assert reason == "not_regular_file" + + +def test_deny_reason_symlink_file(tmp_path: Path): + pol = IgnorePolicy() + real_file = tmp_path / "real" + real_file.write_text("content", encoding="utf-8") + link = tmp_path / "link" + os.symlink(str(real_file), str(link)) + reason = pol.deny_reason(str(link)) + assert reason == "not_regular_file" + + +def test_deny_reason_logs(tmp_path: Path): + pol = IgnorePolicy() + log = tmp_path / "test.log" + log.write_text("log content", encoding="utf-8") + assert pol.deny_reason(str(log)) == "log_file" + + +def test_deny_reason_backup_file(tmp_path: Path): + pol = IgnorePolicy() + backup = tmp_path / "file~" + backup.write_text("backup", encoding="utf-8") + assert pol.deny_reason(str(backup)) == "backup_file" + + +def test_deny_reason_shadow_file(): + pol = IgnorePolicy() + assert pol.deny_reason("/etc/shadow") == "denied_path" + assert pol.deny_reason("/etc/gshadow") == "denied_path" + + +def test_deny_reason_ssl_private(): + pol = IgnorePolicy() + assert pol.deny_reason("/etc/ssl/private/key.pem") == "denied_path" + + +def test_deny_reason_ssh_host_keys(): + pol = IgnorePolicy() + assert pol.deny_reason("/etc/ssh/ssh_host_rsa_key") == "denied_path" + assert pol.deny_reason("/etc/ssh/ssh_host_ed25519_key") == "denied_path" + + +def test_deny_reason_letsencrypt(): + pol = IgnorePolicy() + assert ( + pol.deny_reason("/etc/letsencrypt/live/example.com/fullchain.pem") + == "denied_path" + ) + + +def test_deny_reason_shadow_backup(): + pol = IgnorePolicy() + assert pol.deny_reason("/etc/shadow-") == "backup_file" + assert pol.deny_reason("/etc/passwd-") == "backup_file" diff --git a/tests/test_manifest.py b/tests/test_manifest.py index 658d77f..1b78bcf 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -892,3 +892,175 @@ def test_manifest_writes_firewall_runtime_role(tmp_path: Path): assert ( out / "roles" / "firewall_runtime" / "files" / "firewall" / "ipset.save" ).exists() + + +def test_try_yaml_with_yaml_installed(): + result = manifest._try_yaml() + # PyYAML should be installed for tests + if result is None: + pytest.skip("PyYAML not installed") + assert hasattr(result, "safe_load") + assert hasattr(result, "dump") + + +def test_yaml_load_mapping_with_yaml(tmp_path: Path): + text = """ +key1: value1 +key2: + nested: value +list: + - item1 + - item2 +""" + result = manifest._yaml_load_mapping(text) + assert result["key1"] == "value1" + assert result["key2"]["nested"] == "value" + assert result["list"] == ["item1", "item2"] + + +def test_yaml_load_mapping_empty(): + result = manifest._yaml_load_mapping("") + assert result == {} + + +def test_yaml_load_mapping_invalid(): + result = manifest._yaml_load_mapping("invalid: yaml: :") + assert result == {} + + +def test_yaml_load_mapping_not_dict(): + result = manifest._yaml_load_mapping("- item1\n- item2") + assert result == {} + + +def test_yaml_load_mapping_none(): + result = manifest._yaml_load_mapping("~") + assert result == {} + + +def test_yaml_dump_mapping_with_yaml(tmp_path: Path): + obj = {"key1": "value1", "key2": 123} + result = manifest._yaml_dump_mapping(obj) + assert "key1: value1" in result + assert "key2:" in result + + +def test_yaml_dump_mapping_empty(): + result = manifest._yaml_dump_mapping({}) + # Empty dict produces '{}' + assert result.strip() == "{}" + + +def test_yaml_dump_mapping_with_nested(tmp_path: Path): + obj = {"key1": {"nested": "value"}} + result = manifest._yaml_dump_mapping(obj) + assert "nested:" in result + + +def test_merge_mappings_overwrite_simple(): + existing = {"key1": "old", "key2": "keep"} + incoming = {"key1": "new", "key3": "added"} + result = manifest._merge_mappings_overwrite(existing, incoming) + assert result["key1"] == "new" + assert result["key2"] == "keep" + assert result["key3"] == "added" + + +def test_merge_mappings_overwrite_nested(): + existing = {"key1": {"a": 1}} + incoming = {"key1": {"b": 2}} + result = manifest._merge_mappings_overwrite(existing, incoming) + # Nested dicts are replaced, not merged + assert result["key1"] == {"b": 2} + + +def test_merge_mappings_overwrite_empty(): + result = manifest._merge_mappings_overwrite({}, {"key": "value"}) + assert result == {"key": "value"} + + result = manifest._merge_mappings_overwrite({"key": "value"}, {}) + assert result == {"key": "value"} + + +def test_copy2_replace(tmp_path: Path): + src = tmp_path / "src.txt" + src.write_text("content", encoding="utf-8") + dst = tmp_path / "dst" / "subdir" / "dst.txt" + + manifest._copy2_replace(str(src), str(dst)) + + assert dst.exists() + assert dst.read_text(encoding="utf-8") == "content" + + +def test_copy2_replace_preserves_metadata(tmp_path: Path): + src = tmp_path / "src.txt" + src.write_text("content", encoding="utf-8") + os.chmod(str(src), 0o644) + dst = tmp_path / "dst.txt" + + manifest._copy2_replace(str(src), str(dst)) + + assert dst.exists() + st = dst.stat() + assert stat.S_IMODE(st.st_mode) == 0o644 + + +def test_copy2_replace_atomic(tmp_path: Path): + src = tmp_path / "src.txt" + src.write_text("content", encoding="utf-8") + dst = tmp_path / "dst.txt" + + # Write initial content + dst.write_text("old", encoding="utf-8") + + manifest._copy2_replace(str(src), str(dst)) + + assert dst.read_text(encoding="utf-8") == "content" + + +def test_render_firewall_runtime_tasks_empty(): + state = {"roles": {}} + result = manifest._render_firewall_runtime_tasks(state) + # Function always returns at least a basic playbook structure + assert isinstance(result, str) + assert len(result) > 0 + + +def test_render_firewall_runtime_tasks_with_iptables(): + state = { + "roles": { + "firewall_runtime": { + "role_name": "firewall_runtime", + "iptables_v4_save": "artifacts/firewall_runtime/iptables.save", + } + } + } + result = manifest._render_firewall_runtime_tasks(state) + assert len(result) >= 1 + + +def test_render_firewall_runtime_tasks_with_ipset(): + state = { + "roles": { + "firewall_runtime": { + "role_name": "firewall_runtime", + "ipset_save": "artifacts/firewall_runtime/ipset.save", + } + } + } + result = manifest._render_firewall_runtime_tasks(state) + assert len(result) >= 1 + + +def test_render_firewall_runtime_tasks_with_ipv6(): + state = { + "roles": { + "firewall_runtime": { + "role_name": "firewall_runtime", + "iptables_v6_save": "artifacts/firewall_runtime/ip6tables.save", + } + } + } + result = manifest._render_firewall_runtime_tasks(state) + assert len(result) >= 1 diff --git a/tests/test_misc_coverage.py b/tests/test_misc_coverage.py deleted file mode 100644 index 1ff6e98..0000000 --- a/tests/test_misc_coverage.py +++ /dev/null @@ -1,416 +0,0 @@ -from __future__ import annotations - -import json -import os -import stat -import subprocess -import sys -import types -from pathlib import Path -from types import SimpleNamespace - -import pytest - -from enroll.cache import _safe_component, new_harvest_cache_dir -from enroll.ignore import IgnorePolicy -from enroll.sopsutil import ( - SopsError, - _pgp_arg, - decrypt_file_binary_to, - encrypt_file_binary, -) - - -def test_safe_component_sanitizes_and_bounds_length(): - assert _safe_component(" ") == "unknown" - assert _safe_component("a/b c") == "a_b_c" - assert _safe_component("x" * 200) == "x" * 64 - - -def test_new_harvest_cache_dir_uses_xdg_cache_home(tmp_path: Path, monkeypatch): - monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "xdg")) - hc = new_harvest_cache_dir(hint="my host/01") - assert hc.dir.exists() - assert "my_host_01" in hc.dir.name - assert str(hc.dir).startswith(str(tmp_path / "xdg")) - # best-effort: ensure directory is not world-readable on typical FS - try: - mode = stat.S_IMODE(hc.dir.stat().st_mode) - assert mode & 0o077 == 0 - except OSError: - pass - - -def test_ignore_policy_denies_binary_and_sensitive_content(tmp_path: Path): - p_bin = tmp_path / "binfile" - p_bin.write_bytes(b"abc\x00def") - assert IgnorePolicy().deny_reason(str(p_bin)) == "binary_like" - - p_secret = tmp_path / "secret.conf" - p_secret.write_text("password=foo\n", encoding="utf-8") - assert IgnorePolicy().deny_reason(str(p_secret)) == "sensitive_content" - - # dangerous mode disables heuristic scanning (but still checks file-ness/size) - assert IgnorePolicy(dangerous=True).deny_reason(str(p_secret)) is None - - -def test_ignore_policy_denies_usr_local_shadow_by_glob(): - # This should short-circuit before stat() (path doesn't need to exist). - assert IgnorePolicy().deny_reason("/usr/local/etc/shadow") == "denied_path" - - -def test_sops_pgp_arg_and_encrypt_decrypt_roundtrip(tmp_path: Path, monkeypatch): - assert _pgp_arg([" ABC ", "DEF"]) == "ABC,DEF" - with pytest.raises(SopsError): - _pgp_arg([]) - - # Stub out sops and subprocess. - import enroll.sopsutil as s - - monkeypatch.setattr(s, "require_sops_cmd", lambda: "sops") - - class R: - def __init__(self, rc: int, out: bytes, err: bytes = b""): - self.returncode = rc - self.stdout = out - self.stderr = err - - calls = [] - - def fake_run(cmd, capture_output, check): - calls.append(cmd) - # Return a deterministic payload so we can assert file writes. - if "--encrypt" in cmd: - return R(0, b"ENCRYPTED") - if "--decrypt" in cmd: - return R(0, b"PLAINTEXT") - return R(1, b"", b"bad") - - monkeypatch.setattr(s.subprocess, "run", fake_run) - - src = tmp_path / "src.bin" - src.write_bytes(b"x") - enc = tmp_path / "out.sops" - dec = tmp_path / "out.bin" - - encrypt_file_binary(src, enc, pgp_fingerprints=["ABC"], mode=0o600) - assert enc.read_bytes() == b"ENCRYPTED" - - decrypt_file_binary_to(enc, dec, mode=0o644) - assert dec.read_bytes() == b"PLAINTEXT" - - # Sanity: we invoked encrypt and decrypt. - assert any("--encrypt" in c for c in calls) - assert any("--decrypt" in c for c in calls) - - -def test_cache_dir_defaults_to_home_cache(monkeypatch, tmp_path: Path): - # Ensure default path uses ~/.cache when XDG_CACHE_HOME is unset. - from enroll.cache import enroll_cache_dir - - monkeypatch.delenv("XDG_CACHE_HOME", raising=False) - monkeypatch.setattr(Path, "home", lambda: tmp_path) - - p = enroll_cache_dir() - assert str(p).startswith(str(tmp_path)) - assert p.name == "enroll" - - -def test_harvest_cache_state_json_property(tmp_path: Path): - from enroll.cache import HarvestCache - - hc = HarvestCache(tmp_path / "h1") - assert hc.state_json == hc.dir / "state.json" - - -def test_cache_dir_security_rejects_symlink(tmp_path: Path): - from enroll.cache import _ensure_dir_secure - - real = tmp_path / "real" - real.mkdir() - link = tmp_path / "link" - link.symlink_to(real, target_is_directory=True) - - with pytest.raises(RuntimeError, match="Refusing to use symlink"): - _ensure_dir_secure(link) - - -def test_cache_dir_chmod_failures_are_ignored(monkeypatch, tmp_path: Path): - from enroll import cache - - # Make the cache base path deterministic and writable. - monkeypatch.setattr(cache, "enroll_cache_dir", lambda: tmp_path) - - # Force os.chmod to fail to cover the "except OSError: pass" paths. - monkeypatch.setattr( - os, "chmod", lambda *a, **k: (_ for _ in ()).throw(OSError("nope")) - ) - - hc = cache.new_harvest_cache_dir() - assert hc.dir.exists() - assert hc.dir.is_dir() - - -def test_stat_triplet_falls_back_to_numeric_ids(monkeypatch, tmp_path: Path): - from enroll.fsutil import stat_triplet - import pwd - import grp - - p = tmp_path / "x" - p.write_text("x", encoding="utf-8") - - # Force username/group resolution failures. - monkeypatch.setattr( - pwd, "getpwuid", lambda _uid: (_ for _ in ()).throw(KeyError("no user")) - ) - monkeypatch.setattr( - grp, "getgrgid", lambda _gid: (_ for _ in ()).throw(KeyError("no group")) - ) - - owner, group, mode = stat_triplet(str(p)) - assert owner.isdigit() - assert group.isdigit() - assert len(mode) == 4 - - -def test_ignore_policy_iter_effective_lines_removes_block_comments(): - from enroll.ignore import IgnorePolicy - - pol = IgnorePolicy() - data = b"""keep1 -/* -drop me -*/ -keep2 -""" - assert list(pol.iter_effective_lines(data)) == [b"keep1", b"keep2"] - - -def test_ignore_policy_deny_reason_dir_variants(tmp_path: Path): - from enroll.ignore import IgnorePolicy - - pol = IgnorePolicy() - - # denied by glob - assert pol.deny_reason_dir("/etc/shadow") == "denied_path" - - # symlink rejected - d = tmp_path / "d" - d.mkdir() - link = tmp_path / "l" - link.symlink_to(d, target_is_directory=True) - assert pol.deny_reason_dir(str(link)) == "symlink" - - # not a directory - f = tmp_path / "f" - f.write_text("x", encoding="utf-8") - assert pol.deny_reason_dir(str(f)) == "not_directory" - - # ok - assert pol.deny_reason_dir(str(d)) is None - - -def test_run_jinjaturtle_parses_outputs(monkeypatch, tmp_path: Path): - # Fully unit-test enroll.jinjaturtle.run_jinjaturtle by stubbing subprocess.run. - from enroll.jinjaturtle import run_jinjaturtle - - def fake_run(cmd, **kwargs): # noqa: ARG001 - # cmd includes "-d -t