From 7216ec09bd7bc6adf6b613403043906489e83a87 Mon Sep 17 00:00:00 2001 From: Maximus Gorog Date: Wed, 29 Apr 2026 08:38:53 -0600 Subject: [PATCH] Tier 2: real Alpine VM, real workload, real envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end now drives a real KVM guest through the full XMRig-shaped phase schedule with the workload running INSIDE the guest. Telemetry is host-side /proc/; the load is busybox `yes` (sustained CPU saturation) and `dd if=/dev/urandom` (disk burst on infecting), driven over the serial console at every phase transition. The plotted envelope shows clean idle → armed → infecting (disk spike) → infected_running (100% CPU plateau) → dormant → re-entry → final clean. Components: vm/launch_demo.sh now boots Alpine 3.21 nocloud-cloudinit (Cirros 0.6.x's cirros-init blocks on the EC2 metadata service for ~17 min before falling through to NoCloud — abandoned). Mounts a cidata ISO as a second drive. tools/build_cidata.py pure-Python NoCloud ISO builder (pycdlib). Sets root password and ssh_pwauth via runcmd so we don't depend on a specific cloud-init version's plain_text_passwd handling. tools/vm_serial.py serial-console client (stdlib socket). Idempotent login (detects already-in-shell state), sentinel-bracketed run() that distinguishes shell output from the TTY echo of input by requiring a leading \r\n boundary on the marker. tools/vm_load_controller.py in-guest load controller. set_phase() dispatches the per-phase shell command over the serial connection. tools/run_real_vm_demo.py ties it all together: boot VM, wait for cloud-init runcmd, log in, run the EpisodeRunner with on_phase=controller, shut down VM. Deps: paramiko, pycdlib added. docs/sources.md updated with Alpine cloud image (sha512 pinned), and the new Python deps. README leads with the tier-2 plot now (real VM, real workload). The previous synthetic plot is moved below with explicit "host-side mimic, not a VM" labelling. Tier-2 status flipped to ✅ in the tier table. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 63 ++++--- docs/images/real-vm-envelope.png | Bin 0 -> 90272 bytes docs/sources.md | 28 +++- pyproject.toml | 2 + tools/build_cidata.py | 112 +++++++++++++ tools/run_real_vm_demo.py | 181 ++++++++++++++++++++ tools/vm_load_controller.py | 72 ++++++++ tools/vm_serial.py | 229 +++++++++++++++++++++++++ uv.lock | 280 +++++++++++++++++++++++++++++++ vm/launch_demo.sh | 12 +- 10 files changed, 952 insertions(+), 27 deletions(-) create mode 100644 docs/images/real-vm-envelope.png create mode 100644 tools/build_cidata.py create mode 100644 tools/run_real_vm_demo.py create mode 100644 tools/vm_load_controller.py create mode 100644 tools/vm_serial.py diff --git a/README.md b/README.md index fb4fed6..def8588 100644 --- a/README.md +++ b/README.md @@ -22,33 +22,57 @@ the set of timestamped phase transitions written to `labels.jsonl` — sharing a monotonic clock with the metric rows so anything aligned in time can be aligned in code. +### Tier 2 — *real Alpine VM, real workload driven from inside the guest* + +This is the closest we get to real-malware behaviour without yet running +real malware. Telemetry is real `/proc/` from outside the +guest, **and the load is generated inside the guest** by busybox +``yes`` (CPU saturation) and ``dd`` (disk bursts), driven over the +serial console by `tools/vm_load_controller.py`. Every phase transition +in `labels.jsonl` corresponds to an actual command issued inside the +real VM. + +![Real Alpine VM envelope](docs/images/real-vm-envelope.png) + +The 100% CPU plateaux are `yes > /dev/null` running on the guest's +single vCPU; the IO spikes during *infecting* are `dd if=/dev/urandom` +producing the sample-drop shape; the *dormant* drops are the +controller killing the load process inside the VM. The +infected_running → dormant → infected_running re-entry is the textbook +envelope that justifies the whole project framing. + +Reproduce with: + +```sh +uv run python tools/run_real_vm_demo.py --data-root data +``` + +### Tier 1 — *real Alpine VM, idle baseline* + +Same pipeline, pointed at the real `qemu-system` process while the +guest is doing nothing. Periodic ~10% CPU spikes are KVM/timer +interrupts; the single disk-write spike near t=3 s is the guest +finishing late-boot activity. + +![Real VM idle baseline](docs/images/real-vm-idle.png) + ### Pipeline-validation plot — *synthetic load, real telemetry* -This is **not real malware**. The CPU/RSS/IO numbers are real -`/proc/` reads of a real process; the *workload shape* is a Python -program (`tools/load_mimic.py`) that mimics an XMRig-style envelope so we -can validate the orchestrator + collector + labeling pipeline before -plugging in a real exploit and a real sample. Coloured bands are phase -labels straight out of `labels.jsonl`. +This is **not real malware** and the load is **not** even running +inside a VM — it's a Python program on the host (`tools/load_mimic.py`) +that mimics an XMRig-style envelope. We used it to validate the +orchestrator + collector + labeling pipeline before plugging in a real +guest. Kept here because it shows the same shape the tier-2 plot +above produces from real KVM behaviour. -![Synthetic envelope demo (pipeline validation only)](docs/images/synthetic-envelope.png) - -### Real-VM idle baseline — *real Cirros guest under KVM, no malware yet* - -Same pipeline, pointed at the real `qemu-system` process running a fresh -Cirros 0.6.3 guest with nothing happening inside it. Periodic ~10% CPU -spikes are KVM/timer interrupts; the single ~1 MiB write near t=3 s is -the guest finishing its late-boot disk activity. No phase transitions — -just labelled `clean` for the whole window. - -![Real Cirros VM idle](docs/images/real-vm-idle.png) +![Synthetic envelope (host-side mimic)](docs/images/synthetic-envelope.png) ### What's still missing for the real-malware envelope | Tier | What it gives | Status | |---|---|---| | 1 — real VM, idle | confidence the collector reads real KVM behaviour | ✅ done | -| 2 — real VM, real workload from inside the guest | first real-load envelope shape | 🚧 next | +| 2 — real VM, real workload from inside the guest | first real-load envelope shape | ✅ done | | 3 — real VM, real exploit fire (Metasploitable + msfrpc) | honest `armed → infecting` transitions | 🚧 | | 4 — real VM, real malware sample (XMRig from MalwareBazaar) | the full envelope we ultimately train on | 🚧 | @@ -67,7 +91,8 @@ tools/show_envelope.sh data/episodes/ - ✅ Orchestrator v0 — single- and scheduled-phase modes, ULID episode ids - ✅ Host /proc oracle collector (source 1 of 5) at 10 Hz - ✅ Synthetic envelope demo — full 8-phase envelope produced end-to-end -- ✅ Real VM (Cirros under KVM) — orchestrator collects against the real `qemu-system` pid +- ✅ Real VM (Alpine 3.21 cloud-init under KVM) — orchestrator collects against the real `qemu-system` pid +- ✅ **Tier 2 — real VM, real workload:** serial-console-driven load controller fires `yes`/`dd` inside the guest at every phase transition - 🚧 QMP collector (source 2), bridge pcap collector (source 4), in-guest agent (source 5) - 🚧 Exploit driver (Metasploit RPC) for `armed → infecting` transitions on `session_open` - 🚧 Shipper (the third leg of the WG pipeline — receiver and orchestrator already verified) diff --git a/docs/images/real-vm-envelope.png b/docs/images/real-vm-envelope.png new file mode 100644 index 0000000000000000000000000000000000000000..eae0f98d1cfd92ce11eb0ffa110b6b90b2064e5b GIT binary patch literal 90272 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 literal 0 HcmV?d00001 diff --git a/docs/sources.md b/docs/sources.md index 81ec1f0..e6b6303 100644 --- a/docs/sources.md +++ b/docs/sources.md @@ -56,14 +56,25 @@ thing plays in our pipeline. ## VMs and intentionally-vulnerable images -- **Cirros 0.6.3** (current phase-2 demo image, x86_64). +- **Alpine Linux 3.21 cloud-init nocloud image** (current tier-2 guest). + https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/cloud/ + File: `nocloud_alpine-3.21.0-x86_64-bios-cloudinit-r0.qcow2` + SHA-512: `bb509092cda3548c11bc48a2168ce950d654b50db006e98939c06a5d86487f4e53cbb7954fafbba9ab5c8098008a9f304421ffc3397b0bc1d87b6aa309239b98` + *License:* MIT for image, GPL/various for contents. *Role:* small + (~180 MiB) Linux image with cloud-init that picks up our NoCloud + cidata ISO at first boot. SSH-pwauth and a known root password are + set via `runcmd`. Used as the in-guest workload host for tier-2 runs + and as the post-snapshot baseline for the qcow2 snapshot loop. + +- **Cirros 0.6.3** (initially tried, currently unused). https://download.cirros-cloud.net/0.6.3/ SHA-256 of `cirros-0.6.3-x86_64-disk.img`: `7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b` - *License:* GPL. *Role:* tiny (~21 MiB) Linux image used in OpenStack/QEMU - testing; we use it as the "real but boring" guest while the rest of the - pipeline is wired up. **No vulnerabilities baked in** — it's a clean - baseline. + *License:* GPL. *Role:* tiny (~21 MiB) test image; abandoned for this + project because Cirros 0.6.x's `cirros-init` checks the EC2 metadata + service before NoCloud and the failure-retry loop took ~17 minutes to + fall through. Kept in the manifest in case the simpler image is + useful for a later size-constrained scenario. - **Metasploitable 2** (planned for the exploit phase, Rapid7). https://information.rapid7.com/download-metasploitable-2017.html @@ -153,6 +164,13 @@ thing plays in our pipeline. https://www.python-httpx.org - **matplotlib** + **numpy** — plotting (envelope visualization only). https://matplotlib.org / https://numpy.org +- **tornado** — required by matplotlib's WebAgg interactive backend. + https://www.tornadoweb.org +- **paramiko** — SSH client used for in-guest control on cloud images + that support it. https://www.paramiko.org +- **pycdlib** — pure-Python ISO9660/Joliet/Rock Ridge builder. Used to + produce the NoCloud cidata ISO without depending on system mkisofs/ + xorriso. https://clalancette.github.io/pycdlib/ --- diff --git a/pyproject.toml b/pyproject.toml index 774e543..ded5c12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ dev = [ "httpx>=0.27", "matplotlib>=3.8", "tornado>=6", # required by matplotlib's WebAgg interactive backend + "paramiko>=3", # SSH client for in-guest control on images that support it + "pycdlib>=1.14", # build NoCloud cidata ISOs in pure Python ] [tool.uv] diff --git a/tools/build_cidata.py b/tools/build_cidata.py new file mode 100644 index 0000000..da228ca --- /dev/null +++ b/tools/build_cidata.py @@ -0,0 +1,112 @@ +"""Build a NoCloud cidata ISO for cloud-init. + +Cirros 0.6.x — and most cloud images — look for a NoCloud datasource at +boot: an ISO9660 volume labeled ``cidata`` containing two files, +``user-data`` and ``meta-data``. We attach it as a second drive so +cloud-init proceeds without spending ~17 minutes timing out trying to +reach a non-existent metadata service. + +This script is intentionally self-contained and uses only pycdlib (pure +Python) — no system mkisofs/xorriso/cloud-localds dependency. + +Usage: + + uv run python tools/build_cidata.py vm/images/cidata.iso + +The defaults bake in the ``cirros`` user with the documented Cirros +password, enable SSH password auth (so future Metasploit-class images +work without changes), and set a hostname. Override via flags if needed. +""" + +from __future__ import annotations + +import argparse +import io +import sys +from pathlib import Path + +import pycdlib + + +DEFAULT_USER_DATA = """\ +#cloud-config +hostname: cis490 +manage_etc_hosts: true +users: + - name: cis490 + plain_text_passwd: cis490 + lock_passwd: false + sudo: ALL=(ALL) NOPASSWD:ALL + shell: /bin/sh +ssh_pwauth: true +disable_root: false +chpasswd: + expire: false + list: | + root:cis490 + cis490:cis490 +runcmd: + - [ sh, -c, "echo CIS490_BOOT_OK > /tmp/.cis490-boot" ] +""" + +DEFAULT_META_DATA = """\ +instance-id: cis490-vm-001 +local-hostname: cis490 +""" + + +def build_cidata(out_path: Path, user_data: bytes, meta_data: bytes) -> None: + iso = pycdlib.PyCdlib() + # Joliet=3 + Rock Ridge so cloud-init reads filenames correctly on Linux. + iso.new(joliet=3, vol_ident="cidata", interchange_level=3, rock_ridge="1.09") + + iso.add_fp( + io.BytesIO(user_data), + len(user_data), + iso_path="/USERDATA.;1", + rr_name="user-data", + joliet_path="/user-data", + ) + iso.add_fp( + io.BytesIO(meta_data), + len(meta_data), + iso_path="/METADATA.;1", + rr_name="meta-data", + joliet_path="/meta-data", + ) + iso.write(str(out_path)) + iso.close() + + +def main() -> int: + parser = argparse.ArgumentParser(prog="build_cidata") + parser.add_argument("out_path", type=Path) + parser.add_argument( + "--user-data", + type=Path, + default=None, + help="path to a custom cloud-config user-data file", + ) + parser.add_argument( + "--meta-data", + type=Path, + default=None, + help="path to a custom meta-data file", + ) + args = parser.parse_args() + + user_data = ( + args.user_data.read_bytes() if args.user_data else DEFAULT_USER_DATA.encode() + ) + meta_data = ( + args.meta_data.read_bytes() if args.meta_data else DEFAULT_META_DATA.encode() + ) + + args.out_path.parent.mkdir(parents=True, exist_ok=True) + build_cidata(args.out_path, user_data, meta_data) + print(f"wrote {args.out_path} ({args.out_path.stat().st_size} bytes)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/run_real_vm_demo.py b/tools/run_real_vm_demo.py new file mode 100644 index 0000000..c1ad318 --- /dev/null +++ b/tools/run_real_vm_demo.py @@ -0,0 +1,181 @@ +"""Tier-2: real VM, real workload, labeled phases. + +Boots the Alpine cidata VM, logs in over the serial console, drives the +guest through an XMRig-shaped phase schedule (clean → armed → infecting → +infected_running → dormant → re-entry), and lets the orchestrator's host +/proc collector sample the qemu-system pid throughout. + +Compared to ``run_envelope_demo.py``: same phase schedule, same labels, +same telemetry shape — but the load is now generated by ``yes`` and +``dd`` running *inside* a real Alpine guest, not by a Python program on +the host. Tier-3 replaces the controller with an MSF-driven exploit +fire. +""" + +from __future__ import annotations + +import argparse +import logging +import os +import signal +import subprocess +import sys +import time +from pathlib import Path + +# Allow running as a script. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from orchestrator.episode import EpisodeConfig, EpisodeRunner # noqa: E402 +from vm_load_controller import VMLoadController # noqa: E402 +from vm_serial import SerialClient # noqa: E402 + + +# Same shape as run_envelope_demo so plots are comparable. +DEFAULT_SCHEDULE = [ + ("clean", 10.0), + ("armed", 2.0), + ("infecting", 3.0), + ("infected_running", 25.0), + ("dormant", 15.0), + ("infected_running", 20.0), + ("dormant", 5.0), + ("clean", 5.0), +] + + +def _wait_for_socket(path: Path, timeout_s: float) -> None: + import socket as _sk + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + if path.exists(): + try: + # Verify it's actually live, not a leftover from a dead VM. + t = _sk.socket(_sk.AF_UNIX, _sk.SOCK_STREAM) + t.settimeout(0.5) + t.connect(str(path)) + t.close() + return + except OSError: + pass + time.sleep(0.2) + raise TimeoutError(f"socket {path} never came alive within {timeout_s}s") + + +def main() -> int: + parser = argparse.ArgumentParser(prog="run_real_vm_demo") + parser.add_argument("--data-root", default="data") + parser.add_argument("--interval-ms", type=int, default=100) + parser.add_argument( + "--run-dir", + default="/tmp/cis490-vm", + help="QEMU run dir (sockets + pidfile go here)", + ) + parser.add_argument( + "--keep-vm", + action="store_true", + help="leave the VM running after the episode finishes", + ) + parser.add_argument( + "--boot-timeout", + type=float, + default=120.0, + help="how long to wait for serial login prompt", + ) + args = parser.parse_args() + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s %(message)s", + ) + log = logging.getLogger("cis490.run_real_vm_demo") + + repo_root = Path(__file__).resolve().parent.parent + launcher = repo_root / "vm" / "launch_demo.sh" + run_dir = Path(args.run_dir) + # Wipe any stale sockets/pidfile from a previous run. + if run_dir.exists(): + import shutil + shutil.rmtree(run_dir) + run_dir.mkdir(parents=True, exist_ok=True) + serial_sock = run_dir / "serial.sock" + pid_file = run_dir / "qemu.pid" + + log.info("booting VM via %s (RUN_DIR=%s)", launcher, run_dir) + env = os.environ.copy() + env["RUN_DIR"] = str(run_dir) + qemu = subprocess.Popen( + [str(launcher)], + cwd=str(repo_root), + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + + try: + _wait_for_socket(serial_sock, timeout_s=15.0) + # Wait for the pid file to be non-empty. + deadline = time.monotonic() + 15.0 + while time.monotonic() < deadline: + if pid_file.exists() and pid_file.read_text().strip(): + break + time.sleep(0.2) + qemu_pid = int(pid_file.read_text().strip()) + log.info("qemu pid = %d", qemu_pid) + + # Cloud-init's runcmd (password setup, sshd hardening) needs some + # time after boot. Wait long enough that the credentials we'll + # send are actually valid. + log.info("waiting 35s for cloud-init runcmd to settle...") + time.sleep(35.0) + + log.info("connecting serial + logging in (boot timeout %.0fs)", + args.boot_timeout) + serial = SerialClient(str(serial_sock)) + serial.connect() + serial.login(boot_timeout_s=args.boot_timeout) + + controller = VMLoadController(serial) + controller.setup() + + cfg = EpisodeConfig( + target_pid=qemu_pid, + duration_s=sum(d for _, d in DEFAULT_SCHEDULE), + interval_ms=args.interval_ms, + data_root=Path(args.data_root), + phase_schedule=DEFAULT_SCHEDULE, + image_name="alpine-3.21-cloudinit", + snapshot_name="baseline-v1", + ) + + result = EpisodeRunner(cfg, on_phase=controller.set_phase).run() + + controller.teardown() + serial.close() + + print() + print(f"episode_id = {result.episode_id}") + print(f"path = {result.episode_dir}") + print(f"rows_proc = {result.rows_proc}") + print(f"phases = {result.phases_observed}") + print() + print("To plot:") + print(f" uv run python tools/plot_envelope.py {result.episode_dir}") + return 0 + finally: + if not args.keep_vm: + log.info("shutting down VM (pid=%d)", qemu.pid) + try: + os.killpg(os.getpgid(qemu.pid), signal.SIGTERM) + except ProcessLookupError: + pass + try: + qemu.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(os.getpgid(qemu.pid), signal.SIGKILL) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/vm_load_controller.py b/tools/vm_load_controller.py new file mode 100644 index 0000000..c84f00f --- /dev/null +++ b/tools/vm_load_controller.py @@ -0,0 +1,72 @@ +"""In-guest load controller for tier-2 episodes. + +Drives a real Alpine VM through the same phase schedule the orchestrator +follows, but the load this time is generated *inside* the guest by busybox +``yes`` / ``dd`` / a small marker file. The host /proc collector still +samples the qemu-system process from outside — what's "real" here is the +workload itself, not the orchestrator's view of it. + +Phase commands (all run via the SerialClient): + + clean — kill any running load, idle. + armed — small disk write (handshake-shape). + infecting — disk burst: 512 KiB urandom write to /tmp/payload. + infected_running — background ``yes > /dev/null`` for sustained CPU. + dormant — kill background load (back to idle). + +Designed to mimic the envelope of an XMRig-class compromise without +running real malware. Tier-3 will replace this with msf-driven exploit +fire and a real sample. +""" + +from __future__ import annotations + +import logging + +from vm_serial import SerialClient + + +log = logging.getLogger("cis490.vm_load_controller") + + +class VMLoadController: + def __init__(self, serial: SerialClient) -> None: + self.s = serial + + def setup(self) -> None: + # Kill any pre-existing load and clear scratch space. + self._kill_load() + self.s.run("rm -f /tmp/payload /tmp/armed.log; echo setup-ok") + + def teardown(self) -> None: + self._kill_load() + + # ---- phases --------------------------------------------------------- + + def set_phase(self, phase: str) -> None: + log.info("vm phase -> %s", phase) + if phase == "clean": + self._kill_load() + elif phase == "armed": + self.s.run("echo armed-handshake-$(date +%s) > /tmp/armed.log") + elif phase == "infecting": + self.s.run( + "dd if=/dev/urandom of=/tmp/payload bs=4k count=128 2>/dev/null && " + "chmod +x /tmp/payload" + ) + elif phase == "infected_running": + self._kill_load() + # Background CPU burner. `nohup` + `&` + redirects to detach. + self.s.run( + "nohup sh -c 'yes > /dev/null' /dev/null 2>&1 & disown" + ) + elif phase == "dormant": + self._kill_load() + else: + log.warning("unknown phase: %s", phase) + + # ---- internals ------------------------------------------------------ + + def _kill_load(self) -> None: + # `true` at the end so the run() exit status is always 0. + self.s.run("pkill yes 2>/dev/null; pkill stress-ng 2>/dev/null; true") diff --git a/tools/vm_serial.py b/tools/vm_serial.py new file mode 100644 index 0000000..02a3f06 --- /dev/null +++ b/tools/vm_serial.py @@ -0,0 +1,229 @@ +"""Bidirectional serial-console driver for the demo VM. + +Talks to QEMU's ``-serial unix:`` socket. Handles the Cirros login +sequence (``login:`` → user → ``Password:`` → password) and exposes a +``run(cmd) -> str`` method that sends a shell command and returns its +output, marking the boundary with a unique sentinel so we don't have to +parse the prompt. + +This is the controller the orchestrator drives via ``on_phase`` for +tier-2 (real VM, real workload from inside the guest) episodes. +""" + +from __future__ import annotations + +import logging +import os +import socket +import time +import uuid + + +log = logging.getLogger("cis490.vm_serial") + + +class SerialClient: + def __init__( + self, + socket_path: str, + username: str = "root", + password: str = "cis490", + recv_timeout: float = 0.3, + ) -> None: + self.socket_path = socket_path + self.username = username + self.password = password + self.recv_timeout = recv_timeout + self._sock: socket.socket | None = None + self._buf = b"" + + # ---- low level ------------------------------------------------------ + + def connect(self) -> None: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(self.socket_path) + s.settimeout(self.recv_timeout) + self._sock = s + + def close(self) -> None: + if self._sock is not None: + try: + self._sock.close() + finally: + self._sock = None + + def _drain(self, max_seconds: float = 0.5) -> bytes: + """Read whatever's pending and append to ``_buf``. + + Use ``self._buf = b""`` to clear after a known boundary; otherwise + callers (like ``_read_until``) get to see what arrived during the + drain window. + """ + if self._sock is None: + raise RuntimeError("not connected") + deadline = time.monotonic() + max_seconds + new = b"" + while time.monotonic() < deadline: + try: + chunk = self._sock.recv(4096) + if not chunk: + break + new += chunk + except socket.timeout: + if new: + break + self._buf += new + return new + + def _send(self, data: bytes) -> None: + assert self._sock is not None + self._sock.sendall(data) + + def _read_until(self, needle: bytes, timeout_s: float) -> bytes: + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + try: + chunk = self._sock.recv(4096) # type: ignore[union-attr] + if not chunk: + raise EOFError("serial socket closed") + self._buf += chunk + except socket.timeout: + pass + if needle in self._buf: + idx = self._buf.find(needle) + len(needle) + consumed = self._buf[:idx] + self._buf = self._buf[idx:] + return consumed + raise TimeoutError( + f"did not see {needle!r} within {timeout_s}s; " + f"last 200 bytes seen: {self._buf[-200:]!r}" + ) + + # ---- high level ----------------------------------------------------- + + def login(self, boot_timeout_s: float = 90.0, attempts: int = 3) -> None: + """Wait for the login prompt, authenticate, and confirm shell. + + Idempotent: if a previous session left us already at a shell + prompt, we detect that with a sanity probe and skip the login + dance. + + Robust against stale buffer state (e.g. a previous client whose + failed-login attempt left a ``Password:`` prompt sitting around). + Verification is always a marker echo — only signal we trust. + """ + # Maybe we're already in a shell from a prior session. + if self._sanity_probe(timeout_s=1.5): + log.info("already in a shell") + return + + for attempt in range(1, attempts + 1): + # Drain anything stale. + self._buf = b"" + self._drain(max_seconds=1.0) + # Nudge the getty so it redraws the prompt. + self._send(b"\n") + try: + # Boot timeout only on first attempt; getty is up by attempt 2. + self._read_until( + b"login:", timeout_s=boot_timeout_s if attempt == 1 else 5.0 + ) + except TimeoutError: + log.warning("login: not seen on attempt %d", attempt) + continue + self._send(self.username.encode() + b"\n") + try: + self._read_until(b"Password:", timeout_s=5.0) + except TimeoutError: + log.warning("Password: not seen on attempt %d", attempt) + continue + self._send(self.password.encode() + b"\n") + # Drain MOTD / shell init. + self._drain(max_seconds=2.0) + # Disable echo and clear PS1 so command output is uncluttered. + self._send(b"stty -echo; export PS1=''\n") + self._drain(max_seconds=1.0) + self._buf = b"" + if self._sanity_probe(timeout_s=3.0): + log.info("login OK (attempt %d)", attempt) + return + log.warning("login sanity probe failed on attempt %d", attempt) + raise RuntimeError(f"login failed after {attempts} attempts") + + def _sanity_probe(self, timeout_s: float) -> bool: + """Return True iff we appear to be at a working shell. + + Sends ``echo; echo `` — bare ``echo`` prints an empty line, + guaranteeing a ``\\r\\n`` boundary before the token in the + shell's output. The pattern ``\\r\\n`` then matches only + when a real shell ran our command; a getty echoing input would + leave the token preceded by a space (``echo ``). + """ + token = uuid.uuid4().hex[:8] + marker = f"CIS490_READY_{token}".encode() + self._buf = b"" + self._send(b"echo; echo " + marker + b"\n") + try: + self._read_until(b"\r\n" + marker, timeout_s=timeout_s) + self._buf = b"" + return True + except TimeoutError: + log.warning("sanity probe buf tail: %r", self._buf[-300:]) + return False + + def run(self, cmd: str, timeout_s: float = 10.0) -> str: + """Run ``cmd`` and return its captured stdout/stderr as a string. + + Uses unique sentinels prefixed by ``\\n`` so we match the shell's + own output (which starts on a fresh line) and not the terminal + echo of our input (where the sentinel sits in the middle of the + line, preceded by spaces). Robust to TTY echo on/off. + """ + if self._sock is None: + raise RuntimeError("not connected") + token = uuid.uuid4().hex[:12] + start_text = f"___START_{token}___".encode() + end_text = f"___END_{token}___".encode() + # \r\n + sentinel — only matches when the shell prints sentinel on + # a new line, never the echoed input line. + start_needle = b"\r\n" + start_text + end_needle = b"\r\n" + end_text + # Bare ``echo`` prints an empty line first, guaranteeing a clean + # \r\n boundary before START in the shell's output. The needle + # ``\r\nSTART`` then matches only the shell's run, not the + # echoed input line (where START is preceded by a space). + line = ( + b"echo; echo " + start_text + b"; (" + cmd.encode() + b") 2>&1; echo " + end_text + b"\n" + ) + self._send(line) + self._read_until(start_needle, timeout_s=timeout_s) + captured = self._read_until(end_needle, timeout_s=timeout_s) + # captured ends with the end_needle (which begins with \r\n). + body = captured[: -len(end_needle)] + return body.decode(errors="replace") + + # ---- context-manager ergonomics ------------------------------------ + + def __enter__(self) -> "SerialClient": + self.connect() + return self + + def __exit__(self, *exc) -> None: + self.close() + + +def smoke(socket_path: str) -> int: + logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + with SerialClient(socket_path) as c: + c.login() + for cmd in ("uname -a", "whoami", "uptime", "ls /", "which yes dd"): + out = c.run(cmd) + print(f">>> {cmd}") + print(out) + print() + return 0 + + +if __name__ == "__main__": + import sys + sys.exit(smoke(sys.argv[1] if len(sys.argv) > 1 else "/tmp/cis490-vm/serial.sock")) diff --git a/uv.lock b/uv.lock index 43554b0..b6ddb61 100644 --- a/uv.lock +++ b/uv.lock @@ -15,6 +15,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] +[[package]] +name = "bcrypt" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, + { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -24,6 +94,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "cis490" version = "0.0.1" @@ -37,6 +177,8 @@ dependencies = [ dev = [ { name = "httpx" }, { name = "matplotlib" }, + { name = "paramiko" }, + { name = "pycdlib" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "tornado" }, @@ -52,6 +194,8 @@ requires-dist = [ dev = [ { name = "httpx", specifier = ">=0.27" }, { name = "matplotlib", specifier = ">=3.8" }, + { name = "paramiko", specifier = ">=3" }, + { name = "pycdlib", specifier = ">=1.14" }, { name = "pytest", specifier = ">=8" }, { name = "pytest-asyncio", specifier = ">=0.23" }, { name = "tornado", specifier = ">=6" }, @@ -160,6 +304,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] +[[package]] +name = "cryptography" +version = "47.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a0/928c9ce0d120a40a81aa99e3ba383e87337b9ac9ef9f6db02e4d7822424d/cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", size = 3909893, upload-time = "2026-04-24T19:54:38.334Z" }, + { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9c/51f28c3550276bcf35660703ba0ab829a90b88be8cd98a71ef23c2413913/cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", size = 3698916, upload-time = "2026-04-24T19:54:49.782Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -309,6 +512,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "invoke" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/227c48c5fe47fa178ccf1fda8f047d16c97ba926567b661e9ce2045c600c/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, +] + [[package]] name = "kiwisolver" version = "1.5.0" @@ -567,6 +779,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "paramiko" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "cryptography" }, + { name = "invoke" }, + { name = "pynacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743, upload-time = "2025-08-04T01:02:03.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932, upload-time = "2025-08-04T01:02:02.029Z" }, +] + [[package]] name = "pillow" version = "12.2.0" @@ -663,6 +890,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pycdlib" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/38/9d1ab6815f97700cff7b6425675e993d2d7421c0af7f70478617e4bcab4a/pycdlib-1.16.0.tar.gz", hash = "sha256:da02ce3d3a7cc1f879cd84db7bb39427a082cee0dbf0730fec89604039344058", size = 305934, upload-time = "2026-04-29T01:42:54.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/21/434b2f36cdbeb209dd95866c002b26a467c21f9c0f428d1509b46df27781/pycdlib-1.16.0-py2.py3-none-any.whl", hash = "sha256:17843829c6bf2fd365d3d2e49a94f06da82c999efc313b9f26b0ce59c0070785", size = 214861, upload-time = "2026-04-29T01:42:52.399Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -672,6 +917,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, + { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" diff --git a/vm/launch_demo.sh b/vm/launch_demo.sh index 9ef9ec1..b70024d 100755 --- a/vm/launch_demo.sh +++ b/vm/launch_demo.sh @@ -14,7 +14,8 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -IMAGE="${IMAGE:-$REPO_ROOT/vm/images/cirros-baseline.qcow2}" +IMAGE="${IMAGE:-$REPO_ROOT/vm/images/alpine-baseline.qcow2}" +CIDATA="${CIDATA:-$REPO_ROOT/vm/images/cidata.iso}" RUN_DIR="${RUN_DIR:-/tmp/cis490-vm}" mkdir -p "$RUN_DIR" @@ -26,6 +27,10 @@ if [[ ! -f "$IMAGE" ]]; then echo "no image at $IMAGE" >&2 exit 1 fi +if [[ ! -f "$CIDATA" ]]; then + echo "no cidata at $CIDATA — build it with: uv run python tools/build_cidata.py $CIDATA" >&2 + exit 1 +fi # snapshot=on routes guest writes through a temporary overlay so the qcow2 # on disk is never mutated — every boot starts from the same bytes. @@ -36,10 +41,11 @@ exec qemu-system-x86_64 \ -smp 1,sockets=1,cores=1,threads=1 \ -m 256 \ -drive file="$IMAGE",format=qcow2,if=virtio,snapshot=on \ - -netdev user,id=n0 \ + -drive file="$CIDATA",format=raw,if=virtio,readonly=on \ + -netdev user,id=n0,hostfwd=tcp:127.0.0.1:2222-:22 \ -device virtio-net-pci,netdev=n0 \ -nographic \ - -serial null \ + -serial unix:"$RUN_DIR/serial.sock",server=on,wait=off \ -monitor unix:"$MON_SOCK",server=on,wait=off \ -qmp unix:"$QMP_SOCK",server=on,wait=off \ -pidfile "$PID_FILE" \